[tarantool-patches] [PATCH 2/2] sql: transactional DDL
Vladislav Shpilevoy
v.shpilevoy at tarantool.org
Sat Jul 20 01:49:41 MSK 2019
Box recently added support of transactional DDL allowing to do
any number of non-yielding DDL operations atomically. This is
really a big relief of one of the biggest pains of SQL. Before
this patch each multirow SQL DDL statement needed to prepare its
own rollback procedure for a case if something would go wrong.
Now with box support SQL wraps each DDL statement into a
transaction, and doesn't need own escape-routes in a form of
'struct save_record' and others.
Closes #4086
---
src/box/sql/build.c | 159 ++-----------
src/box/sql/parse.y | 12 +-
src/box/sql/prepare.c | 1 -
src/box/sql/sqlInt.h | 7 +-
src/box/sql/trigger.c | 6 +-
src/box/sql/vdbe.c | 19 +-
test/sql/ddl.result | 356 +++++++++++++++++++++++++++++
test/sql/ddl.test.lua | 187 +++++++++++++++
test/sql/errinj.result | 50 ----
test/sql/errinj.test.lua | 22 --
test/sql/view_delayed_wal.result | 12 +-
test/sql/view_delayed_wal.test.lua | 8 +-
12 files changed, 598 insertions(+), 241 deletions(-)
create mode 100644 test/sql/ddl.result
create mode 100644 test/sql/ddl.test.lua
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 2aefa2a3f..4884a7855 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -57,64 +57,6 @@
#include "box/tuple_format.h"
#include "box/coll_id_cache.h"
-/**
- * Structure that contains information about record that was
- * inserted into system space.
- */
-struct saved_record
-{
- /** A link in a record list. */
- struct rlist link;
- /** Id of space in which the record was inserted. */
- uint32_t space_id;
- /** First register of the key of the record. */
- int reg_key;
- /** Number of registers the key consists of. */
- int reg_key_count;
- /** The address of the opcode. */
- int op_addr;
- /** Flag to show that operation is SInsert. */
- bool is_insert;
-};
-
-/**
- * Save inserted in system space record in list. This procedure is
- * called after generation of either OP_SInsert or OP_NoColflict +
- * OP_SetDiag. In the first case, record inserted to the system
- * space is supposed to be deleted on error; in the latter - jump
- * target specified in OP_SetDiag should be adjusted to the start
- * of clean-up routines (current entry isn't inserted to the space
- * yet, so there's no need to delete it).
- *
- * @param parser SQL Parser object.
- * @param space_id Id of table in which record is inserted.
- * @param reg_key Register that contains first field of the key.
- * @param reg_key_count Exact number of fields of the key.
- * @param op_addr Address of opcode (OP_SetDiag or OP_SInsert).
- * Used to fix jump target (see
- * sql_finish_coding()).
- * @param is_insert_op Whether opcode is OP_SInsert or not.
- */
-static inline void
-save_record(struct Parse *parser, uint32_t space_id, int reg_key,
- int reg_key_count, int op_addr, bool is_insert_op)
-{
- struct saved_record *record =
- region_alloc(&parser->region, sizeof(*record));
- if (record == NULL) {
- diag_set(OutOfMemory, sizeof(*record), "region_alloc",
- "record");
- parser->is_aborted = true;
- return;
- }
- record->space_id = space_id;
- record->reg_key = reg_key;
- record->reg_key_count = reg_key_count;
- record->op_addr = op_addr;
- record->is_insert = is_insert_op;
- rlist_add_entry(&parser->record_list, record, link);
-}
-
void
sql_finish_coding(struct Parse *parse_context)
{
@@ -122,52 +64,6 @@ sql_finish_coding(struct Parse *parse_context)
struct sql *db = parse_context->db;
struct Vdbe *v = sqlGetVdbe(parse_context);
sqlVdbeAddOp0(v, OP_Halt);
- /*
- * In case statement "CREATE TABLE ..." fails it can
- * left some records in system spaces that shouldn't be
- * there. To clean-up properly this code is added. Last
- * record isn't deleted because if statement fails than
- * it won't be created. This code works the same way for
- * other "CREATE ..." statements but it won't delete
- * anything as these statements create no more than one
- * record. Hence for processed insertions we should remove
- * entries from corresponding system spaces alongside
- * with fixing jump address for OP_SInsert opcode in
- * case it fails during VDBE runtime; for OP_SetDiag only
- * adjust jump target to the start of clean-up program
- * for already inserted entries.
- */
- if (!rlist_empty(&parse_context->record_list)) {
- struct saved_record *record =
- rlist_shift_entry(&parse_context->record_list,
- struct saved_record, link);
- /*
- * Set jump target for OP_SetDiag and OP_SInsert.
- */
- sqlVdbeChangeP2(v, record->op_addr, v->nOp);
- MAYBE_UNUSED const char *comment =
- "Delete entry from %s if CREATE TABLE fails";
- rlist_foreach_entry(record, &parse_context->record_list, link) {
- if (record->is_insert) {
- int rec_reg = ++parse_context->nMem;
- sqlVdbeAddOp3(v, OP_MakeRecord, record->reg_key,
- record->reg_key_count, rec_reg);
- sqlVdbeAddOp2(v, OP_SDelete, record->space_id,
- rec_reg);
- MAYBE_UNUSED struct space *space =
- space_by_id(record->space_id);
- VdbeComment((v, comment, space_name(space)));
- }
- /*
- * Set jump target for OP_SetDiag and
- * OP_SInsert.
- */
- sqlVdbeChangeP2(v, record->op_addr, v->nOp);
- }
- sqlVdbeAddOp1(v, OP_Halt, -1);
- VdbeComment((v,
- "Exit with an error if CREATE statement fails"));
- }
if (db->mallocFailed)
parse_context->is_aborted = true;
@@ -933,8 +829,7 @@ vdbe_emit_create_index(struct Parse *parse, struct space_def *def,
sqlVdbeAddOp4(v, OP_Blob, index_parts_sz, entry_reg + 5,
SQL_SUBTYPE_MSGPACK, index_parts, P4_STATIC);
sqlVdbeAddOp3(v, OP_MakeRecord, entry_reg, 6, tuple_reg);
- sqlVdbeAddOp3(v, OP_SInsert, BOX_INDEX_ID, 0, tuple_reg);
- save_record(parse, BOX_INDEX_ID, entry_reg, 2, v->nOp - 1, true);
+ sqlVdbeAddOp2(v, OP_SInsert, BOX_INDEX_ID, tuple_reg);
return;
error:
parse->is_aborted = true;
@@ -991,9 +886,8 @@ vdbe_emit_space_create(struct Parse *pParse, int space_id_reg,
sqlVdbeAddOp4(v, OP_Blob, table_stmt_sz, iFirstCol + 6,
SQL_SUBTYPE_MSGPACK, table_stmt, P4_STATIC);
sqlVdbeAddOp3(v, OP_MakeRecord, iFirstCol, 7, tuple_reg);
- sqlVdbeAddOp3(v, OP_SInsert, BOX_SPACE_ID, 0, tuple_reg);
+ sqlVdbeAddOp2(v, OP_SInsert, BOX_SPACE_ID, tuple_reg);
sqlVdbeChangeP5(v, OPFLAG_NCHANGE);
- save_record(pParse, BOX_SPACE_ID, iFirstCol, 1, v->nOp - 1, true);
return;
error:
pParse->is_aborted = true;
@@ -1102,8 +996,7 @@ vdbe_emit_ck_constraint_create(struct Parse *parser,
* Occupy registers for 5 fields: each member in
* _ck_constraint space plus one for final msgpack tuple.
*/
- int ck_constraint_reg = parser->nMem + 1;
- parser->nMem += 6;
+ int ck_constraint_reg = sqlGetTempRange(parser, 6);
sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg);
sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 1, 0,
sqlDbStrDup(db, ck_def->name), P4_DYNAMIC);
@@ -1120,13 +1013,12 @@ vdbe_emit_ck_constraint_create(struct Parse *parser,
if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0,
ck_constraint_reg, 2,
ER_CONSTRAINT_EXISTS, error_msg,
- false, OP_NoConflict, true) != 0)
+ false, OP_NoConflict) != 0)
return;
- sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0,
+ sqlVdbeAddOp2(v, OP_SInsert, BOX_CK_CONSTRAINT_ID,
ck_constraint_reg + 5);
- save_record(parser, BOX_CK_CONSTRAINT_ID, ck_constraint_reg, 2,
- v->nOp - 1, true);
VdbeComment((v, "Create CK constraint %s", ck_def->name));
+ sqlReleaseTempRange(parser, ck_constraint_reg, 6);
}
/**
@@ -1148,8 +1040,7 @@ vdbe_emit_fk_constraint_create(struct Parse *parse_context,
* Occupy registers for 9 fields: each member in
* _fk_constraint space plus one for final msgpack tuple.
*/
- int constr_tuple_reg = parse_context->nMem + 1;
- parse_context->nMem += 10;
+ int constr_tuple_reg = sqlGetTempRange(parse_context, 10);
char *name_copy = sqlDbStrDup(parse_context->db, fk->name);
if (name_copy == NULL)
return;
@@ -1185,7 +1076,7 @@ vdbe_emit_fk_constraint_create(struct Parse *parse_context,
BOX_FK_CONSTRAINT_ID, 0,
constr_tuple_reg, 2,
ER_CONSTRAINT_EXISTS, error_msg,
- false, OP_NoConflict, true) != 0)
+ false, OP_NoConflict) != 0)
return;
sqlVdbeAddOp2(vdbe, OP_Bool, fk->is_deferred, constr_tuple_reg + 3);
sqlVdbeAddOp4(vdbe, OP_String8, 0, constr_tuple_reg + 4, 0,
@@ -1228,14 +1119,13 @@ vdbe_emit_fk_constraint_create(struct Parse *parse_context,
parent_links, P4_DYNAMIC);
sqlVdbeAddOp3(vdbe, OP_MakeRecord, constr_tuple_reg, 9,
constr_tuple_reg + 9);
- sqlVdbeAddOp3(vdbe, OP_SInsert, BOX_FK_CONSTRAINT_ID, 0,
- constr_tuple_reg + 9);
+ sqlVdbeAddOp2(vdbe, OP_SInsert, BOX_FK_CONSTRAINT_ID,
+ constr_tuple_reg + 9);
if (parse_context->create_table_def.new_space == NULL) {
sqlVdbeCountChanges(vdbe);
sqlVdbeChangeP5(vdbe, OPFLAG_NCHANGE);
}
- save_record(parse_context, BOX_FK_CONSTRAINT_ID, constr_tuple_reg, 2,
- vdbe->nOp - 1, true);
+ sqlReleaseTempRange(parse_context, constr_tuple_reg, 10);
return;
error:
parse_context->is_aborted = true;
@@ -1331,7 +1221,7 @@ sqlEndTable(struct Parse *pParse)
if (vdbe_emit_halt_with_presence_test(pParse, BOX_SPACE_ID, 2,
name_reg, 1, ER_SPACE_EXISTS,
error_msg, (no_err != 0),
- OP_NoConflict, false) != 0)
+ OP_NoConflict) != 0)
return;
int reg_space_id = getNewSpaceId(pParse);
@@ -1354,18 +1244,13 @@ sqlEndTable(struct Parse *pParse)
int reg_seq_record =
emitNewSysSequenceRecord(pParse, reg_seq_id,
new_space->def->name);
- sqlVdbeAddOp3(v, OP_SInsert, BOX_SEQUENCE_ID, 0,
- reg_seq_record);
- save_record(pParse, BOX_SEQUENCE_ID, reg_seq_record + 1, 1,
- v->nOp - 1, true);
+ sqlVdbeAddOp2(v, OP_SInsert, BOX_SEQUENCE_ID, reg_seq_record);
/* Do an insertion into _space_sequence. */
int reg_space_seq_record = emitNewSysSpaceSequenceRecord(pParse,
reg_space_id, reg_seq_id,
new_space->index[0]->def);
- sqlVdbeAddOp3(v, OP_SInsert, BOX_SPACE_SEQUENCE_ID, 0,
- reg_space_seq_record);
- save_record(pParse, BOX_SPACE_SEQUENCE_ID,
- reg_space_seq_record + 1, 1, v->nOp - 1, true);
+ sqlVdbeAddOp2(v, OP_SInsert, BOX_SPACE_SEQUENCE_ID,
+ reg_space_seq_record);
}
/* Code creation of FK constraints, if any. */
struct fk_constraint_parse *fk_parse;
@@ -1492,7 +1377,7 @@ sql_create_view(struct Parse *parse_context)
if (vdbe_emit_halt_with_presence_test(parse_context, BOX_SPACE_ID, 2,
name_reg, 1, ER_SPACE_EXISTS,
error_msg, (no_err != 0),
- OP_NoConflict, false) != 0)
+ OP_NoConflict) != 0)
goto create_view_fail;
vdbe_emit_space_create(parse_context, getNewSpaceId(parse_context),
@@ -1611,7 +1496,7 @@ vdbe_emit_fk_constraint_drop(struct Parse *parse_context, char *constraint_name,
BOX_FK_CONSTRAINT_ID, 0,
key_reg, 2, ER_NO_SUCH_CONSTRAINT,
error_msg, false,
- OP_Found, false) != 0) {
+ OP_Found) != 0) {
sqlDbFree(parse_context->db, constraint_name);
return;
}
@@ -1643,8 +1528,7 @@ vdbe_emit_ck_constraint_drop(struct Parse *parser, const char *ck_name,
tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT), ck_name);
if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0,
key_reg, 2, ER_NO_SUCH_CONSTRAINT,
- error_msg, false,
- OP_Found, false) != 0)
+ error_msg, false, OP_Found) != 0)
return;
sqlVdbeAddOp3(v, OP_MakeRecord, key_reg, 2, key_reg + 2);
sqlVdbeAddOp2(v, OP_SDelete, BOX_CK_CONSTRAINT_ID, key_reg + 2);
@@ -3310,7 +3194,7 @@ vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
int index_id, int key_reg, uint32_t key_len,
int tarantool_error_code,
const char *error_src, bool no_error,
- int cond_opcode, bool is_clean_needed)
+ int cond_opcode)
{
assert(cond_opcode == OP_NoConflict || cond_opcode == OP_Found);
struct Vdbe *v = sqlGetVdbe(parser);
@@ -3331,10 +3215,7 @@ vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
} else {
sqlVdbeAddOp4(v, OP_SetDiag, tarantool_error_code, 0, 0, error,
P4_DYNAMIC);
- if (is_clean_needed)
- save_record(parser, 0, 0, 0, v->nOp - 1, false);
- else
- sqlVdbeAddOp1(v, OP_Halt, -1);
+ sqlVdbeAddOp2(v, OP_Halt, -1, ON_CONFLICT_ACTION_ABORT);
}
sqlVdbeJumpHere(v, addr);
sqlVdbeAddOp1(v, OP_Close, cursor);
diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y
index 2a60ad25b..06eab7f2d 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -172,6 +172,7 @@ cmd ::= create_table create_table_args.
create_table ::= CREATE TABLE ifnotexists(E) nm(Y). {
create_table_def_init(&pParse->create_table_def, &Y, E);
pParse->create_table_def.new_space = sqlStartTable(pParse, &Y);
+ pParse->initiateTTrans = true;
}
%type ifnotexists {int}
@@ -380,12 +381,14 @@ resolvetype(A) ::= REPLACE. {A = ON_CONFLICT_ACTION_REPLACE;}
cmd ::= DROP TABLE ifexists(E) fullname(X) . {
struct Token t = Token_nil;
drop_table_def_init(&pParse->drop_table_def, X, &t, E);
+ pParse->initiateTTrans = true;
sql_drop_table(pParse);
}
cmd ::= DROP VIEW ifexists(E) fullname(X) . {
struct Token t = Token_nil;
drop_view_def_init(&pParse->drop_view_def, X, &t, E);
+ pParse->initiateTTrans = true;
sql_drop_table(pParse);
}
@@ -399,6 +402,7 @@ cmd ::= CREATE(X) VIEW ifnotexists(E) nm(Y) eidlist_opt(C)
AS select(S). {
if (!pParse->parse_only) {
create_view_def_init(&pParse->create_view_def, &Y, &X, C, S, E);
+ pParse->initiateTTrans = true;
sql_create_view(pParse);
} else {
sql_store_select(pParse, S);
@@ -1404,6 +1408,7 @@ cmd ::= CREATE uniqueflag(U) INDEX ifnotexists(NE) nm(X)
}
create_index_def_init(&pParse->create_index_def, src_list, &X, Z, U,
SORT_ORDER_ASC, NE);
+ pParse->initiateTTrans = true;
sql_create_index(pParse);
}
@@ -1456,6 +1461,7 @@ eidlist(A) ::= nm(Y). {
//
cmd ::= DROP INDEX ifexists(E) nm(X) ON fullname(Y). {
drop_index_def_init(&pParse->drop_index_def, Y, &X, E);
+ pParse->initiateTTrans = true;
sql_drop_index(pParse);
}
@@ -1502,7 +1508,7 @@ cmd ::= CREATE trigger_decl(A) BEGIN trigger_cmd_list(S) END(Z). {
Token all;
all.z = A.z;
all.n = (int)(Z.z - A.z) + Z.n;
- pParse->initiateTTrans = false;
+ pParse->initiateTTrans = true;
sql_trigger_finish(pParse, S, &all);
}
@@ -1652,6 +1658,7 @@ raisetype(A) ::= FAIL. {A = ON_CONFLICT_ACTION_FAIL;}
cmd ::= DROP TRIGGER ifexists(NOERR) fullname(X). {
struct Token t = Token_nil;
drop_trigger_def_init(&pParse->drop_trigger_def, X, &t, NOERR);
+ pParse->initiateTTrans = true;
sql_drop_trigger(pParse);
}
@@ -1671,6 +1678,7 @@ alter_table_start(A) ::= ALTER TABLE fullname(T) . { A = T; }
alter_add_constraint(A) ::= alter_table_start(T) ADD CONSTRAINT nm(N). {
A.table_name = T;
A.name = N;
+ pParse->initiateTTrans = true;
}
cmd ::= alter_add_constraint(N) FOREIGN KEY LP eidlist(FA) RP REFERENCES
@@ -1697,11 +1705,13 @@ unique_spec(U) ::= PRIMARY KEY. { U = SQL_INDEX_TYPE_CONSTRAINT_PK; }
cmd ::= alter_table_start(A) RENAME TO nm(N). {
rename_entity_def_init(&pParse->rename_entity_def, A, &N);
+ pParse->initiateTTrans = true;
sql_alter_table_rename(pParse);
}
cmd ::= ALTER TABLE fullname(X) DROP CONSTRAINT nm(Z). {
drop_fk_def_init(&pParse->drop_fk_def, X, &Z, false);
+ pParse->initiateTTrans = true;
sql_drop_foreign_key(pParse);
}
diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c
index 84fb31bcd..36c21a221 100644
--- a/src/box/sql/prepare.c
+++ b/src/box/sql/prepare.c
@@ -243,7 +243,6 @@ sql_parser_create(struct Parse *parser, struct sql *db, uint32_t sql_flags)
memset(parser, 0, sizeof(struct Parse));
parser->db = db;
parser->sql_flags = sql_flags;
- rlist_create(&parser->record_list);
region_create(&parser->region, &cord()->slabc);
}
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 4f5bad287..99296b4b3 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -2329,11 +2329,6 @@ struct Parse {
* sqlEndTable() function).
*/
struct create_table_def create_table_def;
- /**
- * List of all records that were inserted in system spaces
- * in current statement.
- */
- struct rlist record_list;
bool initiateTTrans; /* Initiate Tarantool transaction */
/** If set - do not emit byte code at all, just parse. */
bool parse_only;
@@ -4542,7 +4537,7 @@ vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
int index_id, int key_reg, uint32_t key_len,
int tarantool_error_code,
const char *error_src, bool no_error,
- int cond_opcode, bool is_clean_needed);
+ int cond_opcode);
/**
* Generate VDBE code to delete records from system _sql_stat1 or
diff --git a/src/box/sql/trigger.c b/src/box/sql/trigger.c
index 562723959..d746ef893 100644
--- a/src/box/sql/trigger.c
+++ b/src/box/sql/trigger.c
@@ -112,8 +112,7 @@ sql_trigger_begin(struct Parse *parse)
name_reg, 1,
ER_TRIGGER_EXISTS,
error_msg, (no_err != 0),
- OP_NoConflict,
- false) != 0)
+ OP_NoConflict) != 0)
goto trigger_cleanup;
}
@@ -421,8 +420,7 @@ sql_drop_trigger(struct Parse *parser)
sqlVdbeAddOp4(v, OP_String8, 0, name_reg, 0, name_copy, P4_DYNAMIC);
if (vdbe_emit_halt_with_presence_test(parser, BOX_TRIGGER_ID, 0,
name_reg, 1, ER_NO_SUCH_TRIGGER,
- error_msg, no_err, OP_Found,
- false) != 0)
+ error_msg, no_err, OP_Found) != 0)
goto drop_trigger_cleanup;
vdbe_code_drop_trigger(parser, trigger_name, true);
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 6a4a303b9..a71b331f8 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -4359,8 +4359,8 @@ case OP_Update: {
break;
}
-/* Opcode: SInsert P1 P2 P3 * P5
- * Synopsis: space id = P1, key = r[P3], on error goto P2
+/* Opcode: SInsert P1 P2 * * P5
+ * Synopsis: space id = P1, key = r[P2]
*
* This opcode is used only during DDL routine.
* In contrast to ordinary insertion, insertion to system spaces
@@ -4373,15 +4373,16 @@ case OP_Update: {
*/
case OP_SInsert: {
assert(pOp->p1 > 0);
- assert(pOp->p2 > 0);
- assert(pOp->p3 >= 0);
+ assert(pOp->p2 >= 0);
- pIn3 = &aMem[pOp->p3];
+ pIn2 = &aMem[pOp->p2];
struct space *space = space_by_id(pOp->p1);
assert(space != NULL);
assert(space_is_system(space));
- if (tarantoolsqlInsert(space, pIn3->z, pIn3->z + pIn3->n) != 0)
- goto jump_to_p2;
+ if (tarantoolsqlInsert(space, pIn2->z, pIn2->z + pIn2->n) != 0) {
+ p->errorAction = ON_CONFLICT_ACTION_ABORT;
+ goto abort_due_to_error;
+ }
if (pOp->p5 & OPFLAG_NCHANGE)
p->nChange++;
break;
@@ -4404,8 +4405,10 @@ case OP_SDelete: {
struct space *space = space_by_id(pOp->p1);
assert(space != NULL);
assert(space_is_system(space));
- if (sql_delete_by_key(space, 0, pIn2->z, pIn2->n) != 0)
+ if (sql_delete_by_key(space, 0, pIn2->z, pIn2->n) != 0) {
+ p->errorAction = ON_CONFLICT_ACTION_ABORT;
goto abort_due_to_error;
+ }
if (pOp->p5 & OPFLAG_NCHANGE)
p->nChange++;
break;
diff --git a/test/sql/ddl.result b/test/sql/ddl.result
new file mode 100644
index 000000000..8f7f91151
--- /dev/null
+++ b/test/sql/ddl.result
@@ -0,0 +1,356 @@
+-- test-run result file version 2
+test_run = require('test_run').new()
+ | ---
+ | ...
+engine = test_run:get_cfg('engine')
+ | ---
+ | ...
+box.execute('pragma sql_default_engine=\''..engine..'\'')
+ | ---
+ | - row_count: 0
+ | ...
+
+--
+-- gh-4086: SQL transactional DDL.
+--
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+box.begin()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY);')
+box.commit();
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | - true
+ | ...
+
+box.space.T1 ~= nil
+ | ---
+ | - true
+ | ...
+box.space.T1.index[0] ~= nil
+ | ---
+ | - true
+ | ...
+box.space.T2 ~= nil
+ | ---
+ | - true
+ | ...
+box.space.T2.index[0] ~= nil
+ | ---
+ | - true
+ | ...
+
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+box.begin()
+box.execute('DROP TABLE t1;')
+assert(box.space.T1 == nil)
+assert(box.space.T2 ~= nil)
+box.execute('DROP TABLE t2;')
+assert(box.space.T2 == nil)
+box.commit();
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | - true
+ | ...
+
+--
+-- Use all the possible SQL DDL statements.
+--
+test_run:cmd("setopt delimiter '$'")
+ | ---
+ | - true
+ | ...
+function monster_ddl()
+ local _, err1, err2, err3
+ box.execute([[CREATE TABLE t1(id INTEGER PRIMARY KEY,
+ a INTEGER,
+ b INTEGER);]])
+ box.execute([[CREATE TABLE t2(id INTEGER PRIMARY KEY,
+ a INTEGER,
+ b INTEGER UNIQUE,
+ CONSTRAINT ck1 CHECK(b < 100));]])
+
+ box.execute('CREATE INDEX t1a ON t1(a);')
+ box.execute('CREATE INDEX t2a ON t2(a);')
+ box.execute('DROP INDEX t2a ON t2;')
+
+ box.execute('ALTER TABLE t1 ADD CONSTRAINT ck1 CHECK(b > 0);')
+ box.execute('ALTER TABLE t1 ADD CONSTRAINT ck2 CHECK(a > 0);')
+ box.space.T1.ck_constraint.CK1:drop()
+
+ box.execute([[ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY
+ (a) REFERENCES t2(b);]])
+ box.execute('ALTER TABLE t1 DROP CONSTRAINT fk1;')
+
+-- Try random errors inside this big batch of DDL to ensure, that
+-- they do not affect normal operation.
+ _, err1 = pcall(box.execute, 'CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+
+ box.execute([[ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY
+ (a) REFERENCES t2(b);]])
+
+ box.execute([[CREATE TABLE trigger_catcher(id INTEGER PRIMARY
+ KEY AUTOINCREMENT);]])
+
+ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW
+ BEGIN
+ INSERT INTO trigger_catcher VALUES(1);
+ END; ]])
+
+ _, err2 = pcall(box.execute, 'DROP TABLE t3;')
+
+ box.execute([[CREATE TRIGGER t2t AFTER INSERT ON t2 FOR EACH ROW
+ BEGIN
+ INSERT INTO trigger_catcher VALUES(1);
+ END; ]])
+
+ _, err3 = pcall(box.execute, 'CREATE INDEX t1a ON t1(a, b);')
+
+ box.execute('DROP TRIGGER t2t;')
+
+ return 'Finished ok, errors in the middle: ', err1, err2, err3
+end$
+ | ---
+ | ...
+function monster_ddl_is_clean()
+ assert(box.space.T1 == nil)
+ assert(box.space.T2 == nil)
+ assert(box.space._trigger:count() == 0)
+ assert(box.space._fk_constraint:count() == 0)
+ assert(box.space._ck_constraint:count() == 0)
+end$
+ | ---
+ | ...
+function monster_ddl_check()
+ local _, err1, err2, err3, err4, res
+ _, err1 = pcall(box.execute, 'INSERT INTO t2 VALUES (1, 1, 101)')
+ box.execute('INSERT INTO t2 VALUES (1, 1, 1)')
+ _, err2 = pcall(box.execute, 'INSERT INTO t2 VALUES(2, 2, 1)')
+ _, err3 = pcall(box.execute, 'INSERT INTO t1 VALUES(1, 20, 1)')
+ _, err4 = pcall(box.execute, 'INSERT INTO t1 VALUES(1, -1, 1)')
+ box.execute('INSERT INTO t1 VALUES (1, 1, 1)')
+ res = box.execute('SELECT * FROM trigger_catcher')
+ return 'Finished ok, errors and trigger catcher content: ', err1, err2,
+ err3, err4, res
+end$
+ | ---
+ | ...
+function monster_ddl_clear()
+ box.execute('DROP TRIGGER IF EXISTS t1t;')
+ box.execute('DROP TABLE IF EXISTS trigger_catcher;')
+ pcall(box.execute, 'ALTER TABLE t1 DROP CONSTRAINT fk1;')
+ box.execute('DROP TABLE IF EXISTS t2')
+ box.execute('DROP TABLE IF EXISTS t1')
+ monster_ddl_is_clean()
+end$
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''")$
+ | ---
+ | - true
+ | ...
+
+-- No txn.
+monster_ddl()
+ | ---
+ | - 'Finished ok, errors in the middle: '
+ | - Space 'T1' already exists
+ | - Space 'T3' does not exist
+ | - Index 'T1A' already exists in space 'T1'
+ | ...
+monster_ddl_check()
+ | ---
+ | - 'Finished ok, errors and trigger catcher content: '
+ | - 'Check constraint failed ''CK1'': b < 100'
+ | - Duplicate key exists in unique index 'unique_unnamed_T2_2' in space 'T2'
+ | - 'Failed to execute SQL statement: FOREIGN KEY constraint failed'
+ | - 'Check constraint failed ''CK2'': a > 0'
+ | - metadata:
+ | - name: ID
+ | type: integer
+ | rows:
+ | - [1]
+ | ...
+monster_ddl_clear()
+ | ---
+ | ...
+
+-- Both DDL and cleanup in one txn.
+res = nil
+ | ---
+ | ...
+box.begin() res = {monster_ddl()} monster_ddl_clear() box.commit()
+ | ---
+ | ...
+res
+ | ---
+ | - - 'Finished ok, errors in the middle: '
+ | - Space 'T1' already exists
+ | - Space 'T3' does not exist
+ | - Index 'T1A' already exists in space 'T1'
+ | ...
+
+-- DDL in txn, cleanup is not.
+box.begin() res = {monster_ddl()} box.commit()
+ | ---
+ | ...
+res
+ | ---
+ | - - 'Finished ok, errors in the middle: '
+ | - Space 'T1' already exists
+ | - Space 'T3' does not exist
+ | - Index 'T1A' already exists in space 'T1'
+ | ...
+monster_ddl_check()
+ | ---
+ | - 'Finished ok, errors and trigger catcher content: '
+ | - 'Check constraint failed ''CK1'': b < 100'
+ | - Duplicate key exists in unique index 'unique_unnamed_T2_2' in space 'T2'
+ | - 'Failed to execute SQL statement: FOREIGN KEY constraint failed'
+ | - 'Check constraint failed ''CK2'': a > 0'
+ | - metadata:
+ | - name: ID
+ | type: integer
+ | rows:
+ | - [1]
+ | ...
+monster_ddl_clear()
+ | ---
+ | ...
+
+-- DDL is not in txn, cleanup is.
+monster_ddl()
+ | ---
+ | - 'Finished ok, errors in the middle: '
+ | - Space 'T1' already exists
+ | - Space 'T3' does not exist
+ | - Index 'T1A' already exists in space 'T1'
+ | ...
+monster_ddl_check()
+ | ---
+ | - 'Finished ok, errors and trigger catcher content: '
+ | - 'Check constraint failed ''CK1'': b < 100'
+ | - Duplicate key exists in unique index 'unique_unnamed_T2_2' in space 'T2'
+ | - 'Failed to execute SQL statement: FOREIGN KEY constraint failed'
+ | - 'Check constraint failed ''CK2'': a > 0'
+ | - metadata:
+ | - name: ID
+ | type: integer
+ | rows:
+ | - [1]
+ | ...
+box.begin() monster_ddl_clear() box.commit()
+ | ---
+ | ...
+
+-- DDL and cleanup in separate txns.
+box.begin() monster_ddl() box.commit()
+ | ---
+ | ...
+monster_ddl_check()
+ | ---
+ | - 'Finished ok, errors and trigger catcher content: '
+ | - 'Check constraint failed ''CK1'': b < 100'
+ | - Duplicate key exists in unique index 'unique_unnamed_T2_2' in space 'T2'
+ | - 'Failed to execute SQL statement: FOREIGN KEY constraint failed'
+ | - 'Check constraint failed ''CK2'': a > 0'
+ | - metadata:
+ | - name: ID
+ | type: integer
+ | rows:
+ | - [1]
+ | ...
+box.begin() monster_ddl_clear() box.commit()
+ | ---
+ | ...
+
+--
+-- Voluntary rollback.
+--
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+box.begin()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+assert(box.space.T1 ~= nil)
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY);')
+assert(box.space.T2 ~= nil)
+box.rollback();
+ | ---
+ | ...
+
+box.space.T1 == nil and box.space.T2 == nil;
+ | ---
+ | - true
+ | ...
+
+box.begin()
+save1 = box.savepoint()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY)')
+save2 = box.savepoint()
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY, a INTEGER)')
+box.execute('CREATE INDEX t2a ON t2(a)')
+save3 = box.savepoint()
+assert(box.space.T1 ~= nil)
+assert(box.space.T2 ~= nil)
+assert(box.space.T2.index.T2A ~= nil)
+box.execute('DROP TABLE t2')
+assert(box.space.T2 == nil)
+box.rollback_to_savepoint(save3)
+assert(box.space.T2 ~= nil)
+assert(box.space.T2.index.T2A ~= nil)
+save3 = box.savepoint()
+box.execute('DROP TABLE t2')
+assert(box.space.T2 == nil)
+box.rollback_to_savepoint(save2)
+assert(box.space.T2 == nil)
+assert(box.space.T1 ~= nil)
+box.rollback_to_savepoint(save1)
+box.commit();
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | - true
+ | ...
+
+box.space.T1 == nil and box.space.T2 == nil
+ | ---
+ | - true
+ | ...
+
+--
+-- Unexpected rollback.
+--
+
+box.begin() res = {monster_ddl()} require('fiber').yield()
+ | ---
+ | ...
+res
+ | ---
+ | - - 'Finished ok, errors in the middle: '
+ | - Space 'T1' already exists
+ | - Space 'T3' does not exist
+ | - Index 'T1A' already exists in space 'T1'
+ | ...
+box.commit()
+ | ---
+ | - error: Transaction has been aborted by a fiber yield
+ | ...
+box.rollback()
+ | ---
+ | ...
+monster_ddl_clear()
+ | ---
+ | ...
diff --git a/test/sql/ddl.test.lua b/test/sql/ddl.test.lua
new file mode 100644
index 000000000..477158796
--- /dev/null
+++ b/test/sql/ddl.test.lua
@@ -0,0 +1,187 @@
+test_run = require('test_run').new()
+engine = test_run:get_cfg('engine')
+box.execute('pragma sql_default_engine=\''..engine..'\'')
+
+--
+-- gh-4086: SQL transactional DDL.
+--
+test_run:cmd("setopt delimiter ';'")
+box.begin()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY);')
+box.commit();
+test_run:cmd("setopt delimiter ''");
+
+box.space.T1 ~= nil
+box.space.T1.index[0] ~= nil
+box.space.T2 ~= nil
+box.space.T2.index[0] ~= nil
+
+test_run:cmd("setopt delimiter ';'")
+box.begin()
+box.execute('DROP TABLE t1;')
+assert(box.space.T1 == nil)
+assert(box.space.T2 ~= nil)
+box.execute('DROP TABLE t2;')
+assert(box.space.T2 == nil)
+box.commit();
+test_run:cmd("setopt delimiter ''");
+
+--
+-- Use all the possible SQL DDL statements.
+--
+test_run:cmd("setopt delimiter '$'")
+function monster_ddl()
+ local _, err1, err2, err3
+ box.execute([[CREATE TABLE t1(id INTEGER PRIMARY KEY,
+ a INTEGER,
+ b INTEGER);]])
+ box.execute([[CREATE TABLE t2(id INTEGER PRIMARY KEY,
+ a INTEGER,
+ b INTEGER UNIQUE,
+ CONSTRAINT ck1 CHECK(b < 100));]])
+
+ box.execute('CREATE INDEX t1a ON t1(a);')
+ box.execute('CREATE INDEX t2a ON t2(a);')
+ box.execute('DROP INDEX t2a ON t2;')
+
+ box.execute('ALTER TABLE t1 ADD CONSTRAINT ck1 CHECK(b > 0);')
+ box.execute('ALTER TABLE t1 ADD CONSTRAINT ck2 CHECK(a > 0);')
+ box.space.T1.ck_constraint.CK1:drop()
+
+ box.execute([[ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY
+ (a) REFERENCES t2(b);]])
+ box.execute('ALTER TABLE t1 DROP CONSTRAINT fk1;')
+
+-- Try random errors inside this big batch of DDL to ensure, that
+-- they do not affect normal operation.
+ _, err1 = pcall(box.execute, 'CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+
+ box.execute([[ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY
+ (a) REFERENCES t2(b);]])
+
+ box.execute([[CREATE TABLE trigger_catcher(id INTEGER PRIMARY
+ KEY AUTOINCREMENT);]])
+
+ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW
+ BEGIN
+ INSERT INTO trigger_catcher VALUES(1);
+ END; ]])
+
+ _, err2 = pcall(box.execute, 'DROP TABLE t3;')
+
+ box.execute([[CREATE TRIGGER t2t AFTER INSERT ON t2 FOR EACH ROW
+ BEGIN
+ INSERT INTO trigger_catcher VALUES(1);
+ END; ]])
+
+ _, err3 = pcall(box.execute, 'CREATE INDEX t1a ON t1(a, b);')
+
+ box.execute('DROP TRIGGER t2t;')
+
+ return 'Finished ok, errors in the middle: ', err1, err2, err3
+end$
+function monster_ddl_is_clean()
+ assert(box.space.T1 == nil)
+ assert(box.space.T2 == nil)
+ assert(box.space._trigger:count() == 0)
+ assert(box.space._fk_constraint:count() == 0)
+ assert(box.space._ck_constraint:count() == 0)
+end$
+function monster_ddl_check()
+ local _, err1, err2, err3, err4, res
+ _, err1 = pcall(box.execute, 'INSERT INTO t2 VALUES (1, 1, 101)')
+ box.execute('INSERT INTO t2 VALUES (1, 1, 1)')
+ _, err2 = pcall(box.execute, 'INSERT INTO t2 VALUES(2, 2, 1)')
+ _, err3 = pcall(box.execute, 'INSERT INTO t1 VALUES(1, 20, 1)')
+ _, err4 = pcall(box.execute, 'INSERT INTO t1 VALUES(1, -1, 1)')
+ box.execute('INSERT INTO t1 VALUES (1, 1, 1)')
+ res = box.execute('SELECT * FROM trigger_catcher')
+ return 'Finished ok, errors and trigger catcher content: ', err1, err2,
+ err3, err4, res
+end$
+function monster_ddl_clear()
+ box.execute('DROP TRIGGER IF EXISTS t1t;')
+ box.execute('DROP TABLE IF EXISTS trigger_catcher;')
+ pcall(box.execute, 'ALTER TABLE t1 DROP CONSTRAINT fk1;')
+ box.execute('DROP TABLE IF EXISTS t2')
+ box.execute('DROP TABLE IF EXISTS t1')
+ monster_ddl_is_clean()
+end$
+test_run:cmd("setopt delimiter ''")$
+
+-- No txn.
+monster_ddl()
+monster_ddl_check()
+monster_ddl_clear()
+
+-- Both DDL and cleanup in one txn.
+res = nil
+box.begin() res = {monster_ddl()} monster_ddl_clear() box.commit()
+res
+
+-- DDL in txn, cleanup is not.
+box.begin() res = {monster_ddl()} box.commit()
+res
+monster_ddl_check()
+monster_ddl_clear()
+
+-- DDL is not in txn, cleanup is.
+monster_ddl()
+monster_ddl_check()
+box.begin() monster_ddl_clear() box.commit()
+
+-- DDL and cleanup in separate txns.
+box.begin() monster_ddl() box.commit()
+monster_ddl_check()
+box.begin() monster_ddl_clear() box.commit()
+
+--
+-- Voluntary rollback.
+--
+test_run:cmd("setopt delimiter ';'")
+box.begin()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY);')
+assert(box.space.T1 ~= nil)
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY);')
+assert(box.space.T2 ~= nil)
+box.rollback();
+
+box.space.T1 == nil and box.space.T2 == nil;
+
+box.begin()
+save1 = box.savepoint()
+box.execute('CREATE TABLE t1(id INTEGER PRIMARY KEY)')
+save2 = box.savepoint()
+box.execute('CREATE TABLE t2(id INTEGER PRIMARY KEY, a INTEGER)')
+box.execute('CREATE INDEX t2a ON t2(a)')
+save3 = box.savepoint()
+assert(box.space.T1 ~= nil)
+assert(box.space.T2 ~= nil)
+assert(box.space.T2.index.T2A ~= nil)
+box.execute('DROP TABLE t2')
+assert(box.space.T2 == nil)
+box.rollback_to_savepoint(save3)
+assert(box.space.T2 ~= nil)
+assert(box.space.T2.index.T2A ~= nil)
+save3 = box.savepoint()
+box.execute('DROP TABLE t2')
+assert(box.space.T2 == nil)
+box.rollback_to_savepoint(save2)
+assert(box.space.T2 == nil)
+assert(box.space.T1 ~= nil)
+box.rollback_to_savepoint(save1)
+box.commit();
+test_run:cmd("setopt delimiter ''");
+
+box.space.T1 == nil and box.space.T2 == nil
+
+--
+-- Unexpected rollback.
+--
+
+box.begin() res = {monster_ddl()} require('fiber').yield()
+res
+box.commit()
+box.rollback()
+monster_ddl_clear()
diff --git a/test/sql/errinj.result b/test/sql/errinj.result
index 8846e5ee8..257dbafde 100644
--- a/test/sql/errinj.result
+++ b/test/sql/errinj.result
@@ -388,56 +388,6 @@ box.execute("DROP TABLE t3;")
---
- row_count: 1
...
--- gh-3780: space without PK raises error if
--- it is used in SQL queries.
---
-errinj = box.error.injection
----
-...
-fiber = require('fiber')
----
-...
-box.execute("CREATE TABLE t (id INT PRIMARY KEY);")
----
-- row_count: 1
-...
-box.execute("INSERT INTO t VALUES (1);")
----
-- row_count: 1
-...
-errinj.set("ERRINJ_WAL_DELAY", true)
----
-- ok
-...
--- DROP TABLE consists of several steps: firstly indexes
--- are deleted, then space itself. Lets make sure that if
--- first part of drop is successfully finished, but resulted
--- in yield, all operations on space will be blocked due to
--- absence of primary key.
---
-function drop_table_yield() box.execute("DROP TABLE t;") end
----
-...
-f = fiber.create(drop_table_yield)
----
-...
-box.execute("SELECT * FROM t;")
----
-- error: SQL does not support spaces without primary key
-...
-box.execute("INSERT INTO t VALUES (2);")
----
-- error: SQL does not support spaces without primary key
-...
-box.execute("UPDATE t SET id = 2;")
----
-- error: SQL does not support spaces without primary key
-...
--- Finish drop space.
-errinj.set("ERRINJ_WAL_DELAY", false)
----
-- ok
-...
--
-- gh-3931: Store regular identifiers in case-normal form
--
diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua
index 48b80a443..3bc1b684d 100644
--- a/test/sql/errinj.test.lua
+++ b/test/sql/errinj.test.lua
@@ -118,28 +118,6 @@ box.execute("INSERT INTO t3 VALUES(1, 1, 3);")
errinj.set("ERRINJ_WAL_IO", false)
box.execute("DROP TABLE t3;")
--- gh-3780: space without PK raises error if
--- it is used in SQL queries.
---
-errinj = box.error.injection
-fiber = require('fiber')
-box.execute("CREATE TABLE t (id INT PRIMARY KEY);")
-box.execute("INSERT INTO t VALUES (1);")
-errinj.set("ERRINJ_WAL_DELAY", true)
--- DROP TABLE consists of several steps: firstly indexes
--- are deleted, then space itself. Lets make sure that if
--- first part of drop is successfully finished, but resulted
--- in yield, all operations on space will be blocked due to
--- absence of primary key.
---
-function drop_table_yield() box.execute("DROP TABLE t;") end
-f = fiber.create(drop_table_yield)
-box.execute("SELECT * FROM t;")
-box.execute("INSERT INTO t VALUES (2);")
-box.execute("UPDATE t SET id = 2;")
--- Finish drop space.
-errinj.set("ERRINJ_WAL_DELAY", false)
-
--
-- gh-3931: Store regular identifiers in case-normal form
--
diff --git a/test/sql/view_delayed_wal.result b/test/sql/view_delayed_wal.result
index 519794931..3faaf07b8 100644
--- a/test/sql/view_delayed_wal.result
+++ b/test/sql/view_delayed_wal.result
@@ -13,8 +13,8 @@ fiber = require('fiber')
...
-- View reference counters are incremented before firing
-- on_commit triggers (i.e. before being written into WAL), so
--- it is impossible to create view on dropped (but not written
--- into WAL) space.
+-- it is impossible to drop a space referenced by a created, but
+-- no committed view.
--
box.execute('CREATE TABLE t1(id INT PRIMARY KEY)')
---
@@ -49,13 +49,13 @@ box.error.injection.set("ERRINJ_WAL_DELAY", false)
fiber.sleep(0.1)
---
...
-box.space.T1
+box.space.T1 ~= nil
---
-- null
+- true
...
-box.space.V1
+box.space.V1 ~= nil
---
-- null
+- true
...
--
-- In the same way, we have to drop all referenced spaces before
diff --git a/test/sql/view_delayed_wal.test.lua b/test/sql/view_delayed_wal.test.lua
index 8e73b03f3..0a10d121b 100644
--- a/test/sql/view_delayed_wal.test.lua
+++ b/test/sql/view_delayed_wal.test.lua
@@ -5,8 +5,8 @@ fiber = require('fiber')
-- View reference counters are incremented before firing
-- on_commit triggers (i.e. before being written into WAL), so
--- it is impossible to create view on dropped (but not written
--- into WAL) space.
+-- it is impossible to drop a space referenced by a created, but
+-- no committed view.
--
box.execute('CREATE TABLE t1(id INT PRIMARY KEY)')
function create_view() box.execute('CREATE VIEW v1 AS SELECT * FROM t1') end
@@ -18,8 +18,8 @@ f2 = fiber.create(drop_index_t1)
f3 = fiber.create(drop_space_t1)
box.error.injection.set("ERRINJ_WAL_DELAY", false)
fiber.sleep(0.1)
-box.space.T1
-box.space.V1
+box.space.T1 ~= nil
+box.space.V1 ~= nil
--
-- In the same way, we have to drop all referenced spaces before
--
2.20.1 (Apple Git-117)
More information about the Tarantool-patches
mailing list