diff --git a/src/box/alter.cc b/src/box/alter.cc index af9fbb52b8bec8658be2e62fa79ea3a77dadbf85..d7191c1bba42d77b3d6f538b72c69e2c77acce59 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -891,6 +891,12 @@ class ModifySpaceFormat: public AlterSpaceOp * names into an old dictionary and deletes new one. */ struct tuple_dictionary *new_dict; + /** + * Old tuple dictionary stored to rollback in destructor, + * if an exception had been raised after alter_def(), but + * before alter(). + */ + struct tuple_dictionary *old_dict; /** * New space definition. It can not be got from alter, * because alter_def() is called before @@ -899,14 +905,12 @@ class ModifySpaceFormat: public AlterSpaceOp struct space_def *new_def; public: ModifySpaceFormat(struct alter_space *alter, struct space_def *new_def) - :AlterSpaceOp(alter), new_dict(NULL), new_def(new_def) {} + : AlterSpaceOp(alter), new_dict(NULL), old_dict(NULL), + new_def(new_def) {} + virtual void alter(struct alter_space *alter); virtual void alter_def(struct alter_space *alter); - virtual void rollback(struct alter_space *alter); - virtual ~ModifySpaceFormat() - { - if (new_dict != NULL) - tuple_dictionary_unref(new_dict); - } + virtual void commit(struct alter_space *alter, int64_t lsn); + virtual ~ModifySpaceFormat(); }; void @@ -918,18 +922,43 @@ ModifySpaceFormat::alter_def(struct alter_space *alter) * object is deleted later, in destructor. */ new_dict = new_def->dict; - struct tuple_dictionary *old_dict = alter->old_space->def->dict; + old_dict = alter->old_space->def->dict; tuple_dictionary_swap(new_dict, old_dict); new_def->dict = old_dict; tuple_dictionary_ref(old_dict); } void -ModifySpaceFormat::rollback(struct alter_space *alter) +ModifySpaceFormat::alter(struct alter_space *alter) { - /* Return old names into the old dict. */ - struct tuple_dictionary *old_dict = alter->old_space->def->dict; - tuple_dictionary_swap(new_dict, old_dict); + struct space *new_space = alter->new_space; + struct space *old_space = alter->old_space; + struct tuple_format *new_format = new_space->format; + struct tuple_format *old_format = old_space->format; + if (old_format != NULL) { + assert(new_format != NULL); + if (! tuple_format1_can_store_format2_tuples(new_format, + old_format)) + space_check_format_xc(new_space, old_space); + } +} + +void +ModifySpaceFormat::commit(struct alter_space *alter, int64_t lsn) +{ + (void) alter; + (void) lsn; + old_dict = NULL; +} + +ModifySpaceFormat::~ModifySpaceFormat() +{ + if (new_dict != NULL) { + /* Return old names into the old dict. */ + if (old_dict != NULL) + tuple_dictionary_swap(new_dict, old_dict); + tuple_dictionary_unref(new_dict); + } } /** Change non-essential properties of a space. */ @@ -941,7 +970,6 @@ class ModifySpace: public AlterSpaceOp /* New space definition. */ struct space_def *def; virtual void alter_def(struct alter_space *alter); - virtual void alter(struct alter_space *alter); virtual ~ModifySpace(); }; @@ -955,51 +983,6 @@ ModifySpace::alter_def(struct alter_space *alter) def = NULL; } -void -ModifySpace::alter(struct alter_space *alter) -{ - struct space *new_space = alter->new_space; - struct space *old_space = alter->old_space; - uint32_t old_field_count = old_space->def->field_count; - uint32_t new_field_count = new_space->def->field_count; - if (old_field_count >= new_field_count) { - /* Is checked by space_def_check_compatibility. */ - return; - } - struct tuple_format *new_format = new_space->format; - struct tuple_format *old_format = old_space->format; - /* - * A tuples validation can be skipped if fields between - * old_space->def->field_count and - * new_space->def->field_count are indexed or have type - * ANY. If they are indexed, then their type is already - * checked. Type ANY can store any values. - * Optimization is inapplicable if - * new_def->def->field_count > old_format->field_count. - */ - if (old_format != NULL && new_field_count <= old_format->field_count) { - assert(new_field_count <= new_format->field_count); - struct tuple_field *fields = new_format->fields; - bool are_new_fields_checked = true; - for (uint32_t i = old_field_count; i < new_field_count; ++i) { - if (!fields[i].is_key_part && - fields[i].type != FIELD_TYPE_ANY) { - are_new_fields_checked = false; - break; - } - } - if (are_new_fields_checked) { - /* - * If the new space fields are already - * used by existing indexes, then tuples - * already are validated by them. - */ - return; - } - } - space_check_format_xc(new_space, old_space); -} - ModifySpace::~ModifySpace() { if (def != NULL) space_def_delete(def); @@ -1098,9 +1081,20 @@ class ModifyIndex: public AlterSpaceOp ModifyIndex(struct alter_space *alter, struct index_def *new_index_def_arg, struct index_def *old_index_def_arg) - :AlterSpaceOp(alter), - new_index_def(new_index_def_arg), - old_index_def(old_index_def_arg) {} + : AlterSpaceOp(alter), new_index_def(new_index_def_arg), + old_index_def(old_index_def_arg) { + if (new_index_def->iid == 0 && + key_part_cmp(new_index_def->key_def->parts, + new_index_def->key_def->part_count, + old_index_def->key_def->parts, + old_index_def->key_def->part_count) != 0) { + /* + * Primary parts have been changed - + * update non-unique secondary indexes. + */ + alter->pk_def = new_index_def->key_def; + } + } struct index_def *new_index_def; struct index_def *old_index_def; virtual void alter_def(struct alter_space *alter); @@ -1696,8 +1690,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) if (index_def_cmp(index_def, old_index->def) == 0) { /* Index is not changed so just move it. */ (void) new MoveIndex(alter, old_index->def->iid); - } - else if (index_def_change_requires_rebuild(old_index->def, index_def)) { + } else if (index_def_change_requires_rebuild(old_index->def, + index_def)) { /* * Operation demands an index rebuild. */ @@ -1705,6 +1699,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) old_index->def); index_def_guard.is_active = false; } else { + (void) new ModifySpaceFormat(alter, old_space->def); /* * Operation can be done without index rebuild. */ diff --git a/src/box/key_def.cc b/src/box/key_def.cc index 5855ed0cce09a92d12ac613cec87b76bf538750f..cf74e3e512b085b153fe8a357fa054cf332af282 100644 --- a/src/box/key_def.cc +++ b/src/box/key_def.cc @@ -225,12 +225,8 @@ key_part_check_compatibility(const struct key_part *old_parts, const struct key_part *old_part = &old_parts[i]; if (old_part->fieldno != new_part->fieldno) return false; - if (! field_type1_contains_type2(new_part->type, old_part->type)) - return false; if (old_part->coll != new_part->coll) return false; - if (old_part->is_nullable != new_part->is_nullable) - return false; } return true; } diff --git a/src/box/memtx_hash.c b/src/box/memtx_hash.c index 78f55eb7d4177e112ecc41b3d8d607dbf9dbea7c..9d769cb4bd3c9cc17711f9b0ebfe11044df16ed0 100644 --- a/src/box/memtx_hash.c +++ b/src/box/memtx_hash.c @@ -138,6 +138,13 @@ memtx_hash_index_destroy(struct index *base) free(index); } +static void +memtx_hash_index_update_def(struct index *base) +{ + struct memtx_hash_index *index = (struct memtx_hash_index *)base; + index->hash_table->arg = index->base.def->key_def; +} + static ssize_t memtx_hash_index_size(struct index *base) { @@ -376,7 +383,7 @@ static const struct index_vtab memtx_hash_index_vtab = { /* .destroy = */ memtx_hash_index_destroy, /* .commit_create = */ generic_index_commit_create, /* .commit_drop = */ generic_index_commit_drop, - /* .update_def = */ generic_index_update_def, + /* .update_def = */ memtx_hash_index_update_def, /* .size = */ memtx_hash_index_size, /* .bsize = */ memtx_hash_index_bsize, /* .min = */ generic_index_min, diff --git a/src/box/space.c b/src/box/space.c index 4fbc0607ec31784a275201a4b6924fb7cac0ba24..1682022716dd6818a66a851ea78d71368e300e09 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -296,7 +296,8 @@ space_def_check_compatibility(const struct space_def *old_def, for (uint32_t i = 0; i < field_count; ++i) { enum field_type old_type = old_def->fields[i].type; enum field_type new_type = new_def->fields[i].type; - if (! field_type1_contains_type2(new_type, old_type)) { + if (!field_type1_contains_type2(new_type, old_type) && + !field_type1_contains_type2(old_type, new_type)) { const char *msg = tt_sprintf("Can not change a field type from "\ "%s to %s on a not empty space", @@ -306,15 +307,6 @@ space_def_check_compatibility(const struct space_def *old_def, msg); return -1; } - if (old_def->fields[i].is_nullable && - !new_def->fields[i].is_nullable) { - const char *msg = - tt_sprintf("Can not disable is_nullable "\ - "on a not empty space"); - diag_set(ClientError, ER_ALTER_SPACE, old_def->name, - msg); - return -1; - } } return 0; } diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c index 3e2c8bf57e9de0c2532eed336f84535a856b1b27..1d6499748db996ea9812180b3ea9e37ff66bf574 100644 --- a/src/box/tuple_format.c +++ b/src/box/tuple_format.c @@ -277,6 +277,41 @@ tuple_format_new(struct tuple_format_vtab *vtab, struct key_def * const *keys, return format; } +bool +tuple_format1_can_store_format2_tuples(const struct tuple_format *format1, + const struct tuple_format *format2) +{ + if (format1->exact_field_count != format2->exact_field_count) + return false; + for (uint32_t i = 0; i < format1->field_count; ++i) { + const struct tuple_field *field1 = &format1->fields[i]; + /* + * The field is formatted in format1, but not + * formatted in format2. + */ + if (i >= format2->field_count) { + /* + * The field can be defined with no type, + * but with a name - it is not + * restriction. Nullability is necessary + * if a field is absend in some tuples. + */ + if (field1->type == FIELD_TYPE_ANY && + field1->is_nullable) + continue; + else + return false; + } + const struct tuple_field *field2 = &format2->fields[i]; + if (! field_type1_contains_type2(field1->type, field2->type)) + return false; + /* Nullability removal - format is restricted. */ + if (field2->is_nullable && !field1->is_nullable) + return false; + } + return true; +} + bool tuple_format_eq(const struct tuple_format *a, const struct tuple_format *b) { diff --git a/src/box/tuple_format.h b/src/box/tuple_format.h index d33c77ae619b7a1c91c894601b7b2dd0e037af64..c047cdb65537afe436141e88c780ea8bfe2baf1a 100644 --- a/src/box/tuple_format.h +++ b/src/box/tuple_format.h @@ -199,6 +199,22 @@ tuple_format_new(struct tuple_format_vtab *vtab, struct key_def * const *keys, const struct field_def *space_fields, uint32_t space_field_count, struct tuple_dictionary *dict); +/** + * Check, if @a format1 can store ANY!!! tuples of @a format2. For + * example, if a field is not nullable in the format1 and the same + * field is nullable in the format2, or the field type is integer + * in the format1 and unsigned in the format2, then the format1 + * can not store the format2 tuples. + * @param format1 Tuple format, that possibly can store tuples of + * @a format2. + * @param format2 Tuple format 2. + * + * @retval True, if @a format1 can store any tuples of @a format2. + */ +bool +tuple_format1_can_store_format2_tuples(const struct tuple_format *format1, + const struct tuple_format *format2); + /** * Check that two tuple formats are identical. * @param a format a diff --git a/src/box/vinyl.c b/src/box/vinyl.c index 57cf7998e31058e8015b69b307258183eb92a37b..2dc11170d36e3247a50a04edff37ef7f9dfd35af 100644 --- a/src/box/vinyl.c +++ b/src/box/vinyl.c @@ -1065,6 +1065,12 @@ vinyl_space_prepare_alter(struct space *old_space, struct space *new_space) "adding an index to a non-empty space"); return -1; } + if (! tuple_format1_can_store_format2_tuples(new_space->format, + old_space->format)) { + diag_set(ClientError, ER_UNSUPPORTED, "Vinyl", + "non-empty space format incompatible change"); + return -1; + } return 0; } diff --git a/test/box/alter.result b/test/box/alter.result index fdd48419b12a1c662253013c58d1bba863d2637e..e69dbc6ecfc07ca712cfb52f636a838e0d30578c 100644 --- a/test/box/alter.result +++ b/test/box/alter.result @@ -1295,7 +1295,7 @@ s = box.schema.space.create('test', {format = format}) pk = s:create_index('pk') --- ... -t = s:replace{1, 2, 3, '4', 5.5, -6, true, 8, {9, 9}, {val = 10}} +t = s:replace{1, {2}, 3, '4', 5.5, -6, true, -8, {9, 9}, {val = 10}} --- ... test_run:cmd("setopt delimiter ';'") @@ -1329,8 +1329,7 @@ test_run:cmd("setopt delimiter ''"); -- any --X--> unsigned fail_format_change(2, 'unsigned') --- -- 'Can''t modify space ''test'': Can not change a field type from any to unsigned - on a not empty space' +- 'Tuple field 2 type does not match one required by operation: expected unsigned' ... -- unsigned -----> any ok_format_change(3, 'any') @@ -1385,8 +1384,7 @@ ok_format_change(5, 'scalar') -- number --X--> integer fail_format_change(5, 'integer') --- -- 'Can''t modify space ''test'': Can not change a field type from number to integer - on a not empty space' +- 'Tuple field 5 type does not match one required by operation: expected integer' ... -- integer -----> any ok_format_change(6, 'any') @@ -1403,8 +1401,7 @@ ok_format_change(6, 'scalar') -- integer --X--> unsigned fail_format_change(6, 'unsigned') --- -- 'Can''t modify space ''test'': Can not change a field type from integer to unsigned - on a not empty space' +- 'Tuple field 6 type does not match one required by operation: expected unsigned' ... -- boolean -----> any ok_format_change(7, 'any') @@ -1427,8 +1424,7 @@ ok_format_change(8, 'any') -- scalar --X--> unsigned fail_format_change(8, 'unsigned') --- -- 'Can''t modify space ''test'': Can not change a field type from scalar to unsigned - on a not empty space' +- 'Tuple field 8 type does not match one required by operation: expected unsigned' ... -- array -----> any ok_format_change(9, 'any') @@ -1574,7 +1570,7 @@ format[2] = {name = 'field2', type = 'unsigned'} ... s:format(format) --- -- error: Vinyl does not support adding new fields to a non-empty space +- error: Vinyl does not support non-empty space format incompatible change ... s:drop() --- @@ -1647,8 +1643,7 @@ format[2].is_nullable = false ... s:format(format) --- -- error: 'Can''t modify space ''test'': Can not disable is_nullable on a not empty - space' +- error: 'Tuple field 2 type does not match one required by operation: expected unsigned' ... s:delete(1) --- @@ -1658,6 +1653,38 @@ s:delete(1) s:format(format) --- ... +-- Disable is_nullable on a non-empty space. +format[2].is_nullable = true +--- +... +s:format(format) +--- +... +s:replace{1, 1} +--- +- [1, 1] +... +format[2].is_nullable = false +--- +... +s:format(format) +--- +... +-- Enable is_nullable on a non-empty space. +format[2].is_nullable = true +--- +... +s:format(format) +--- +... +s:replace{1, box.NULL} +--- +- [1, null] +... +s:delete{1} +--- +- [1, null] +... s:format({}) --- ... @@ -1789,6 +1816,142 @@ s:drop() --- ... -- +-- Allow to restrict space format, if corresponding restrictions +-- already are defined in indexes. +-- +test_run:cmd("setopt delimiter ';'") +--- +- true +... +function check_format_restriction(engine, name) + local s = box.schema.create_space(name, {engine = engine}) + local pk = s:create_index('pk') + local format = {} + format[1] = {name = 'field1'} + s:replace{1} + s:replace{100} + s:replace{0} + s:format(format) + s:format() + format[1].type = 'unsigned' + s:format(format) +end; +--- +... +test_run:cmd("setopt delimiter ''"); +--- +- true +... +check_format_restriction('memtx', 'test1') +--- +... +check_format_restriction('vinyl', 'test2') +--- +... +box.space.test1:format() +--- +- [{'name': 'field1', 'type': 'unsigned'}] +... +box.space.test1:select{} +--- +- - [0] + - [1] + - [100] +... +box.space.test2:format() +--- +- [{'name': 'field1', 'type': 'unsigned'}] +... +box.space.test2:select{} +--- +- - [0] + - [1] + - [100] +... +box.space.test1:drop() +--- +... +box.space.test2:drop() +--- +... +-- +-- Allow to change is_nullable in index definition on non-empty +-- space. +-- +s = box.schema.create_space('test') +--- +... +pk = s:create_index('pk') +--- +... +sk1 = s:create_index('sk1', {parts = {{2, 'unsigned', is_nullable = true}}}) +--- +... +sk2 = s:create_index('sk2', {parts = {{3, 'unsigned', is_nullable = false}}}) +--- +... +s:replace{1, box.NULL, 1} +--- +- [1, null, 1] +... +sk1:alter({parts = {{2, 'unsigned', is_nullable = false}}}) +--- +- error: 'Tuple field 2 type does not match one required by operation: expected unsigned' +... +s:replace{1, 1, 1} +--- +- [1, 1, 1] +... +sk1:alter({parts = {{2, 'unsigned', is_nullable = false}}}) +--- +... +s:replace{1, 1, box.NULL} +--- +- error: 'Tuple field 3 type does not match one required by operation: expected unsigned' +... +sk2:alter({parts = {{3, 'unsigned', is_nullable = true}}}) +--- +... +s:replace{1, 1, box.NULL} +--- +- [1, 1, null] +... +s:replace{2, 10, 100} +--- +- [2, 10, 100] +... +s:replace{3, 0, 20} +--- +- [3, 0, 20] +... +s:replace{4, 15, 150} +--- +- [4, 15, 150] +... +s:replace{5, 9, box.NULL} +--- +- [5, 9, null] +... +sk1:select{} +--- +- - [3, 0, 20] + - [1, 1, null] + - [5, 9, null] + - [2, 10, 100] + - [4, 15, 150] +... +sk2:select{} +--- +- - [1, 1, null] + - [5, 9, null] + - [3, 0, 20] + - [2, 10, 100] + - [4, 15, 150] +... +s:drop() +--- +... +-- -- gh-2914: Allow any space name which consists of printable characters -- identifier = require("identifier") @@ -1933,3 +2096,55 @@ t3.field_1 s:drop() --- ... +-- +-- gh-3008. Ensure the change of hash index parts updates hash +-- key_def. +-- +s = box.schema.create_space('test') +--- +... +pk = s:create_index('pk', {type = 'hash'}) +--- +... +pk:alter{parts = {{1, 'string'}}} +--- +... +s:replace{'1', '1'} +--- +- ['1', '1'] +... +s:replace{'1', '2'} +--- +- ['1', '2'] +... +pk:select{} +--- +- - ['1', '2'] +... +pk:select{'1'} +--- +- - ['1', '2'] +... +s:drop() +--- +... +-- +-- Ensure that incompatible key parts change validates format. +-- +s = box.schema.create_space('test') +--- +... +pk = s:create_index('pk') +--- +... +s:replace{1} +--- +- [1] +... +pk:alter{parts = {{1, 'string'}}} -- Must fail. +--- +- error: 'Tuple field 1 type does not match one required by operation: expected string' +... +s:drop() +--- +... diff --git a/test/box/alter.test.lua b/test/box/alter.test.lua index 08db0ec2588308651ea5d212937658cab82495de..0050ecd12a4698f0aecaf6341ab8826fa486ff79 100644 --- a/test/box/alter.test.lua +++ b/test/box/alter.test.lua @@ -474,7 +474,7 @@ format[9] = {name = 'field9', type = 'array'} format[10] = {name = 'field10', type = 'map'} s = box.schema.space.create('test', {format = format}) pk = s:create_index('pk') -t = s:replace{1, 2, 3, '4', 5.5, -6, true, 8, {9, 9}, {val = 10}} +t = s:replace{1, {2}, 3, '4', 5.5, -6, true, -8, {9, 9}, {val = 10}} test_run:cmd("setopt delimiter ';'") function fail_format_change(fieldno, new_type) @@ -635,6 +635,17 @@ s:format(format) s:delete(1) -- Disable is_nullable on empty space s:format(format) +-- Disable is_nullable on a non-empty space. +format[2].is_nullable = true +s:format(format) +s:replace{1, 1} +format[2].is_nullable = false +s:format(format) +-- Enable is_nullable on a non-empty space. +format[2].is_nullable = true +s:format(format) +s:replace{1, box.NULL} +s:delete{1} s:format({}) s:create_index('secondary', { parts = {{2, 'string', is_nullable = true}} }) @@ -685,6 +696,57 @@ s:replace{-2} s:select{} s:drop() +-- +-- Allow to restrict space format, if corresponding restrictions +-- already are defined in indexes. +-- +test_run:cmd("setopt delimiter ';'") +function check_format_restriction(engine, name) + local s = box.schema.create_space(name, {engine = engine}) + local pk = s:create_index('pk') + local format = {} + format[1] = {name = 'field1'} + s:replace{1} + s:replace{100} + s:replace{0} + s:format(format) + s:format() + format[1].type = 'unsigned' + s:format(format) +end; +test_run:cmd("setopt delimiter ''"); +check_format_restriction('memtx', 'test1') +check_format_restriction('vinyl', 'test2') +box.space.test1:format() +box.space.test1:select{} +box.space.test2:format() +box.space.test2:select{} +box.space.test1:drop() +box.space.test2:drop() + +-- +-- Allow to change is_nullable in index definition on non-empty +-- space. +-- +s = box.schema.create_space('test') +pk = s:create_index('pk') +sk1 = s:create_index('sk1', {parts = {{2, 'unsigned', is_nullable = true}}}) +sk2 = s:create_index('sk2', {parts = {{3, 'unsigned', is_nullable = false}}}) +s:replace{1, box.NULL, 1} +sk1:alter({parts = {{2, 'unsigned', is_nullable = false}}}) +s:replace{1, 1, 1} +sk1:alter({parts = {{2, 'unsigned', is_nullable = false}}}) +s:replace{1, 1, box.NULL} +sk2:alter({parts = {{3, 'unsigned', is_nullable = true}}}) +s:replace{1, 1, box.NULL} +s:replace{2, 10, 100} +s:replace{3, 0, 20} +s:replace{4, 15, 150} +s:replace{5, 9, box.NULL} +sk1:select{} +sk2:select{} +s:drop() + -- -- gh-2914: Allow any space name which consists of printable characters -- @@ -756,3 +818,25 @@ t2.field_1 t3.field1 t3.field_1 s:drop() + +-- +-- gh-3008. Ensure the change of hash index parts updates hash +-- key_def. +-- +s = box.schema.create_space('test') +pk = s:create_index('pk', {type = 'hash'}) +pk:alter{parts = {{1, 'string'}}} +s:replace{'1', '1'} +s:replace{'1', '2'} +pk:select{} +pk:select{'1'} +s:drop() + +-- +-- Ensure that incompatible key parts change validates format. +-- +s = box.schema.create_space('test') +pk = s:create_index('pk') +s:replace{1} +pk:alter{parts = {{1, 'string'}}} -- Must fail. +s:drop() diff --git a/test/vinyl/ddl.result b/test/vinyl/ddl.result index 64e1b77cb6d69d97926ede9a96ff0ca8a9bf3a92..a2c68e428cf980b83447e1953b97bfbd7e3d430a 100644 --- a/test/vinyl/ddl.result +++ b/test/vinyl/ddl.result @@ -692,6 +692,89 @@ index = space:create_index('test', { type = 'tree', parts = { 2, 'map' }}) space:drop() --- ... +-- +-- Allow compatible changes of a non-empty vinyl space. +-- +space = box.schema.create_space('test', { engine = 'vinyl' }) +--- +... +pk = space:create_index('primary') +--- +... +space:replace{1} +--- +- [1] +... +space:replace{2} +--- +- [2] +... +format = {} +--- +... +format[1] = {name = 'field1'} +--- +... +format[2] = {name = 'field2', is_nullable = true} +--- +... +format[3] = {name = 'field3', is_nullable = true} +--- +... +space:format(format) +--- +... +t1 = space:replace{3,4,5} +--- +... +t2 = space:replace{4,5} +--- +... +t1.field1, t1.field2, t1.field3 +--- +- 3 +- 4 +- 5 +... +t2.field1, t2.field2, t2.field3 +--- +- 4 +- 5 +- null +... +t1 = pk:get{1} +--- +... +t1.field1, t1.field2, t1.field3 +--- +- 1 +- null +- null +... +box.snapshot() +--- +- ok +... +t1 = pk:get{2} +--- +... +t1.field1, t1.field2, t1.field3 +--- +- 2 +- null +- null +... +-- Forbid incompatible change. +format[2].is_nullable = false +--- +... +space:format(format) +--- +- error: Vinyl does not support non-empty space format incompatible change +... +space:drop() +--- +... -- gh-3019 default index options box.space._space:insert{512, 1, 'test', 'vinyl', 0, setmetatable({}, {__serialize = 'map'}), {}} --- diff --git a/test/vinyl/ddl.test.lua b/test/vinyl/ddl.test.lua index e0f453bd917e6de9f15cc2541e6b2a384f620cb5..f050799ea00cc54fd98fecf043ef042017775996 100644 --- a/test/vinyl/ddl.test.lua +++ b/test/vinyl/ddl.test.lua @@ -261,6 +261,32 @@ index = space:create_index('test', { type = 'tree', parts = { 2, 'array' }}) index = space:create_index('test', { type = 'tree', parts = { 2, 'map' }}) space:drop() +-- +-- Allow compatible changes of a non-empty vinyl space. +-- +space = box.schema.create_space('test', { engine = 'vinyl' }) +pk = space:create_index('primary') +space:replace{1} +space:replace{2} +format = {} +format[1] = {name = 'field1'} +format[2] = {name = 'field2', is_nullable = true} +format[3] = {name = 'field3', is_nullable = true} +space:format(format) +t1 = space:replace{3,4,5} +t2 = space:replace{4,5} +t1.field1, t1.field2, t1.field3 +t2.field1, t2.field2, t2.field3 +t1 = pk:get{1} +t1.field1, t1.field2, t1.field3 +box.snapshot() +t1 = pk:get{2} +t1.field1, t1.field2, t1.field3 +-- Forbid incompatible change. +format[2].is_nullable = false +space:format(format) +space:drop() + -- gh-3019 default index options box.space._space:insert{512, 1, 'test', 'vinyl', 0, setmetatable({}, {__serialize = 'map'}), {}} box.space._index:insert{512, 0, 'pk', 'tree', {unique = true}, {{0, 'unsigned'}}}