[PATCH 12/12] vinyl: allow to modify format of non-empty spaces

Vladimir Davydov vdavydov.dev at gmail.com
Sat Apr 7 16:38:09 MSK 2018


This patch implements space_vtab::check_space_format callback for vinyl
spaces. The callback iterates over all tuples stored in the primary
index and validates them against the new format. Since vinyl read
iterator may yield, this callback also installs an on_replace trigger
for the altered space. The trigger checks that tuples inserted during
ALTER conform to the new format and aborts ALTER if they do not.

To test the feature, this patch enables memtx space format tests for
vinyl spaces by moving them from box/alter to engine/ddl. It adds just
one new vinyl-specific test case to vinyl/ddl. The test case checks
that tuples inserted during ALTER are validated against the new format.
---
 src/box/vinyl.c            |  95 +++++-
 test/box/alter.result      | 810 +--------------------------------------------
 test/box/alter.test.lua    | 319 ------------------
 test/engine/ddl.result     | 787 ++++++++++++++++++++++++++++++++++++++++++-
 test/engine/ddl.test.lua   | 311 ++++++++++++++++-
 test/vinyl/ddl.result      |  83 -----
 test/vinyl/ddl.test.lua    |  26 --
 test/vinyl/errinj.result   |  75 +++++
 test/vinyl/errinj.test.lua |  35 ++
 9 files changed, 1288 insertions(+), 1253 deletions(-)

diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index c99b518e..c2769e6d 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1028,21 +1028,98 @@ vinyl_space_prepare_alter(struct space *old_space, struct space *new_space)
 	return 0;
 }
 
+/** Argument passed to vy_check_format_on_replace(). */
+struct vy_check_format_ctx {
+	/** Format to check new tuples against. */
+	struct tuple_format *format;
+	/** Set if a new tuple doesn't conform to the format. */
+	bool is_failed;
+	/** Container for storing errors. */
+	struct diag diag;
+};
+
+/**
+ * This is an on_replace trigger callback that checks inserted
+ * tuples against a new format.
+ */
+static void
+vy_check_format_on_replace(struct trigger *trigger, void *event)
+{
+	struct txn *txn = event;
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct vy_check_format_ctx *ctx = trigger->data;
+
+	if (stmt->new_tuple == NULL)
+		return; /* DELETE, nothing to do */
+
+	if (ctx->is_failed)
+		return; /* already failed, nothing to do */
+
+	if (tuple_validate(ctx->format, stmt->new_tuple) != 0) {
+		ctx->is_failed = true;
+		diag_move(diag_get(), &ctx->diag);
+	}
+}
+
 static int
 vinyl_space_check_format(struct space *space, struct tuple_format *format)
 {
-	(void)format;
 	struct vy_env *env = vy_env(space->engine);
-	if (space->index_count == 0)
+
+	/*
+	 * If this is local recovery, the space was checked before
+	 * restart so there's nothing we need to do.
+	 */
+	if (env->status == VINYL_INITIAL_RECOVERY_LOCAL ||
+	    env->status == VINYL_FINAL_RECOVERY_LOCAL)
 		return 0;
+
+	if (space->index_count == 0)
+		return 0; /* space is empty, nothing to do */
+
+	/*
+	 * Iterate over all tuples stored in the given space and
+	 * check each of them for conformity to the new format.
+	 * Since read iterator may yield, we install an on_replace
+	 * trigger to check tuples inserted after we started the
+	 * iteration.
+	 */
 	struct vy_lsm *pk = vy_lsm(space->index[0]);
-	if (env->status != VINYL_ONLINE)
-		return 0;
-	if (pk->stat.disk.count.rows == 0 && pk->stat.memory.count.rows == 0)
-		return 0;
-	diag_set(ClientError, ER_UNSUPPORTED, "Vinyl",
-		 "changing format of a non-empty space");
-	return -1;
+
+	struct tuple *key = vy_stmt_new_select(pk->env->key_format, NULL, 0);
+	if (key == NULL)
+		return -1;
+
+	struct trigger on_replace;
+	struct vy_check_format_ctx ctx;
+	ctx.format = format;
+	ctx.is_failed = false;
+	diag_create(&ctx.diag);
+	trigger_create(&on_replace, vy_check_format_on_replace, &ctx, NULL);
+	trigger_add(&space->on_replace, &on_replace);
+
+	struct vy_read_iterator itr;
+	vy_read_iterator_open(&itr, pk, NULL, ITER_ALL, key,
+			      &env->xm->p_global_read_view);
+	int rc;
+	struct tuple *tuple;
+	while ((rc = vy_read_iterator_next(&itr, &tuple)) == 0) {
+		if (tuple == NULL)
+			break;
+		if (ctx.is_failed) {
+			diag_move(&ctx.diag, diag_get());
+			rc = -1;
+			break;
+		}
+		rc = tuple_validate(format, tuple);
+		if (rc != 0)
+			break;
+	}
+	vy_read_iterator_close(&itr);
+	diag_destroy(&ctx.diag);
+	trigger_clear(&on_replace);
+	tuple_unref(key);
+	return rc;
 }
 
 static void
diff --git a/test/box/alter.result b/test/box/alter.result
index 945b0cfd..a78dd3e9 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -817,753 +817,6 @@ ts:drop()
 ---
 ...
 --
