From c0ef6821001b90f278d5c84a13a5e32f8e8452be Mon Sep 17 00:00:00 2001
From: Vladislav Shpilevoy <v.shpilevoy@tarantool.org>
Date: Tue, 13 Feb 2018 21:51:40 +0300
Subject: [PATCH] alter: allow to restrict space format of a non-empty space

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

The workaround was format removal and reset. Lets allow normal format
restriction followed by space format checking.

At second, now an index is rebuild on any change of its key definition.
But there is no sense to rebuild it, if uniqueness is turned off, part
field numbers and collations are not changed, because it means, that
only types was restricted or extended. So format checking is enough.

Needed for #3008
---
 src/box/alter.cc        | 121 ++++++++++----------
 src/box/key_def.cc      |   4 -
 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         |   6 +
 test/box/alter.result   | 239 ++++++++++++++++++++++++++++++++++++++--
 test/box/alter.test.lua |  86 ++++++++++++++-
 test/vinyl/ddl.result   |  83 ++++++++++++++
 test/vinyl/ddl.test.lua |  26 +++++
 11 files changed, 546 insertions(+), 91 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index af9fbb52b8..d7191c1bba 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 5855ed0cce..cf74e3e512 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 78f55eb7d4..9d769cb4bd 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 4fbc0607ec..1682022716 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 3e2c8bf57e..1d6499748d 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 d33c77ae61..c047cdb655 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 57cf7998e3..2dc11170d3 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 fdd48419b1..e69dbc6ecf 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 08db0ec258..0050ecd12a 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 64e1b77cb6..a2c68e428c 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 e0f453bd91..f050799ea0 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'}}}
-- 
GitLab