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

Konstantin Osipov kostja at tarantool.org
Mon Apr 9 23:55:04 MSK 2018


* Vladimir Davydov <vdavydov.dev at gmail.com> [18/04/09 10:33]:
> 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.

The patch is OK to push. Please consider doing all vinyl alter in
vinyl worker threads, a major compaction is IMHO an OK way to
achieve the same.

Please check out one comment below.

> ---
>  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;
> +	}

Please don't assume this yields naturally - it won't in a whole
bunch of critical cases. Better yet to move it to compaction
thread and perform the check along with compaction.

> +	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
> 

-- 
Konstantin Osipov, Moscow, Russia, +7 903 626 22 32
http://tarantool.io - www.twitter.com/kostja_osipov



More information about the Tarantool-patches mailing list