--- gh-2652: validate space format.
---
-s = box.schema.space.create('test', { format = "format" })
----
-- error: Illegal parameters, options parameter 'format' should be of type table
-...
-format = { { name = 100 } }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-- error: 'Illegal parameters, format[1]: name (string) is expected'
-...
-long = string.rep('a', box.schema.NAME_MAX + 1)
----
-...
-format = { { name = long } }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-- error: 'Failed to create space ''test'': field 1 name is too long'
-...
-format = { { name = 'id', type = '100' } }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-- error: 'Failed to create space ''test'': field 1 has unknown field type'
-...
-format = { utils.setmap({}) }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-- error: 'Illegal parameters, format[1]: name (string) is expected'
-...
--- Ensure the format is updated after index drop.
-format = { { name = 'id', type = 'unsigned' } }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-...
-pk = s:create_index('pk')
----
-...
-sk = s:create_index('sk', { parts = { 2, 'string' } })
----
-...
-s:replace{1, 1}
----
-- error: 'Tuple field 2 type does not match one required by operation: expected string'
-...
-sk:drop()
----
-...
-s:replace{1, 1}
----
-- [1, 1]
-...
-s:drop()
----
-...
--- Check index parts conflicting with space format.
-format = { { name='field1', type='unsigned' }, { name='field2', type='string' }, { name='field3', type='scalar' } }
----
-...
-s = box.schema.space.create('test', { format = format })
----
-...
-pk = s:create_index('pk')
----
-...
-sk1 = s:create_index('sk1', { parts = { 2, 'unsigned' } })
----
-- error: Field 'field2' has type 'string' in space format, but type 'unsigned' in
-    index definition
-...
--- Check space format conflicting with index parts.
-sk3 = s:create_index('sk3', { parts = { 2, 'string' } })
----
-...
-format[2].type = 'unsigned'
----
-...
-s:format(format)
----
-- error: Field 'field2' has type 'unsigned' in space format, but type 'string' in
-    index definition
-...
-s:format()
----
-- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'string'}, {
-    'name': 'field3', 'type': 'scalar'}]
-...
-s.index.sk3.parts
----
-- - type: string
-    is_nullable: false
-    fieldno: 2
-...
--- Space format can be updated, if conflicted index is deleted.
-sk3:drop()
----
-...
-s:format(format)
----
-...
-s:format()
----
-- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'unsigned'},
-  {'name': 'field3', 'type': 'scalar'}]
-...
--- Check deprecated field types.
-format[2].type = 'num'
----
-...
-format[3].type = 'str'
----
-...
-format[4] = { name = 'field4', type = '*' }
----
-...
-format
----
-- - name: field1
-    type: unsigned
-  - name: field2
-    type: num
-  - name: field3
-    type: str
-  - name: field4
-    type: '*'
-...
-s:format(format)
----
-...
-s:format()
----
-- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'num'}, {'name': 'field3',
-    'type': 'str'}, {'name': 'field4', 'type': '*'}]
-...
-s:replace{1, 2, '3', {4, 4, 4}}
----
-- [1, 2, '3', [4, 4, 4]]
-...
--- Check not indexed fields checking.
-s:truncate()
----
-...
-format[2] = {name='field2', type='string'}
----
-...
-format[3] = {name='field3', type='array'}
----
-...
-format[4] = {name='field4', type='number'}
----
-...
-format[5] = {name='field5', type='integer'}
----
-...
-format[6] = {name='field6', type='scalar'}
----
-...
-format[7] = {name='field7', type='map'}
----
-...
-format[8] = {name='field8', type='any'}
----
-...
-format[9] = {name='field9'}
----
-...
-s:format(format)
----
-...
--- Check incorrect field types.
-format[9] = {name='err', type='any'}
----
-...
-s:format(format)
----
-...
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
----
-- [1, '2', [3, 3], 4.4, -5, true, {'value': 7}, 8, 9]
-...
-s:replace{1, 2, {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
----
-- error: 'Tuple field 2 type does not match one required by operation: expected string'
-...
-s:replace{1, '2', 3, 4.4, -5, true, {value=7}, 8, 9}
----
-- error: 'Tuple field 3 type does not match one required by operation: expected array'
-...
-s:replace{1, '2', {3, 3}, '4', -5, true, {value=7}, 8, 9}
----
-- error: 'Tuple field 4 type does not match one required by operation: expected number'
-...
-s:replace{1, '2', {3, 3}, 4.4, -5.5, true, {value=7}, 8, 9}
----
-- error: 'Tuple field 5 type does not match one required by operation: expected integer'
-...
-s:replace{1, '2', {3, 3}, 4.4, -5, {6, 6}, {value=7}, 8, 9}
----
-- error: 'Tuple field 6 type does not match one required by operation: expected scalar'
-...
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {7}, 8, 9}
----
-- error: 'Tuple field 7 type does not match one required by operation: expected map'
-...
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}}
----
-- error: Tuple field count 7 is less than required by space format or defined indexes
-    (expected at least 9)
-...
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8}
----
-- error: Tuple field count 8 is less than required by space format or defined indexes
-    (expected at least 9)
-...
-s:truncate()
----
-...
---
--- gh-1014: field names.
---
-format = {}
----
-...
-format[1] = {name = 'field1', type = 'unsigned'}
----
-...
-format[2] = {name = 'field2'}
----
-...
-format[3] = {name = 'field1'}
----
-...
-s:format(format)
----
-- error: Space field 'field1' is duplicate
-...
-s:drop()
----
-...
--- https://github.com/tarantool/tarantool/issues/2815
--- Extend space format definition syntax
-format = {{name='key',type='unsigned'}, {name='value',type='string'}}
----
-...
-s = box.schema.space.create('test', { format = format })
----
-...
-s:format()
----
-- [{'name': 'key', 'type': 'unsigned'}, {'name': 'value', 'type': 'string'}]
-...
-s:format({'id', 'name'})
----
-...
-s:format()
----
-- [{'name': 'id', 'type': 'any'}, {'name': 'name', 'type': 'any'}]
-...
-s:format({'id', {'name1'}})
----
-...
-s:format()
----
-- [{'name': 'id', 'type': 'any'}, {'name': 'name1', 'type': 'any'}]
-...
-s:format({'id', {'name2', 'string'}})
----
-...
-s:format()
----
-- [{'name': 'id', 'type': 'any'}, {'name': 'name2', 'type': 'string'}]
-...
-s:format({'id', {'name', type = 'string'}})
----
-...
-s:format()
----
-- [{'name': 'id', 'type': 'any'}, {'name': 'name', 'type': 'string'}]
-...
-s:drop()
----
-...
-format = {'key', {'value',type='string'}}
----
-...
-s = box.schema.space.create('test', { format = format })
----
-...
-s:format()
----
-- [{'name': 'key', 'type': 'any'}, {'name': 'value', 'type': 'string'}]
-...
-s:drop()
----
-...
-s = box.schema.space.create('test')
----
-...
-s:create_index('test', {parts = {'test'}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test', {parts = {{'test'}}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test', {parts = {{field = 'test'}}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test', {parts = {1}}).parts
----
-- - type: scalar
-    is_nullable: false
-    fieldno: 1
-...
-s:drop()
----
-...
-s = box.schema.space.create('test')
----
-...
-s:format{{'test1', 'integer'}, 'test2', {'test3', 'integer'}, {'test4','scalar'}}
----
-...
-s:create_index('test', {parts = {'test'}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test', {parts = {{'test'}}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test', {parts = {{field = 'test'}}})
----
-- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
-...
-s:create_index('test1', {parts = {'test1'}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 1
-...
-s:create_index('test2', {parts = {'test2'}}).parts
----
-- error: 'Can''t create or modify index ''test2'' in space ''test'': field type ''any''
-    is not supported'
-...
-s:create_index('test3', {parts = {{'test1', 'integer'}}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 1
-...
-s:create_index('test4', {parts = {{'test2', 'integer'}}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 2
-...
-s:create_index('test5', {parts = {{'test2', 'integer'}}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 2
-...
-s:create_index('test6', {parts = {1, 3}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 1
-  - type: integer
-    is_nullable: false
-    fieldno: 3
-...
-s:create_index('test7', {parts = {'test1', 4}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 1
-  - type: scalar
-    is_nullable: false
-    fieldno: 4
-...
-s:create_index('test8', {parts = {{1, 'integer'}, {'test4', 'scalar'}}}).parts
----
-- - type: integer
-    is_nullable: false
-    fieldno: 1
-  - type: scalar
-    is_nullable: false
-    fieldno: 4
-...
-s:drop()
----
-...
---
--- gh-2800: space formats checking is broken.
---
--- Ensure that vinyl correctly process field count change.
-s = box.schema.space.create('test', {engine = 'vinyl', field_count = 2})
----
-...
-pk = s:create_index('pk')
----
-...
-s:replace{1, 2}
----
-- [1, 2]
-...
-t = box.space._space:select{s.id}[1]:totable()
----
-...
-t[5] = 1
----
-...
-box.space._space:replace(t)
----
-- error: Vinyl does not support changing format of a non-empty space
-...
-s:drop()
----
-...
--- Check field type changes.
-format = {}
----
-...
-format[1] = {name = 'field1', type = 'unsigned'}
----
-...
-format[2] = {name = 'field2', type = 'any'}
----
-...
-format[3] = {name = 'field3', type = 'unsigned'}
----
-...
-format[4] = {name = 'field4', type = 'string'}
----
-...
-format[5] = {name = 'field5', type = 'number'}
----
-...
-format[6] = {name = 'field6', type = 'integer'}
----
-...
-format[7] = {name = 'field7', type = 'boolean'}
----
-...
-format[8] = {name = 'field8', type = 'scalar'}
----
-...
-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}}
----
-...
-test_run:cmd("setopt delimiter ';'")
----
-- true
-...
-function fail_format_change(fieldno, new_type)
-    local old_type = format[fieldno].type
-    format[fieldno].type = new_type
-    local ok, msg = pcall(s.format, s, format)
-    format[fieldno].type = old_type
-    return msg
-end;
----
-...
-function ok_format_change(fieldno, new_type)
-    local old_type = format[fieldno].type
-    format[fieldno].type = new_type
-    s:format(format)
-    s:delete{1}
-    format[fieldno].type = old_type
-    s:format(format)
-    s:replace(t)
-end;
----
-...
-test_run:cmd("setopt delimiter ''");
----
-- true
-...
--- any --X--> unsigned
-fail_format_change(2, 'unsigned')
----
-- 'Tuple field 2 type does not match one required by operation: expected unsigned'
-...
--- unsigned -----> any
-ok_format_change(3, 'any')
----
-...
--- unsigned --X--> string
-fail_format_change(3, 'string')
----
-- 'Tuple field 3 type does not match one required by operation: expected string'
-...
--- unsigned -----> number
-ok_format_change(3, 'number')
----
-...
--- unsigned -----> integer
-ok_format_change(3, 'integer')
----
-...
--- unsigned -----> scalar
-ok_format_change(3, 'scalar')
----
-...
--- unsigned --X--> map
-fail_format_change(3, 'map')
----
-- 'Tuple field 3 type does not match one required by operation: expected map'
-...
--- string -----> any
-ok_format_change(4, 'any')
----
-...
--- string -----> scalar
-ok_format_change(4, 'scalar')
----
-...
--- string --X--> boolean
-fail_format_change(4, 'boolean')
----
-- 'Tuple field 4 type does not match one required by operation: expected boolean'
-...
--- number -----> any
-ok_format_change(5, 'any')
----
-...
--- number -----> scalar
-ok_format_change(5, 'scalar')
----
-...
--- number --X--> integer
-fail_format_change(5, 'integer')
----
-- 'Tuple field 5 type does not match one required by operation: expected integer'
-...
--- integer -----> any
-ok_format_change(6, 'any')
----
-...
--- integer -----> number
-ok_format_change(6, 'number')
----
-...
--- integer -----> scalar
-ok_format_change(6, 'scalar')
----
-...
--- integer --X--> unsigned
-fail_format_change(6, 'unsigned')
----
-- 'Tuple field 6 type does not match one required by operation: expected unsigned'
-...
--- boolean -----> any
-ok_format_change(7, 'any')
----
-...
--- boolean -----> scalar
-ok_format_change(7, 'scalar')
----
-...
--- boolean --X--> string
-fail_format_change(7, 'string')
----
-- 'Tuple field 7 type does not match one required by operation: expected string'
-...
--- scalar -----> any
-ok_format_change(8, 'any')
----
-...
--- scalar --X--> unsigned
-fail_format_change(8, 'unsigned')
----
-- 'Tuple field 8 type does not match one required by operation: expected unsigned'
-...
--- array -----> any
-ok_format_change(9, 'any')
----
-...
--- array --X--> scalar
-fail_format_change(9, 'scalar')
----
-- 'Tuple field 9 type does not match one required by operation: expected scalar'
-...
--- map -----> any
-ok_format_change(10, 'any')
----
-...
--- map --X--> scalar
-fail_format_change(10, 'scalar')
----
-- 'Tuple field 10 type does not match one required by operation: expected scalar'
-...
-s:drop()
----
-...
--- Check new fields adding.
-format = {}
----
-...
-s = box.schema.space.create('test')
----
-...
-format[1] = {name = 'field1', type = 'unsigned'}
----
-...
-s:format(format) -- Ok, no indexes.
----
-...
-pk = s:create_index('pk')
----
-...
-format[2] = {name = 'field2', type = 'unsigned'}
----
-...
-s:format(format) -- Ok, empty space.
----
-...
-s:replace{1, 1}
----
-- [1, 1]
-...
-format[2] = nil
----
-...
-s:format(format) -- Ok, can delete fields with no checks.
----
-...
-s:delete{1}
----
-- [1, 1]
-...
-sk1 = s:create_index('sk1', {parts = {2, 'unsigned'}})
----
-...
-sk2 = s:create_index('sk2', {parts = {3, 'unsigned'}})
----
-...
-sk5 = s:create_index('sk5', {parts = {5, 'unsigned'}})
----
-...
-s:replace{1, 1, 1, 1, 1}
----
-- [1, 1, 1, 1, 1]
-...
-format[2] = {name = 'field2', type = 'unsigned'}
----
-...
-format[3] = {name = 'field3', type = 'unsigned'}
----
-...
-format[4] = {name = 'field4', type = 'any'}
----
-...
-format[5] = {name = 'field5', type = 'unsigned'}
----
-...
--- Ok, all new fields are indexed or have type ANY, and new
--- field_count <= old field_count.
-s:format(format)
----
-...
-s:replace{1, 1, 1, 1, 1, 1}
----
-- [1, 1, 1, 1, 1, 1]
-...
-format[6] = {name = 'field6', type = 'unsigned'}
----
-...
--- Ok, but check existing tuples for a new field[6].
-s:format(format)
----
-...
--- Fail, not enough fields.
-s:replace{2, 2, 2, 2, 2}
----
-- error: Tuple field count 5 is less than required by space format or defined indexes
-    (expected at least 6)
-...
-s:replace{2, 2, 2, 2, 2, 2, 2}
----
-- [2, 2, 2, 2, 2, 2, 2]
-...
-format[7] = {name = 'field7', type = 'unsigned'}
----
-...
--- Fail, the tuple {1, ... 1} is invalid for a new format.
-s:format(format)
----
-- error: Tuple field count 6 is less than required by space format or defined indexes
-    (expected at least 7)
-...
-s:drop()
----
-...
--- Vinyl does not support adding fields to a not empty space.
-s = box.schema.space.create('test', {engine = 'vinyl'})
----
-...
-pk = s:create_index('pk')
----
-...
-s:replace{1,1}
----
-- [1, 1]
-...
-format = {}
----
-...
-format[1] = {name = 'field1', type = 'unsigned'}
----
-...
-format[2] = {name = 'field2', type = 'unsigned'}
----
-...
-s:format(format)
----
-- error: Vinyl does not support changing format of a non-empty space
-...
-s:drop()
----
-...
---
 -- gh-1557: NULL in indexes.
 --
 NULL = require('msgpack').NULL
@@ -1607,7 +860,7 @@ s:create_index('primary', { parts = {'field1'} })
     is_nullable: false
     fieldno: 1
   id: 0
-  space_id: 747
+  space_id: 733
   name: primary
   type: TREE
 ...
@@ -1684,7 +937,7 @@ s:create_index('secondary', { parts = {{2, 'string', is_nullable = true}} })
     is_nullable: true
     fieldno: 2
   id: 1
-  space_id: 747
+  space_id: 733
   name: secondary
   type: TREE
 ...
@@ -1804,65 +1057,6 @@ 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.
 --
diff --git a/test/box/alter.test.lua b/test/box/alter.test.lua
index f6b2eb49..ab713584 100644
--- a/test/box/alter.test.lua
+++ b/test/box/alter.test.lua
@@ -316,297 +316,6 @@ n
 ts:drop()
 
 --
--- gh-2652: validate space format.
---
-s = box.schema.space.create('test', { format = "format" })
-format = { { name = 100 } }
-s = box.schema.space.create('test', { format = format })
-long = string.rep('a', box.schema.NAME_MAX + 1)
-format = { { name = long } }
-s = box.schema.space.create('test', { format = format })
-format = { { name = 'id', type = '100' } }
-s = box.schema.space.create('test', { format = format })
-format = { utils.setmap({}) }
-s = box.schema.space.create('test', { format = format })
-
--- Ensure the format is updated after index drop.
-format = { { name = 'id', type = 'unsigned' } }
-s = box.schema.space.create('test', { format = format })
-pk = s:create_index('pk')
-sk = s:create_index('sk', { parts = { 2, 'string' } })
-s:replace{1, 1}
-sk:drop()
-s:replace{1, 1}
-s:drop()
-
--- Check index parts conflicting with space format.
-format = { { name='field1', type='unsigned' }, { name='field2', type='string' }, { name='field3', type='scalar' } }
-s = box.schema.space.create('test', { format = format })
-pk = s:create_index('pk')
-sk1 = s:create_index('sk1', { parts = { 2, 'unsigned' } })
-
--- Check space format conflicting with index parts.
-sk3 = s:create_index('sk3', { parts = { 2, 'string' } })
-format[2].type = 'unsigned'
-s:format(format)
-s:format()
-s.index.sk3.parts
-
--- Space format can be updated, if conflicted index is deleted.
-sk3:drop()
-s:format(format)
-s:format()
-
--- Check deprecated field types.
-format[2].type = 'num'
-format[3].type = 'str'
-format[4] = { name = 'field4', type = '*' }
-format
-s:format(format)
-s:format()
-s:replace{1, 2, '3', {4, 4, 4}}
-
--- Check not indexed fields checking.
-s:truncate()
-format[2] = {name='field2', type='string'}
-format[3] = {name='field3', type='array'}
-format[4] = {name='field4', type='number'}
-format[5] = {name='field5', type='integer'}
-format[6] = {name='field6', type='scalar'}
-format[7] = {name='field7', type='map'}
-format[8] = {name='field8', type='any'}
-format[9] = {name='field9'}
-s:format(format)
-
--- Check incorrect field types.
-format[9] = {name='err', type='any'}
-s:format(format)
-
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
-s:replace{1, 2, {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
-s:replace{1, '2', 3, 4.4, -5, true, {value=7}, 8, 9}
-s:replace{1, '2', {3, 3}, '4', -5, true, {value=7}, 8, 9}
-s:replace{1, '2', {3, 3}, 4.4, -5.5, true, {value=7}, 8, 9}
-s:replace{1, '2', {3, 3}, 4.4, -5, {6, 6}, {value=7}, 8, 9}
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {7}, 8, 9}
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}}
-s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8}
-s:truncate()
-
---
--- gh-1014: field names.
---
-format = {}
-format[1] = {name = 'field1', type = 'unsigned'}
-format[2] = {name = 'field2'}
-format[3] = {name = 'field1'}
-s:format(format)
-
-s:drop()
-
--- https://github.com/tarantool/tarantool/issues/2815
--- Extend space format definition syntax
-format = {{name='key',type='unsigned'}, {name='value',type='string'}}
-s = box.schema.space.create('test', { format = format })
-s:format()
-s:format({'id', 'name'})
-s:format()
-s:format({'id', {'name1'}})
-s:format()
-s:format({'id', {'name2', 'string'}})
-s:format()
-s:format({'id', {'name', type = 'string'}})
-s:format()
-s:drop()
-
-format = {'key', {'value',type='string'}}
-s = box.schema.space.create('test', { format = format })
-s:format()
-s:drop()
-
-s = box.schema.space.create('test')
-s:create_index('test', {parts = {'test'}})
-s:create_index('test', {parts = {{'test'}}})
-s:create_index('test', {parts = {{field = 'test'}}})
-s:create_index('test', {parts = {1}}).parts
-s:drop()
-
-s = box.schema.space.create('test')
-s:format{{'test1', 'integer'}, 'test2', {'test3', 'integer'}, {'test4','scalar'}}
-s:create_index('test', {parts = {'test'}})
-s:create_index('test', {parts = {{'test'}}})
-s:create_index('test', {parts = {{field = 'test'}}})
-s:create_index('test1', {parts = {'test1'}}).parts
-s:create_index('test2', {parts = {'test2'}}).parts
-s:create_index('test3', {parts = {{'test1', 'integer'}}}).parts
-s:create_index('test4', {parts = {{'test2', 'integer'}}}).parts
-s:create_index('test5', {parts = {{'test2', 'integer'}}}).parts
-s:create_index('test6', {parts = {1, 3}}).parts
-s:create_index('test7', {parts = {'test1', 4}}).parts
-s:create_index('test8', {parts = {{1, 'integer'}, {'test4', 'scalar'}}}).parts
-s:drop()
-
---
--- gh-2800: space formats checking is broken.
---
-
--- Ensure that vinyl correctly process field count change.
-s = box.schema.space.create('test', {engine = 'vinyl', field_count = 2})
-pk = s:create_index('pk')
-s:replace{1, 2}
-t = box.space._space:select{s.id}[1]:totable()
-t[5] = 1
-box.space._space:replace(t)
-s:drop()
-
--- Check field type changes.
-format = {}
-format[1] = {name = 'field1', type = 'unsigned'}
-format[2] = {name = 'field2', type = 'any'}
-format[3] = {name = 'field3', type = 'unsigned'}
-format[4] = {name = 'field4', type = 'string'}
-format[5] = {name = 'field5', type = 'number'}
-format[6] = {name = 'field6', type = 'integer'}
-format[7] = {name = 'field7', type = 'boolean'}
-format[8] = {name = 'field8', type = 'scalar'}
-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}}
-
-test_run:cmd("setopt delimiter ';'")
-function fail_format_change(fieldno, new_type)
-    local old_type = format[fieldno].type
-    format[fieldno].type = new_type
-    local ok, msg = pcall(s.format, s, format)
-    format[fieldno].type = old_type
-    return msg
-end;
-
-function ok_format_change(fieldno, new_type)
-    local old_type = format[fieldno].type
-    format[fieldno].type = new_type
-    s:format(format)
-    s:delete{1}
-    format[fieldno].type = old_type
-    s:format(format)
-    s:replace(t)
-end;
-test_run:cmd("setopt delimiter ''");
-
--- any --X--> unsigned
-fail_format_change(2, 'unsigned')
-
--- unsigned -----> any
-ok_format_change(3, 'any')
--- unsigned --X--> string
-fail_format_change(3, 'string')
--- unsigned -----> number
-ok_format_change(3, 'number')
--- unsigned -----> integer
-ok_format_change(3, 'integer')
--- unsigned -----> scalar
-ok_format_change(3, 'scalar')
--- unsigned --X--> map
-fail_format_change(3, 'map')
-
--- string -----> any
-ok_format_change(4, 'any')
--- string -----> scalar
-ok_format_change(4, 'scalar')
--- string --X--> boolean
-fail_format_change(4, 'boolean')
-
--- number -----> any
-ok_format_change(5, 'any')
--- number -----> scalar
-ok_format_change(5, 'scalar')
--- number --X--> integer
-fail_format_change(5, 'integer')
-
--- integer -----> any
-ok_format_change(6, 'any')
--- integer -----> number
-ok_format_change(6, 'number')
--- integer -----> scalar
-ok_format_change(6, 'scalar')
--- integer --X--> unsigned
-fail_format_change(6, 'unsigned')
-
--- boolean -----> any
-ok_format_change(7, 'any')
--- boolean -----> scalar
-ok_format_change(7, 'scalar')
--- boolean --X--> string
-fail_format_change(7, 'string')
-
--- scalar -----> any
-ok_format_change(8, 'any')
--- scalar --X--> unsigned
-fail_format_change(8, 'unsigned')
-
--- array -----> any
-ok_format_change(9, 'any')
--- array --X--> scalar
-fail_format_change(9, 'scalar')
-
--- map -----> any
-ok_format_change(10, 'any')
--- map --X--> scalar
-fail_format_change(10, 'scalar')
-
-s:drop()
-
--- Check new fields adding.
-format = {}
-s = box.schema.space.create('test')
-format[1] = {name = 'field1', type = 'unsigned'}
-s:format(format) -- Ok, no indexes.
-pk = s:create_index('pk')
-format[2] = {name = 'field2', type = 'unsigned'}
-s:format(format) -- Ok, empty space.
-s:replace{1, 1}
-format[2] = nil
-s:format(format) -- Ok, can delete fields with no checks.
-s:delete{1}
-sk1 = s:create_index('sk1', {parts = {2, 'unsigned'}})
-sk2 = s:create_index('sk2', {parts = {3, 'unsigned'}})
-sk5 = s:create_index('sk5', {parts = {5, 'unsigned'}})
-s:replace{1, 1, 1, 1, 1}
-format[2] = {name = 'field2', type = 'unsigned'}
-format[3] = {name = 'field3', type = 'unsigned'}
-format[4] = {name = 'field4', type = 'any'}
-format[5] = {name = 'field5', type = 'unsigned'}
--- Ok, all new fields are indexed or have type ANY, and new
--- field_count <= old field_count.
-s:format(format)
-
-s:replace{1, 1, 1, 1, 1, 1}
-format[6] = {name = 'field6', type = 'unsigned'}
--- Ok, but check existing tuples for a new field[6].
-s:format(format)
-
--- Fail, not enough fields.
-s:replace{2, 2, 2, 2, 2}
-
-s:replace{2, 2, 2, 2, 2, 2, 2}
-format[7] = {name = 'field7', type = 'unsigned'}
--- Fail, the tuple {1, ... 1} is invalid for a new format.
-s:format(format)
-s:drop()
-
--- Vinyl does not support adding fields to a not empty space.
-s = box.schema.space.create('test', {engine = 'vinyl'})
-pk = s:create_index('pk')
-s:replace{1,1}
-format = {}
-format[1] = {name = 'field1', type = 'unsigned'}
-format[2] = {name = 'field2', type = 'unsigned'}
-s:format(format)
-s:drop()
-
---
 -- gh-1557: NULL in indexes.
 --
 
@@ -696,34 +405,6 @@ 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.
 --
diff --git a/test/engine/ddl.result b/test/engine/ddl.result
index 308aefb0..04062ac1 100644
--- a/test/engine/ddl.result
+++ b/test/engine/ddl.result
@@ -357,7 +357,7 @@ space:drop()
 format = {{'field1', 'scalar'}}
 ---
 ...
-s = box.schema.create_space('test', {format = format})
+s = box.schema.space.create('test', {engine = engine, format = format})
 ---
 ...
 pk = s:create_index('pk')
@@ -374,7 +374,7 @@ s:drop()
 format = {{'field1'}}
 ---
 ...
-s = box.schema.create_space('test', {format = format})
+s = box.schema.space.create('test', {engine = engine, format = format})
 ---
 ...
 pk = s:create_index('pk')
@@ -391,7 +391,7 @@ s:drop()
 -- gh-3229: update optionality if a space format is changed too,
 -- not only when indexes are updated.
 --
-s = box.schema.create_space('test', {engine = engine})
+s = box.schema.space.create('test', {engine = engine})
 ---
 ...
 format = {}
@@ -466,7 +466,7 @@ s:drop()
 --
 -- Modify key definition without index rebuild.
 --
-s = box.schema.create_space('test', {engine = engine})
+s = box.schema.space.create('test', {engine = engine})
 ---
 ...
 i1 = s:create_index('i1', {unique = true,  parts = {1, 'unsigned'}})
@@ -573,3 +573,782 @@ i3:select()
 s:drop()
 ---
 ...
+--
+-- gh-2652: validate space format.
+--
+s = box.schema.space.create('test', { engine = engine, format = "format" })
+---
+- error: Illegal parameters, options parameter 'format' should be of type table
+...
+format = { { name = 100 } }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+- error: 'Illegal parameters, format[1]: name (string) is expected'
+...
+long = string.rep('a', box.schema.NAME_MAX + 1)
+---
+...
+format = { { name = long } }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+- error: 'Failed to create space ''test'': field 1 name is too long'
+...
+format = { { name = 'id', type = '100' } }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+- error: 'Failed to create space ''test'': field 1 has unknown field type'
+...
+format = { setmetatable({}, { __serialize = 'map' }) }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+- error: 'Illegal parameters, format[1]: name (string) is expected'
+...
+-- Ensure the format is updated after index drop.
+format = { { name = 'id', type = 'unsigned' } }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+...
+pk = s:create_index('pk')
+---
+...
+sk = s:create_index('sk', { parts = { 2, 'string' } })
+---
+...
+s:replace{1, 1}
+---
+- error: 'Tuple field 2 type does not match one required by operation: expected string'
+...
+sk:drop()
+---
+...
+s:replace{1, 1}
+---
+- [1, 1]
+...
+s:drop()
+---
+...
+-- Check index parts conflicting with space format.
+format = { { name='field1', type='unsigned' }, { name='field2', type='string' }, { name='field3', type='scalar' } }
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+...
+pk = s:create_index('pk')
+---
+...
+sk1 = s:create_index('sk1', { parts = { 2, 'unsigned' } })
+---
+- error: Field 'field2' has type 'string' in space format, but type 'unsigned' in
+    index definition
+...
+-- Check space format conflicting with index parts.
+sk3 = s:create_index('sk3', { parts = { 2, 'string' } })
+---
+...
+format[2].type = 'unsigned'
+---
+...
+s:format(format)
+---
+- error: Field 'field2' has type 'unsigned' in space format, but type 'string' in
+    index definition
+...
+s:format()
+---
+- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'string'}, {
+    'name': 'field3', 'type': 'scalar'}]
+...
+s.index.sk3.parts
+---
+- - type: string
+    is_nullable: false
+    fieldno: 2
+...
+-- Space format can be updated, if conflicted index is deleted.
+sk3:drop()
+---
+...
+s:format(format)
+---
+...
+s:format()
+---
+- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'unsigned'},
+  {'name': 'field3', 'type': 'scalar'}]
+...
+-- Check deprecated field types.
+format[2].type = 'num'
+---
+...
+format[3].type = 'str'
+---
+...
+format[4] = { name = 'field4', type = '*' }
+---
+...
+format
+---
+- - name: field1
+    type: unsigned
+  - name: field2
+    type: num
+  - name: field3
+    type: str
+  - name: field4
+    type: '*'
+...
+s:format(format)
+---
+...
+s:format()
+---
+- [{'name': 'field1', 'type': 'unsigned'}, {'name': 'field2', 'type': 'num'}, {'name': 'field3',
+    'type': 'str'}, {'name': 'field4', 'type': '*'}]
+...
+s:replace{1, 2, '3', {4, 4, 4}}
+---
+- [1, 2, '3', [4, 4, 4]]
+...
+-- Check not indexed fields checking.
+s:truncate()
+---
+...
+format[2] = {name='field2', type='string'}
+---
+...
+format[3] = {name='field3', type='array'}
+---
+...
+format[4] = {name='field4', type='number'}
+---
+...
+format[5] = {name='field5', type='integer'}
+---
+...
+format[6] = {name='field6', type='scalar'}
+---
+...
+format[7] = {name='field7', type='map'}
+---
+...
+format[8] = {name='field8', type='any'}
+---
+...
+format[9] = {name='field9'}
+---
+...
+s:format(format)
+---
+...
+-- Check incorrect field types.
+format[9] = {name='err', type='any'}
+---
+...
+s:format(format)
+---
+...
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
+---
+- [1, '2', [3, 3], 4.4, -5, true, {'value': 7}, 8, 9]
+...
+s:replace{1, 2, {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
+---
+- error: 'Tuple field 2 type does not match one required by operation: expected string'
+...
+s:replace{1, '2', 3, 4.4, -5, true, {value=7}, 8, 9}
+---
+- error: 'Tuple field 3 type does not match one required by operation: expected array'
+...
+s:replace{1, '2', {3, 3}, '4', -5, true, {value=7}, 8, 9}
+---
+- error: 'Tuple field 4 type does not match one required by operation: expected number'
+...
+s:replace{1, '2', {3, 3}, 4.4, -5.5, true, {value=7}, 8, 9}
+---
+- error: 'Tuple field 5 type does not match one required by operation: expected integer'
+...
+s:replace{1, '2', {3, 3}, 4.4, -5, {6, 6}, {value=7}, 8, 9}
+---
+- error: 'Tuple field 6 type does not match one required by operation: expected scalar'
+...
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {7}, 8, 9}
+---
+- error: 'Tuple field 7 type does not match one required by operation: expected map'
+...
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}}
+---
+- error: Tuple field count 7 is less than required by space format or defined indexes
+    (expected at least 9)
+...
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8}
+---
+- error: Tuple field count 8 is less than required by space format or defined indexes
+    (expected at least 9)
+...
+s:truncate()
+---
+...
+--
+-- gh-1014: field names.
+--
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {name = 'field2'}
+---
+...
+format[3] = {name = 'field1'}
+---
+...
+s:format(format)
+---
+- error: Space field 'field1' is duplicate
+...
+s:drop()
+---
+...
+-- https://github.com/tarantool/tarantool/issues/2815
+-- Extend space format definition syntax
+format = {{name='key',type='unsigned'}, {name='value',type='string'}}
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+...
+s:format()
+---
+- [{'name': 'key', 'type': 'unsigned'}, {'name': 'value', 'type': 'string'}]
+...
+s:format({'id', 'name'})
+---
+...
+s:format()
+---
+- [{'name': 'id', 'type': 'any'}, {'name': 'name', 'type': 'any'}]
+...
+s:format({'id', {'name1'}})
+---
+...
+s:format()
+---
+- [{'name': 'id', 'type': 'any'}, {'name': 'name1', 'type': 'any'}]
+...
+s:format({'id', {'name2', 'string'}})
+---
+...
+s:format()
+---
+- [{'name': 'id', 'type': 'any'}, {'name': 'name2', 'type': 'string'}]
+...
+s:format({'id', {'name', type = 'string'}})
+---
+...
+s:format()
+---
+- [{'name': 'id', 'type': 'any'}, {'name': 'name', 'type': 'string'}]
+...
+s:drop()
+---
+...
+format = {'key', {'value',type='string'}}
+---
+...
+s = box.schema.space.create('test', { engine = engine, format = format })
+---
+...
+s:format()
+---
+- [{'name': 'key', 'type': 'any'}, {'name': 'value', 'type': 'string'}]
+...
+s:drop()
+---
+...
+s = box.schema.space.create('test', { engine = engine })
+---
+...
+s:create_index('test', {parts = {'test'}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test', {parts = {{'test'}}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test', {parts = {{field = 'test'}}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test', {parts = {1}}).parts
+---
+- - type: scalar
+    is_nullable: false
+    fieldno: 1
+...
+s:drop()
+---
+...
+s = box.schema.space.create('test', { engine = engine })
+---
+...
+s:format{{'test1', 'integer'}, 'test2', {'test3', 'integer'}, {'test4','scalar'}}
+---
+...
+s:create_index('test', {parts = {'test'}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test', {parts = {{'test'}}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test', {parts = {{field = 'test'}}})
+---
+- error: 'Illegal parameters, options.parts[1]: field was not found by name ''test'''
+...
+s:create_index('test1', {parts = {'test1'}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 1
+...
+s:create_index('test2', {parts = {'test2'}}).parts
+---
+- error: 'Can''t create or modify index ''test2'' in space ''test'': field type ''any''
+    is not supported'
+...
+s:create_index('test3', {parts = {{'test1', 'integer'}}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 1
+...
+s:create_index('test4', {parts = {{'test2', 'integer'}}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 2
+...
+s:create_index('test5', {parts = {{'test2', 'integer'}}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 2
+...
+s:create_index('test6', {parts = {1, 3}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 1
+  - type: integer
+    is_nullable: false
+    fieldno: 3
+...
+s:create_index('test7', {parts = {'test1', 4}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 1
+  - type: scalar
+    is_nullable: false
+    fieldno: 4
+...
+s:create_index('test8', {parts = {{1, 'integer'}, {'test4', 'scalar'}}}).parts
+---
+- - type: integer
+    is_nullable: false
+    fieldno: 1
+  - type: scalar
+    is_nullable: false
+    fieldno: 4
+...
+s:drop()
+---
+...
+--
+-- gh-2800: space formats checking is broken.
+--
+-- Ensure that vinyl correctly process field count change.
+s = box.schema.space.create('test', {engine = engine, field_count = 2})
+---
+...
+pk = s:create_index('pk')
+---
+...
+s:replace{1, 2}
+---
+- [1, 2]
+...
+t = box.space._space:select{s.id}[1]:totable()
+---
+...
+t[5] = 1
+---
+...
+box.space._space:replace(t)
+---
+- error: Tuple field count 2 does not match space field count 1
+...
+s:drop()
+---
+...
+-- Check field type changes.
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {name = 'field2', type = 'any'}
+---
+...
+format[3] = {name = 'field3', type = 'unsigned'}
+---
+...
+format[4] = {name = 'field4', type = 'string'}
+---
+...
+format[5] = {name = 'field5', type = 'number'}
+---
+...
+format[6] = {name = 'field6', type = 'integer'}
+---
+...
+format[7] = {name = 'field7', type = 'boolean'}
+---
+...
+format[8] = {name = 'field8', type = 'scalar'}
+---
+...
+format[9] = {name = 'field9', type = 'array'}
+---
+...
+format[10] = {name = 'field10', type = 'map'}
+---
+...
+s = box.schema.space.create('test', {engine = engine, format = format})
+---
+...
+pk = s:create_index('pk')
+---
+...
+t = s:replace{1, {2}, 3, '4', 5.5, -6, true, -8, {9, 9}, {val = 10}}
+---
+...
+inspector:cmd("setopt delimiter ';'")
+---
+- true
+...
+function fail_format_change(fieldno, new_type)
+    local old_type = format[fieldno].type
+    format[fieldno].type = new_type
+    local ok, msg = pcall(s.format, s, format)
+    format[fieldno].type = old_type
+    return msg
+end;
+---
+...
+function ok_format_change(fieldno, new_type)
+    local old_type = format[fieldno].type
+    format[fieldno].type = new_type
+    s:format(format)
+    s:delete{1}
+    format[fieldno].type = old_type
+    s:format(format)
+    s:replace(t)
+end;
+---
+...
+inspector:cmd("setopt delimiter ''");
+---
+- true
+...
+-- any --X--> unsigned
+fail_format_change(2, 'unsigned')
+---
+- 'Tuple field 2 type does not match one required by operation: expected unsigned'
+...
+-- unsigned -----> any
+ok_format_change(3, 'any')
+---
+...
+-- unsigned --X--> string
+fail_format_change(3, 'string')
+---
+- 'Tuple field 3 type does not match one required by operation: expected string'
+...
+-- unsigned -----> number
+ok_format_change(3, 'number')
+---
+...
+-- unsigned -----> integer
+ok_format_change(3, 'integer')
+---
+...
+-- unsigned -----> scalar
+ok_format_change(3, 'scalar')
+---
+...
+-- unsigned --X--> map
+fail_format_change(3, 'map')
+---
+- 'Tuple field 3 type does not match one required by operation: expected map'
+...
+-- string -----> any
+ok_format_change(4, 'any')
+---
+...
+-- string -----> scalar
+ok_format_change(4, 'scalar')
+---
+...
+-- string --X--> boolean
+fail_format_change(4, 'boolean')
+---
+- 'Tuple field 4 type does not match one required by operation: expected boolean'
+...
+-- number -----> any
+ok_format_change(5, 'any')
+---
+...
+-- number -----> scalar
+ok_format_change(5, 'scalar')
+---
+...
+-- number --X--> integer
+fail_format_change(5, 'integer')
+---
+- 'Tuple field 5 type does not match one required by operation: expected integer'
+...
+-- integer -----> any
+ok_format_change(6, 'any')
+---
+...
+-- integer -----> number
+ok_format_change(6, 'number')
+---
+...
+-- integer -----> scalar
+ok_format_change(6, 'scalar')
+---
+...
+-- integer --X--> unsigned
+fail_format_change(6, 'unsigned')
+---
+- 'Tuple field 6 type does not match one required by operation: expected unsigned'
+...
+-- boolean -----> any
+ok_format_change(7, 'any')
+---
+...
+-- boolean -----> scalar
+ok_format_change(7, 'scalar')
+---
+...
+-- boolean --X--> string
+fail_format_change(7, 'string')
+---
+- 'Tuple field 7 type does not match one required by operation: expected string'
+...
+-- scalar -----> any
+ok_format_change(8, 'any')
+---
+...
+-- scalar --X--> unsigned
+fail_format_change(8, 'unsigned')
+---
+- 'Tuple field 8 type does not match one required by operation: expected unsigned'
+...
+-- array -----> any
+ok_format_change(9, 'any')
+---
+...
+-- array --X--> scalar
+fail_format_change(9, 'scalar')
+---
+- 'Tuple field 9 type does not match one required by operation: expected scalar'
+...
+-- map -----> any
+ok_format_change(10, 'any')
+---
+...
+-- map --X--> scalar
+fail_format_change(10, 'scalar')
+---
+- 'Tuple field 10 type does not match one required by operation: expected scalar'
+...
+s:drop()
+---
+...
+-- Check new fields adding.
+format = {}
+---
+...
+s = box.schema.space.create('test', {engine = engine})
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+s:format(format) -- Ok, no indexes.
+---
+...
+pk = s:create_index('pk')
+---
+...
+format[2] = {name = 'field2', type = 'unsigned'}
+---
+...
+s:format(format) -- Ok, empty space.
+---
+...
+s:replace{1, 1}
+---
+- [1, 1]
+...
+format[2] = nil
+---
+...
+s:format(format) -- Ok, can delete fields with no checks.
+---
+...
+s:drop()
+---
+...
+s = box.schema.space.create('test', {engine = engine, format = format})
+---
+...
+pk = s:create_index('pk')
+---
+...
+sk1 = s:create_index('sk1', {parts = {2, 'unsigned'}})
+---
+...
+sk2 = s:create_index('sk2', {parts = {3, 'unsigned'}})
+---
+...
+sk5 = s:create_index('sk5', {parts = {5, 'unsigned'}})
+---
+...
+s:replace{1, 1, 1, 1, 1}
+---
+- [1, 1, 1, 1, 1]
+...
+format[2] = {name = 'field2', type = 'unsigned'}
+---
+...
+format[3] = {name = 'field3', type = 'unsigned'}
+---
+...
+format[4] = {name = 'field4', type = 'any'}
+---
+...
+format[5] = {name = 'field5', type = 'unsigned'}
+---
+...
+-- Ok, all new fields are indexed or have type ANY, and new
+-- field_count <= old field_count.
+s:format(format)
+---
+...
+s:replace{1, 1, 1, 1, 1, 1}
+---
+- [1, 1, 1, 1, 1, 1]
+...
+format[6] = {name = 'field6', type = 'unsigned'}
+---
+...
+-- Ok, but check existing tuples for a new field[6].
+s:format(format)
+---
+...
+-- Fail, not enough fields.
+s:replace{2, 2, 2, 2, 2}
+---
+- error: Tuple field count 5 is less than required by space format or defined indexes
+    (expected at least 6)
+...
+s:replace{2, 2, 2, 2, 2, 2, 2}
+---
+- [2, 2, 2, 2, 2, 2, 2]
+...
+format[7] = {name = 'field7', type = 'unsigned'}
+---
+...
+-- Fail, the tuple {1, ... 1} is invalid for a new format.
+s:format(format)
+---
+- error: Tuple field count 6 is less than required by space format or defined indexes
+    (expected at least 7)
+...
+s:drop()
+---
+...
+--
+-- Allow to restrict space format, if corresponding restrictions
+-- already are defined in indexes.
+--
+s = box.schema.space.create('test', {engine = engine})
+---
+...
+_ = s:create_index('pk')
+---
+...
+format = {}
+---
+...
+format[1] = {name = 'field1'}
+---
+...
+s:replace{1}
+---
+- [1]
+...
+s:replace{100}
+---
+- [100]
+...
+s:replace{0}
+---
+- [0]
+...
+s:format(format)
+---
+...
+s:format()
+---
+- [{'name': 'field1', 'type': 'any'}]
+...
+format[1].type = 'unsigned'
+---
+...
+s:format(format)
+---
+...
+s:format()
+---
+- [{'name': 'field1', 'type': 'unsigned'}]
+...
+s:select()
+---
+- - [0]
+  - [1]
+  - [100]
+...
+s:drop()
+---
+...
diff --git a/test/engine/ddl.test.lua b/test/engine/ddl.test.lua
index 019e18a1..f3d68e1b 100644
--- a/test/engine/ddl.test.lua
+++ b/test/engine/ddl.test.lua
@@ -125,14 +125,14 @@ space:drop()
 -- is omited.
 --
 format = {{'field1', 'scalar'}}
-s = box.schema.create_space('test', {format = format})
+s = box.schema.space.create('test', {engine = engine, format = format})
 pk = s:create_index('pk')
 pk.parts[1].type
 s:drop()
 
 -- Ensure type 'any' to be not inherited.
 format = {{'field1'}}
-s = box.schema.create_space('test', {format = format})
+s = box.schema.space.create('test', {engine = engine, format = format})
 pk = s:create_index('pk')
 pk.parts[1].type
 s:drop()
@@ -141,7 +141,7 @@ s:drop()
 -- gh-3229: update optionality if a space format is changed too,
 -- not only when indexes are updated.
 --
-s = box.schema.create_space('test', {engine = engine})
+s = box.schema.space.create('test', {engine = engine})
 format = {}
 format[1] = {'field1', 'unsigned'}
 format[2] = {'field2', 'unsigned', is_nullable = true}
@@ -165,7 +165,7 @@ s:drop()
 --
 -- Modify key definition without index rebuild.
 --
-s = box.schema.create_space('test', {engine = engine})
+s = box.schema.space.create('test', {engine = engine})
 i1 = s:create_index('i1', {unique = true,  parts = {1, 'unsigned'}})
 i2 = s:create_index('i2', {unique = false, parts = {2, 'unsigned'}})
 i3 = s:create_index('i3', {unique = true,  parts = {3, 'unsigned'}})
@@ -195,3 +195,306 @@ i2:select()
 i3:select()
 
 s:drop()
+
+--
+-- gh-2652: validate space format.
+--
+s = box.schema.space.create('test', { engine = engine, format = "format" })
+format = { { name = 100 } }
+s = box.schema.space.create('test', { engine = engine, format = format })
+long = string.rep('a', box.schema.NAME_MAX + 1)
+format = { { name = long } }
+s = box.schema.space.create('test', { engine = engine, format = format })
+format = { { name = 'id', type = '100' } }
+s = box.schema.space.create('test', { engine = engine, format = format })
+format = { setmetatable({}, { __serialize = 'map' }) }
+s = box.schema.space.create('test', { engine = engine, format = format })
+
+-- Ensure the format is updated after index drop.
+format = { { name = 'id', type = 'unsigned' } }
+s = box.schema.space.create('test', { engine = engine, format = format })
+pk = s:create_index('pk')
+sk = s:create_index('sk', { parts = { 2, 'string' } })
+s:replace{1, 1}
+sk:drop()
+s:replace{1, 1}
+s:drop()
+
+-- Check index parts conflicting with space format.
+format = { { name='field1', type='unsigned' }, { name='field2', type='string' }, { name='field3', type='scalar' } }
+s = box.schema.space.create('test', { engine = engine, format = format })
+pk = s:create_index('pk')
+sk1 = s:create_index('sk1', { parts = { 2, 'unsigned' } })
+
+-- Check space format conflicting with index parts.
+sk3 = s:create_index('sk3', { parts = { 2, 'string' } })
+format[2].type = 'unsigned'
+s:format(format)
+s:format()
+s.index.sk3.parts
+
+-- Space format can be updated, if conflicted index is deleted.
+sk3:drop()
+s:format(format)
+s:format()
+
+-- Check deprecated field types.
+format[2].type = 'num'
+format[3].type = 'str'
+format[4] = { name = 'field4', type = '*' }
+format
+s:format(format)
+s:format()
+s:replace{1, 2, '3', {4, 4, 4}}
+
+-- Check not indexed fields checking.
+s:truncate()
+format[2] = {name='field2', type='string'}
+format[3] = {name='field3', type='array'}
+format[4] = {name='field4', type='number'}
+format[5] = {name='field5', type='integer'}
+format[6] = {name='field6', type='scalar'}
+format[7] = {name='field7', type='map'}
+format[8] = {name='field8', type='any'}
+format[9] = {name='field9'}
+s:format(format)
+
+-- Check incorrect field types.
+format[9] = {name='err', type='any'}
+s:format(format)
+
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
+s:replace{1, 2, {3, 3}, 4.4, -5, true, {value=7}, 8, 9}
+s:replace{1, '2', 3, 4.4, -5, true, {value=7}, 8, 9}
+s:replace{1, '2', {3, 3}, '4', -5, true, {value=7}, 8, 9}
+s:replace{1, '2', {3, 3}, 4.4, -5.5, true, {value=7}, 8, 9}
+s:replace{1, '2', {3, 3}, 4.4, -5, {6, 6}, {value=7}, 8, 9}
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {7}, 8, 9}
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}}
+s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8}
+s:truncate()
+
+--
+-- gh-1014: field names.
+--
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {name = 'field2'}
+format[3] = {name = 'field1'}
+s:format(format)
+
+s:drop()
+
+-- https://github.com/tarantool/tarantool/issues/2815
+-- Extend space format definition syntax
+format = {{name='key',type='unsigned'}, {name='value',type='string'}}
+s = box.schema.space.create('test', { engine = engine, format = format })
+s:format()
+s:format({'id', 'name'})
+s:format()
+s:format({'id', {'name1'}})
+s:format()
+s:format({'id', {'name2', 'string'}})
+s:format()
+s:format({'id', {'name', type = 'string'}})
+s:format()
+s:drop()
+
+format = {'key', {'value',type='string'}}
+s = box.schema.space.create('test', { engine = engine, format = format })
+s:format()
+s:drop()
+
+s = box.schema.space.create('test', { engine = engine })
+s:create_index('test', {parts = {'test'}})
+s:create_index('test', {parts = {{'test'}}})
+s:create_index('test', {parts = {{field = 'test'}}})
+s:create_index('test', {parts = {1}}).parts
+s:drop()
+
+s = box.schema.space.create('test', { engine = engine })
+s:format{{'test1', 'integer'}, 'test2', {'test3', 'integer'}, {'test4','scalar'}}
+s:create_index('test', {parts = {'test'}})
+s:create_index('test', {parts = {{'test'}}})
+s:create_index('test', {parts = {{field = 'test'}}})
+s:create_index('test1', {parts = {'test1'}}).parts
+s:create_index('test2', {parts = {'test2'}}).parts
+s:create_index('test3', {parts = {{'test1', 'integer'}}}).parts
+s:create_index('test4', {parts = {{'test2', 'integer'}}}).parts
+s:create_index('test5', {parts = {{'test2', 'integer'}}}).parts
+s:create_index('test6', {parts = {1, 3}}).parts
+s:create_index('test7', {parts = {'test1', 4}}).parts
+s:create_index('test8', {parts = {{1, 'integer'}, {'test4', 'scalar'}}}).parts
+s:drop()
+
+--
+-- gh-2800: space formats checking is broken.
+--
+
+-- Ensure that vinyl correctly process field count change.
+s = box.schema.space.create('test', {engine = engine, field_count = 2})
+pk = s:create_index('pk')
+s:replace{1, 2}
+t = box.space._space:select{s.id}[1]:totable()
+t[5] = 1
+box.space._space:replace(t)
+s:drop()
+
+-- Check field type changes.
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {name = 'field2', type = 'any'}
+format[3] = {name = 'field3', type = 'unsigned'}
+format[4] = {name = 'field4', type = 'string'}
+format[5] = {name = 'field5', type = 'number'}
+format[6] = {name = 'field6', type = 'integer'}
+format[7] = {name = 'field7', type = 'boolean'}
+format[8] = {name = 'field8', type = 'scalar'}
+format[9] = {name = 'field9', type = 'array'}
+format[10] = {name = 'field10', type = 'map'}
+s = box.schema.space.create('test', {engine = engine, format = format})
+pk = s:create_index('pk')
+t = s:replace{1, {2}, 3, '4', 5.5, -6, true, -8, {9, 9}, {val = 10}}
+
+inspector:cmd("setopt delimiter ';'")
+function fail_format_change(fieldno, new_type)
+    local old_type = format[fieldno].type
+    format[fieldno].type = new_type
+    local ok, msg = pcall(s.format, s, format)
+    format[fieldno].type = old_type
+    return msg
+end;
+
+function ok_format_change(fieldno, new_type)
+    local old_type = format[fieldno].type
+    format[fieldno].type = new_type
+    s:format(format)
+    s:delete{1}
+    format[fieldno].type = old_type
+    s:format(format)
+    s:replace(t)
+end;
+inspector:cmd("setopt delimiter ''");
+
+-- any --X--> unsigned
+fail_format_change(2, 'unsigned')
+
+-- unsigned -----> any
+ok_format_change(3, 'any')
+-- unsigned --X--> string
+fail_format_change(3, 'string')
+-- unsigned -----> number
+ok_format_change(3, 'number')
+-- unsigned -----> integer
+ok_format_change(3, 'integer')
+-- unsigned -----> scalar
+ok_format_change(3, 'scalar')
+-- unsigned --X--> map
+fail_format_change(3, 'map')
+
+-- string -----> any
+ok_format_change(4, 'any')
+-- string -----> scalar
+ok_format_change(4, 'scalar')
+-- string --X--> boolean
+fail_format_change(4, 'boolean')
+
+-- number -----> any
+ok_format_change(5, 'any')
+-- number -----> scalar
+ok_format_change(5, 'scalar')
+-- number --X--> integer
+fail_format_change(5, 'integer')
+
+-- integer -----> any
+ok_format_change(6, 'any')
+-- integer -----> number
+ok_format_change(6, 'number')
+-- integer -----> scalar
+ok_format_change(6, 'scalar')
+-- integer --X--> unsigned
+fail_format_change(6, 'unsigned')
+
+-- boolean -----> any
+ok_format_change(7, 'any')
+-- boolean -----> scalar
+ok_format_change(7, 'scalar')
+-- boolean --X--> string
+fail_format_change(7, 'string')
+
+-- scalar -----> any
+ok_format_change(8, 'any')
+-- scalar --X--> unsigned
+fail_format_change(8, 'unsigned')
+
+-- array -----> any
+ok_format_change(9, 'any')
+-- array --X--> scalar
+fail_format_change(9, 'scalar')
+
+-- map -----> any
+ok_format_change(10, 'any')
+-- map --X--> scalar
+fail_format_change(10, 'scalar')
+
+s:drop()
+
+-- Check new fields adding.
+format = {}
+s = box.schema.space.create('test', {engine = engine})
+format[1] = {name = 'field1', type = 'unsigned'}
+s:format(format) -- Ok, no indexes.
+pk = s:create_index('pk')
+format[2] = {name = 'field2', type = 'unsigned'}
+s:format(format) -- Ok, empty space.
+s:replace{1, 1}
+format[2] = nil
+s:format(format) -- Ok, can delete fields with no checks.
+s:drop()
+
+s = box.schema.space.create('test', {engine = engine, format = format})
+pk = s:create_index('pk')
+sk1 = s:create_index('sk1', {parts = {2, 'unsigned'}})
+sk2 = s:create_index('sk2', {parts = {3, 'unsigned'}})
+sk5 = s:create_index('sk5', {parts = {5, 'unsigned'}})
+s:replace{1, 1, 1, 1, 1}
+format[2] = {name = 'field2', type = 'unsigned'}
+format[3] = {name = 'field3', type = 'unsigned'}
+format[4] = {name = 'field4', type = 'any'}
+format[5] = {name = 'field5', type = 'unsigned'}
+-- Ok, all new fields are indexed or have type ANY, and new
+-- field_count <= old field_count.
+s:format(format)
+
+s:replace{1, 1, 1, 1, 1, 1}
+format[6] = {name = 'field6', type = 'unsigned'}
+-- Ok, but check existing tuples for a new field[6].
+s:format(format)
+
+-- Fail, not enough fields.
+s:replace{2, 2, 2, 2, 2}
+
+s:replace{2, 2, 2, 2, 2, 2, 2}
+format[7] = {name = 'field7', type = 'unsigned'}
+-- Fail, the tuple {1, ... 1} is invalid for a new format.
+s:format(format)
+s:drop()
+
+--
+-- Allow to restrict space format, if corresponding restrictions
+-- already are defined in indexes.
+--
+s = box.schema.space.create('test', {engine = engine})
+_ = s:create_index('pk')
+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)
+s:format()
+s:select()
+s:drop()
diff --git a/test/vinyl/ddl.result b/test/vinyl/ddl.result
index 5142f0f2..4607a44e 100644
--- a/test/vinyl/ddl.result
+++ b/test/vinyl/ddl.result
@@ -696,89 +696,6 @@ 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 changing format of a non-empty space
-...
-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 c4bd36bb..637a331d 100644
--- a/test/vinyl/ddl.test.lua
+++ b/test/vinyl/ddl.test.lua
@@ -260,32 +260,6 @@ 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/errinj.result b/test/vinyl/errinj.result
index 91351086..fd21f7bb 100644
--- a/test/vinyl/errinj.result
+++ b/test/vinyl/errinj.result
@@ -1320,3 +1320,78 @@ ret
 s:drop()
 ---
 ...
+--
+-- Check that ALTER is abroted if a tuple inserted during space
+-- format change does not conform to the new format.
+--
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {name = 'field2', type = 'string', is_nullable = true}
+---
+...
+s = box.schema.space.create('test', {engine = 'vinyl', format = format})
+---
+...
+_ = s:create_index('pk', {page_size = 16})
+---
+...
+pad = string.rep('x', 16)
+---
+...
+for i = 101, 200 do s:replace{i, pad} end
+---
+...
+box.snapshot()
+---
+- ok
+...
+ch = fiber.channel(1)
+---
+...
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+_ = fiber.create(function()
+    fiber.sleep(0.01)
+    for i = 1, 100 do
+        s:replace{i, box.NULL}
+    end
+    ch:put(true)
+end);
+---
+...
+test_run:cmd("setopt delimiter ''");
+---
+- true
+...
+errinj.set("ERRINJ_VY_READ_PAGE_TIMEOUT", 0.001)
+---
+- ok
+...
+format[2].is_nullable = false
+---
+...
+s:format(format) -- must fail
+---
+- error: 'Tuple field 2 type does not match one required by operation: expected string'
+...
+errinj.set("ERRINJ_VY_READ_PAGE_TIMEOUT", 0)
+---
+- ok
+...
+ch:get()
+---
+- true
+...
+s:count() -- 200
+---
+- 200
+...
+s:drop()
+---
+...
diff --git a/test/vinyl/errinj.test.lua b/test/vinyl/errinj.test.lua
index 9724a69b..64d04c62 100644
--- a/test/vinyl/errinj.test.lua
+++ b/test/vinyl/errinj.test.lua
@@ -513,3 +513,38 @@ errinj.set("ERRINJ_VY_DELAY_PK_LOOKUP", false)
 while ret == nil do fiber.sleep(0.01) end
 ret
 s:drop()
+
+--
+-- Check that ALTER is abroted if a tuple inserted during space
+-- format change does not conform to the new format.
+--
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {name = 'field2', type = 'string', is_nullable = true}
+s = box.schema.space.create('test', {engine = 'vinyl', format = format})
+_ = s:create_index('pk', {page_size = 16})
+
+pad = string.rep('x', 16)
+for i = 101, 200 do s:replace{i, pad} end
+box.snapshot()
+
+ch = fiber.channel(1)
+test_run:cmd("setopt delimiter ';'")
+_ = fiber.create(function()
+    fiber.sleep(0.01)
+    for i = 1, 100 do
+        s:replace{i, box.NULL}
+    end
+    ch:put(true)
+end);
+test_run:cmd("setopt delimiter ''");
+
+errinj.set("ERRINJ_VY_READ_PAGE_TIMEOUT", 0.001)
+format[2].is_nullable = false
+s:format(format) -- must fail
+errinj.set("ERRINJ_VY_READ_PAGE_TIMEOUT", 0)
+
+ch:get()
+
+s:count() -- 200
+s:drop()
-- 
2.11.0




More information about the Tarantool-patches mailing list