From 373342aaab75a7a19d3c8a901a6615520c8fd779 Mon Sep 17 00:00:00 2001 From: Georgy Moshkin <gmoshkin@picodata.io> Date: Thu, 24 Aug 2023 16:46:34 +0300 Subject: [PATCH] box: fully temporary spaces Introduce fully temporary spaces: same as data-temporary space but with temporary metadata. Basically temporary spaces now do not exist on restart and do not exist on replicas. They can also be created, altered and deleted when box.cfg.read_only = true. To avoid conflicts with spaces created on replicas, the temporary space ids by default start in a special range starting at BOX_SPACE_ID_TEMPORARY_MIN. Temporary spaces currently do not support several features e.g. foreign key references (to and from), functional indexes, sql sequences, sql triggers, etc. This may change in the future. Implementing temporary spaces requires temporary tuples to be inserted into system spaces: tuples which are neither replicated or persisted. This mostly done in on_replace_dd_* triggers by dropping the txn->stmt->row. Closes #8323 @TarantoolBot document Title: Introduce fully temporary spaces with temporary metadata Temporary spaces are now data-temporary spaces with temporary metadata. Created by specifying { type = "temporary" } in the options. Temporary spaces will not exist upon server restart and will not exist on replicas. They can also be created in read-only mode. --- .../unreleased/fully-temporary-spaces.md | 6 + src/box/alter.cc | 88 ++- src/box/box.cc | 56 +- src/box/box.h | 2 +- src/box/lua/misc.cc | 5 +- src/box/lua/schema.lua | 39 +- src/box/lua/space.cc | 2 + src/box/memtx_engine.cc | 53 ++ src/box/schema_def.h | 9 +- src/box/space.h | 7 + src/box/space_def.c | 40 ++ src/box/space_def.h | 26 + src/box/sql/select.c | 2 +- src/box/sql/vdbe.c | 2 +- src/box/tuple_constraint_fkey.c | 6 + src/box/txn.c | 13 +- src/box/txn.h | 18 + .../fully-temporary_spaces_test.lua | 523 ++++++++++++++++++ test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 6 +- test/box/alter.test.lua | 2 +- test/box/stat.result | 17 +- test/box/stat.test.lua | 3 + 24 files changed, 877 insertions(+), 52 deletions(-) create mode 100644 changelogs/unreleased/fully-temporary-spaces.md create mode 100644 test/box-luatest/fully-temporary_spaces_test.lua diff --git a/changelogs/unreleased/fully-temporary-spaces.md b/changelogs/unreleased/fully-temporary-spaces.md new file mode 100644 index 0000000000..c92aa212d9 --- /dev/null +++ b/changelogs/unreleased/fully-temporary-spaces.md @@ -0,0 +1,6 @@ +## feature/space + +* Introduces the fully temporary space type. It is the same as data-temporary + but also has temporary metadata. Temporary spaces can now be created in + read_only mode, they disappear after server restart and don't exist on + replicas (gh-8323). diff --git a/src/box/alter.cc b/src/box/alter.cc index c065a4a210..7811fd2787 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -531,6 +531,11 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode, "local space can't be synchronous"); return NULL; } + if (space_opts_is_temporary(&opts) && opts.constraint_count > 0) { + diag_set(ClientError, ER_UNSUPPORTED, "temporary space", + "constraints"); + return NULL; + } struct space_def *def = space_def_new(id, uid, exact_field_count, name, name_len, engine_name, engine_name_len, &opts, fields, @@ -1932,6 +1937,11 @@ update_view_references(struct Select *select, int update_value) diag_set(ClientError, ER_NO_SUCH_SPACE, space_name); goto error; } + if (space_is_temporary(space)) { + diag_set(ClientError, ER_UNSUPPORTED, + "CREATE VIEW", "temporary spaces"); + goto error; + } } /* Secondly do the job. */ for (int i = 0; i < from_tables_count; ++i) { @@ -2112,6 +2122,27 @@ space_check_alter(struct space *old_space, struct space_def *new_space_def) return 0; } +/* + * box_process1() bypasses the read-only check for the _space system space + * because there it's not yet known if the related space is temporary. Perform + * the check here if the space isn't temporary and the statement was issued by + * this replica. + */ +static int +filter_temporary_ddl_stmt(struct txn *txn, const struct space_def *def) +{ + if (def == NULL) + return 0; + struct txn_stmt *stmt = txn_current_stmt(txn); + if (space_opts_is_temporary(&def->opts)) { + txn_stmt_mark_as_temporary(txn, stmt); + return 0; + } + if (stmt->row->replica_id == 0 && recovery_state != INITIAL_RECOVERY) + return box_check_writable(); + return 0; +} + /** * A trigger which is invoked on replace in a data dictionary * space _space. @@ -2195,6 +2226,9 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) ER_CREATE_SPACE : ER_ALTER_SPACE; def = space_def_new_from_tuple(new_tuple, errcode, region); } + if (filter_temporary_ddl_stmt(txn, old_space != NULL ? + old_space->def : def) != 0) + return -1; if (new_tuple != NULL && old_space == NULL) { /* INSERT */ if (def == NULL) return -1; @@ -2380,6 +2414,13 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) "replication group is immutable"); return -1; } + if (space_is_temporary(old_space) != + space_opts_is_temporary(&def->opts)) { + diag_set(ClientError, ER_ALTER_SPACE, + old_space->def->name, + "temporariness cannot change"); + return -1; + } if (def->opts.is_view != old_space->def->opts.is_view) { diag_set(ClientError, ER_ALTER_SPACE, space_name(old_space), @@ -2518,6 +2559,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) struct space *old_space = space_cache_find(id); if (old_space == NULL) return -1; + if (filter_temporary_ddl_stmt(txn, old_space->def) != 0) + return -1; if (old_space->def->opts.is_view) { diag_set(ClientError, ER_ALTER_SPACE, space_name(old_space), "can not add index on a view"); @@ -2778,21 +2821,28 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) struct txn_stmt *stmt = txn_current_stmt(txn); struct tuple *new_tuple = stmt->new_tuple; - if (new_tuple == NULL) { - /* Space drop - nothing to do. */ - return 0; - } if (recovery_state == INITIAL_RECOVERY) { /* Space creation during initial recovery - nothing to do. */ return 0; } + struct tuple *any_tuple = new_tuple; + if (any_tuple == NULL) + any_tuple = stmt->old_tuple; uint32_t space_id; - if (tuple_field_u32(new_tuple, BOX_TRUNCATE_FIELD_SPACE_ID, &space_id) != 0) + if (tuple_field_u32(any_tuple, BOX_TRUNCATE_FIELD_SPACE_ID, + &space_id) != 0) return -1; struct space *old_space = space_cache_find(space_id); if (old_space == NULL) return -1; + if (space_is_temporary(old_space)) + txn_stmt_mark_as_temporary(txn, stmt); + + if (new_tuple == NULL) { + /* Space drop - nothing else to do. */ + return 0; + } /* * box_process1() bypasses the read-only check for the _truncate system @@ -2838,9 +2888,11 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) /* * Modify the WAL header to prohibit * replication of local & data-temporary - * spaces truncation. + * spaces truncation + * unless it's a temporary space + * in which case the header doesn't exist. */ - if (is_temp) { + if (is_temp && !space_is_temporary(old_space)) { stmt->row->group_id = GROUP_LOCAL; /* * The trigger is invoked after txn->n_local_rows @@ -3874,6 +3926,11 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type) grantor->def->name); return -1; } + if (space_is_temporary(space)) { + diag_set(ClientError, ER_UNSUPPORTED, + "temporary space", "privileges"); + return -1; + } break; } case SC_FUNCTION: @@ -4782,6 +4839,11 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event) struct space *space = space_cache_find(space_id); if (space == NULL) return -1; + if (space_is_temporary(space)) { + diag_set(ClientError, ER_SQL_EXECUTE, + "sequences are not supported for temporary spaces"); + return -1; + } struct sequence *seq = sequence_by_id(sequence_id); if (seq == NULL) { diag_set(ClientError, ER_NO_SUCH_SEQUENCE, int2str(sequence_id)); @@ -5023,6 +5085,13 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event) "resolved on AST building from SQL"); return -1; } + struct space *space = space_cache_find(space_id); + if (space != NULL && space_is_temporary(space)) { + diag_set(ClientError, ER_SQL_EXECUTE, + "triggers are not supported for " + "temporary spaces"); + return -1; + } struct sql_trigger *old_trigger; if (sql_trigger_replace(trigger_name, @@ -5077,6 +5146,11 @@ on_replace_dd_func_index(struct trigger *trigger, void *event) space = space_cache_find(space_id); if (space == NULL) return -1; + if (space_is_temporary(space)) { + diag_set(ClientError, ER_UNSUPPORTED, + "temporary space", "functional indexes"); + return -1; + } index = index_find(space, index_id); if (index == NULL) return -1; diff --git a/src/box/box.cc b/src/box/box.cc index b561405138..588b7d29f4 100644 --- a/src/box/box.cc +++ b/src/box/box.cc @@ -3417,11 +3417,16 @@ box_process1(struct request *request, box_tuple_t **result) return -1; /* * Allow to write to data-temporary and local spaces in the read-only - * mode. To handle space truncation, we postpone the read-only check for - * the _truncate system space till the on_replace trigger is called, - * when we know which space is truncated. + * mode. To handle space truncation and/or ddl operations on temporary + * spaces, we postpone the read-only check for the _truncate, _space & + * _index system spaces till the on_replace trigger is called, when + * we know which spaces are concerned. */ - if (space_id(space) != BOX_TRUNCATE_ID && + uint32_t id = space_id(space); + if (is_ro_summary && + id != BOX_TRUNCATE_ID && + id != BOX_SPACE_ID && + id != BOX_INDEX_ID && !space_is_data_temporary(space) && !space_is_local(space) && box_check_writable() != 0) @@ -5477,37 +5482,50 @@ box_read_ffi_enable(void) } int -box_generate_space_id(uint32_t *new_space_id) +box_generate_space_id(uint32_t *new_space_id, bool is_temporary) { assert(new_space_id != NULL); - assert(mp_sizeof_array(0) == 1); - char empty_key[1]; - char *empty_key_end = mp_encode_array(empty_key, 0); - struct tuple *res = NULL; + uint32_t id_range_begin = !is_temporary ? + BOX_SYSTEM_ID_MAX + 1 : BOX_SPACE_ID_TEMPORARY_MIN; + uint32_t id_range_end = !is_temporary ? + (uint32_t)BOX_SPACE_ID_TEMPORARY_MIN : + (uint32_t)BOX_SPACE_MAX + 1; + char key_buf[16]; + char *key_end = key_buf; + key_end = mp_encode_array(key_end, 1); + key_end = mp_encode_uint(key_end, id_range_end); struct credentials *orig_credentials = effective_user(); fiber_set_user(fiber(), &admin_credentials); - int rc = box_index_max(BOX_SPACE_ID, 0, empty_key, empty_key_end, - &res); - fiber_set_user(fiber(), orig_credentials); + auto guard = make_scoped_guard([=] { + fiber_set_user(fiber(), orig_credentials); + }); + box_iterator_t *it = box_index_iterator(BOX_SPACE_ID, 0, ITER_LT, + key_buf, key_end); + if (it == NULL) + return -1; + struct tuple *res = NULL; + int rc = box_iterator_next(it, &res); + box_iterator_free(it); if (rc != 0) return -1; + assert(res != NULL); uint32_t max_id = 0; - if (res != NULL && tuple_field_u32(res, 0, &max_id) != 0) - return -1; - if (max_id > BOX_SPACE_MAX || max_id < BOX_SYSTEM_ID_MAX) - max_id = BOX_SYSTEM_ID_MAX; + rc = tuple_field_u32(res, 0, &max_id); + assert(rc == 0); + if (max_id < id_range_begin) + max_id = id_range_begin - 1; *new_space_id = space_cache_find_next_unused_id(max_id); /* Try again if overflowed. */ - if (*new_space_id > BOX_SPACE_MAX) { + if (*new_space_id >= id_range_end) { *new_space_id = - space_cache_find_next_unused_id(BOX_SYSTEM_ID_MAX); + space_cache_find_next_unused_id(id_range_begin - 1); /* * The second overflow means all ids are occupied. * This situation cannot happen in real world with limited * memory, and its pretty hard to test it, so let's just panic * if we've run out of ids. */ - if (*new_space_id > BOX_SPACE_MAX) + if (*new_space_id >= id_range_end) panic("Space id limit is reached"); } return 0; diff --git a/src/box/box.h b/src/box/box.h index af95eedc0c..5f2a70ad96 100644 --- a/src/box/box.h +++ b/src/box/box.h @@ -759,7 +759,7 @@ boxk(int type, uint32_t space_id, const char *format, ...); /** Generate unique id for non-system space. */ int -box_generate_space_id(uint32_t *new_space_id); +box_generate_space_id(uint32_t *new_space_id, bool is_temporary); /** * Broadcast the identification of the instance diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc index f0676873af..6da577f54e 100644 --- a/src/box/lua/misc.cc +++ b/src/box/lua/misc.cc @@ -225,8 +225,11 @@ port_msgpack_dump_lua(struct port *base, struct lua_State *L, bool is_flat) static int lbox_generate_space_id(lua_State *L) { + assert(lua_gettop(L) >= 1); + assert(lua_isboolean(L, 1) == 1); + bool is_temporary = lua_toboolean(L, 1) != 0; uint32_t ret = 0; - if (box_generate_space_id(&ret) != 0) + if (box_generate_space_id(&ret, is_temporary) != 0) return luaT_error(L); lua_pushnumber(L, ret); return 1; diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 2097128f55..7a978636da 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -677,6 +677,7 @@ end local space_types = { 'normal', 'data-temporary', + 'temporary', } local function check_space_type(space_type) if space_type == nil then @@ -737,7 +738,7 @@ box.schema.space.create = function(name, options) end local id = options.id if not id then - id = internal.generate_space_id() + id = internal.generate_space_id(options.type == 'temporary') end local uid = session.euid() if options.user then @@ -807,10 +808,15 @@ box.schema.space.drop = function(space_id, space_name, opts) local _truncate = box.space[box.schema.TRUNCATE_ID] local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] local _func_index = box.space[box.schema.FUNC_INDEX_ID] - local sequence_tuple = _space_sequence:delete{space_id} - if sequence_tuple ~= nil and sequence_tuple.is_generated == true then - -- Delete automatically generated sequence. - box.schema.sequence.drop(sequence_tuple.sequence_id) + -- This is needed to support dropping temporary spaces + -- in read-only mode, because sequences aren't supported for them yet + -- and therefore such requests aren't allowed in read-only mode. + if _space_sequence:get(space_id) ~= nil then + local sequence_tuple = _space_sequence:delete{space_id} + if sequence_tuple.is_generated == true then + -- Delete automatically generated sequence. + box.schema.sequence.drop(sequence_tuple.sequence_id) + end end for _, t in _trigger.index.space_id:pairs({space_id}) do _trigger:delete({t.name}) @@ -824,7 +830,15 @@ box.schema.space.drop = function(space_id, space_name, opts) _index:delete{v.id, v.iid} end revoke_object_privs('space', space_id) - _truncate:delete{space_id} + -- Deleting from _truncate currently adds a delete entry into WAL even + -- if the corresponding space was never truncated. This is a problem for + -- temporary spaces, because in such situations it's impossible to + -- filter out such entries from the within on_replace trigger which + -- basically results in temporary space's metadata getting into WAL + -- which breaks some invariants. + if _truncate:get{space_id} ~= nil then + _truncate:delete{space_id} + end if _space:delete{space_id} == nil then if space_name == nil then space_name = '#'..tostring(space_id) @@ -1569,10 +1583,15 @@ box.schema.index.drop = function(space_id, index_id) check_param(index_id, 'index_id', 'number') if index_id == 0 then local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] - local sequence_tuple = _space_sequence:delete{space_id} - if sequence_tuple ~= nil and sequence_tuple.is_generated == true then - -- Delete automatically generated sequence. - box.schema.sequence.drop(sequence_tuple.sequence_id) + -- This is needed to support dropping temporary spaces + -- in read-only mode, because sequences aren't supported for them yet + -- and therefore such requests aren't allowed in read-only mode. + if _space_sequence:get(space_id) ~= nil then + local sequence_tuple = _space_sequence:delete{space_id} + if sequence_tuple.is_generated == true then + -- Delete automatically generated sequence. + box.schema.sequence.drop(sequence_tuple.sequence_id) + end end end local _index = box.space[box.schema.INDEX_ID] diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index 953077bde7..ad554462f4 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -780,6 +780,8 @@ box_lua_space_init(struct lua_State *L) lua_setfield(L, -2, "REPLICA_MAX"); lua_pushnumber(L, SQL_BIND_PARAMETER_MAX); lua_setfield(L, -2, "SQL_BIND_PARAMETER_MAX"); + lua_pushnumber(L, BOX_SPACE_ID_TEMPORARY_MIN); + lua_setfield(L, -2, "SPACE_ID_TEMPORARY_MIN"); lua_pop(L, 2); /* box, schema */ static const struct luaL_Reg space_internal_lib[] = { diff --git a/src/box/memtx_engine.cc b/src/box/memtx_engine.cc index c57fe518d6..8d9ded8789 100644 --- a/src/box/memtx_engine.cc +++ b/src/box/memtx_engine.cc @@ -808,6 +808,46 @@ primary_index_filter(struct space *space, struct index *index, void *arg) return index->def->iid == 0; } +/* + * Return true if tuple @a data represents temporary space's metadata. + * @a space_id is used to determine the tuple's format. + */ +static bool +is_tuple_temporary(const char *data, uint32_t space_id, + struct mh_i32_t *temp_space_ids) +{ + static_assert(BOX_SPACE_ID < BOX_INDEX_ID && + BOX_SPACE_ID < BOX_TRUNCATE_ID, + "in this function temporary space ids are collected " + "while processing tuples of _space and then this info is " + "used when going over _index & _truncate"); + switch (space_id) { + case BOX_SPACE_ID: { + uint32_t space_id = BOX_ID_NIL; + bool t = space_def_tuple_is_temporary(data, &space_id); + if (t) + mh_i32_put(temp_space_ids, &space_id, + NULL, NULL); + return t; + } + case BOX_INDEX_ID: + case BOX_TRUNCATE_ID: { + static_assert(BOX_INDEX_FIELD_SPACE_ID == 0 && + BOX_TRUNCATE_FIELD_SPACE_ID == 0, + "the following code assumes this is true"); + uint32_t field_count = mp_decode_array(&data); + if (field_count < 1 || mp_typeof(*data) != MP_UINT) + return false; + uint32_t space_id = mp_decode_uint(&data); + mh_int_t pos = mh_i32_find(temp_space_ids, space_id, + NULL); + return pos != mh_end(temp_space_ids); + } + default: + return false; + } +} + static struct checkpoint * checkpoint_new(const char *snap_dirname, uint64_t snap_io_rate_limit) { @@ -993,6 +1033,8 @@ checkpoint_f(va_list ap) return -1; bool is_synchro_written = false; + struct mh_i32_t *temp_space_ids = mh_i32_new(); + say_info("saving snapshot `%s'", snap.filename); ERROR_INJECT_SLEEP(ERRINJ_SNAP_WRITE_DELAY); ERROR_INJECT(ERRINJ_SNAP_SKIP_ALL_ROWS, goto done); @@ -1033,6 +1075,10 @@ checkpoint_f(va_list ap) rc = index_read_view_iterator_next_raw(&it, &result); if (rc != 0 || result.data == NULL) break; + if (is_tuple_temporary(result.data, + space_rv->id, + temp_space_ids)) + continue; rc = checkpoint_write_tuple(&snap, space_rv->id, space_rv->group_id, result.data, result.size); @@ -1043,6 +1089,7 @@ checkpoint_f(va_list ap) if (rc != 0) break; } + mh_i32_delete(temp_space_ids); if (rc != 0) goto fail; ERROR_INJECT(ERRINJ_SNAP_WRITE_CORRUPTED_INSERT_ROW, { @@ -1356,6 +1403,7 @@ memtx_join_f(va_list ap) { int rc = 0; struct memtx_join_ctx *ctx = va_arg(ap, struct memtx_join_ctx *); + struct mh_i32_t *temp_space_ids = mh_i32_new(); struct space_read_view *space_rv; read_view_foreach_space(space_rv, &ctx->rv) { FiberGCChecker gc_check; @@ -1374,6 +1422,10 @@ memtx_join_f(va_list ap) rc = index_read_view_iterator_next_raw(&it, &result); if (rc != 0 || result.data == NULL) break; + if (is_tuple_temporary(result.data, + space_rv->id, + temp_space_ids)) + continue; rc = memtx_join_send_tuple(ctx->stream, space_rv->id, result.data, result.size); if (rc != 0) @@ -1383,6 +1435,7 @@ memtx_join_f(va_list ap) if (rc != 0) break; } + mh_i32_delete(temp_space_ids); return rc; } diff --git a/src/box/schema_def.h b/src/box/schema_def.h index 34529790d2..7d2bf8602b 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -57,7 +57,14 @@ enum { /** Yet another arbitrary limit which simply needs to * exist. */ - BOX_INDEX_PART_MAX = UINT8_MAX + BOX_INDEX_PART_MAX = UINT8_MAX, + /** + * Start of the range of default temporary space ids. + * By default they get ids from a special range to avoid conflicts with + * spaces which could arrive via replication. But the user is free to + * choose an id from outside this range. + */ + BOX_SPACE_ID_TEMPORARY_MIN = (1 << 30), }; static_assert(BOX_INVALID_NAME_MAX <= BOX_NAME_MAX, "invalid name max is less than name max"); diff --git a/src/box/space.h b/src/box/space.h index 36cbe1d848..84d2f72c06 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -347,6 +347,13 @@ space_is_data_temporary(const struct space *space) return space_opts_is_data_temporary(&space->def->opts); } +/** Return true if space is temporary. */ +static inline bool +space_is_temporary(const struct space *space) +{ + return space_opts_is_temporary(&space->def->opts); +} + /** Return true if space is synchronous. */ static inline bool space_is_sync(const struct space *space) diff --git a/src/box/space_def.c b/src/box/space_def.c index b698dd27e9..e148536db7 100644 --- a/src/box/space_def.c +++ b/src/box/space_def.c @@ -284,6 +284,7 @@ space_opts_parse_temporary(const char **data, void *vopts, const char *space_type_strs[] = { /* [SPACE_TYPE_NORMAL] = */ "normal", /* [SPACE_TYPE_DATA_TEMPORARY] = */ "data-temporary", + /* [SPACE_TYPE_TEMPORARY] = */ "temporary", }; static int @@ -311,3 +312,42 @@ space_opts_parse_type(const char **data, void *vopts, struct region *region) opts->type = space_type; return 0; } + +bool +space_def_tuple_is_temporary(const char *data, uint32_t *space_id) +{ + uint32_t field_count = mp_decode_array(&data); + if (field_count < BOX_SPACE_FIELD_OPTS + 1) + return false; + + static_assert(BOX_SPACE_FIELD_ID == 0, + "the following code relies on this assumption"); + if (mp_typeof(*data) != MP_UINT) + return false; + assert(space_id != NULL); + *space_id = mp_decode_uint(&data); + + for (uint32_t i = 1; i < BOX_SPACE_FIELD_OPTS; i++) + mp_next(&data); + if (mp_typeof(*data) != MP_MAP) + return false; + uint32_t map_size = mp_decode_map(&data); + + for (uint32_t i = 0; i < map_size; i++) { + if (mp_typeof(*data) != MP_STR) + return false; + uint32_t len; + const char *str = mp_decode_str(&data, &len); + if (len != strlen("type") || memcmp("type", str, len) != 0) { + mp_next(&data); + continue; + } + if (mp_typeof(*data) != MP_STR) + return false; + str = mp_decode_str(&data, &len); + enum space_type space_type = strnindex(space_type_strs, str, + len, SPACE_TYPE_DEFAULT); + return space_type == SPACE_TYPE_TEMPORARY; + } + return false; +} diff --git a/src/box/space_def.h b/src/box/space_def.h index e63b3cdf44..4e21f748a8 100644 --- a/src/box/space_def.h +++ b/src/box/space_def.h @@ -56,6 +56,7 @@ enum space_type { SPACE_TYPE_DEFAULT = -1, SPACE_TYPE_NORMAL = 0, SPACE_TYPE_DATA_TEMPORARY = 1, + SPACE_TYPE_TEMPORARY = 2, space_type_MAX, }; @@ -80,6 +81,12 @@ struct space_opts { * - changes are not part of a snapshot * - in SQL: space_def memory is allocated on region and * does not require manual release. + * + * If set to SPACE_TYPE_TEMPORARY: + * - all of the above, but + * - metadata is not persisted (doesn't exist at server start) + * - metadata is not replicated (doesn't exist on replicas) + * - this value cannot be changed from or to even for an empty space */ enum space_type type; /** @@ -138,6 +145,16 @@ space_opts_is_data_temporary(const struct space_opts *opts) return opts->type != SPACE_TYPE_NORMAL; } +/** + * Check if the space is temporary. + */ +static inline bool +space_opts_is_temporary(const struct space_opts *opts) +{ + assert(opts->type != SPACE_TYPE_DEFAULT); + return opts->type == SPACE_TYPE_TEMPORARY; +} + /** Space metadata. */ struct space_def { /** Space id. */ @@ -233,6 +250,15 @@ space_tuple_format_new(struct tuple_format_vtab *vtab, void *engine, struct key_def *const *keys, uint16_t key_count, const struct space_def *def); +/** + * Check if msgpack array pointed to by @a data represents a space definition + * tuple which corresponds to a temporary space. + * If @a space_id is not NULL the id of the space will be written into it in + * case of success. + */ +bool +space_def_tuple_is_temporary(const char *data, uint32_t *space_id); + #if defined(__cplusplus) } /* extern "C" */ diff --git a/src/box/sql/select.c b/src/box/sql/select.c index bd452b4398..fecf59728c 100644 --- a/src/box/sql/select.c +++ b/src/box/sql/select.c @@ -4866,7 +4866,7 @@ selectExpander(Walker * pWalker, Select * p) ExprList *pEList; struct SrcList_item *pFrom; Expr *pE, *pRight, *pExpr; - u16 selFlags = p->selFlags; + u32 selFlags = p->selFlags; p->selFlags |= SF_Expanded; if (NEVER(p->pSrc == 0) || (selFlags & SF_Expanded) != 0) { diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c index 927affbcf9..a7e57068b7 100644 --- a/src/box/sql/vdbe.c +++ b/src/box/sql/vdbe.c @@ -4336,7 +4336,7 @@ case OP_GenSpaceid: { assert(pOp->p1 > 0); pOut = vdbe_prepare_null_out(p, pOp->p1); uint32_t u; - if (box_generate_space_id(&u) != 0) + if (box_generate_space_id(&u, false) != 0) goto abort_due_to_error; mem_set_uint(pOut, u); break; diff --git a/src/box/tuple_constraint_fkey.c b/src/box/tuple_constraint_fkey.c index a1a1209de3..9c7c343839 100644 --- a/src/box/tuple_constraint_fkey.c +++ b/src/box/tuple_constraint_fkey.c @@ -629,6 +629,12 @@ tuple_constraint_fkey_check_spaces(struct tuple_constraint *constr, struct space *space, struct space *foreign_space) { + if (space_is_temporary(foreign_space)) { + diag_set(ClientError, ER_CREATE_FOREIGN_KEY, + constr->def.name, constr->space->def->name, + "foreign space can't be temporary"); + return -1; + } if (space_is_data_temporary(foreign_space) && !space_is_data_temporary(space)) { diag_set(ClientError, ER_CREATE_FOREIGN_KEY, diff --git a/src/box/txn.c b/src/box/txn.c index b824ceff4f..11837c0327 100644 --- a/src/box/txn.c +++ b/src/box/txn.c @@ -877,7 +877,9 @@ txn_journal_entry_new(struct txn *txn) rlist_splice(&txn->on_commit, &stmt->on_commit); } - /* A read (e.g. select) request */ + /* A read (e.g. select) request or + * a temporary's space metadata update. + */ if (stmt->row == NULL) continue; @@ -1554,3 +1556,12 @@ txn_attach(struct txn *txn) trigger_add(&fiber()->on_yield, &txn->fiber_on_yield); trigger_add(&fiber()->on_stop, &txn->fiber_on_stop); } + +void +txn_stmt_mark_as_temporary(struct txn *txn, struct txn_stmt *stmt) +{ + assert(stmt->row != NULL); + /* Revert row counter increases. */ + txn_update_row_counts(txn, stmt, -1); + stmt->row = NULL; +} diff --git a/src/box/txn.h b/src/box/txn.h index e946f66cf5..f0fd45d95b 100644 --- a/src/box/txn.h +++ b/src/box/txn.h @@ -841,6 +841,24 @@ txn_is_fully_local(const struct txn *txn) txn->n_applier_rows == 0; } +/** + * Mark @a stmt as temporary by removing the associated stmt->row + * and update @a txn accordingly. + * + * This function is called from on_replace_dd_* triggers to filter + * temporary space's metadata updates from WAL. + * + * NOTE: This could also be implemented by just not creating the stmt->row + * when txn_commit_stmt is called, but it would be less efficient that way + * as it would require putting an expensive check on a hot path. + * Doing the check inside the on_replace trigger is much cheaper + * as the data we're interested in (result of space_is_temporary(space)) + * is readily available at that point. + * The downside of confusing control flow is outweighed by the efficiency. + */ +void +txn_stmt_mark_as_temporary(struct txn *txn, struct txn_stmt *stmt); + /** * End a statement. In autocommit mode, end * the current transaction as well. diff --git a/test/box-luatest/fully-temporary_spaces_test.lua b/test/box-luatest/fully-temporary_spaces_test.lua new file mode 100644 index 0000000000..9e6e108b48 --- /dev/null +++ b/test/box-luatest/fully-temporary_spaces_test.lua @@ -0,0 +1,523 @@ +local fio = require('fio') +local xlog = require('xlog') +local server = require('luatest.server') +local replica_set = require('luatest.replica_set') +local t = require('luatest') +local g = t.group() +local _ = nil + +g.before_all(function() + g.server = server:new({alias = 'master'}) +end +) +g.after_all(function() + _ = g.server.process and g.server:stop() + _ = g.replica_set and g.replica_set:drop() +end) + +g.before_each(function() + if not g.server.process then + g.server:start() + end +end) + +g.after_each(function() + _ = g.server.process and g.server:exec(function() + box.cfg { read_only = false } + for _, s in pairs({'test', 'datatemp', 'temp'}) do + local _ = box.space[s] and box.space[s]:drop() + end + end) +end) + +-- check basic invariants of temporary spaces +g.test_temporary_create = function() + g.server:exec(function() + local _space = box.space._space + + local s = box.schema.space.create('temp', { type = 'temporary' }) + t.assert_equals(s.temporary, true) + t.assert_equals(s.type, 'temporary') + + -- + -- Check automatic/manual space id. + -- + s = box.schema.space.create('temp2', { type = 'temporary', id = 9999 }) + t.assert_equals(s.type, 'temporary') + s:drop() + + -- If id is not specified, it will be chosen in the special range. + s = box.schema.space.create('temp2', { type = 'temporary' }) + t.assert_equals(s.id, box.space.temp.id + 1) + + s = box.schema.space.create('temp3', { type = 'temporary' }) + t.assert_equals(s.id, box.space.temp.id + 2) + + -- Now we have an unoccupied space id box.space.temp.id + 1 + box.space.temp2:drop() + + -- But it's not chosen, because space ids grow, until they can't + s = box.schema.space.create('temp4', { type = 'temporary' }) + t.assert_equals(s.id, box.space.temp.id + 3) + s:drop() + + -- Now there's no more room to grow... + local BOX_SPACE_MAX = 0x7fffffff + box.schema.space.create('temp2', { type = 'temporary', + id = BOX_SPACE_MAX }) + + -- ... therefore we start filling in the gaps + s = box.schema.space.create('temp4', { type = 'temporary' }) + t.assert_equals(s.id, box.space.temp.id + 1) + s:drop() + + box.space.temp2:drop() + box.space.temp3:drop() + + -- + -- Here's all the ways you can create a data-temporary space now + -- + s = box.schema.space.create('datatemp', { temporary = true }) + t.assert_equals(s.temporary, true) + t.assert_equals(s.type, 'data-temporary') + s:drop() + + s = box.schema.space.create('datatemp', { type = 'data-temporary' }) + t.assert_equals(s.temporary, true) + t.assert_equals(s.type, 'data-temporary') + s:drop() + + t.assert_error_msg_contains( + "only one of 'type' or 'temporary' may be specified", + box.schema.space.create, 'temp', + { type = 'data-temporary', temporary = true } + ) + + -- + -- Here's all the ways you can create a normal space now + -- + s = box.schema.space.create('normal', { temporary = false }) + t.assert_equals(s.temporary, false) + t.assert_equals(s.type, 'normal') + s:drop() + + s = box.schema.space.create('normal', { type = 'normal' }) + t.assert_equals(s.temporary, false) + t.assert_equals(s.type, 'normal') + s:drop() + + t.assert_error_msg_contains( + "only one of 'type' or 'temporary' may be specified", + box.schema.space.create, 'temp', + { type = 'normal', temporary = false } + ) + + + t.assert_error_msg_contains( + "only one of 'type' or 'temporary' may be specified", + box.schema.space.create, 'temp', + { type = 'temporary', temporary = true } + ) + + t.assert_error_msg_contains( + "unknown space type, must be one of: 'normal', 'data-temporary'," .. + " 'temporary'", + box.schema.space.create, 'super', { type = 'super-temporary' } + ) + + t.assert_error_msg_contains( + "engine does not support data-temporary spaces", + box.schema.space.create, + 'no-vinyl', { engine = 'vinyl', type = 'temporary' } + ) + + s = box.schema.space.create('datatemp', { type = 'data-temporary' }) + t.assert_error_msg_contains( + "temporariness cannot change", + s.alter, s, { type = 'temporary' } + ) + t.assert_error_msg_contains( + "temporariness cannot change", + _space.update, + _space, s.id, {{'=', 6, { type = 'temporary' }}} + ) + + s = box.space.temp + t.assert_error_msg_contains( + "temporariness cannot change", + s.alter, s, { type = 'data-temporary' } + ) + t.assert_error_msg_contains( + "temporariness cannot change", + _space.update, + _space, s.id, {{'=', 6, { type = 'data-temporary' }}} + ) + end) +end + +-- check which features aren't supported for temporary spaces +g.test_temporary_dont_support = function() + g.server:exec(function() + local s = box.schema.space.create('temp', { type = 'temporary', + id = 1111111111 }) + s:format {{'k', 'unsigned'}} + s:create_index('pk') + box.schema.func.create('foo', { + is_sandboxed = true, + is_deterministic = true, + body = "function() end" + }) + t.assert_error_msg_equals( + "temporary space does not support functional indexes", + s.create_index, s, 'func', { func = 'foo' } + ) + + t.assert_error_msg_equals( + "temporary space does not support privileges", + box.schema.user.grant, 'guest', 'read', 'space', s.name + ) + end) +end + +-- check which sql features aren't supported for temporary spaces +g.test_temporary_sql_dont_support = function() + g.server:exec(function() + local _space = box.space._space + + box.schema.space.create('temp', { type = 'temporary', + id = 1111111112 }) + box.schema.space.create('test', { format = { 'k', 'unsigned' } }) + + local function sql(stmt) + local ok, err = box.execute(stmt) + local _ = ok == nil and error(err) + end + + t.assert_error_msg_contains( + "triggers are not supported for temporary spaces", + sql, [[ + create trigger "my_trigger" before insert on "temp" + for each row begin + insert into "temp" values (1); + end; + ]] + ) + + t.assert_error_msg_contains( + "sequences are not supported for temporary spaces", + sql, [[ + alter table "temp" + add column "k" integer primary key autoincrement + ]] + ) + + -- alter table works though + sql([[ alter table "temp" add column "k" int primary key ]]) + + t.assert_error_msg_contains( + "foreign space can't be temporary", + sql, [[ + create table "my_table" ( + "id" integer primary key, + "k" integer, + foreign key ("k") references "temp" ("k") + ) + ]] + ) + + t.assert_error_msg_contains( + "temporary space does not support constraints", + sql, [[ + alter table "temp" + add constraint "fk" foreign key ("k") references "test" ("k") + ]] + ) + + -- CREATE VIEW + + local function assert_create_view_not_supported(create_view_sql) + t.assert_error_msg_contains( + "CREATE VIEW does not support temporary spaces", + sql, create_view_sql + ) + t.assert_error_msg_contains( + "CREATE VIEW does not support temporary spaces", + _space.insert, + _space, { + 9999, 1, 'my_view', 'memtx', 1, + { sql = create_view_sql, view = true }, + {{ type = 'unsigned', name = 'ID', + nullable_action = 'none', is_nullable = true }}, + } + ) + end + + box.space._session_settings:update({'sql_seq_scan'}, {{'=', 2, true}}) + assert_create_view_not_supported([[ + create view "my_view" as select * from "temp" + ]]) + + assert_create_view_not_supported([[ + create view "my_view" as + select * from (select * from (select * from "temp")) + ]]) + + assert_create_view_not_supported([[ + create view "my_view" as + select * from (select 1 x) where x in (select * from "temp") + ]]) + + assert_create_view_not_supported([[ + create view "my_view" as values ((select * from "temp")) + ]]) + + assert_create_view_not_supported([[ + create view "my_view" as + values (1) union select * from "temp" + ]]) + + -- CHECK constraints + + t.assert_error_msg_contains( + "temporary space does not support constraints", + sql, [[ + alter table "temp" add constraint "c2" check ( "k" > 0 ) + ]] + ) + end) +end + +-- check that CRUD operations on space meta-data works in read-only mode +g.test_temporary_read_only = function() + g.server:exec(function() + local _space, _index = box.space._space, box.space._index + box.cfg { read_only = true } + t.assert(box.info.ro) + + t.assert_error_msg_contains( + "Can't modify data on a read-only instance", + box.schema.space.create, 'datatemp', { temporary = true } + ) + + -- space create works + local s = box.schema.space.create('temp', { type = 'temporary', + id = 1111111113 }) + + -- space rename works + s:rename('newname') + t.assert_equals(s.name, box.space.newname.name) + _space:update(s.id, {{'=', 3, 'temp'}}) + t.assert_equals(s.name, box.space.temp.name) + + -- format change works + t.assert_equals(s:format(), {}) + s:format {{'k', 'number'}} + t.assert_equals(s:format(), {{name = 'k', type = 'number'}}) + s:alter { format = {{'k', 'number'}, {'v', 'any'}} } + t.assert_equals( + s:format(), + {{name = 'k', type = 'number'}, {name = 'v', type = 'any'}} + ) + + -- index create works + local i = s:create_index('first', { type = 'HASH' }) + t.assert(s.index.first) + + -- index rename works + i:rename('pk') + t.assert_equals(i.name, s.index.pk.name) + _index:update({s.id, i.id}, {{'=', 3, 'first'}}) + t.assert_equals(i.name, s.index.first.name) + + -- index alter works + t.assert_equals(i.type, 'HASH') + i:alter { type = 'TREE' } + t.assert_equals(i.type, 'TREE') + + -- on_replace triggers even work + local tbl = {} + s:on_replace(function(_, new_tuple) table.insert(tbl, new_tuple) end) + t.assert_equals(#s:on_replace(), 1) + s:auto_increment{'foo'} + t.assert_equals(tbl, {{1, 'foo'}}) + s:on_replace(nil, s:on_replace()[1]) + t.assert_equals(#s:on_replace(), 0) + + -- truncate works + t.assert_equals(s:len(), 1) + s:truncate() + t.assert_equals(s:len(), 0) + + box.space._session_settings:update({'sql_seq_scan'}, {{'=', 2, true}}) + -- basic sql works + box.execute [[ insert into "temp" values (420, 69), (13, 37) ]] + t.assert_equals( + box.execute [[ select * from "temp" ]].rows, + {{13, 37}, {420, 69}} + ) + box.execute [[ truncate table "temp" ]] + local count = box.execute [[ select count(*) from "temp" ]].rows[1] + t.assert_equals(count, {0}) + box.execute [[ drop table "temp" ]] + t.assert(not box.space.temp) + + s = box.schema.space.create('temp', { type = 'temporary', + id = 1111111114 }) + + -- all kinds of indexes work + s:create_index('tree', { type = 'TREE' }) + s:create_index('hash', { type = 'HASH' }) + s:create_index('rtree', { + type = 'RTREE', unique = false, parts = {2, 'array'} + }) + s:create_index('bitset', { + type = 'BITSET', unique = false, parts = {3, 'unsigned'} + }) + + local row = box.tuple.new {1, {2, 3}, 4} + s:insert(row) + + t.assert_equals(s.index.hash:get(1), row) + t.assert_equals(s.index.tree:get(1), row) + t.assert_equals(s.index.rtree:select({2, 3}), {row}) + t.assert_equals(s.index.bitset:select(4), {row}) + + s:truncate() + + -- index drop works + s.index.bitset:drop() + s.index.rtree:drop() + s.index.hash:drop() + s.index.tree:drop() + + -- space drop works + s:drop() + end) +end + +-- check temporary space definitions aren't replicated +g.test_meta_data_not_replicated = function() + g.server:stop() + g.replica_set = replica_set:new{} + local replication = { + server.build_listen_uri('master', g.replica_set.id), + server.build_listen_uri('replica_1', g.replica_set.id), + } + g.replica_set:build_and_add_server{ + alias = 'master', + box_cfg = { read_only = false, replication = replication }, + } + g.replica_set:build_and_add_server{ + alias = 'replica_1', + box_cfg = { read_only = true, replication = replication }, + } + g.replica_set:start() + + g.master = g.replica_set:get_server('master') + g.master:exec(function() + box.schema.space.create('temp', { type = 'temporary', + id = 1111111115 }) + box.schema.space.create('datatemp', { temporary = true }) + end) + + g.replica_1 = g.replica_set:get_server('replica_1') + g.replica_1:wait_for_vclock_of(g.master) + -- temporary is not replicated via relay + g.replica_1:exec(function() + t.assert(box.space.datatemp) + t.assert(not box.space.temp) + end) + + g.replica_2 = g.replica_set:build_and_add_server{ + alias = 'replica_2', + box_cfg = { replication = replication }, + } + g.replica_2:start{wait_until_ready = true} + g.replica_2:wait_for_vclock_of(g.master) + -- temporary is not replicated via snapshot + g.replica_2:exec(function() + t.assert(box.space.datatemp) + t.assert(not box.space.temp) + end) +end + +g.after_test('test_meta_data_not_replicated', function() + g.replica_set:drop() +end) + +local function check_wal_and_snap(expected) + local id_min = g.server:eval 'return box.schema.SPACE_ID_TEMPORARY_MIN' + + local function is_meta_local_space_id(id) + return type(id) == 'number' and (id >= id_min or id == 9999) + end + + local function contains_meta_local_space_id(entry) + local key, tuple = entry.BODY.key, entry.BODY.tuple + return key and key:pairs():any(is_meta_local_space_id) + or tuple and tuple:pairs():any(is_meta_local_space_id) + end + + for _, f in pairs(fio.listdir(g.server.workdir)) do + if not f:endswith('.xlog') and not f:endswith('.xlog') then + goto continue + end + + local path = fio.pathjoin(g.server.workdir, f) + local x = xlog.pairs(path) + :filter(contains_meta_local_space_id) + :totable() + if #x > 0 then + x = x[1] + x = { + type = x.HEADER.type, + space_id = x.BODY.space_id, + tuple = x.BODY.tuple, + key = x.BODY.key, + } + t.assert_equals({f, x}, {f, expected}) + end + + ::continue:: + end +end + +-- check temporary space definitions aren't persisted +g.test_meta_data_not_persisted = function() + g.server:exec(function() + -- temporary space's metadata will go into WAL and snapshot + local s = box.schema.space.create('datatemp', { temporary = true }) + s:create_index('pk') + s:put{1,2,3} + s:truncate() + + -- temporary space's metadata will not go into WAL or snapshot + s = box.schema.space.create('temp', { type = 'temporary', + id = 1111111116 }) + s:create_index('pk') + s:put{1,2,3} + s:truncate() + + -- there's nothing special about space ids + s = box.schema.space.create('test', { type = 'temporary', + id = 9999 }) + s:create_index('pk') + s:put{1,2,3} + s:truncate() + end) + + check_wal_and_snap({}) + + g.server:restart() + + g.server:exec(function() + -- temporary space exists after restart + t.assert(box.space.datatemp) + -- temporary space doesn't + t.assert(not box.space.temp) + + box.snapshot() + end) + + check_wal_and_snap({}) +end diff --git a/test/box/access.result b/test/box/access.result index 684b48282b..fc4f7478bd 100644 --- a/test/box/access.result +++ b/test/box/access.result @@ -1519,6 +1519,9 @@ box.schema.user.grant("tester", "read", "space", "_user") box.schema.user.grant("tester", "read", "space", "_func") --- ... +box.schema.user.grant("tester", "read", "space", "_truncate") +--- +... -- failed create box.session.su("tester") --- diff --git a/test/box/access.test.lua b/test/box/access.test.lua index 0c0a885865..29e092ac39 100644 --- a/test/box/access.test.lua +++ b/test/box/access.test.lua @@ -588,6 +588,7 @@ box.schema.func.create('test_func') box.session.su("admin") box.schema.user.grant("tester", "read", "space", "_user") box.schema.user.grant("tester", "read", "space", "_func") +box.schema.user.grant("tester", "read", "space", "_truncate") -- failed create box.session.su("tester") box.schema.space.create("test_space") diff --git a/test/box/access_misc.result b/test/box/access_misc.result index c4b64e2bb7..be2464104f 100644 --- a/test/box/access_misc.result +++ b/test/box/access_misc.result @@ -78,7 +78,7 @@ s:delete(1) ... s:drop() --- -- error: Write access to space '_space_sequence' is denied for user 'testus' +- error: Read access to space '_space_sequence' is denied for user 'testus' ... -- -- Check double revoke @@ -126,7 +126,7 @@ s:insert({3}) ... s:drop() --- -- error: Write access to space '_space_sequence' is denied for user 'testus' +- error: Read access to space '_space_sequence' is denied for user 'testus' ... session.su('admin') --- @@ -169,7 +169,7 @@ s:delete({3}) ... s:drop() --- -- error: Write access to space '_space_sequence' is denied for user 'guest' +- error: Read access to space '_space_sequence' is denied for user 'guest' ... gs = box.schema.space.create('guest_space') --- diff --git a/test/box/alter.test.lua b/test/box/alter.test.lua index 9413bf0f6b..8ebf3d9daf 100644 --- a/test/box/alter.test.lua +++ b/test/box/alter.test.lua @@ -660,4 +660,4 @@ s.index.test1:bsize() < s.index.test2:bsize() s.index.test1:bsize() == s.index.test33:bsize() s.index.test1:bsize() < s.index.test4:bsize() -s:drop() \ No newline at end of file +s:drop() diff --git a/test/box/stat.result b/test/box/stat.result index 9bd9bb22b5..45b51e9529 100644 --- a/test/box/stat.result +++ b/test/box/stat.result @@ -33,6 +33,11 @@ box.stat.ERROR.total space = box.schema.space.create('tweedledum') --- ... +-- create_space performs a select when choosing a new space id +box.stat.SELECT.total +--- +- 1 +... index = space:create_index('primary', { type = 'hash' }) --- ... @@ -59,7 +64,7 @@ box.stat.REPLACE.total ... box.stat.SELECT.total --- -- 4 +- 5 ... -- check exceptions space:get('Impossible value') @@ -77,14 +82,14 @@ space:get(1) ... box.stat.SELECT.total --- -- 5 +- 6 ... space:get(11) --- ... box.stat.SELECT.total --- -- 6 +- 7 ... space:select(5) --- @@ -92,7 +97,7 @@ space:select(5) ... box.stat.SELECT.total --- -- 7 +- 8 ... space:select(15) --- @@ -100,14 +105,14 @@ space:select(15) ... box.stat.SELECT.total --- -- 8 +- 9 ... for _ in space:pairs() do end --- ... box.stat.SELECT.total --- -- 9 +- 10 ... -- reset box.stat.reset() diff --git a/test/box/stat.test.lua b/test/box/stat.test.lua index 6ee1ec440b..ab4b3d134f 100644 --- a/test/box/stat.test.lua +++ b/test/box/stat.test.lua @@ -11,6 +11,9 @@ box.stat.SELECT.total box.stat.ERROR.total space = box.schema.space.create('tweedledum') +-- create_space performs a select when choosing a new space id +box.stat.SELECT.total + index = space:create_index('primary', { type = 'hash' }) -- check stat_cleanup -- GitLab