[patches] [PATCH 1/2] alter: allow to restrict space format of a non-empty space

Vladislav Shpilevoy v.shpilevoy at tarantool.org
Wed Feb 14 21:27:51 MSK 2018


Now if a space is not empty, its format can not be restricted by
nullability removal, or by restriction of a field type.

Allow format restriction followed by space format checking.

Do not rebuild index, if it is not empty. A rebuild is not needed
for such change, because types can not be altered on a not empty space
in such way, that index order is changed.

But do rebuild, if an index is not unique, and a primary index parts
have been changed. It changes cmp_def of the secondary index, and
possibly sort order.

Needed for #3008

Signed-off-by: Vladislav Shpilevoy <v.shpilevoy at tarantool.org>
---
 src/box/alter.cc        | 121 +++++++++++++++++-------------------
 src/box/key_def.cc      |   2 -
 src/box/memtx_hash.c    |   9 ++-
 src/box/space.c         |  12 +---
 src/box/tuple_format.c  |  35 +++++++++++
 src/box/tuple_format.h  |  16 +++++
 src/box/vinyl.c         |   7 +++
 test/box/alter.result   | 162 ++++++++++++++++++++++++++++++++++++++++++++----
 test/box/alter.test.lua |  63 ++++++++++++++++++-
 test/vinyl/ddl.result   |  95 ++++++++++++++++++++++++++--
 test/vinyl/ddl.test.lua |  26 ++++++++
 test/vinyl/gh.result    |   2 +-
 12 files changed, 454 insertions(+), 96 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 46c90c883..ba043210a 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -871,6 +871,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
@@ -879,14 +885,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
@@ -898,18 +902,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. */
@@ -921,7 +950,6 @@ public:
 	/* New space definition. */
 	struct space_def *def;
 	virtual void alter_def(struct alter_space *alter);
-	virtual void alter(struct alter_space *alter);
 	virtual ~ModifySpace();
 };
 
@@ -935,51 +963,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);
@@ -1078,9 +1061,20 @@ public:
 	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);
@@ -1676,8 +1670,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.
 			 */
@@ -1685,6 +1679,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 5c91fb9c7..54784b624 100644
--- a/src/box/key_def.cc
+++ b/src/box/key_def.cc
@@ -225,8 +225,6 @@ 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)
diff --git a/src/box/memtx_hash.c b/src/box/memtx_hash.c
index 78f55eb7d..9d769cb4b 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 4fbc0607e..168202271 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 3e2c8bf57..1d6499748 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 d33c77ae6..c047cdb65 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 94d0d038a..712f278b4 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1036,6 +1036,13 @@ vinyl_space_prepare_alter(struct space *old_space, struct space *new_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;
+	}
+
 	if (old_space->index_count == new_space->index_count) {
 		/* Check index_defs to be unchanged. */
 		for (uint32_t i = 0; i < old_space->index_count; ++i) {
diff --git a/test/box/alter.result b/test/box/alter.result
index fdd48419b..0f49c44bf 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,65 @@ 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()
+---
+...
+--
 -- gh-2914: Allow any space name which consists of printable characters
 --
 identifier = require("identifier")
@@ -1933,3 +2019,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 08db0ec25..775407fe5 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,34 @@ 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()
+
 --
 -- gh-2914: Allow any space name which consists of printable characters
 --
@@ -756,3 +795,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 45a383442..a17455856 100644
--- a/test/vinyl/ddl.result
+++ b/test/vinyl/ddl.result
@@ -47,7 +47,7 @@ index2 = space:create_index('secondary', { parts = {2, 'unsigned'} })
 ...
 space.index.primary:alter({parts = {1, 'unsigned', 2, 'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 #box.space._index:select({space.id})
 ---
@@ -76,7 +76,7 @@ index2 = space:create_index('secondary', { parts = {2, 'unsigned'} })
 ...
 space.index.primary:alter({parts = {1, 'unsigned', 2, 'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 #box.space._index:select({space.id})
 ---
@@ -105,7 +105,7 @@ index2 = space:create_index('secondary', { parts = {2, 'unsigned'} })
 ...
 space.index.primary:alter({parts = {1, 'unsigned', 2, 'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 #box.space._index:select({space.id})
 ---
@@ -125,7 +125,7 @@ index2 = space:create_index('secondary', { parts = {2, 'unsigned'} })
 ...
 space.index.primary:alter({parts = {1, 'unsigned', 2, 'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 box.snapshot()
 ---
@@ -196,7 +196,7 @@ index2 = space:create_index('secondary', { parts = {2, 'unsigned'} })
 ...
 space.index.primary:alter({parts = {1, 'unsigned', 2, 'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 -- After compaction the REPLACE + DELETE + DELETE = nothing, so
 -- the space is now empty and can be altered.
@@ -251,7 +251,7 @@ space:auto_increment{3}
 ...
 box.space._index:replace{space.id, 0, 'pk', 'tree', {unique=true}, {{0, 'unsigned'}, {1, 'unsigned'}}}
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 space:select{}
 ---
@@ -656,6 +656,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 53205df99..aa408dc5c 100644
--- a/test/vinyl/ddl.test.lua
+++ b/test/vinyl/ddl.test.lua
@@ -249,6 +249,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'}}}
diff --git a/test/vinyl/gh.result b/test/vinyl/gh.result
index 9c72acc81..fdfd9a0f7 100644
--- a/test/vinyl/gh.result
+++ b/test/vinyl/gh.result
@@ -144,7 +144,7 @@ s:insert{5, 5}
 ...
 s.index.primary:alter({parts={2,'unsigned'}})
 ---
-- error: Vinyl does not support changing the definition of a non-empty index
+- error: Vinyl does not support non-empty space format incompatible change
 ...
 s:drop()
 ---
-- 
2.14.3 (Apple Git-98)




More information about the Tarantool-patches mailing list