diff --git a/changelogs/unreleased/gh-8157-default-field-value.md b/changelogs/unreleased/gh-8157-default-field-value.md new file mode 100644 index 0000000000000000000000000000000000000000..d719476f03f90a5b04a33cec09618f75944f88c3 --- /dev/null +++ b/changelogs/unreleased/gh-8157-default-field-value.md @@ -0,0 +1,3 @@ +## feature/core + +* Introduced the default field values in the space format (gh-8157). diff --git a/src/box/errcode.h b/src/box/errcode.h index bde19049ea5fbbaa17fc033492782ea7622b61e1..9f5b1530ebe57f46a2fea5ea36d0e6c9c9b2b0c4 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -300,7 +300,7 @@ struct errcode_record { /*245 */_(ER_OLD_TERM, "The term is outdated: old - %llu, new - %llu") \ /*246 */_(ER_INTERFERING_ELECTIONS, "Interfering elections started")\ /*247 */_(ER_ITERATOR_POSITION, "Iterator position is invalid") \ - /*248 */_(ER_UNUSED, "") \ + /*248 */_(ER_DEFAULT_VALUE_TYPE, "Type of the default value does not match tuple field %s type: expected %s, got %s") \ /*249 */_(ER_UNKNOWN_AUTH_METHOD, "Unknown authentication method '%s'") \ /*250 */_(ER_INVALID_AUTH_DATA, "Invalid '%s' data: %s") \ /*251 */_(ER_INVALID_AUTH_REQUEST, "Invalid '%s' request: %s") \ diff --git a/src/box/field_def.c b/src/box/field_def.c index 2a82ec9b734ce22d83eb825e34e633598c956a6d..281a4f1a1a6cdce78a1035773a29157bc0f85f00 100644 --- a/src/box/field_def.c +++ b/src/box/field_def.c @@ -168,6 +168,14 @@ field_type1_contains_type2(enum field_type type1, enum field_type type2) return field_type_compatibility[idx]; } +/** + * Callback to parse a value with 'default' key in msgpack field definition. + * See function definition below. + */ +static int +field_def_parse_default_value(const char **data, void *opts, + struct region *region); + /** * Callback to parse a value with 'constraint' key in msgpack field definition. * See function definition below. @@ -195,6 +203,7 @@ static const struct opt_def field_def_reg[] = { OPT_DEF("sql_default", OPT_STRPTR, struct field_def, sql_default_value), OPT_DEF_ENUM("compression", compression_type, struct field_def, compression_type, NULL), + OPT_DEF_CUSTOM("default", field_def_parse_default_value), OPT_DEF_CUSTOM("constraint", field_def_parse_constraint), OPT_DEF_CUSTOM("foreign_key", field_def_parse_foreign_key), OPT_END, @@ -207,6 +216,8 @@ const struct field_def field_def_default = { .nullable_action = ON_CONFLICT_ACTION_DEFAULT, .coll_id = COLL_NONE, .sql_default_value = NULL, + .default_value = NULL, + .default_value_size = 0, .constraint_count = 0, .constraint_def = NULL, }; @@ -228,6 +239,30 @@ field_type_by_name(const char *name, size_t len) return field_type_MAX; } +/** + * Parse default field value from msgpack. + * Used as callback to parse a value with 'default' key in field definition. + * Move @a data msgpack pointer to the end of msgpack value. + * By convention @a opts must point to corresponding struct field_def. + * Allocate a temporary copy of a default value on @a region and set pointer to + * it as field_def->default_value, also setting field_def->default_value_size. + */ +static int +field_def_parse_default_value(const char **data, void *opts, + struct region *region) +{ + struct field_def *def = (struct field_def *)opts; + const char *default_value = *data; + mp_next(data); + const char *default_value_end = *data; + size_t size = default_value_end - default_value; + + def->default_value = xregion_alloc(region, size); + def->default_value_size = size; + memcpy(def->default_value, default_value, size); + return 0; +} + /** * Parse constraint array from msgpack. * Used as callback to parse a value with 'constraint' key in field definition. @@ -399,6 +434,7 @@ field_def_array_dup(const struct field_def *fields, uint32_t field_count) grp_alloc_reserve_str0(&all, fields[i].sql_default_value); } + grp_alloc_reserve_data(&all, fields[i].default_value_size); } grp_alloc_use(&all, xmalloc(grp_alloc_size(&all))); struct field_def *copy = grp_alloc_create_data( @@ -410,6 +446,12 @@ field_def_array_dup(const struct field_def *fields, uint32_t field_count) copy[i].sql_default_value = grp_alloc_create_str0( &all, fields[i].sql_default_value); } + if (fields[i].default_value != NULL) { + size_t size = fields[i].default_value_size; + char *buf = grp_alloc_create_data(&all, size); + memcpy(buf, fields[i].default_value, size); + copy[i].default_value = buf; + } copy[i].constraint_def = tuple_constraint_def_array_dup( fields[i].constraint_def, fields[i].constraint_count); } diff --git a/src/box/field_def.h b/src/box/field_def.h index 6b0fe6677390a8bf7757f81e17b9fe510847dc04..1a9438fe5064cfbe5acc98bd7c825413126e92ab 100644 --- a/src/box/field_def.h +++ b/src/box/field_def.h @@ -148,6 +148,10 @@ struct field_def { uint32_t coll_id; /** 0-terminated SQL expression for DEFAULT value. */ char *sql_default_value; + /** MsgPack with the default value. */ + char *default_value; + /** Size of the default value. */ + size_t default_value_size; /** Compression type for this field. */ enum compression_type compression_type; /** Array of constraints. Can be NULL if constraints_count == 0. */ diff --git a/src/box/request.c b/src/box/request.c index 4c677c7304c43ce19fdfe35ee393f585ac1c10ac..4c923c6ac32a4beb04f1ca848d551e054ebd9fea 100644 --- a/src/box/request.c +++ b/src/box/request.c @@ -69,13 +69,17 @@ request_update_header(struct request *request, struct xrow_header *row, int request_create_from_tuple(struct request *request, struct space *space, - struct tuple *old_tuple, struct tuple *new_tuple, - struct region *region) + const char *old_data, uint32_t old_size, + const char *new_data, uint32_t new_size, + struct region *region, bool preserve_request_type) { + const char *upsert_ops = request->ops; + const char *upsert_ops_end = request->ops_end; + enum iproto_type request_type = request->type; struct xrow_header *row = request->header; memset(request, 0, sizeof(*request)); - if (old_tuple == new_tuple) { + if (old_data == new_data) { /* * Old and new tuples are the same, * turn this request into no-op. @@ -89,11 +93,10 @@ request_create_from_tuple(struct request *request, struct space *space, * this line is not reached. */ request->space_id = space->def->id; - if (new_tuple == NULL) { - uint32_t size, key_size; - const char *data = tuple_data_range(old_tuple, &size); + if (new_data == NULL) { + uint32_t key_size; request->key = tuple_extract_key_raw_to_region( - data, data + size, + old_data, old_data + old_size, space->index[0]->def->key_def, MULTIKEY_NONE, &key_size, region); if (request->key == NULL) @@ -101,8 +104,6 @@ request_create_from_tuple(struct request *request, struct space *space, request->key_end = request->key + key_size; request->type = IPROTO_DELETE; } else { - uint32_t size; - const char *data = tuple_data_range(new_tuple, &size); /* * We have to copy the tuple data to region, because * the tuple is allocated on runtime arena and not @@ -110,15 +111,16 @@ request_create_from_tuple(struct request *request, struct space *space, * current transaction ends while we need to write * the tuple data to WAL on commit. */ - char *buf = region_alloc(region, size); - if (buf == NULL) { - diag_set(OutOfMemory, size, "region_alloc", "tuple"); - return -1; - } - memcpy(buf, data, size); + char *buf = xregion_alloc(region, new_size); + memcpy(buf, new_data, new_size); request->tuple = buf; - request->tuple_end = buf + size; - request->type = IPROTO_REPLACE; + request->tuple_end = buf + new_size; + request->type = preserve_request_type ? request_type : + IPROTO_REPLACE; + if (request->type == IPROTO_UPSERT) { + request->ops = upsert_ops; + request->ops_end = upsert_ops_end; + } } request_update_header(request, row, region); return 0; diff --git a/src/box/request.h b/src/box/request.h index a1ffd15c63486dd072e6fb1484b69485587a5b04..acf0226c2aac05ae377e34519c0c7b55e4ac0d1f 100644 --- a/src/box/request.h +++ b/src/box/request.h @@ -30,6 +30,8 @@ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ +#include <stdbool.h> +#include <stdint.h> #if defined(__cplusplus) extern "C" { @@ -46,18 +48,24 @@ struct region; * * @param request - request to fix * @param space - space corresponding to request - * @param old_tuple - the old tuple - * @param new_tuple - the new tuple + * @param old_data - the old tuple data + * @param old_size - size of the old data + * @param new_data - the new tuple data + * @param new_size - size of the new data * @param region - region where request parts will be allocated + * @param preserve_insert_type - do not turn INSERT requests into REPLACE * - * If old_tuple and new_tuple are the same, the request is turned into NOP. - * If new_tuple is NULL, the request is turned into DELETE(old_tuple). - * If new_tuple is not NULL, the request is turned into REPLACE(new_tuple). + * If old_data and new_data are the same, the request is turned into NOP. + * If new_data is NULL, the request is turned into DELETE(old_data). + * If new_data is not NULL, the request is turned into REPLACE(new_data), + * or into INSERT(new_data)/UPSERT(new_data) when preserve_request_type is true + * and the original request was INSERT or UPSERT. */ int request_create_from_tuple(struct request *request, struct space *space, - struct tuple *old_tuple, struct tuple *new_tuple, - struct region *region); + const char *old_data, uint32_t old_size, + const char *new_data, uint32_t new_size, + struct region *region, bool preserve_request_type); /** * Convert a request accessing a secondary key to a primary diff --git a/src/box/space.c b/src/box/space.c index 5477a58d907fbf7d4fad3d788b4201a621ac20e1..58dfd41726cdf1c56cab2fa82b2fe2bbe56705d2 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -516,6 +516,41 @@ space_index_def(struct space *space, int n) return space->index[n]->def; } +/** + * Apply default values from the space format to the null (or absent) fields of + * request->tuple. + */ +static int +space_apply_defaults(struct space *space, struct txn *txn, + struct request *request) +{ + assert(request->type == IPROTO_INSERT || + request->type == IPROTO_REPLACE || + request->type == IPROTO_UPSERT); + + const char *new_data = request->tuple; + const char *new_data_end = request->tuple_end; + size_t region_svp = region_used(&fiber()->gc); + + bool changed = tuple_format_apply_defaults(space->format, &new_data, + &new_data_end); + if (!changed) { + region_truncate(&fiber()->gc, region_svp); + return 0; + } + /* + * Field defaults changed the resulting tuple. + * Fix the request to conform. + */ + struct region *txn_region = tx_region_acquire(txn); + int rc = request_create_from_tuple(request, space, NULL, 0, new_data, + new_data_end - new_data, txn_region, + true); + tx_region_release(txn, TX_ALLOC_SYSTEM); + region_truncate(&fiber()->gc, region_svp); + return rc; +} + /** * Run BEFORE triggers and foreign key constraint checks registered for a space. * If a trigger changes the current statement, this function updates the @@ -584,13 +619,18 @@ after_old_tuple_lookup:; /* * Create the new tuple. */ - uint32_t new_size, old_size; + uint32_t new_size, old_size = 0; const char *new_data, *new_data_end; - const char *old_data, *old_data_end; + const char *old_data = NULL, *old_data_end; switch (request->type) { case IPROTO_INSERT: + new_data = request->tuple; + new_data_end = request->tuple_end; + break; case IPROTO_REPLACE: + if (old_tuple != NULL) + old_data = tuple_data_range(old_tuple, &old_size); new_data = request->tuple; new_data_end = request->tuple_end; break; @@ -615,6 +655,7 @@ after_old_tuple_lookup:; /* Nothing to delete. */ return 0; } + old_data = tuple_data_range(old_tuple, &old_size); new_data = new_data_end = NULL; break; case IPROTO_UPSERT: @@ -700,7 +741,7 @@ after_old_tuple_lookup:; int rc = trigger_run(&space->before_replace, txn); /* - * BEFORE riggers cannot change the old tuple, + * BEFORE triggers cannot change the old tuple, * but they may replace the new tuple. */ bool request_changed = (stmt->new_tuple != new_tuple); @@ -731,10 +772,13 @@ after_old_tuple_lookup:; * Fix the request to conform. */ if (request_changed) { + new_data = new_tuple == NULL ? NULL : + tuple_data_range(new_tuple, &new_size); struct region *txn_region = tx_region_acquire(txn); rc = request_create_from_tuple(request, space, - old_tuple, new_tuple, - txn_region); + old_data, old_size, + new_data, new_size, + txn_region, false); tx_region_release(txn, TX_ALLOC_SYSTEM); } out: @@ -774,6 +818,16 @@ space_execute_dml(struct space *space, struct txn *txn, } } } + + bool need_defaults_apply = tuple_format_has_defaults(space->format) && + recovery_state == FINISHED_RECOVERY && + request->type != IPROTO_UPDATE && + request->type != IPROTO_DELETE; + if (unlikely(need_defaults_apply)) { + if (space_apply_defaults(space, txn, request) != 0) + return -1; + } + if (unlikely((!rlist_empty(&space->before_replace) && space->run_triggers) || need_foreign_key_check)) { /* diff --git a/src/box/sql.c b/src/box/sql.c index a5248a30fc471fe2a20e8f18aa10630c5deb0498..0da9c1e572f40228fb9a84be704ded2361d4c880 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -378,6 +378,8 @@ sql_ephemeral_space_new(const struct sql_space_info *info) fields[i].is_nullable = true; fields[i].nullable_action = ON_CONFLICT_ACTION_NONE; fields[i].sql_default_value = NULL; + fields[i].default_value = NULL; + fields[i].default_value_size = 0; fields[i].type = info->types[i]; fields[i].coll_id = info->coll_ids[i]; fields[i].compression_type = COMPRESSION_TYPE_NONE; diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c index acf6938d14244f2608837599ffebec3d55786bd4..353da7972c826b4784277e025d89e092957d2d4c 100644 --- a/src/box/tuple_format.c +++ b/src/box/tuple_format.c @@ -34,6 +34,7 @@ #include "json/json.h" #include "coll_id_cache.h" #include "trivia/util.h" +#include "tuple_builder.h" #include "tuple_constraint.h" #include "tt_static.h" @@ -181,6 +182,8 @@ tuple_field_new(void) field->multikey_required_fields = NULL; field->constraint_count = 0; field->constraint = NULL; + field->default_value = NULL; + field->default_value_size = 0; return field; } @@ -193,6 +196,7 @@ tuple_field_delete(struct tuple_field *field) free(field->constraint); if (field->sql_default_value_expr != NULL) tuple_format_expr_delete(field->sql_default_value_expr); + free(field->default_value); free(field); } @@ -510,6 +514,25 @@ tuple_format_create(struct tuple_format *format, struct key_def *const *keys, if (field->sql_default_value_expr == NULL) return -1; } + char *default_value = fields[i].default_value; + if (default_value != NULL) { + bool is_compatible = field_mp_type_is_compatible( + field->type, default_value, false); + if (!is_compatible) { + enum mp_type type = mp_typeof(*default_value); + diag_set(ClientError, ER_DEFAULT_VALUE_TYPE, + tuple_field_path(field, format), + field_type_strs[field->type], + mp_type_strs[type]); + return -1; + } + size_t size = fields[i].default_value_size; + char *buf = xmalloc(size); + memcpy(buf, default_value, size); + field->default_value = buf; + field->default_value_size = size; + format->default_field_count = i + 1; + } } int current_slot = 0; @@ -719,6 +742,7 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count, format->epoch = 0; format->constraint_count = 0; format->constraint = NULL; + format->default_field_count = 0; return format; error: tuple_format_destroy_fields(format); @@ -1018,9 +1042,10 @@ tuple_field_map_create_plain(struct tuple_format *format, const char *tuple, mp_next(&next_pos); field = json_tree_entry(*token, struct tuple_field, token); if (validate) { - bool nullable = tuple_field_is_nullable(field); + bool allow_null = tuple_field_is_nullable(field) || + tuple_field_has_default(field); if(!field_mp_type_is_compatible(field->type, pos, - nullable)) { + allow_null)) { diag_set(ClientError, ER_FIELD_TYPE, tuple_field_path(field, format), field_type_strs[field->type], @@ -1397,3 +1422,69 @@ tuple_format_iterator_next(struct tuple_format_iterator *it, entry->data = NULL; return 0; } + +bool +tuple_format_apply_defaults(struct tuple_format *format, const char **data, + const char **data_end) +{ + struct tuple_builder builder; + tuple_builder_new(&builder, &fiber()->gc); + bool is_tuple_changed = false; + /* + * Process fields that are present in both the format and the tuple. + * Break prematurely when all defaults are applied. + */ + const char *p = *data; + uint32_t tuple_field_count = mp_decode_array(&p); + size_t i; + for (i = 0; i < MIN(tuple_field_count, + format->default_field_count); i++) { + const char *p_next = p; + mp_next(&p_next); + + struct tuple_field *field = NULL; + bool is_null = mp_typeof(*p) == MP_NIL; + if (is_null) + field = tuple_format_field(format, i); + + if (is_null && tuple_field_has_default(field)) { + tuple_builder_add(&builder, field->default_value, + field->default_value_size, 1); + is_tuple_changed = true; + } else { + tuple_builder_add(&builder, p, p_next - p, 1); + } + p = p_next; + } + /* + * Process fields that are present in the format, but not in the tuple. + * Break prematurely when all defaults are applied. + */ + for ( ; i < format->default_field_count; i++) { + struct tuple_field *field = tuple_format_field(format, i); + if (tuple_field_has_default(field)) { + tuple_builder_add(&builder, field->default_value, + field->default_value_size, 1); + is_tuple_changed = true; + } else { + tuple_builder_add_nil(&builder); + } + } + /* + * Return if no fields were changed. + */ + if (!is_tuple_changed) + return false; + /* + * If the tuple has more fields, append them as is. + */ + if (tuple_field_count > i) { + uint32_t field_count = tuple_field_count - i; + tuple_builder_add(&builder, p, *data_end - p, field_count); + } + /* + * Allocate a buffer and encode all elements into the new MsgPack array. + */ + tuple_builder_finalize(&builder, data, data_end); + return true; +} diff --git a/src/box/tuple_format.h b/src/box/tuple_format.h index eb4a1eb0672bfe298566c47060d4d283d76dbc6e..8f320ae737f8031e7a2c97832ff86ad1da46bb4c 100644 --- a/src/box/tuple_format.h +++ b/src/box/tuple_format.h @@ -65,7 +65,6 @@ enum { TUPLE_INDEX_BASE = 1 }; enum { TUPLE_OFFSET_SLOT_NIL = INT32_MAX }; struct tuple; -struct tuple_chunk; struct tuple_format; struct coll; struct Expr; @@ -163,6 +162,10 @@ struct tuple_field { uint32_t constraint_count; /** AST for parsed SQL default value. */ struct Expr *sql_default_value_expr; + /** MsgPack with the default value. */ + char *default_value; + /** Size of the default value. */ + size_t default_value_size; }; /** @@ -177,6 +180,15 @@ tuple_field_is_nullable(const struct tuple_field *tuple_field) return tuple_field->nullable_action == ON_CONFLICT_ACTION_NONE; } +/** + * Return true if tuple_field has a default value. + */ +static inline bool +tuple_field_has_default(const struct tuple_field *tuple_field) +{ + return tuple_field->default_value != NULL; +} + /** * Return path to a tuple field. Used for error reporting. */ @@ -252,6 +264,11 @@ struct tuple_format { * path fields. See also tuple_format::fields. */ uint32_t total_field_count; + /** + * An upper bound for the number of fields with a default value. + * In other words, max fieldno with a default value + 1. + */ + uint32_t default_field_count; /** * Bitmap of fields that must be present in a tuple * conforming to the format. Indexed by tuple_field::id. @@ -438,6 +455,15 @@ tuple_format_min_field_count(struct key_def * const *keys, uint16_t key_count, const struct field_def *space_fields, uint32_t space_field_count); +/** + * Return true if format has at least one field with a default value. + */ +static inline bool +tuple_format_has_defaults(const struct tuple_format *format) +{ + return format->default_field_count > 0; +} + typedef struct tuple_format box_tuple_format_t; /** \cond public */ @@ -641,6 +667,17 @@ int tuple_format_iterator_next(struct tuple_format_iterator *it, struct tuple_format_iterator_entry *entry); +/** + * Replace null (or absent) fields of msgpack with the default values from the + * format. The input msgpack is located at [*data .. *data_end). + * Return true if at least one field is changed, in that case data and data_end + * are updated to point to the new buffer with the modified msgpack. The buffer + * is allocated on current fiber's region. + */ +bool +tuple_format_apply_defaults(struct tuple_format *format, const char **data, + const char **data_end); + #if defined(__cplusplus) } /* extern "C" */ #endif /* defined(__cplusplus) */ diff --git a/test/box/error.result b/test/box/error.result index c0973dd006de22ad181b8b58e7748e15eab0e322..b0e1cab38464247f2f9536f559d7b3f45b5982b7 100644 --- a/test/box/error.result +++ b/test/box/error.result @@ -466,6 +466,7 @@ t; | 245: box.error.OLD_TERM | 246: box.error.INTERFERING_ELECTIONS | 247: box.error.ITERATOR_POSITION + | 248: box.error.DEFAULT_VALUE_TYPE | 249: box.error.UNKNOWN_AUTH_METHOD | 250: box.error.INVALID_AUTH_DATA | 251: box.error.INVALID_AUTH_REQUEST diff --git a/test/engine-luatest/gh_8157_default_field_value_test.lua b/test/engine-luatest/gh_8157_default_field_value_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..3131a55dd41c77339c1a224f0ab262857ff5f677 --- /dev/null +++ b/test/engine-luatest/gh_8157_default_field_value_test.lua @@ -0,0 +1,283 @@ +local t = require('luatest') +local server = require('luatest.server') + +local function before_all(cg) + cg.server = server:new({alias = 'master'}) + cg.server:start() +end + +local function after_all(cg) + cg.server:drop() +end + +local function after_each(cg) + cg.server:exec(function() + if box.space.test then box.space.test:drop() end + end) +end + +local g = t.group('gh-8157-1', {{engine = 'memtx'}, {engine = 'vinyl'}}) +g.before_all(before_all) +g.after_all(after_all) +g.after_each(after_each) + +-- Test default field values. +g.test_basics = function(cg) + cg.server:exec(function(engine) + local format = { + {name='c1', type='any'}, + {name='c2', type='any', default={key='val'}}, + {name='id', type='unsigned', default=0}, + {name='c4', type='integer', is_nullable=true, default=-100500}, + {name='c5', type='unsigned', is_nullable=false, default=0}, + {name='c6', type='string', is_nullable=true, default='hello'} + } + local opts = {engine = engine, format = format} + local s = box.schema.space.create('test', opts) + s:create_index('pk', {parts={'id'}}) + local sk1 = s:create_index('sk1', {parts={'c4'}}) + local sk2 = s:create_index('sk2', {parts={{'c5'}, {'c6'}}, + unique=false}) + -- c5 and c6 are null + s:insert{11, 12, 13, 14} + t.assert_equals(sk1:select{14}, {{11, 12, 13, 14, 0, 'hello'}}) + + -- c4 is null + s:insert{21, 22, 23, nil, 25, '26'} + t.assert_equals(sk2:select{25}, {{21, 22, 23, -100500, 25, '26'}}) + + -- c2, c5, c6 are null + s:insert{nil, nil, 33, 34, box.NULL} + t.assert_equals(s:select{33}, {{nil, {key='val'}, 33, 34, 0, 'hello'}}) + + -- c6 is null + more fields + s:insert{41, 42, 43, 44, 45, nil, ',', 'world', '!'} + t.assert_equals(s:select{43}, {{41, 42, 43, 44, 45, 'hello', + ',', 'world', '!'}}) + + -- Primary indexed field id is null + s:insert{51, 52, nil, 54, 55, ''} + t.assert_equals(s:select{0}, {{51, 52, 0, 54, 55, ''}}) + end, {cg.params.engine}) +end + +-- Test default field values with UPDATE and UPSERT operations. +g.test_update_upsert = function(cg) + cg.server:exec(function(engine) + local s = box.schema.space.create('test', {engine=engine}) + s:create_index('pk') + s:insert{1000, nil, 'qwerty'} + + -- Check that s:format(format) is successful although the space contains + -- a tuple with non-nullable field `name', which is null. + local format = {{name='id', type='unsigned'}, + {name='name', type='string', default='Noname'}, + {name='pass', type='string'}, + {name='shell', type='string', default='/bin/sh', + is_nullable=true}} + t.assert_equals(s:format(format), nil) + + -- Note that existing tuples are not updated automatically by s:format() + t.assert_equals(s:select{1000}, {{1000, nil, 'qwerty'}}) + + -- Check that UPDATE does not change the `name' field, which is null. + s:update({1000}, {{'=', 'pass', 'secret'}}) + t.assert_equals(s:select{1000}, {{1000, nil, 'secret'}}) + + -- Test UPSERT (acts as UPDATE) + s:upsert({1000, nil, 'love'}, {{'=', 'pass', '123456'}}) + t.assert_equals(s:select{1000}, {{1000, nil, '123456'}}) + + -- Test UPSERT (acts as INSERT) + s:upsert({1001, nil, 'god'}, {}) + t.assert_equals(s:select{1001}, {{1001, 'Noname', 'god', '/bin/sh'}}) + end, {cg.params.engine}) +end + +-- Test default field values in a space with the field_count option set. +g.test_exact_field_count = function(cg) + cg.server:exec(function(engine) + local format = {{name='id', type='integer'}, + {name='id1', type='integer'}, + {name='id2', type='integer', default=0}} + local opts = {engine = engine, format = format, field_count = 3} + local s = box.schema.space.create('test', opts) + s:create_index('pk') + + -- Default value is applied to field 3 (id2), but field 2 is null. + t.assert_error_msg_content_equals('Tuple field 2 (id1) type does ' .. + 'not match one required by operation: expected integer, got nil', + s.insert, s, {1}) + + t.assert_equals(s:insert{2, 2}, {2, 2, 0}) + t.assert_equals(s:insert{3, 3, 3}, {3, 3, 3}) + + t.assert_error_msg_content_equals( + 'Tuple field count 4 does not match space field count 3', + s.insert, s, {4, 4, nil, 4}) + end, {cg.params.engine}) +end + +-- Test error messages. +g.test_errors = function(cg) + cg.server:exec(function(engine) + -- Bad type of the default value. + local format = {{name='id', type='integer'}, + {name='c2', type='integer', default='not_integer'}} + local opts = {engine = engine, format = format} + t.assert_error_msg_content_equals('Type of the default value does ' .. + 'not match tuple field 2 (c2) type: expected integer, got string', + box.schema.space.create, 'test', opts) + + format = {{name='id', type='integer'}, + {name='c2', type='integer', default=-1}} + opts = {engine = engine, format = format} + local s = box.schema.space.create('test', opts) + + -- Check upsert into a space without the primary index. + t.assert_error_msg_content_equals( + "No index #0 is defined in space 'test'", + s.upsert, s, {1}, {}) + + -- Check upsert with a wrong key type. + s:create_index('pk') + s:create_index('sk', {parts={'c2'}}) + t.assert_error_msg_content_equals('Tuple field 1 (id) type does not ' .. + 'match one required by operation: expected integer, got string', + s.upsert, s, {'bad'}, {}) + + -- Check upsert with a wrong update operation. + t.assert_error_msg_content_equals( + 'Illegal parameters, update operation must be an array {op,..}', + s.upsert, s, {0}, {'bad'}) + s:insert{0} + t.assert_error_msg_content_equals( + 'Illegal parameters, update operation must be an array {op,..}', + s.upsert, s, {0}, {'bad'}) + + -- Check "duplicate key exists" error messages. + t.assert_error_msg_content_equals('Duplicate key exists in unique ' .. + 'index "pk" in space "test" with old tuple - [0, -1] and new ' .. + 'tuple - [0, -1]', s.insert, s, {0}) + t.assert_error_msg_content_equals('Duplicate key exists in unique ' .. + 'index "sk" in space "test" with old tuple - [0, -1] and new ' .. + 'tuple - [1, -1]', s.insert, s, {1}) + s:truncate() + + -- Space format has more fields than the inserted tuple. + -- Check that the error message complains only about c3. + s:format{{name='id', type='integer', default=0}, + {name='c2', type='integer', default=-1}, + {name='c3', type='integer'}} + t.assert_error_msg_content_equals( + 'Tuple field 3 (c3) required by space format is missing', + s.insert, s, {0}) + end, {cg.params.engine}) +end + +-- Test default field values in conjunction with before_replace triggers. +g.test_triggers = function(cg) + cg.server:exec(function(engine) + local format = {{name='id', type='unsigned'}, + {name='name', type='string', default='Noname'}, + {name='pass', type='string'}, + {name='shell', type='string', default='/bin/sh'}} + local s = box.schema.space.create('test', {engine = engine}) + s:create_index('pk') + s:insert{1000, nil, '0000', '/bin/nologin'} + s:format(format) + + local trigger1 = function(_, new) + return box.tuple.update(new, {{'=', 3, 'hacked'}}) + end + s:before_replace(trigger1) + + -- Check that UPSERT (acts as INSERT) applies default values. + t.assert_equals(s:upsert({1001, nil, 'xxxxxxxxx'}, {}), + {1001, 'Noname', 'hacked', '/bin/sh'}) + + -- Check that UPSERT (acts as UPDATE) doesn't apply the default. + t.assert_equals(s:upsert({1000, nil, '0000', '/bin/zsh'}, + {{'=', 'shell', '/bin/zsh'}}), + {1000, nil, 'hacked', '/bin/zsh'}) + + -- Check that REPLACE applies default values. + t.assert_equals(s:replace{1000, nil, '123'}, + {1000, 'Noname', 'hacked', '/bin/sh'}) + + -- Check that defaults can be applied inside a trigger. + local trigger2 = function(_, new) + box.space.test:run_triggers(false) + box.space.test:insert{9000, nil, new[3]} + end + s:before_replace(trigger2, trigger1) + s:insert{1002, 'user', 'secret'} + t.assert_equals(s:select{9000}, {{9000, 'Noname', 'secret', '/bin/sh'}}) + end, {cg.params.engine}) +end + +-- Test default field values with access via net.box. +g.test_netbox = function(cg) + cg.server:exec(function(engine) + local format = {{name='id1', type='integer', default=1000}, + {name='id2', type='integer', default=2000}, + {name='id3', type='integer', default=3000}} + local opts = {engine = engine, format = format} + local s = box.schema.space.create('test', opts) + s:create_index('pk') + end, {cg.params.engine}) + + local netbox = require('net.box') + local c = netbox.connect(cg.server.net_box_uri) + local s = c.space.test + + t.assert_equals(s:insert{1, nil, 1}, {1, 2000, 1}) + t.assert_equals(s:insert{2}, {2, 2000, 3000}) + t.assert_equals(s:insert{}, {1000, 2000, 3000}) +end + +local g2 = t.group('gh-8157-2') +g2.before_all(before_all) +g2.after_all(after_all) +g2.after_each(after_each) + +-- Test default field values after recovery from xlog and snap. +g2.test_recovery = function(cg) + cg.server:exec(function() + local format = {{name='id', type='integer'}, + {name='name', type='string', default='guest'}} + local opts = {format = format, id = 666} + local s = box.schema.space.create('test', opts) + s:create_index('pk') + s:insert{-1} + s:upsert({-2}, {}) + end) + + -- Check recovery from xlog. + cg.server:restart() + cg.server:exec(function() + t.assert_equals(box.space.test:select{-1}, {{-1, 'guest'}}) + t.assert_equals(box.space.test:select{-2}, {{-2, 'guest'}}) + end) + + -- Check that xlog contains the actual default values. + local fio = require('fio') + local xlog = require('xlog') + local xlog_tuples = {} + local xlog_path = fio.pathjoin(cg.server.workdir, + string.format("%020d.xlog", 0)) + for _, row in xlog.pairs(xlog_path) do + if row.BODY.space_id == 666 then + table.insert(xlog_tuples, row.BODY.tuple) + end + end + t.assert_equals(xlog_tuples, {{-1, 'guest'}, {-2, 'guest'}}) + + --- Check recovery from snap. + cg.server:eval('box.snapshot()') + cg.server:restart() + cg.server:exec(function() + t.assert_equals(box.space.test:select{-1}, {{-1, 'guest'}}) + t.assert_equals(box.space.test:select{-2}, {{-2, 'guest'}}) + end) +end