diff --git a/changelogs/unreleased/fully-temporary-spaces.md b/changelogs/unreleased/fully-temporary-spaces.md new file mode 100644 index 0000000000000000000000000000000000000000..c92aa212d90026b68cdc5a6d67862d65abbc7702 --- /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 c065a4a210aaaac7ea30f42c68ebc9a77b30e0ce..7811fd278776f8e2f4b59cab4e96e28c4b099f0e 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 b5614051382dfa2686fbdfe71eebc643426e2ad7..588b7d29f4e8399e5f6c6a575d84bb447942e7a6 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 af95eedc0c94489fb884de826b1150a9d496f1f4..5f2a70ad96e0460b6b21e81cf7b4032f1c2fea77 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 f0676873afe68bc2e7f932ea4a10a13b25ad560c..6da577f54e84c1b587475bcf8c2ae6c9985fc41a 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 2097128f55d03a3d9301492bba736db6235eed51..7a978636dad6eafb575b313fb123f1a861b5d957 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 953077bde704c312270cde63f007f042bb53fbec..ad554462f41789d20b863f48f6c4d9f393b8517e 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 c57fe518d622e3fa14362c75f46ebbddf7ae1ae6..8d9ded8789243d8a8b77761f1e7b94680df92a38 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 34529790d275db9490c7231ed988d49da036f45d..7d2bf8602b344d36ddd3337867e1fac9440d0782 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 36cbe1d8481384257629ecf9b704ad34e736b3cc..84d2f72c06a93d7df36811b467a09bdf7acfdf2a 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 b698dd27e9f079cdec6e40c333c29da7db8e0c21..e148536db755c1f4e7e48dbcc99734724fbe7171 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 e63b3cdf4474c2cf7989211d71dc9d1ec9ebc482..4e21f748a82c52e06fffc555502efd0f68ff09a5 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 bd452b43986b02f06742264ecffd8960206d0d1c..fecf59728cfd8cfe47cb4a3c680eb6562ef78550 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 927affbcf9b5ef290839f4b190467047f72011eb..a7e57068b724e4d241f104a7e9c1fa9da75ac47a 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 a1a1209de36301ad02712263391011d1fa2d30c5..9c7c343839ca1ef6e528dd54dc2017dcf8c6329a 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 b824ceff4f4b2e7fc0f91557574c7969ea922204..11837c0327b0850c9a4abd5c5bb24639e84639fb 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 e946f66cf5575b2494e0c18c8ff52dc2a0f4de16..f0fd45d95b237dc4d77202aecea0b404ff876c26 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 0000000000000000000000000000000000000000..9e6e108b482dab80cd3c8eecbec0cacac7f22a5e --- /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 684b48282bd29e23da432e82541fad631e37995c..fc4f7478bd0685a4edb46c9380948ca20185d3d3 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 0c0a885865f07997c9db887847cb25a465a181c7..29e092ac39377a551d0ba8bed220c1682e0635c6 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 c4b64e2bb7f73ee00ea8d05a56f239c52afa4146..be2464104f95db6613121d802b2d4f1972198f1f 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 9413bf0f6b904e107b0f9277dedc680773c9568b..8ebf3d9daf6d3c96a0f60616f4ae9403748dce38 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 9bd9bb22b5dd9e4932358a7b60292f8db0b8a7d9..45b51e952910b67661d165c6ed5ff2ec55dba61a 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 6ee1ec440bbaa4220b0133bf9e20552a5d6ba2fa..ab4b3d134f71b4f89e59b5b323bb404dac63b0bd 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