From: "n.pettik" <korablev@tarantool.org> To: tarantool-patches@freelists.org Cc: Vladislav Shpilevoy <v.shpilevoy@tarantool.org> Subject: [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement Date: Wed, 1 Aug 2018 23:54:31 +0300 [thread overview] Message-ID: <D9D14B81-D00C-43ED-A2DE-3866CA0F1542@tarantool.org> (raw) In-Reply-To: <e9aa106f-d3cb-4604-a40f-00ed0d499f7e@tarantool.org> >> Except fixes mentioned below, I disabled (temporary) sql-tap/alter2.test.lua >> (it checks work of ALTER TABLE ADD CONSTRAINT) for vinyl engine. >> Since in previous patch we prohibited creation of FK constraints on >> non-empty spaces and as condition used ‘index_size()’, some tests turn out >> to be flaky. (I don’t think that we should disable these tests for vinyl, but didn’t >> come up with satisfactory solution.) > > Vinyl indexes has method 'compact' available in Lua. If a space is > logically empty, you can trigger compaction for each index to clean > the space garbage. Ok, but compact is not usable here. Vova suggested to use box.snapshot() +++ b/test/sql-tap/alter2.test.lua @@ -1,6 +1,6 @@ #!/usr/bin/env tarantool test = require("sqltester") -test:plan(17) +test:plan(18) -- This suite is aimed to test ALTER TABLE ADD CONSTRAINT statement. -- @@ -87,10 +87,23 @@ test:do_execsql_test( -- </alter2-1.7> }) +test:do_test( + "alter2-1.7.1", + function() + test:execsql([[DELETE FROM t1;]]) + t1 = box.space.T1 + if t1.engine ~= 'vinyl' then + return + end + box.snapshot() + end, { + -- <alter2-1.7.1> + -- </alter2-1.7.1> + }) + test:do_catchsql_test( "alter2-1.8", [[ - DELETE FROM t1; ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY (a) REFERENCES t1(id); ALTER TABLE t1 ADD CONSTRAINT fk2 FOREIGN KEY (a, b) REFERENCES t1(b, a); DROP TABLE t1; diff --git a/test/sql-tap/engine.cfg b/test/sql-tap/engine.cfg index 006e31c37..ce9dd68d8 100644 --- a/test/sql-tap/engine.cfg +++ b/test/sql-tap/engine.cfg @@ -2,9 +2,6 @@ "analyze9.test.lua": { "memtx": {"engine": "memtx"} }, - "alter2.test.lua" : { - "memtx": {"engine": "memtx"} - }, > >>>> -/* >>>> - * This function is called before generating code to update or delete a >>>> - * row contained in table pTab. >>>> - */ >>>> -u32 >>>> -sqlite3FkOldmask(Parse * pParse, /* Parse context */ >>>> - Table * pTab /* Table being modified */ >>>> - ) >>>> +uint32_t >>>> +fkey_old_mask(uint32_t space_id) >>> >>> 25. I think we should calculate this mask once on fk creation >>> like it is done for key_def.columnm_mask. >> In fact, this mask is calculated for whole space (i.e. all of its FK constraints), >> not for particular FK. So basically, we need to add this mask to space_def/space >> and update on each FK creation. Is this OK? > > I think it is worth the cost. Lets add it to struct space where > fkeys are stored. Ok, but it leads to additional manipulations in commit/rollback triggers: +++ b/src/box/alter.cc @@ -3680,6 +3680,50 @@ fkey_grab_by_name(struct rlist *list, const char *fkey_name) return NULL; } +static void +fkey_set_mask(const struct fkey *fk, uint64_t *parent_mask, + uint64_t *child_mask) +{ + for (uint32_t i = 0; i < fk->def->field_count; ++i) { + *parent_mask |= FKEY_MASK(fk->def->links[i].parent_field); + *child_mask |= FKEY_MASK(fk->def->links[i].child_field); + } +} + +/** + * When we discard FK constraint (due to drop or rollback + * trigger), we can't simply unset appropriate bits in mask, + * since other constraints may refer to them as well. Thus, + * we have nothing left to do but completely rebuild mask. + */ +static void +space_reset_fkey_mask(struct space *space) +{ + space->fkey_mask = 0; + struct fkey *fk; + rlist_foreach_entry(fk, &space->child_fkey, child_link) { + struct fkey_def *def = fk->def; + for (uint32_t i = 0; i < def->field_count; ++i) + space->fkey_mask |= + FKEY_MASK(def->links[i].child_field); + } + rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { + struct fkey_def *def = fk->def; + for (uint32_t i = 0; i < def->field_count; ++i) + space->fkey_mask |= + FKEY_MASK(def->links[i].parent_field); + } +} + +static void +fkey_update_mask(const struct fkey *fkey) +{ + struct space *child = space_by_id(fkey->def->child_id); + space_reset_fkey_mask(child); + struct space *parent = space_by_id(fkey->def->parent_id); + space_reset_fkey_mask(parent); +} + @@ -3693,6 +3737,7 @@ on_create_fkey_rollback(struct trigger *trigger, void *event) rlist_del_entry(fk, parent_link); rlist_del_entry(fk, child_link); fkey_delete(fk); + fkey_update_mask(fk); } /** Return old FK and release memory for the new one. */ @@ -3708,6 +3753,7 @@ on_replace_fkey_rollback(struct trigger *trigger, void *event) fkey_delete(old_fkey); rlist_add_entry(&child->child_fkey, fk, child_link); rlist_add_entry(&parent->parent_fkey, fk, parent_link); + fkey_update_mask(fk); } /** On rollback of drop simply return back FK to DD. */ @@ -3720,6 +3766,7 @@ on_drop_fkey_rollback(struct trigger *trigger, void *event) struct space *child = space_by_id(fk_to_restore->def->child_id); rlist_add_entry(&child->child_fkey, fk_to_restore, child_link); rlist_add_entry(&parent->parent_fkey, fk_to_restore, parent_link); + fkey_set_mask(fk_to_restore, &parent->fkey_mask, &child->fkey_mask); } @@ -3732,6 +3779,7 @@ on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event) { (void) event; struct fkey *fk = (struct fkey *)trigger->data; + fkey_update_mask(fk); fkey_delete(fk); } @@ -3884,6 +3932,8 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) txn_alter_trigger_new(on_create_fkey_rollback, fkey); txn_on_rollback(txn, on_rollback); + fkey_set_mask(fkey, &parent_space->fkey_mask, + &child_space->fkey_mask); +++ b/src/box/fkey.h @@ -102,6 +102,13 @@ struct fkey { struct rlist child_link; }; +/** + * FIXME: as SQLite legacy temporary we use such mask throught + * SQL code. It should be replaced later with regular + * mask from column_mask.h + */ +#define FKEY_MASK(x) (((x)>31) ? 0xffffffff : ((uint64_t)1<<(x))) +++ b/src/box/space.h @@ -192,6 +192,7 @@ struct space { */ struct rlist parent_fkey; struct rlist child_fkey; + /** + * Mask indicates which fields are involved in foreign + * key constraint checking routine. Includes fields + * of parent constraints as well as child ones. + */ + uint64_t fkey_mask; +++ b/src/box/sql/fkey.c @@ -135,8 +135,6 @@ * generation code to query for this information are: * * fkey_is_required() - Test to see if FK processing is required. - * fkey_old_mask() - Query for the set of required old.* columns. - * * * Externally accessible module functions * -------------------------------------- @@ -682,30 +680,6 @@ fkey_emit_check(struct Parse *parser, struct Table *tab, int reg_old, } } -#define COLUMN_MASK(x) (((x)>31) ? 0xffffffff : ((u32)1<<(x))) - -uint32_t -fkey_old_mask(uint32_t space_id) -{ - uint32_t mask = 0; - struct session *user_session = current_session(); - if ((user_session->sql_flags & SQLITE_ForeignKeys) != 0) { - struct space *space = space_by_id(space_id); - struct fkey *fk; - rlist_foreach_entry(fk, &space->child_fkey, child_link) { - struct fkey_def *def = fk->def; - for (uint32_t i = 0; i < def->field_count; ++i) - mask |= COLUMN_MASK(def->links[i].child_field); - } - rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { - struct fkey_def *def = fk->def; - for (uint32_t i = 0; i < def->field_count; ++i) - mask |= COLUMN_MASK(def->links[i].parent_field); - } - } - return mask; -} - +++ b/src/box/sql/delete.c @@ -444,7 +444,9 @@ sql_generate_row_delete(struct Parse *parse, struct Table *table, sql_trigger_colmask(parse, trigger_list, 0, 0, TRIGGER_BEFORE | TRIGGER_AFTER, table, onconf); - mask |= fkey_old_mask(table->def->id); + struct space *space = space_by_id(table->def->id); + assert(space != NULL); + mask |= space->fkey_mask; +++ b/src/box/sql/sqliteInt.h @@ -4767,16 +4767,6 @@ fkey_emit_actions(struct Parse *parser, struct Table *tab, int reg_old, bool fkey_is_required(uint32_t space_id, const int *changes); -/** - * This function is called before generating code to update or - * delete a row contained in given table. - * - * @param space_id Id of space being modified. - * @retval Mask containing fields to be involved in FK testing. - */ -uint32_t -fkey_old_mask(uint32_t space_id); +++ b/src/box/sql/update.c @@ -433,7 +433,9 @@ sqlite3Update(Parse * pParse, /* The parser context */ * information is needed */ if (chngPk != 0 || hasFK != 0 || trigger != NULL) { - u32 oldmask = hasFK ? fkey_old_mask(pTab->def->id) : 0; + struct space *space = space_by_id(pTab->def->id); + assert(space != NULL); + u32 oldmask = hasFK ? space->fkey_mask : 0; > >> diff --git a/extra/mkopcodeh.sh b/extra/mkopcodeh.sh >> index 63ad0d56a..9e97a50f0 100755 >> --- a/extra/mkopcodeh.sh >> +++ b/extra/mkopcodeh.sh >> @@ -220,8 +224,12 @@ while [ "$i" -lt "$nOp" ]; do >> i=$((i + 1)) >> done >> max="$cnt" >> +echo "//*************** $max $nOp $mxTk" > > 1. This echo seems to be debug print. > >> @@ -283,11 +303,16 @@ printf '%s\n' "#define OPFLG_OUT2 0x10 /* out2: P2 is an output */" >> printf '%s\n' "#define OPFLG_OUT3 0x20 /* out3: P3 is an output */" >> printf '%s\n' "#define OPFLG_INITIALIZER {\\" >> i=0 >> -while [ "$i" -le "$max" ]; do >> +while [ "$i" -le "$mxTk" ]; do >> if [ "$((i % 8))" -eq 0 ]; then >> printf '/* %3d */' "$i" >> fi >> - eval "bv=\$ARRAY_bv_$i" >> + eval "is_exists=\${ARRAY_bv_$i:-}" > > 2. 'is_exists'? I have refactored this changes > slightly on the branch. Sorry, I didn’t review this script, just simply copied it and include some Alex’s fixes. Also, he promised to review this script carefully, so I guess it is worth to detach this it from patch-set and send as a separate patch (AFAIK Kirill Y. already done that). >> diff --git a/src/box/sql/alter.c b/src/box/sql/alter.c >> index 8c1c36b9b..0e770272e 100644 >> --- a/src/box/sql/alter.c >> +++ b/src/box/sql/alter.c >> @@ -190,12 +189,6 @@ sqlite3AlterFinishAddColumn(Parse * pParse, Token * pColDef) >> sqlite3ErrorMsg(pParse, "Cannot add a UNIQUE column"); >> return; >> } >> - if ((user_session->sql_flags & SQLITE_ForeignKeys) && pNew->pFKey > > 3. After we've found that defer fkey flag was checked in the parser I > checked other flags of fkeys. And it emerged that SQLITE_ForeignKeys is > still used in the parser in such places as sqlite3FkCheck, fkey_old_mask, > fkey_is_required, sqlite3FkActions, sqlite3GenerateConstraintChecks, > xferOptimization. I think, that we should check it during execution, not > parsing. It is not? > > Actually it is not the only flag checked during parsing: I've found also > SQLITE_CountRows, SQLITE_IgnoreChecks (btw what is the difference between > ignore_checks and !foreign_keys?), SQLITE_RecTriggers, SQLITE_FullColNames, > SQLITE_ShortColNames, SQLITE_EnableTrigger, SQLITE_ReverseOrder. > > And we have 3 options as I understand: > > * somehow merge these things into VDBE; > > * when we will have PREPARE API, rebuild prepared statements of > the user session on change of any of these flags; > > * we announce these flags as parsing stage only and if you changed them, > the already prepared statements would not be affected. Well, personally the most I like second variant. First one also looks OK, but need to investigate whether it would be easy to integrate checks into VDBE or not. Actually, if I could I would throw away these runtime options… Anyway, lets ask for advise. Then, I am going to open an issue. Also, I’ve noticed that during previous review removal of SQLITE_DeferFK from sql/fkey.c was a mistake: test/sql/transitive-transaction.test.lua fails without routine connected with it. So, I returned back checks on this macros: +++ b/src/box/sql/fkey.c @@ -261,8 +261,10 @@ fkey_lookup_parent(struct Parse *parse_context, struct space *parent, sqlite3VdbeAddOp4Int(v, OP_Found, cursor, ok_label, rec_reg, 0); sqlite3ReleaseTempReg(parse_context, rec_reg); sqlite3ReleaseTempRange(parse_context, temp_regs, field_count); - if (!fk_def->is_deferred && parse_context->pToplevel == NULL && - !parse_context->isMultiWrite) { + struct session *session = current_session(); + if (!fk_def->is_deferred && + (session->sql_flags & SQLITE_DeferFKs) == 0 && + parse_context->pToplevel == NULL && !parse_context->isMultiWrite) { @@ -605,8 +607,9 @@ fkey_emit_check(struct Parse *parser, struct Table *tab, int reg_old, if (changed_cols != NULL && !fkey_parent_is_modified(fk_def, changed_cols)) continue; - if (!fk_def->is_deferred && parser->pToplevel == NULL && - !parser->isMultiWrite) { + if (!fk_def->is_deferred && + (user_session->sql_flags & SQLITE_DeferFKs) == 0 && + parser->pToplevel == NULL && !parser->isMultiWrite) { > >> - && pDflt) { >> - sqlite3ErrorMsg(pParse, >> - "Cannot add a REFERENCES column with non-NULL default value"); >> - return; >> - } >> assert(pNew->def->fields[pNew->def->field_count - 1].is_nullable == >> action_is_nullable(pNew->def->fields[ >> pNew->def->field_count - 1].nullable_action)); >> diff --git a/src/box/sql/build.c b/src/box/sql/build.c >> index 789a628d6..fc097a319 100644 >> --- a/src/box/sql/build.c >> +++ b/src/box/sql/build.c >> + >> +static int >> +resolve_link(struct Parse *parse_context, const struct space_def *def, >> + const char *field_name, uint32_t *link) >> +{ >> + assert(link != NULL); >> + for (uint32_t j = 0; j < def->field_count; ++j) { >> + if (strcmp(field_name, def->fields[j].name) == 0) { >> + *link = j; >> + return 0; >> + } >> + } >> + sqlite3ErrorMsg(parse_context, "unknown column %s in foreign key " >> + "definition", field_name); > > 4. ER_CREATE_FK_CONSTRAINT ? +++ b/src/box/sql/build.c @@ -1620,7 +1620,7 @@ vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk) static int resolve_link(struct Parse *parse_context, const struct space_def *def, - const char *field_name, uint32_t *link) + const char *field_name, uint32_t *link, const char *fk_name) { assert(link != NULL); for (uint32_t j = 0; j < def->field_count; ++j) { @@ -1629,8 +1629,10 @@ resolve_link(struct Parse *parse_context, const struct space_def *def, return 0; } } - sqlite3ErrorMsg(parse_context, "unknown column %s in foreign key " - "definition", field_name); + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, fk_name, + tt_sprintf("unknown column %s in foreign key definition", + field_name)); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; @@ -1813,7 +1815,8 @@ sqlite3EndTable(Parse * pParse, /* Parse context */ for (uint32_t i = 0; i < fk->field_count; ++i) { if (resolve_link(pParse, p->def, cols->a[i].zName, - &fk->links[i].parent_field) != 0) + &fk->links[i].parent_field, + fk->name) != 0) return; @@ -2463,7 +2471,8 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, } if (resolve_link(parse_context, new_tab->def, child_cols->a[i].zName, - &fk->links[i].child_field) != 0) + &fk->links[i].child_field, + constraint_name) != 0) > >> + return -1; >> +} >> + >> /* >> * This routine is called to report the final ")" that terminates >> * a CREATE TABLE statement. >> @@ -1720,6 +1803,43 @@ sqlite3EndTable(Parse * pParse, /* Parse context */ >> /* Reparse everything to update our internal data structures */ >> parseTableSchemaRecord(pParse, iSpaceId, zStmt); /* consumes zStmt */ >> + >> + /* Code creation of FK constraints, if any. */ >> + struct fkey_parse *fk_parse; >> + rlist_foreach_entry(fk_parse, &pParse->new_fkey, link) { >> + struct fkey_def *fk = fk_parse->fkey; >> + if (fk_parse->selfref_cols != NULL) { >> + struct ExprList *cols = fk_parse->selfref_cols; >> + for (uint32_t i = 0; i < fk->field_count; ++i) { >> + if (resolve_link(pParse, p->def, >> + cols->a[i].zName, >> + &fk->links[i].parent_field) != 0) >> + return; >> + } >> + fk->parent_id = iSpaceId; >> + } else if (fk_parse->is_self_referenced) { >> + struct Index *pk = sqlite3PrimaryKeyIndex(p); >> + if (pk->def->key_def->part_count != >> + fk->field_count) { >> + diag_set(ClientError, >> + ER_CREATE_FK_CONSTRAINT, >> + fk->name, "number of columns " >> + "in foreign key does not " >> + "match the number of columns " >> + "in the referenced table"); > > 5. However, this message was 3 times duplicated as I said. Now it > is 2: here and in sql_create_foreign_key. Indeed, I was wrong.. > > 6. It does not match the problem. You check for field count > in primary index, not in the whole table. So the message should be > like "... the number of columns in the referenced table's primary index" > or something. @@ -1826,7 +1830,8 @@ sqlite3EndTable(Parse * pParse, /* Parse context */ fk->name, "number of columns " "in foreign key does not " "match the number of columns " - "in the referenced table"); + "in the primary index of " + "referenced table”); @@ -2400,8 +2408,8 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, if (constraint_name == NULL) goto exit_create_fk; const char *error_msg = "number of columns in foreign key does not " - "match the number of columns in the " - "referenced table"; + "match the number of columns in the primary " + "index of referenced table"; >> +static int >> +columnno_by_name(struct Parse *parse_context, const struct space *space, >> + const char *column_name, uint32_t *colno) >> +{ >> + assert(colno != NULL); >> + uint32_t column_len = strlen(column_name); >> + if (tuple_fieldno_by_name(space->def->dict, column_name, column_len, >> + field_name_hash(column_name, column_len), >> + colno) != 0) { >> + sqlite3ErrorMsg(parse_context, >> + "table \"%s\" doesn't feature column %s", >> + space->def->name, column_name); > > 7. diag_set? @@ -2270,21 +2274,23 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list, * @param space Space which column belongs to. * @param column_name Name of column to investigate. * @param[out] colno Found name of column. + * @param fk_name Name of FK constraint to be created. * * @retval 0 on success, -1 on fault. */ static int columnno_by_name(struct Parse *parse_context, const struct space *space, - const char *column_name, uint32_t *colno) + const char *column_name, uint32_t *colno, const char *fk_name) { assert(colno != NULL); uint32_t column_len = strlen(column_name); if (tuple_fieldno_by_name(space->def->dict, column_name, column_len, field_name_hash(column_name, column_len), colno) != 0) { - sqlite3ErrorMsg(parse_context, - "table \"%s\" doesn't feature column %s", - space->def->name, column_name); + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, fk_name, + tt_sprintf("foreign key refers to nonexistent field %s", + column_name)); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; return -1; } @@ -2445,7 +2451,8 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, } else if (!is_self_referenced && columnno_by_name(parse_context, parent_space, parent_cols->a[i].zName, - &fk->links[i].parent_field) != 0) { + &fk->links[i].parent_field, + constraint_name) != 0) { @@ -2463,12 +2470,14 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, goto exit_create_fk; /* In case of ALTER parent table must exist. */ } else if (columnno_by_name(parse_context, child_space, child_cols->a[i].zName, - &fk->links[i].child_field) != 0) { + &fk->links[i].child_field, + constraint_name) != 0) { > >> + return -1; >> + } >> + return 0; >> +} >> + >> void >> -sqlite3CreateForeignKey(Parse * pParse, /* Parsing context */ >> - ExprList * pFromCol, /* Columns in this table that point to other table */ >> - Token * pTo, /* Name of the other table */ >> - ExprList * pToCol, /* Columns in the other table */ >> - int flags /* Conflict resolution algorithms. */ >> - ) >> +sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, >> + struct Token *constraint, struct ExprList *child_cols, >> + struct Token *parent, struct ExprList *parent_cols, >> + bool is_deferred, int actions) > > 8. We have problems with actions. Lets take a look at the parser: > > %type refarg {struct {int value; int mask;}} > refarg(A) ::= MATCH matcharg(X). { A.value = X; A.mask = 0xff0000; } > refarg(A) ::= ON INSERT refact. { A.value = 0; A.mask = 0x000000; } > refarg(A) ::= ON DELETE refact(X). { A.value = X; A.mask = 0x0000ff; } > refarg(A) ::= ON UPDATE refact(X). { A.value = X<<8; A.mask = 0x00ff00; } > > It builds actions mask. Then lets look at the actions decoding: > > fk->match = (enum fkey_match) ((actions >> 16) & 0xff); > fk->on_update = (enum fkey_action) ((actions >> 8) & 0xff); > fk->on_delete = (enum fkey_action) (actions & 0xff); > > As you can see, it is expected, that the mask has the layout > {on_delete, on_update, match}, each field is byte. > > But the parser stores them as {on_delete/match, on_update} now. > > So I've found this test that ignores my MATCH: > > box.cfg{} > box.sql.execute('CREATE TABLE test (id int primary key, '.. > 'a int unique, b int unique)') > box.sql.execute('CREATE TABLE test2 (id int primary key'.. > ', a int references test(a) ON DELETE SET NULL MATCH FULL)') > box.space._fk_constraint:select{} > --- > - - ['FK_CONSTRAINT_1_TEST2', 513, 512, false, 'simple', 'cascade', 'no_action', [ > {'child': 1, 'parent': 1}]] > > As you can see, I specified MATCH as FULL, but it turned into SIMPLE. I do > not know what MATCH even means, but it is stored incorrectly. I guess this will fix it (it looks like it was before previous review): +++ b/src/box/sql/parse.y @@ -300,7 +300,7 @@ autoinc(X) ::= AUTOINCR. {X = 1;} refargs(A) ::= . { A = FKEY_NO_ACTION; } refargs(A) ::= refargs(A) refarg(Y). { A = (A & ~Y.mask) | Y.value; } %type refarg {struct {int value; int mask;}} -refarg(A) ::= MATCH matcharg(X). { A.value = X; A.mask = 0xff0000; } +refarg(A) ::= MATCH matcharg(X). { A.value = X<<16; A.mask = 0xff0000; } +++ b/test/sql/foreign-keys.test.lua @@ -152,5 +152,15 @@ box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred'] box.space.CHILD:drop() box.space.PARENT:drop() --- Clean-up SQL DD hash. -test_run:cmd('restart server default with cleanup=1') +-- Check that parser correctly handles MATCH, ON DELETE and +-- ON UPDATE clauses. +-- +box.sql.execute('CREATE TABLE tp (id INT PRIMARY KEY, a INT UNIQUE)') +box.sql.execute('CREATE TABLE tc (id INT PRIMARY KEY, a INT REFERENCES tp(a) ON DELETE SET NULL MATCH FULL)') +box.sql.execute('ALTER TABLE tc ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES tp(id) MATCH PARTIAL ON DELETE CASCADE ON UPDATE SET NULL') +box.space._fk_constraint:select{} +box.sql.execute('DROP TABLE tc') +box.sql.execute('DROP TABLE tp') + +--- Clean-up SQL DD hash. +-test_run:cmd('restart server default with cleanup=1’) +++ b/test/sql/foreign-keys.result @@ -340,5 +340,29 @@ box.space.CHILD:drop() box.space.PARENT:drop() --- ... --- Clean-up SQL DD hash. -test_run:cmd('restart server default with cleanup=1') +-- Check that parser correctly handles MATCH, ON DELETE and +-- ON UPDATE clauses. +-- +box.sql.execute('CREATE TABLE tp (id INT PRIMARY KEY, a INT UNIQUE)') +--- +... +box.sql.execute('CREATE TABLE tc (id INT PRIMARY KEY, a INT REFERENCES tp(a) ON DELETE SET NULL MATCH FULL)') +--- +... +box.sql.execute('ALTER TABLE tc ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES tp(id) MATCH PARTIAL ON DELETE CASCADE ON UPDATE SET NULL') +--- +... +box.space._fk_constraint:select{} +--- +- - ['FK1', 518, 517, false, 'partial', 'cascade', 'set_null', [{'child': 0, 'parent': 0}]] + - ['FK_CONSTRAINT_1_TC', 518, 517, false, 'full', 'set_null', 'no_action', [{'child': 1, + 'parent': 1}]] +... +box.sql.execute('DROP TABLE tc') +--- +... +box.sql.execute('DROP TABLE tp') +--- +... +--- Clean-up SQL DD hash. +-test_run:cmd('restart server default with cleanup=1') Updated patch: ======================================================================= diff --git a/extra/mkkeywordhash.c b/extra/mkkeywordhash.c index 1ec153815..6a6f96f53 100644 --- a/extra/mkkeywordhash.c +++ b/extra/mkkeywordhash.c @@ -159,6 +159,7 @@ static Keyword aKeywordTable[] = { { "FOR", "TK_FOR", TRIGGER, true }, { "FOREIGN", "TK_FOREIGN", FKEY, true }, { "FROM", "TK_FROM", ALWAYS, true }, + { "FULL", "TK_FULL", ALWAYS, true }, { "GLOB", "TK_LIKE_KW", ALWAYS, false }, { "GROUP", "TK_GROUP", ALWAYS, true }, { "HAVING", "TK_HAVING", ALWAYS, true }, @@ -191,6 +192,7 @@ static Keyword aKeywordTable[] = { { "OR", "TK_OR", ALWAYS, true }, { "ORDER", "TK_ORDER", ALWAYS, true }, { "OUTER", "TK_JOIN_KW", ALWAYS, true }, + { "PARTIAL", "TK_PARTIAL", ALWAYS, true }, { "PLAN", "TK_PLAN", EXPLAIN, false }, { "PRAGMA", "TK_PRAGMA", PRAGMA, true }, { "PRIMARY", "TK_PRIMARY", ALWAYS, true }, @@ -210,6 +212,7 @@ static Keyword aKeywordTable[] = { { "SAVEPOINT", "TK_SAVEPOINT", ALWAYS, true }, { "SELECT", "TK_SELECT", ALWAYS, true }, { "SET", "TK_SET", ALWAYS, true }, + { "SIMPLE", "TK_SIMPLE", ALWAYS, true }, { "START", "TK_START", ALWAYS, true }, { "TABLE", "TK_TABLE", ALWAYS, true }, { "THEN", "TK_THEN", ALWAYS, true }, diff --git a/extra/mkopcodeh.sh b/extra/mkopcodeh.sh index 63ad0d56a..5f31f2b7d 100755 --- a/extra/mkopcodeh.sh +++ b/extra/mkopcodeh.sh @@ -35,6 +35,7 @@ set -f # disable pathname expansion currentOp="" nOp=0 +mxTk=-1 newline="$(printf '\n')" IFS="$newline" while read line; do @@ -106,6 +107,9 @@ while read line; do eval "ARRAY_used_$val=1" eval "ARRAY_sameas_$val=$sym" eval "ARRAY_def_$val=$name" + if [ $val -gt $mxTk ] ; then + mxTk=$val + fi fi ;; jump) eval "ARRAY_jump_$name=1" ;; @@ -219,9 +223,11 @@ while [ "$i" -lt "$nOp" ]; do fi i=$((i + 1)) done -max="$cnt" +if [ $mxTk -lt $nOp ] ; then + mxTk=$nOp +fi i=0 -while [ "$i" -lt "$nOp" ]; do +while [ "$i" -le "$mxTk" ]; do eval "used=\${ARRAY_used_$i:-}" if [ -z "$used" ]; then eval "ARRAY_def_$i=OP_NotUsed_$i" @@ -251,9 +257,19 @@ done # Generate the bitvectors: ARRAY_bv_0=0 i=0 -while [ "$i" -le "$max" ]; do +while [ "$i" -le "$mxTk" ]; do + eval "is_existing=\${ARRAY_def_$i:-}" + if [ ! -n "$is_existing" ] ; then + i=$((i + 1)) + continue + fi eval "name=\$ARRAY_def_$i" x=0 + eval "is_existing=\${ARRAY_jump_$name:-}" + if [ ! -n "$is_existing" ] ; then + i=$((i + 1)) + continue + fi eval "jump=\$ARRAY_jump_$name" eval "in1=\$ARRAY_in1_$name" eval "in2=\$ARRAY_in2_$name" @@ -283,11 +299,16 @@ printf '%s\n' "#define OPFLG_OUT2 0x10 /* out2: P2 is an output */" printf '%s\n' "#define OPFLG_OUT3 0x20 /* out3: P3 is an output */" printf '%s\n' "#define OPFLG_INITIALIZER {\\" i=0 -while [ "$i" -le "$max" ]; do +while [ "$i" -le "$mxTk" ]; do if [ "$((i % 8))" -eq 0 ]; then printf '/* %3d */' "$i" fi - eval "bv=\$ARRAY_bv_$i" + eval "is_existing=\${ARRAY_bv_$i:-}" + if [ ! -n "$is_existing" ] ; then + bv=0 + else + eval "bv=\$ARRAY_bv_$i" + fi printf ' 0x%02x,' "$bv" if [ "$((i % 8))" -eq 7 ]; then printf '%s\n' "\\" diff --git a/src/box/alter.cc b/src/box/alter.cc index 5b55bfd7a..6b9e29470 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -3660,6 +3660,50 @@ fkey_grab_by_name(struct rlist *list, const char *fkey_name) return NULL; } +static void +fkey_set_mask(const struct fkey *fk, uint64_t *parent_mask, + uint64_t *child_mask) +{ + for (uint32_t i = 0; i < fk->def->field_count; ++i) { + *parent_mask |= FKEY_MASK(fk->def->links[i].parent_field); + *child_mask |= FKEY_MASK(fk->def->links[i].child_field); + } +} + +/** + * When we discard FK constraint (due to drop or rollback + * trigger), we can't simply unset appropriate bits in mask, + * since other constraints may refer to them as well. Thus, + * we have nothing left to do but completely rebuild mask. + */ +static void +space_reset_fkey_mask(struct space *space) +{ + space->fkey_mask = 0; + struct fkey *fk; + rlist_foreach_entry(fk, &space->child_fkey, child_link) { + struct fkey_def *def = fk->def; + for (uint32_t i = 0; i < def->field_count; ++i) + space->fkey_mask |= + FKEY_MASK(def->links[i].child_field); + } + rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { + struct fkey_def *def = fk->def; + for (uint32_t i = 0; i < def->field_count; ++i) + space->fkey_mask |= + FKEY_MASK(def->links[i].parent_field); + } +} + +static void +fkey_update_mask(const struct fkey *fkey) +{ + struct space *child = space_by_id(fkey->def->child_id); + space_reset_fkey_mask(child); + struct space *parent = space_by_id(fkey->def->parent_id); + space_reset_fkey_mask(parent); +} + /** * On rollback of creation we remove FK constraint from DD, i.e. * from parent's and child's lists of constraints and @@ -3673,6 +3717,7 @@ on_create_fkey_rollback(struct trigger *trigger, void *event) rlist_del_entry(fk, parent_link); rlist_del_entry(fk, child_link); fkey_delete(fk); + fkey_update_mask(fk); } /** Return old FK and release memory for the new one. */ @@ -3688,6 +3733,7 @@ on_replace_fkey_rollback(struct trigger *trigger, void *event) fkey_delete(old_fkey); rlist_add_entry(&child->child_fkey, fk, child_link); rlist_add_entry(&parent->parent_fkey, fk, parent_link); + fkey_update_mask(fk); } /** On rollback of drop simply return back FK to DD. */ @@ -3700,6 +3746,7 @@ on_drop_fkey_rollback(struct trigger *trigger, void *event) struct space *child = space_by_id(fk_to_restore->def->child_id); rlist_add_entry(&child->child_fkey, fk_to_restore, child_link); rlist_add_entry(&parent->parent_fkey, fk_to_restore, parent_link); + fkey_set_mask(fk_to_restore, &parent->fkey_mask, &child->fkey_mask); } /** @@ -3712,6 +3759,7 @@ on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event) { (void) event; struct fkey *fk = (struct fkey *)trigger->data; + fkey_update_mask(fk); fkey_delete(fk); } @@ -3864,6 +3912,8 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) txn_alter_trigger_new(on_create_fkey_rollback, fkey); txn_on_rollback(txn, on_rollback); + fkey_set_mask(fkey, &parent_space->fkey_mask, + &child_space->fkey_mask); } else { struct fkey *old_fk = fkey_grab_by_name(&child_space->child_fkey, diff --git a/src/box/fkey.h b/src/box/fkey.h index ed99617ca..6597473b4 100644 --- a/src/box/fkey.h +++ b/src/box/fkey.h @@ -102,6 +102,13 @@ struct fkey { struct rlist child_link; }; +/** + * FIXME: as SQLite legacy temporary we use such mask throught + * SQL code. It should be replaced later with regular + * mask from column_mask.h + */ +#define FKEY_MASK(x) (((x)>31) ? 0xffffffff : ((uint64_t)1<<(x))) + /** * Alongside with struct fkey_def itself, we reserve memory for * string containing its name and for array of links. diff --git a/src/box/space.h b/src/box/space.h index d60ba6c56..f3e9e1e21 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -192,6 +192,12 @@ struct space { */ struct rlist parent_fkey; struct rlist child_fkey; + /** + * Mask indicates which fields are involved in foreign + * key constraint checking routine. Includes fields + * of parent constraints as well as child ones. + */ + uint64_t fkey_mask; }; /** Initialize a base space instance. */ diff --git a/src/box/sql.c b/src/box/sql.c index 9795ad2ac..46a0c3472 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -55,6 +55,7 @@ #include "session.h" #include "xrow.h" #include "iproto_constants.h" +#include "fkey.h" static sqlite3 *db = NULL; @@ -839,103 +840,6 @@ rename_fail: return SQL_TARANTOOL_ERROR; } -/* - * Acts almost as tarantoolSqlite3RenameTable, but doesn't change - * name of table, only statement. - */ -int tarantoolSqlite3RenameParentTable(int space_id, const char *old_parent_name, - const char *new_parent_name) -{ - assert(space_id != 0); - assert(old_parent_name != NULL); - assert(new_parent_name != NULL); - - box_tuple_t *tuple; - uint32_t key_len = mp_sizeof_uint(space_id) + mp_sizeof_array(1); - - char *key_begin = (char*) region_alloc(&fiber()->gc, key_len); - if (key_begin == NULL) { - diag_set(OutOfMemory, key_len, "region_alloc", "key_begin"); - return SQL_TARANTOOL_ERROR; - } - char *key = mp_encode_array(key_begin, 1); - key = mp_encode_uint(key, space_id); - if (box_index_get(BOX_SPACE_ID, 0, key_begin, key, &tuple) != 0) - return SQL_TARANTOOL_ERROR; - assert(tuple != NULL); - - assert(tuple_field_count(tuple) == 7); - const char *sql_stmt_map = box_tuple_field(tuple, 5); - - if (sql_stmt_map == NULL || mp_typeof(*sql_stmt_map) != MP_MAP) - goto rename_fail; - uint32_t map_size = mp_decode_map(&sql_stmt_map); - if (map_size != 1) - goto rename_fail; - const char *sql_str = mp_decode_str(&sql_stmt_map, &key_len); - if (sqlite3StrNICmp(sql_str, "sql", 3) != 0) - goto rename_fail; - uint32_t create_stmt_decoded_len; - const char *create_stmt_old = mp_decode_str(&sql_stmt_map, - &create_stmt_decoded_len); - uint32_t old_name_len = strlen(old_parent_name); - uint32_t new_name_len = strlen(new_parent_name); - char *create_stmt_new = (char*) region_alloc(&fiber()->gc, - create_stmt_decoded_len + 1); - if (create_stmt_new == NULL) { - diag_set(OutOfMemory, create_stmt_decoded_len + 1, - "region_alloc", "create_stmt_new"); - return SQL_TARANTOOL_ERROR; - } - memcpy(create_stmt_new, create_stmt_old, create_stmt_decoded_len); - create_stmt_new[create_stmt_decoded_len] = '\0'; - uint32_t numb_of_quotes = 0; - uint32_t numb_of_occurrences = 0; - create_stmt_new = rename_parent_table(db, create_stmt_new, old_parent_name, - new_parent_name, &numb_of_occurrences, - &numb_of_quotes); - uint32_t create_stmt_new_len = create_stmt_decoded_len - - numb_of_occurrences * - (old_name_len - new_name_len) + - 2 * numb_of_quotes; - assert(create_stmt_new_len > 0); - - key_len = tuple->bsize + mp_sizeof_str(create_stmt_new_len); - char *new_tuple = (char*)region_alloc(&fiber()->gc, key_len); - if (new_tuple == NULL) { - sqlite3DbFree(db, create_stmt_new); - diag_set(OutOfMemory, key_len, "region_alloc", "new_tuple"); - return SQL_TARANTOOL_ERROR; - } - - char *new_tuple_end = new_tuple; - const char *data_begin = tuple_data(tuple); - const char *data_end = tuple_field(tuple, 5); - uint32_t data_size = data_end - data_begin; - memcpy(new_tuple, data_begin, data_size); - new_tuple_end += data_size; - new_tuple_end = mp_encode_map(new_tuple_end, 1); - new_tuple_end = mp_encode_str(new_tuple_end, "sql", 3); - new_tuple_end = mp_encode_str(new_tuple_end, create_stmt_new, - create_stmt_new_len); - sqlite3DbFree(db, create_stmt_new); - data_begin = tuple_field(tuple, 6); - data_end = (char*) tuple + tuple_size(tuple); - data_size = data_end - data_begin; - memcpy(new_tuple_end, data_begin, data_size); - new_tuple_end += data_size; - - if (box_replace(BOX_SPACE_ID, new_tuple, new_tuple_end, NULL) != 0) - return SQL_TARANTOOL_ERROR; - else - return SQLITE_OK; - -rename_fail: - diag_set(ClientError, ER_SQL_EXECUTE, "can't modify name of space " - "created not via SQL facilities"); - return SQL_TARANTOOL_ERROR; -} - int tarantoolSqlite3IdxKeyCompare(struct BtCursor *cursor, struct UnpackedRecord *unpacked) @@ -1489,6 +1393,23 @@ tarantoolSqlite3MakeTableOpts(Table *pTable, const char *zSql, char *buf) return p - buf; } +int +fkey_encode_links(uint32_t *links, uint32_t link_count, char *buf) +{ + const struct Enc *enc = get_enc(buf); + char *p = enc->encode_array(buf, link_count); + for (uint32_t i = 0; i < link_count; ++i) { + /* + * field_link consists of two uin32_t members, + * so if we calculate proper offset, we will + * get next parent/child member. + */ + size_t offset = sizeof(struct field_link) * i; + p = enc->encode_uint(p, *((char *) links + offset)); + } + return p - buf; +} + /* * Format "parts" array for _index entry. * Returns result size. diff --git a/src/box/sql/alter.c b/src/box/sql/alter.c index 8c1c36b9b..0e770272e 100644 --- a/src/box/sql/alter.c +++ b/src/box/sql/alter.c @@ -151,7 +151,6 @@ sqlite3AlterFinishAddColumn(Parse * pParse, Token * pColDef) Expr *pDflt; /* Default value for the new column */ sqlite3 *db; /* The database connection; */ Vdbe *v = pParse->pVdbe; /* The prepared statement under construction */ - struct session *user_session = current_session(); db = pParse->db; if (pParse->nErr || db->mallocFailed) @@ -190,12 +189,6 @@ sqlite3AlterFinishAddColumn(Parse * pParse, Token * pColDef) sqlite3ErrorMsg(pParse, "Cannot add a UNIQUE column"); return; } - if ((user_session->sql_flags & SQLITE_ForeignKeys) && pNew->pFKey - && pDflt) { - sqlite3ErrorMsg(pParse, - "Cannot add a REFERENCES column with non-NULL default value"); - return; - } assert(pNew->def->fields[pNew->def->field_count - 1].is_nullable == action_is_nullable(pNew->def->fields[ pNew->def->field_count - 1].nullable_action)); @@ -403,81 +396,6 @@ rename_table(sqlite3 *db, const char *sql_stmt, const char *table_name, return new_sql_stmt; } -/* - * This function is used by the ALTER TABLE ... RENAME command to modify the - * definition of any foreign key constraints that used the table being renamed - * as the parent table. All substituted occurrences will be quoted. - * It returns the new CREATE TABLE statement. Memory for the new statement - * will be automatically freed by VDBE. - * - * Usage example: - * - * sqlite_rename_parent('CREATE TABLE t1(a REFERENCES t2)', 't2', 't3') - * -> 'CREATE TABLE t1(a REFERENCES "t3")' - * - * @param sql_stmt text of a child CREATE TABLE statement being modified - * @param old_name old name of the table being renamed - * @param new_name new name of the table being renamed - * @param[out] numb_of_occurrences number of occurrences of old_name in sql_stmt - * @param[out] numb_of_unquoted number of unquoted occurrences of old_name - * - * @retval new SQL statement on success, empty string otherwise. - */ -char* -rename_parent_table(sqlite3 *db, const char *sql_stmt, const char *old_name, - const char *new_name, uint32_t *numb_of_occurrences, - uint32_t *numb_of_unquoted) -{ - assert(sql_stmt); - assert(old_name); - assert(new_name); - assert(numb_of_occurrences); - assert(numb_of_unquoted); - - char *output = NULL; - char *new_sql_stmt; - const char *csr; /* Pointer to token */ - int n; /* Length of token z */ - int token; /* Type of token */ - bool unused; - bool is_quoted; - - for (csr = sql_stmt; *csr; csr = csr + n) { - n = sql_token(csr, &token, &unused); - if (token == TK_REFERENCES) { - char *zParent; - do { - csr += n; - n = sql_token(csr, &token, &unused); - } while (token == TK_SPACE); - if (token == TK_ILLEGAL) - break; - zParent = sqlite3DbStrNDup(db, csr, n); - if (zParent == 0) - break; - is_quoted = *zParent == '"' ? true : false; - sqlite3NormalizeName(zParent); - if (0 == strcmp(old_name, zParent)) { - (*numb_of_occurrences)++; - if (!is_quoted) - (*numb_of_unquoted)++; - char *zOut = sqlite3MPrintf(db, "%s%.*s\"%w\"", - (output ? output : ""), - (int)((char*)csr - sql_stmt), - sql_stmt, new_name); - sqlite3DbFree(db, output); - output = zOut; - sql_stmt = &csr[n]; - } - sqlite3DbFree(db, zParent); - } - } - - new_sql_stmt = sqlite3MPrintf(db, "%s%s", (output ? output : ""), sql_stmt); - sqlite3DbFree(db, output); - return new_sql_stmt; -} - /* This function is used to implement the ALTER TABLE command. * The table name in the CREATE TRIGGER statement is replaced with the third * argument and the result returned. This is analagous to rename_table() diff --git a/src/box/sql/build.c b/src/box/sql/build.c index 819c2626a..13013ee5a 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -47,6 +47,7 @@ #include "vdbeInt.h" #include "tarantoolInt.h" #include "box/box.h" +#include "box/fkey.h" #include "box/sequence.h" #include "box/session.h" #include "box/identifier.h" @@ -332,9 +333,6 @@ deleteTable(sqlite3 * db, Table * pTable) freeIndex(db, pIndex); } - /* Delete any foreign keys attached to this table. */ - sqlite3FkDelete(db, pTable); - /* Delete the Table structure itself. */ sqlite3HashClear(&pTable->idxHash); @@ -1551,6 +1549,126 @@ emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id return first_col; } +/** + * Generate opcodes to serialize foreign key into MsgPack and + * insert produced tuple into _fk_constraint space. + * + * @param parse_context Parsing context. + * @param fk Foreign key to be created. + */ +static void +vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk) +{ + assert(parse_context != NULL); + assert(fk != NULL); + struct Vdbe *vdbe = sqlite3GetVdbe(parse_context); + assert(vdbe != NULL); + /* + * Occupy registers for 8 fields: each member in + * _constraint space plus one for final msgpack tuple. + */ + int constr_tuple_reg = sqlite3GetTempRange(parse_context, 10); + char *name_copy = sqlite3DbStrDup(parse_context->db, fk->name); + if (name_copy == NULL) + return; + sqlite3VdbeAddOp4(vdbe, OP_String8, 0, constr_tuple_reg, 0, name_copy, + P4_DYNAMIC); + /* + * In case we are adding FK constraints during execution + * of <CREATE TABLE ...> statement, we don't have child + * id, but we know register where it will be stored. + * */ + if (parse_context->pNewTable != NULL) { + sqlite3VdbeAddOp2(vdbe, OP_SCopy, fk->child_id, + constr_tuple_reg + 1); + } else { + sqlite3VdbeAddOp2(vdbe, OP_Integer, fk->child_id, + constr_tuple_reg + 1); + } + if (parse_context->pNewTable != NULL && fkey_is_self_referenced(fk)) { + sqlite3VdbeAddOp2(vdbe, OP_SCopy, fk->parent_id, + constr_tuple_reg + 2); + } else { + sqlite3VdbeAddOp2(vdbe, OP_Integer, fk->parent_id, + constr_tuple_reg + 2); + } + sqlite3VdbeAddOp2(vdbe, OP_Bool, 0, constr_tuple_reg + 3); + sqlite3VdbeChangeP4(vdbe, -1, (char*)&fk->is_deferred, P4_BOOL); + sqlite3VdbeAddOp4(vdbe, OP_String8, 0, constr_tuple_reg + 4, 0, + fkey_match_strs[fk->match], P4_STATIC); + sqlite3VdbeAddOp4(vdbe, OP_String8, 0, constr_tuple_reg + 5, 0, + fkey_action_strs[fk->on_delete], P4_STATIC); + sqlite3VdbeAddOp4(vdbe, OP_String8, 0, constr_tuple_reg + 6, 0, + fkey_action_strs[fk->on_update], P4_STATIC); + size_t encoded_parent_sz = fkey_encode_links(&fk->links[0].parent_field, + fk->field_count, NULL); + size_t encoded_child_sz = fkey_encode_links(&fk->links[0].child_field, + fk->field_count, NULL); + /* + * We are allocating memory for both parent and child + * arrays in the same chunk. Thus, first OP_Blob opcode + * interprets it as static memory, and the second one - + * as dynamic and releases memory. + */ + char *encoded_links = sqlite3DbMallocRaw(parse_context->db, + encoded_child_sz + + encoded_parent_sz); + if (encoded_links == NULL) { + sqlite3DbFree(parse_context->db, (void *) name_copy); + return; + } + /* + * Here we use small memory trick: parent and child links + * are quite similar but assigned to different fields. + * So to avoid code duplication, we calculate offset + * and fetch proper parent or child link: + * + * +--------------------------------------+ + * | child | parent | child | parent| ... | + * |--------------------------------------| + * | link[0] | link[1] | ... | + * +--------------------------------------+ + */ + size_t real_parent_sz = fkey_encode_links(&fk->links[0].parent_field, + fk->field_count, + encoded_links); + size_t real_child_sz = fkey_encode_links(&fk->links[0].child_field, + fk->field_count, + encoded_links + + real_parent_sz); + sqlite3VdbeAddOp4(vdbe, OP_Blob, real_child_sz, constr_tuple_reg + 7, + SQL_SUBTYPE_MSGPACK, encoded_links + real_parent_sz, + P4_STATIC); + sqlite3VdbeAddOp4(vdbe, OP_Blob, real_parent_sz, constr_tuple_reg + 8, + SQL_SUBTYPE_MSGPACK, encoded_links, + P4_DYNAMIC); + sqlite3VdbeAddOp3(vdbe, OP_MakeRecord, constr_tuple_reg, 9, + constr_tuple_reg + 9); + sqlite3VdbeAddOp2(vdbe, OP_SInsert, BOX_FK_CONSTRAINT_ID, + constr_tuple_reg + 9); + sqlite3VdbeChangeP5(vdbe, OPFLAG_NCHANGE); + sqlite3ReleaseTempRange(parse_context, constr_tuple_reg, 10); +} + +static int +resolve_link(struct Parse *parse_context, const struct space_def *def, + const char *field_name, uint32_t *link, const char *fk_name) +{ + assert(link != NULL); + for (uint32_t j = 0; j < def->field_count; ++j) { + if (strcmp(field_name, def->fields[j].name) == 0) { + *link = j; + return 0; + } + } + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, fk_name, + tt_sprintf("unknown column %s in foreign key definition", + field_name)); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; + return -1; +} + /* * This routine is called to report the final ")" that terminates * a CREATE TABLE statement. @@ -1720,6 +1838,45 @@ sqlite3EndTable(Parse * pParse, /* Parse context */ /* Reparse everything to update our internal data structures */ parseTableSchemaRecord(pParse, iSpaceId, zStmt); /* consumes zStmt */ + + /* Code creation of FK constraints, if any. */ + struct fkey_parse *fk_parse; + rlist_foreach_entry(fk_parse, &pParse->new_fkey, link) { + struct fkey_def *fk = fk_parse->fkey; + if (fk_parse->selfref_cols != NULL) { + struct ExprList *cols = fk_parse->selfref_cols; + for (uint32_t i = 0; i < fk->field_count; ++i) { + if (resolve_link(pParse, p->def, + cols->a[i].zName, + &fk->links[i].parent_field, + fk->name) != 0) + return; + } + fk->parent_id = iSpaceId; + } else if (fk_parse->is_self_referenced) { + struct Index *pk = sqlite3PrimaryKeyIndex(p); + if (pk->def->key_def->part_count != + fk->field_count) { + diag_set(ClientError, + ER_CREATE_FK_CONSTRAINT, + fk->name, "number of columns " + "in foreign key does not " + "match the number of columns " + "in the primary index of " + "referenced table"); + pParse->rc = SQL_TARANTOOL_ERROR; + pParse->nErr++; + return; + } + for (uint32_t i = 0; i < fk->field_count; ++i) { + fk->links[i].parent_field = + pk->def->key_def->parts[i].fieldno; + } + fk->parent_id = iSpaceId; + } + fk->child_id = iSpaceId; + vdbe_emit_fkey_create(pParse, fk); + } } /* Add the table to the in-memory representation of the database. @@ -1927,6 +2084,32 @@ sql_clear_stat_spaces(struct Parse *parse, const char *table_name, vdbe_emit_stat_space_clear(parse, "_sql_stat1", idx_name, table_name); } +/** + * Generate VDBE program to remove entry from _fk_constraint space. + * + * @param parse_context Parsing context. + * @param constraint_name Name of FK constraint to be dropped. + * Must be allocated on head by sqlite3DbMalloc(). + * It will be freed in VDBE. + * @param child_id Id of table which constraint belongs to. + */ +static void +vdbe_emit_fkey_drop(struct Parse *parse_context, const char *constraint_name, + uint32_t child_id) +{ + struct Vdbe *vdbe = sqlite3GetVdbe(parse_context); + assert(vdbe != NULL); + int key_reg = sqlite3GetTempRange(parse_context, 3); + sqlite3VdbeAddOp4(vdbe, OP_String8, 0, key_reg, 0, constraint_name, + P4_DYNAMIC); + sqlite3VdbeAddOp2(vdbe, OP_Integer, child_id, key_reg + 1); + sqlite3VdbeAddOp3(vdbe, OP_MakeRecord, key_reg, 2, key_reg + 2); + sqlite3VdbeAddOp2(vdbe, OP_SDelete, BOX_FK_CONSTRAINT_ID, key_reg + 2); + sqlite3VdbeChangeP5(vdbe, OPFLAG_NCHANGE); + VdbeComment((vdbe, "Delete FK constraint %s", constraint_name)); + sqlite3ReleaseTempRange(parse_context, key_reg, 3); +} + /** * Generate code to drop a table. * This routine includes dropping triggers, sequences, @@ -1982,6 +2165,15 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space, sqlite3VdbeAddOp2(v, OP_SDelete, BOX_SEQUENCE_ID, idx_rec_reg); VdbeComment((v, "Delete entry from _sequence")); } + /* Delete all child FK constraints. */ + struct fkey *child_fk; + rlist_foreach_entry (child_fk, &space->child_fkey, child_link) { + const char *fk_name_dup = sqlite3DbStrDup(v->db, + child_fk->def->name); + if (fk_name_dup == NULL) + return; + vdbe_emit_fkey_drop(parse_context, fk_name_dup, space_id); + } /* * Drop all _space and _index entries that refer to the * table. @@ -2090,14 +2282,15 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list, * removing indexes from _index space and eventually * tuple with corresponding space_id from _space. */ - struct Table *tab = sqlite3HashFind(&db->pSchema->tblHash, space_name); - struct FKey *fk = sqlite3FkReferences(tab); - if (fk != NULL && (fk->pFrom->def->id != tab->def->id)) { - diag_set(ClientError, ER_DROP_SPACE, space_name, - "other objects depend on it"); - parse_context->rc = SQL_TARANTOOL_ERROR; - parse_context->nErr++; - goto exit_drop_table; + struct fkey *fk; + rlist_foreach_entry (fk, &space->parent_fkey, parent_link) { + if (! fkey_is_self_referenced(fk->def)) { + diag_set(ClientError, ER_DROP_SPACE, space_name, + "other objects depend on it"); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; + goto exit_drop_table; + } } sql_clear_stat_spaces(parse_context, space_name, NULL); sql_code_drop_table(parse_context, space, is_view); @@ -2106,177 +2299,281 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list, sqlite3SrcListDelete(db, table_name_list); } -/* - * This routine is called to create a new foreign key on the table - * currently under construction. pFromCol determines which columns - * in the current table point to the foreign key. If pFromCol==0 then - * connect the key to the last column inserted. pTo is the name of - * the table referred to (a.k.a the "parent" table). pToCol is a list - * of tables in the parent pTo table. flags contains all - * information about the conflict resolution algorithms specified - * in the ON DELETE, ON UPDATE and ON INSERT clauses. +/** + * Return ordinal number of column by name. In case of error, + * set error message. * - * An FKey structure is created and added to the table currently - * under construction in the pParse->pNewTable field. + * @param parse_context Parsing context. + * @param space Space which column belongs to. + * @param column_name Name of column to investigate. + * @param[out] colno Found name of column. + * @param fk_name Name of FK constraint to be created. * - * The foreign key is set for IMMEDIATE processing. A subsequent call - * to sqlite3DeferForeignKey() might change this to DEFERRED. + * @retval 0 on success, -1 on fault. */ -void -sqlite3CreateForeignKey(Parse * pParse, /* Parsing context */ - ExprList * pFromCol, /* Columns in this table that point to other table */ - Token * pTo, /* Name of the other table */ - ExprList * pToCol, /* Columns in the other table */ - int flags /* Conflict resolution algorithms. */ - ) +static int +columnno_by_name(struct Parse *parse_context, const struct space *space, + const char *column_name, uint32_t *colno, const char *fk_name) { - sqlite3 *db = pParse->db; -#ifndef SQLITE_OMIT_FOREIGN_KEY - FKey *pFKey = 0; - FKey *pNextTo; - Table *p = pParse->pNewTable; - int nByte; - int i; - int nCol; - char *z; - - assert(pTo != 0); - char *normalized_name = strndup(pTo->z, pTo->n); - if (normalized_name == NULL) { - diag_set(OutOfMemory, pTo->n, "strndup", "normalized name"); - goto fk_end; - } - sqlite3NormalizeName(normalized_name); - uint32_t parent_id = box_space_id_by_name(normalized_name, - strlen(normalized_name)); - if (parent_id == BOX_ID_NIL && - strcmp(normalized_name, p->def->name) != 0) { - diag_set(ClientError, ER_NO_SUCH_SPACE, normalized_name); - pParse->rc = SQL_TARANTOOL_ERROR; - pParse->nErr++; - goto fk_end; - } - struct space *parent_space = space_by_id(parent_id); - if (parent_space != NULL && parent_space->def->opts.is_view) { - sqlite3ErrorMsg(pParse, "can't create foreign key constraint "\ - "referencing view: %s", normalized_name); - goto fk_end; + assert(colno != NULL); + uint32_t column_len = strlen(column_name); + if (tuple_fieldno_by_name(space->def->dict, column_name, column_len, + field_name_hash(column_name, column_len), + colno) != 0) { + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, fk_name, + tt_sprintf("foreign key refers to nonexistent field %s", + column_name)); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; + return -1; } - if (p == 0) - goto fk_end; - if (pFromCol == 0) { - int iCol = p->def->field_count - 1; - if (NEVER(iCol < 0)) - goto fk_end; - if (pToCol && pToCol->nExpr != 1) { - sqlite3ErrorMsg(pParse, "foreign key on %s" - " should reference only one column of table %T", - p->def->fields[iCol].name, pTo); - goto fk_end; + return 0; +} + +void +sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, + struct Token *constraint, struct ExprList *child_cols, + struct Token *parent, struct ExprList *parent_cols, + bool is_deferred, int actions) +{ + struct sqlite3 *db = parse_context->db; + /* + * When this function is called second time during + * <CREATE TABLE ...> statement (i.e. at VDBE runtime), + * don't even try to do something. + */ + if (db->init.busy) + return; + /* + * Beforehand initialization for correct clean-up + * while emergency exiting in case of error. + */ + const char *parent_name = NULL; + const char *constraint_name = NULL; + bool is_self_referenced = false; + /* + * Table under construction during CREATE TABLE + * processing. NULL for ALTER TABLE statement handling. + */ + struct Table *new_tab = parse_context->pNewTable; + /* Whether we are processing ALTER TABLE or CREATE TABLE. */ + bool is_alter = new_tab == NULL; + uint32_t child_cols_count; + if (child_cols == NULL) { + assert(!is_alter); + child_cols_count = 1; + } else { + child_cols_count = child_cols->nExpr; + } + assert(!is_alter || (child != NULL && child->nSrc == 1)); + struct space *child_space = NULL; + uint32_t child_id = 0; + if (is_alter) { + const char *child_name = child->a[0].zName; + child_id = box_space_id_by_name(child_name, + strlen(child_name)); + if (child_id == BOX_ID_NIL) { + diag_set(ClientError, ER_NO_SUCH_SPACE, child_name); + goto tnt_error; } - nCol = 1; - } else if (pToCol && pToCol->nExpr != pFromCol->nExpr) { - sqlite3ErrorMsg(pParse, - "number of columns in foreign key does not match the number of " - "columns in the referenced table"); - goto fk_end; + child_space = space_by_id(child_id); + assert(child_space != NULL); } else { - nCol = pFromCol->nExpr; - } - nByte = sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) + - strlen(normalized_name) + 1; - if (pToCol) { - for (i = 0; i < pToCol->nExpr; i++) { - nByte += sqlite3Strlen30(pToCol->a[i].zName) + 1; + struct fkey_parse *fk = region_alloc(&parse_context->region, + sizeof(*fk)); + if (fk == NULL) { + diag_set(OutOfMemory, sizeof(*fk), "region_alloc", + "fk"); + goto tnt_error; + } + memset(fk, 0, sizeof(*fk)); + rlist_add_entry(&parse_context->new_fkey, fk, link); + } + assert(parent != NULL); + parent_name = sqlite3NameFromToken(db, parent); + if (parent_name == NULL) + goto exit_create_fk; + uint32_t parent_id = box_space_id_by_name(parent_name, + strlen(parent_name)); + /* + * Within ALTER TABLE ADD CONSTRAINT FK also can be + * self-referenced, but in this case parent (which is + * also child) table will definitely exist. + */ + is_self_referenced = !is_alter && + strcmp(parent_name, new_tab->def->name) == 0; + struct space *parent_space; + if (parent_id == BOX_ID_NIL) { + parent_space = NULL; + if (is_self_referenced) { + struct fkey_parse *fk = + rlist_first_entry(&parse_context->new_fkey, + struct fkey_parse, link); + fk->selfref_cols = parent_cols; + fk->is_self_referenced = true; + } else { + diag_set(ClientError, ER_NO_SUCH_SPACE, parent_name);; + goto tnt_error; } - } - pFKey = sqlite3DbMallocZero(db, nByte); - if (pFKey == 0) { - goto fk_end; - } - pFKey->pFrom = p; - pFKey->pNextFrom = p->pFKey; - z = (char *)&pFKey->aCol[nCol]; - pFKey->zTo = z; - memcpy(z, normalized_name, strlen(normalized_name) + 1); - z += strlen(normalized_name) + 1; - pFKey->nCol = nCol; - if (pFromCol == 0) { - pFKey->aCol[0].iFrom = p->def->field_count - 1; } else { - for (i = 0; i < nCol; i++) { - int j; - for (j = 0; j < (int)p->def->field_count; j++) { - if (strcmp(p->def->fields[j].name, - pFromCol->a[i].zName) == 0) { - pFKey->aCol[i].iFrom = j; - break; - } - } - if (j >= (int)p->def->field_count) { - sqlite3ErrorMsg(pParse, - "unknown column \"%s\" in foreign key definition", - pFromCol->a[i].zName); - goto fk_end; - } + parent_space = space_by_id(parent_id); + assert(parent_space != NULL); + if (parent_space->def->opts.is_view) { + sqlite3ErrorMsg(parse_context, + "referenced table can't be view"); + goto exit_create_fk; } } - if (pToCol) { - for (i = 0; i < nCol; i++) { - int n = sqlite3Strlen30(pToCol->a[i].zName); - pFKey->aCol[i].zCol = z; - memcpy(z, pToCol->a[i].zName, n); - z[n] = 0; - z += n + 1; + if (constraint == NULL && !is_alter) { + if (parse_context->constraintName.n == 0) { + constraint_name = + sqlite3MPrintf(db, "fk_constraint_%d_%s", + ++parse_context->fkey_count, + new_tab->def->name); + } else { + struct Token *cnstr_nm = &parse_context->constraintName; + constraint_name = sqlite3NameFromToken(db, cnstr_nm); + } + } else { + constraint_name = sqlite3NameFromToken(db, constraint); + } + if (constraint_name == NULL) + goto exit_create_fk; + const char *error_msg = "number of columns in foreign key does not " + "match the number of columns in the primary " + "index of referenced table"; + if (parent_cols != NULL) { + if (parent_cols->nExpr != (int) child_cols_count) { + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, + constraint_name, error_msg); + goto tnt_error; + } + } else if (!is_self_referenced) { + /* + * If parent columns are not specified, then PK + * columns of parent table are used as referenced. + */ + struct index *parent_pk = space_index(parent_space, 0); + assert(parent_pk != NULL); + if (parent_pk->def->key_def->part_count != child_cols_count) { + diag_set(ClientError, ER_CREATE_FK_CONSTRAINT, + constraint_name, error_msg); + goto tnt_error; } } - pFKey->isDeferred = 0; - pFKey->aAction[0] = (u8) (flags & 0xff); /* ON DELETE action */ - pFKey->aAction[1] = (u8) ((flags >> 8) & 0xff); /* ON UPDATE action */ - - pNextTo = (FKey *) sqlite3HashInsert(&p->pSchema->fkeyHash, - pFKey->zTo, (void *)pFKey); - if (pNextTo == pFKey) { - sqlite3OomFault(db); - goto fk_end; + int name_len = strlen(constraint_name); + size_t fk_size = fkey_def_sizeof(child_cols_count, name_len); + struct fkey_def *fk = region_alloc(&parse_context->region, fk_size); + if (fk == NULL) { + diag_set(OutOfMemory, fk_size, "region", "struct fkey"); + goto tnt_error; } - if (pNextTo) { - assert(pNextTo->pPrevTo == 0); - pFKey->pNextTo = pNextTo; - pNextTo->pPrevTo = pFKey; + fk->field_count = child_cols_count; + fk->child_id = child_id; + fk->parent_id = parent_id; + fk->is_deferred = is_deferred; + fk->match = (enum fkey_match) ((actions >> 16) & 0xff); + fk->on_update = (enum fkey_action) ((actions >> 8) & 0xff); + fk->on_delete = (enum fkey_action) (actions & 0xff); + fk->links = (struct field_link *) ((char *) fk->name + name_len + 1); + /* Fill links map. */ + for (uint32_t i = 0; i < fk->field_count; ++i) { + if (!is_self_referenced && parent_cols == NULL) { + struct key_def *pk_def = + parent_space->index[0]->def->key_def; + fk->links[i].parent_field = pk_def->parts[i].fieldno; + } else if (!is_self_referenced && + columnno_by_name(parse_context, parent_space, + parent_cols->a[i].zName, + &fk->links[i].parent_field, + constraint_name) != 0) { + goto exit_create_fk; + } + if (!is_alter) { + if (child_cols == NULL) { + assert(i == 0); + /* + * In this case there must be only + * one link (the last column + * added), so we can break + * immediately. + */ + fk->links[0].child_field = + new_tab->def->field_count - 1; + break; + } + if (resolve_link(parse_context, new_tab->def, + child_cols->a[i].zName, + &fk->links[i].child_field, + constraint_name) != 0) + goto exit_create_fk; + /* In case of ALTER parent table must exist. */ + } else if (columnno_by_name(parse_context, child_space, + child_cols->a[i].zName, + &fk->links[i].child_field, + constraint_name) != 0) { + goto exit_create_fk; + } } - - /* Link the foreign key to the table as the last step. + memcpy(fk->name, constraint_name, name_len); + fk->name[name_len] = '\0'; + sqlite3NormalizeName(fk->name); + /* + * In case of CREATE TABLE processing, all foreign keys + * constraints must be created after space itself, so + * lets delay it until sqlite3EndTable() call and simply + * maintain list of all FK constraints inside parser. */ - p->pFKey = pFKey; - pFKey = 0; + if (!is_alter) { + struct fkey_parse *parse_fk = + rlist_first_entry(&parse_context->new_fkey, + struct fkey_parse, link); + parse_fk->fkey = fk; + } else { + vdbe_emit_fkey_create(parse_context, fk); + } - fk_end: - sqlite3DbFree(db, pFKey); - free(normalized_name); -#endif /* !defined(SQLITE_OMIT_FOREIGN_KEY) */ - sql_expr_list_delete(db, pFromCol); - sql_expr_list_delete(db, pToCol); +exit_create_fk: + sql_expr_list_delete(db, child_cols); + if (!is_self_referenced) + sql_expr_list_delete(db, parent_cols); + sqlite3DbFree(db, (void *) parent_name); + sqlite3DbFree(db, (void *) constraint_name); + return; +tnt_error: + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; + goto exit_create_fk; } -/* - * This routine is called when an INITIALLY IMMEDIATE or INITIALLY DEFERRED - * clause is seen as part of a foreign key definition. The isDeferred - * parameter is 1 for INITIALLY DEFERRED and 0 for INITIALLY IMMEDIATE. - * The behavior of the most recently created foreign key is adjusted - * accordingly. - */ void -sqlite3DeferForeignKey(Parse * pParse, int isDeferred) +fkey_change_defer_mode(struct Parse *parse_context, bool is_deferred) { -#ifndef SQLITE_OMIT_FOREIGN_KEY - Table *pTab; - FKey *pFKey; - if ((pTab = pParse->pNewTable) == 0 || (pFKey = pTab->pFKey) == 0) + if (parse_context->db->init.busy || + rlist_empty(&parse_context->new_fkey)) return; - assert(isDeferred == 0 || isDeferred == 1); /* EV: R-30323-21917 */ - pFKey->isDeferred = (u8) isDeferred; -#endif + rlist_first_entry(&parse_context->new_fkey, struct fkey_parse, + link)->fkey->is_deferred = is_deferred; +} + +void +sql_drop_foreign_key(struct Parse *parse_context, struct SrcList *table, + struct Token *constraint) +{ + assert(table != NULL && table->nSrc == 1); + const char *table_name = table->a[0].zName; + uint32_t child_id = box_space_id_by_name(table_name, + strlen(table_name)); + if (child_id == BOX_ID_NIL) { + diag_set(ClientError, ER_NO_SUCH_SPACE, table_name); + parse_context->rc = SQL_TARANTOOL_ERROR; + parse_context->nErr++; + return; + } + const char *constraint_name = sqlite3NameFromToken(parse_context->db, + constraint); + if (constraint_name != NULL) + vdbe_emit_fkey_drop(parse_context, constraint_name, child_id); } /* diff --git a/src/box/sql/callback.c b/src/box/sql/callback.c index 01e8dd8f1..c630bf21d 100644 --- a/src/box/sql/callback.c +++ b/src/box/sql/callback.c @@ -294,7 +294,6 @@ sqlite3SchemaClear(sqlite3 * db) sqlite3DeleteTable(0, pTab); } sqlite3HashClear(&temp1); - sqlite3HashClear(&pSchema->fkeyHash); db->pSchema = NULL; } @@ -303,13 +302,10 @@ sqlite3SchemaClear(sqlite3 * db) Schema * sqlite3SchemaCreate(sqlite3 * db) { - Schema *p; - p = (Schema *) sqlite3DbMallocZero(0, sizeof(Schema)); - if (!p) { + struct Schema *p = (Schema *) sqlite3DbMallocZero(0, sizeof(Schema)); + if (p == NULL) sqlite3OomFault(db); - } else { + else sqlite3HashInit(&p->tblHash); - sqlite3HashInit(&p->fkeyHash); - } return p; } diff --git a/src/box/sql/delete.c b/src/box/sql/delete.c index 06811778f..57a067760 100644 --- a/src/box/sql/delete.c +++ b/src/box/sql/delete.c @@ -130,7 +130,7 @@ sql_table_delete_from(struct Parse *parse, struct SrcList *tab_list, assert(space != NULL); trigger_list = sql_triggers_exist(table, TK_DELETE, NULL, NULL); is_complex = trigger_list != NULL || - sqlite3FkRequired(table, NULL); + fkey_is_required(table->def->id, NULL); } assert(space != NULL); @@ -437,14 +437,16 @@ sql_generate_row_delete(struct Parse *parse, struct Table *table, * use for the old.* references in the triggers. */ if (table != NULL && - (sqlite3FkRequired(table, NULL) || trigger_list != NULL)) { + (fkey_is_required(table->def->id, NULL) || trigger_list != NULL)) { /* Mask of OLD.* columns in use */ /* TODO: Could use temporary registers here. */ uint32_t mask = sql_trigger_colmask(parse, trigger_list, 0, 0, TRIGGER_BEFORE | TRIGGER_AFTER, table, onconf); - mask |= sqlite3FkOldmask(parse, table); + struct space *space = space_by_id(table->def->id); + assert(space != NULL); + mask |= space->fkey_mask; first_old_reg = parse->nMem + 1; parse->nMem += (1 + (int)table->def->field_count); @@ -488,7 +490,7 @@ sql_generate_row_delete(struct Parse *parse, struct Table *table, * constraints attached to other tables) are not * violated by deleting this row. */ - sqlite3FkCheck(parse, table, first_old_reg, 0, NULL); + fkey_emit_check(parse, table, first_old_reg, 0, NULL); } /* Delete the index and table entries. Skip this step if @@ -518,7 +520,7 @@ sql_generate_row_delete(struct Parse *parse, struct Table *table, * key to the row just deleted. */ - sqlite3FkActions(parse, table, 0, first_old_reg, 0); + fkey_emit_actions(parse, table, first_old_reg, NULL); /* Invoke AFTER DELETE trigger programs. */ vdbe_code_row_trigger(parse, trigger_list, TK_DELETE, 0, diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c index c941b6e58..39213e5dc 100644 --- a/src/box/sql/fkey.c +++ b/src/box/sql/fkey.c @@ -38,9 +38,6 @@ #include "box/fkey.h" #include "box/schema.h" #include "box/session.h" -#include "tarantoolInt.h" - -#ifndef SQLITE_OMIT_FOREIGN_KEY /* * Deferred and Immediate FKs @@ -137,19 +134,14 @@ * coding an INSERT operation. The functions used by the UPDATE/DELETE * generation code to query for this information are: * - * sqlite3FkRequired() - Test to see if FK processing is required. - * sqlite3FkOldmask() - Query for the set of required old.* columns. - * + * fkey_is_required() - Test to see if FK processing is required. * * Externally accessible module functions * -------------------------------------- * - * sqlite3FkCheck() - Check for foreign key violations. - * sqlite3FkActions() - Code triggers for ON UPDATE/ON DELETE actions. - * sqlite3FkDelete() - Delete an FKey structure. - */ - -/* + * fkey_emit_check() - Check for foreign key violations. + * fkey_emit_actions() - Code triggers for ON UPDATE/ON DELETE actions. + * * VDBE Calling Convention * ----------------------- * @@ -166,332 +158,134 @@ * Register (x+3): 3.1 (type real) */ -/* - * A foreign key constraint requires that the key columns in the parent - * table are collectively subject to a UNIQUE or PRIMARY KEY constraint. - * Given that pParent is the parent table for foreign key constraint pFKey, - * search the schema for a unique index on the parent key columns. - * - * If successful, zero is returned. If the parent key is an INTEGER PRIMARY - * KEY column, then output variable *ppIdx is set to NULL. Otherwise, *ppIdx - * is set to point to the unique index. +/** + * This function is called when a row is inserted into or deleted + * from the child table of foreign key constraint. If an SQL + * UPDATE is executed on the child table of fkey, this function is + * invoked twice for each row affected - once to "delete" the old + * row, and then again to "insert" the new row. * - * If the parent key consists of a single column (the foreign key constraint - * is not a composite foreign key), output variable *paiCol is set to NULL. - * Otherwise, it is set to point to an allocated array of size N, where - * N is the number of columns in the parent key. The first element of the - * array is the index of the child table column that is mapped by the FK - * constraint to the parent table column stored in the left-most column - * of index *ppIdx. The second element of the array is the index of the - * child table column that corresponds to the second left-most column of - * *ppIdx, and so on. + * Each time it is called, this function generates VDBE code to + * locate the row in the parent table that corresponds to the row + * being inserted into or deleted from the child table. If the + * parent row can be found, no special action is taken. Otherwise, + * if the parent row can *not* be found in the parent table: * - * If the required index cannot be found, either because: + * Op | FK type | Action taken + * --------------------------------------------------------------- + * INSERT immediate Increment the "immediate constraint counter". * - * 1) The named parent key columns do not exist, or + * DELETE immediate Decrement the "immediate constraint counter". * - * 2) The named parent key columns do exist, but are not subject to a - * UNIQUE or PRIMARY KEY constraint, or + * INSERT deferred Increment the "deferred constraint counter". * - * 3) No parent key columns were provided explicitly as part of the - * foreign key definition, and the parent table does not have a - * PRIMARY KEY, or + * DELETE deferred Decrement the "deferred constraint counter". * - * 4) No parent key columns were provided explicitly as part of the - * foreign key definition, and the PRIMARY KEY of the parent table - * consists of a different number of columns to the child key in - * the child table. + * These operations are identified in the comment at the top of + * this file as "I.1" and "D.1". * - * then non-zero is returned, and a "foreign key mismatch" error loaded - * into pParse. If an OOM error occurs, non-zero is returned and the - * pParse->db->mallocFailed flag is set. + * @param parse_context Current parsing context. + * @param parent Parent table of FK constraint. + * @param fk_def FK constraint definition. + * @param referenced_idx Id of referenced index. + * @param reg_data Address of array containing child table row. + * @param incr_count Increment constraint counter by this value. */ -int -sqlite3FkLocateIndex(Parse * pParse, /* Parse context to store any error in */ - Table * pParent, /* Parent table of FK constraint pFKey */ - FKey * pFKey, /* Foreign key to find index for */ - Index ** ppIdx, /* OUT: Unique index on parent table */ - int **paiCol /* OUT: Map of index columns in pFKey */ - ) +static void +fkey_lookup_parent(struct Parse *parse_context, struct space *parent, + struct fkey_def *fk_def, uint32_t referenced_idx, + int reg_data, int incr_count) { - int *aiCol = 0; /* Value to return via *paiCol */ - int nCol = pFKey->nCol; /* Number of columns in parent key */ - char *zKey = pFKey->aCol[0].zCol; /* Name of left-most parent key column */ - - /* The caller is responsible for zeroing output parameters. */ - assert(ppIdx && *ppIdx == 0); - assert(!paiCol || *paiCol == 0); - assert(pParse); - - /* If this is a non-composite (single column) foreign key, check if it - * maps to the INTEGER PRIMARY KEY of table pParent. If so, leave *ppIdx - * and *paiCol set to zero and return early. + assert(incr_count == -1 || incr_count == 1); + struct Vdbe *v = sqlite3GetVdbe(parse_context); + int cursor = parse_context->nTab - 1; + int ok_label = sqlite3VdbeMakeLabel(v); + /* + * If incr_count is less than zero, then check at runtime + * if there are any outstanding constraints to resolve. + * If there are not, there is no need to check if deleting + * this row resolves any outstanding violations. * - * Otherwise, for a composite foreign key (more than one column), allocate - * space for the aiCol array (returned via output parameter *paiCol). - * Non-composite foreign keys do not require the aiCol array. + * Check if any of the key columns in the child table row + * are NULL. If any are, then the constraint is considered + * satisfied. No need to search for a matching row in the + * parent table. */ - if (paiCol && nCol > 1) { - aiCol = - (int *)sqlite3DbMallocRawNN(pParse->db, nCol * sizeof(int)); - if (!aiCol) - return 1; - *paiCol = aiCol; + if (incr_count < 0) { + sqlite3VdbeAddOp2(v, OP_FkIfZero, fk_def->is_deferred, + ok_label); } - - struct Index *index = NULL; - for (index = pParent->pIndex; index != NULL; index = index->pNext) { - int part_count = index->def->key_def->part_count; - if (part_count != nCol || !index->def->opts.is_unique || - index->pPartIdxWhere != NULL) - continue; - /* - * Index is a UNIQUE index (or a PRIMARY KEY) and - * has the right number of columns. If each - * indexed column corresponds to a foreign key - * column of pFKey, then this index is a winner. - */ - if (zKey == NULL) { - /* - * If zKey is NULL, then this foreign key - * is implicitly mapped to the PRIMARY KEY - * of table pParent. The PRIMARY KEY index - * may be identified by the test. - */ - if (IsPrimaryKeyIndex(index)) { - if (aiCol != NULL) { - for (int i = 0; i < nCol; i++) - aiCol[i] = pFKey->aCol[i].iFrom; - } - break; - } - } else { - /* - * If zKey is non-NULL, then this foreign - * key was declared to map to an explicit - * list of columns in table pParent. Check - * if this index matches those columns. - * Also, check that the index uses the - * default collation sequences for each - * column. - */ - int i, j; - struct key_part *part = index->def->key_def->parts; - for (i = 0; i < nCol; i++, part++) { - /* - * Index of column in parent - * table. - */ - i16 iCol = (int) part->fieldno; - /* - * If the index uses a collation - * sequence that is different from - * the default collation sequence - * for the column, this index is - * unusable. Bail out early in - * this case. - */ - uint32_t id; - struct coll *def_coll = - sql_column_collation(pParent->def, - iCol, &id); - struct coll *coll = part->coll; - if (def_coll != coll) - break; - - char *zIdxCol = pParent->def->fields[iCol].name; - for (j = 0; j < nCol; j++) { - if (strcmp(pFKey->aCol[j].zCol, - zIdxCol) != 0) - continue; - if (aiCol) - aiCol[i] = pFKey->aCol[j].iFrom; - break; - } - if (j == nCol) - break; - } - if (i == nCol) { - /* Index is usable. */ - break; - } - } + struct field_link *link = fk_def->links; + for (uint32_t i = 0; i < fk_def->field_count; ++i, ++link) { + int reg = link->child_field + reg_data + 1; + sqlite3VdbeAddOp2(v, OP_IsNull, reg, ok_label); } - - if (index == NULL) { - sqlite3ErrorMsg(pParse, "foreign key mismatch - " - "\"%w\" referencing \"%w\"", - pFKey->pFrom->def->name, pFKey->zTo); - } - - *ppIdx = index; - return 0; -} - -/* - * This function is called when a row is inserted into or deleted from the - * child table of foreign key constraint pFKey. If an SQL UPDATE is executed - * on the child table of pFKey, this function is invoked twice for each row - * affected - once to "delete" the old row, and then again to "insert" the - * new row. - * - * Each time it is called, this function generates VDBE code to locate the - * row in the parent table that corresponds to the row being inserted into - * or deleted from the child table. If the parent row can be found, no - * special action is taken. Otherwise, if the parent row can *not* be - * found in the parent table: - * - * Operation | FK type | Action taken - * -------------------------------------------------------------------------- - * INSERT immediate Increment the "immediate constraint counter". - * - * DELETE immediate Decrement the "immediate constraint counter". - * - * INSERT deferred Increment the "deferred constraint counter". - * - * DELETE deferred Decrement the "deferred constraint counter". - * - * These operations are identified in the comment at the top of this file - * (fkey.c) as "I.1" and "D.1". - */ -static void -fkLookupParent(Parse * pParse, /* Parse context */ - Table * pTab, /* Parent table of FK pFKey */ - Index * pIdx, /* Unique index on parent key columns in pTab */ - FKey * pFKey, /* Foreign key constraint */ - int *aiCol, /* Map from parent key columns to child table columns */ - int regData, /* Address of array containing child table row */ - int nIncr, /* Increment constraint counter by this */ - int isIgnore /* If true, pretend pTab contains all NULL values */ - ) -{ - int i; /* Iterator variable */ - Vdbe *v = sqlite3GetVdbe(pParse); /* Vdbe to add code to */ - int iCur = pParse->nTab - 1; /* Cursor number to use */ - int iOk = sqlite3VdbeMakeLabel(v); /* jump here if parent key found */ - struct session *user_session = current_session(); - - /* If nIncr is less than zero, then check at runtime if there are any - * outstanding constraints to resolve. If there are not, there is no need - * to check if deleting this row resolves any outstanding violations. + uint32_t field_count = fk_def->field_count; + int temp_regs = sqlite3GetTempRange(parse_context, field_count); + int rec_reg = sqlite3GetTempReg(parse_context); + vdbe_emit_open_cursor(parse_context, cursor, referenced_idx, parent); + link = fk_def->links; + for (uint32_t i = 0; i < field_count; ++i, ++link) { + sqlite3VdbeAddOp2(v, OP_Copy, link->child_field + 1 + reg_data, + temp_regs + i); + } + /* + * If the parent table is the same as the child table, and + * we are about to increment the constraint-counter (i.e. + * this is an INSERT operation), then check if the row + * being inserted matches itself. If so, do not increment + * the constraint-counter. * - * Check if any of the key columns in the child table row are NULL. If - * any are, then the constraint is considered satisfied. No need to - * search for a matching row in the parent table. + * If any of the parent-key values are NULL, then the row + * cannot match itself. So set JUMPIFNULL to make sure we + * do the OP_Found if any of the parent-key values are + * NULL (at this point it is known that none of the child + * key values are). */ - if (nIncr < 0) { - sqlite3VdbeAddOp2(v, OP_FkIfZero, pFKey->isDeferred, iOk); - VdbeCoverage(v); - } - for (i = 0; i < pFKey->nCol; i++) { - int iReg = aiCol[i] + regData + 1; - sqlite3VdbeAddOp2(v, OP_IsNull, iReg, iOk); - VdbeCoverage(v); - } - - if (isIgnore == 0) { - if (pIdx == 0) { - /* If pIdx is NULL, then the parent key is the INTEGER PRIMARY KEY - * column of the parent table (table pTab). - */ - int regTemp = sqlite3GetTempReg(pParse); - - /* Invoke MustBeInt to coerce the child key value to an integer (i.e. - * apply the affinity of the parent key). If this fails, then there - * is no matching parent key. Before using MustBeInt, make a copy of - * the value. Otherwise, the value inserted into the child key column - * will have INTEGER affinity applied to it, which may not be correct. - */ - sqlite3VdbeAddOp2(v, OP_SCopy, aiCol[0] + 1 + regData, - regTemp); - VdbeCoverage(v); - - /* If the parent table is the same as the child table, and we are about - * to increment the constraint-counter (i.e. this is an INSERT operation), - * then check if the row being inserted matches itself. If so, do not - * increment the constraint-counter. - */ - if (pTab == pFKey->pFrom && nIncr == 1) { - sqlite3VdbeAddOp3(v, OP_Eq, regData, iOk, - regTemp); - VdbeCoverage(v); - sqlite3VdbeChangeP5(v, SQLITE_NOTNULL); - } - - } else { - int nCol = pFKey->nCol; - int regTemp = sqlite3GetTempRange(pParse, nCol); - int regRec = sqlite3GetTempReg(pParse); - struct space *space = - space_by_id(pIdx->pTable->def->id); - vdbe_emit_open_cursor(pParse, iCur, pIdx->def->iid, - space); - for (i = 0; i < nCol; i++) { - sqlite3VdbeAddOp2(v, OP_Copy, - aiCol[i] + 1 + regData, - regTemp + i); - } - - /* If the parent table is the same as the child table, and we are about - * to increment the constraint-counter (i.e. this is an INSERT operation), - * then check if the row being inserted matches itself. If so, do not - * increment the constraint-counter. - * - * If any of the parent-key values are NULL, then the row cannot match - * itself. So set JUMPIFNULL to make sure we do the OP_Found if any - * of the parent-key values are NULL (at this point it is known that - * none of the child key values are). - */ - if (pTab == pFKey->pFrom && nIncr == 1) { - int iJump = - sqlite3VdbeCurrentAddr(v) + nCol + 1; - struct key_part *part = - pIdx->def->key_def->parts; - for (i = 0; i < nCol; ++i, ++part) { - int iChild = aiCol[i] + 1 + regData; - int iParent = 1 + regData + - (int)part->fieldno; - sqlite3VdbeAddOp3(v, OP_Ne, iChild, - iJump, iParent); - VdbeCoverage(v); - sqlite3VdbeChangeP5(v, - SQLITE_JUMPIFNULL); - } - sqlite3VdbeGoto(v, iOk); - } - - sqlite3VdbeAddOp4(v, OP_MakeRecord, regTemp, nCol, - regRec, - sqlite3IndexAffinityStr(pParse->db, - pIdx), nCol); - sqlite3VdbeAddOp4Int(v, OP_Found, iCur, iOk, regRec, 0); - VdbeCoverage(v); - - sqlite3ReleaseTempReg(pParse, regRec); - sqlite3ReleaseTempRange(pParse, regTemp, nCol); + if (fkey_is_self_referenced(fk_def) && incr_count == 1) { + int jump = sqlite3VdbeCurrentAddr(v) + field_count + 1; + link = fk_def->links; + for (uint32_t i = 0; i < field_count; ++i, ++link) { + int chcol = link->child_field + 1 + reg_data; + int pcol = link->parent_field + 1 + reg_data; + sqlite3VdbeAddOp3(v, OP_Ne, chcol, jump, pcol); + sqlite3VdbeChangeP5(v, SQLITE_JUMPIFNULL); } + sqlite3VdbeGoto(v, ok_label); } - - if (!pFKey->isDeferred && !(user_session->sql_flags & SQLITE_DeferFKs) - && !pParse->pToplevel && !pParse->isMultiWrite) { - /* Special case: If this is an INSERT statement that will insert exactly - * one row into the table, raise a constraint immediately instead of - * incrementing a counter. This is necessary as the VM code is being - * generated for will not open a statement transaction. + struct index *idx = space_index(parent, referenced_idx); + assert(idx != NULL); + sqlite3VdbeAddOp4(v, OP_MakeRecord, temp_regs, field_count, rec_reg, + sql_index_affinity_str(parse_context->db, idx->def), + P4_DYNAMIC); + sqlite3VdbeAddOp4Int(v, OP_Found, cursor, ok_label, rec_reg, 0); + sqlite3ReleaseTempReg(parse_context, rec_reg); + sqlite3ReleaseTempRange(parse_context, temp_regs, field_count); + struct session *session = current_session(); + if (!fk_def->is_deferred && + (session->sql_flags & SQLITE_DeferFKs) == 0 && + parse_context->pToplevel == NULL && !parse_context->isMultiWrite) { + /* + * If this is an INSERT statement that will insert + * exactly one row into the table, raise a + * constraint immediately instead of incrementing + * a counter. This is necessary as the VM code is + * being generated for will not open a statement + * transaction. */ - assert(nIncr == 1); - sqlite3HaltConstraint(pParse, SQLITE_CONSTRAINT_FOREIGNKEY, + assert(incr_count == 1); + sqlite3HaltConstraint(parse_context, + SQLITE_CONSTRAINT_FOREIGNKEY, ON_CONFLICT_ACTION_ABORT, 0, P4_STATIC, P5_ConstraintFK); } else { - if (nIncr > 0 && pFKey->isDeferred == 0) { - sqlite3MayAbort(pParse); - } - sqlite3VdbeAddOp2(v, OP_FkCounter, pFKey->isDeferred, nIncr); + if (incr_count > 0 && !fk_def->is_deferred) + sqlite3MayAbort(parse_context); + sqlite3VdbeAddOp2(v, OP_FkCounter, fk_def->is_deferred, + incr_count); } - - sqlite3VdbeResolveLabel(v, iOk); - sqlite3VdbeAddOp1(v, OP_Close, iCur); + sqlite3VdbeResolveLabel(v, ok_label); + sqlite3VdbeAddOp1(v, OP_Close, cursor); } /* @@ -551,520 +345,393 @@ exprTableColumn(sqlite3 * db, struct space_def *def, int cursor, i16 column) } /* - * This function is called to generate code executed when a row is deleted - * from the parent table of foreign key constraint pFKey and, if pFKey is - * deferred, when a row is inserted into the same table. When generating - * code for an SQL UPDATE operation, this function may be called twice - - * once to "delete" the old row and once to "insert" the new row. - * - * Parameter nIncr is passed -1 when inserting a row (as this may decrease - * the number of FK violations in the db) or +1 when deleting one (as this - * may increase the number of FK constraint problems). - * - * The code generated by this function scans through the rows in the child - * table that correspond to the parent table row being deleted or inserted. - * For each child row found, one of the following actions is taken: - * - * Operation | FK type | Action taken - * -------------------------------------------------------------------------- - * DELETE immediate Increment the "immediate constraint counter". - * Or, if the ON (UPDATE|DELETE) action is RESTRICT, - * throw a "FOREIGN KEY constraint failed" exception. - * - * INSERT immediate Decrement the "immediate constraint counter". - * - * DELETE deferred Increment the "deferred constraint counter". - * Or, if the ON (UPDATE|DELETE) action is RESTRICT, - * throw a "FOREIGN KEY constraint failed" exception. - * - * INSERT deferred Decrement the "deferred constraint counter". - * - * These operations are identified in the comment at the top of this file - * (fkey.c) as "I.2" and "D.2". + * This function is called to generate code executed when a row is + * deleted from the parent table of foreign key constraint @a fkey + * and, if @a fkey is deferred, when a row is inserted into the + * same table. When generating code for an SQL UPDATE operation, + * this function may be called twice - once to "delete" the old + * row and once to "insert" the new row. + * + * Parameter incr_count is passed -1 when inserting a row (as this + * may decrease the number of FK violations in the db) or +1 when + * deleting one (as this may increase the number of FK constraint + * problems). + * + * The code generated by this function scans through the rows in + * the child table that correspond to the parent table row being + * deleted or inserted. For each child row found, one of the + * following actions is taken: + * + * Op | FK type | Action taken + * --------------------------------------------------------------- + * DELETE immediate Increment the "immediate constraint counter". + * Or, if the ON (UPDATE|DELETE) action is + * RESTRICT, throw a "FOREIGN KEY constraint + * failed" exception. + * + * INSERT immediate Decrement the "immediate constraint counter". + * + * DELETE deferred Increment the "deferred constraint counter". + * Or, if the ON (UPDATE|DELETE) action is + * RESTRICT, throw a "FOREIGN KEY constraint + * failed" exception. + * + * INSERT deferred Decrement the "deferred constraint counter". + * + * These operations are identified in the comment at the top of + * this file as "I.2" and "D.2". + * @param parser SQL parser. + * @param src The child table to be scanned. + * @param tab Parent table. + * @param fkey The foreign key linking src to tab. + * @param reg_data Register from which parent row data starts. + * @param incr_count Amount to increment deferred counter by. */ static void -fkScanChildren(Parse * pParse, /* Parse context */ - SrcList * pSrc, /* The child table to be scanned */ - Table * pTab, /* The parent table */ - Index * pIdx, /* Index on parent covering the foreign key */ - FKey * pFKey, /* The foreign key linking pSrc to pTab */ - int *aiCol, /* Map from pIdx cols to child table cols */ - int regData, /* Parent row data starts here */ - int nIncr /* Amount to increment deferred counter by */ - ) +fkey_scan_children(struct Parse *parser, struct SrcList *src, struct Table *tab, + struct fkey_def *fkey, int reg_data, int incr_count) { - sqlite3 *db = pParse->db; /* Database handle */ - Expr *pWhere = 0; /* WHERE clause to scan with */ - NameContext sNameContext; /* Context used to resolve WHERE clause */ - WhereInfo *pWInfo; /* Context used by sqlite3WhereXXX() */ - int iFkIfZero = 0; /* Address of OP_FkIfZero */ - Vdbe *v = sqlite3GetVdbe(pParse); - - assert(pIdx == NULL || pIdx->pTable == pTab); - assert(pIdx == NULL || (int) pIdx->def->key_def->part_count == pFKey->nCol); - assert(pIdx != NULL); - - if (nIncr < 0) { - iFkIfZero = - sqlite3VdbeAddOp2(v, OP_FkIfZero, pFKey->isDeferred, 0); + assert(incr_count == -1 || incr_count == 1); + struct sqlite3 *db = parser->db; + struct Expr *where = NULL; + /* Address of OP_FkIfZero. */ + int fkifzero_label = 0; + struct Vdbe *v = sqlite3GetVdbe(parser); + + if (incr_count < 0) { + fkifzero_label = sqlite3VdbeAddOp2(v, OP_FkIfZero, + fkey->is_deferred, 0); VdbeCoverage(v); } - /* Create an Expr object representing an SQL expression like: + struct space *child_space = space_by_id(fkey->child_id); + assert(child_space != NULL); + /* + * Create an Expr object representing an SQL expression + * like: * - * <parent-key1> = <child-key1> AND <parent-key2> = <child-key2> ... + * <parent-key1> = <child-key1> AND <parent-key2> = <child-key2> ... * - * The collation sequence used for the comparison should be that of - * the parent key columns. The affinity of the parent key column should - * be applied to each child key value before the comparison takes place. + * The collation sequence used for the comparison should + * be that of the parent key columns. The affinity of the + * parent key column should be applied to each child key + * value before the comparison takes place. */ - for (int i = 0; i < pFKey->nCol; i++) { - Expr *pLeft; /* Value from parent table row */ - Expr *pRight; /* Column ref to child table */ - Expr *pEq; /* Expression (pLeft = pRight) */ - i16 iCol; /* Index of column in child table */ - const char *column_name; - - iCol = pIdx != NULL ? - (int) pIdx->def->key_def->parts[i].fieldno : -1; - pLeft = exprTableRegister(pParse, pTab, regData, iCol); - iCol = aiCol ? aiCol[i] : pFKey->aCol[0].iFrom; - assert(iCol >= 0); - column_name = pFKey->pFrom->def->fields[iCol].name; - pRight = sqlite3Expr(db, TK_ID, column_name); - pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); - pWhere = sqlite3ExprAnd(db, pWhere, pEq); + for (uint32_t i = 0; i < fkey->field_count; i++) { + uint32_t fieldno = fkey->links[i].parent_field; + struct Expr *pexpr = + exprTableRegister(parser, tab, reg_data, fieldno); + fieldno = fkey->links[i].child_field; + const char *field_name = child_space->def->fields[fieldno].name; + struct Expr *chexpr = sqlite3Expr(db, TK_ID, field_name); + struct Expr *eq = sqlite3PExpr(parser, TK_EQ, pexpr, chexpr); + where = sqlite3ExprAnd(db, where, eq); } - /* If the child table is the same as the parent table, then add terms - * to the WHERE clause that prevent this entry from being scanned. - * The added WHERE clause terms are like this: + /* + * If the child table is the same as the parent table, + * then add terms to the WHERE clause that prevent this + * entry from being scanned. The added WHERE clause terms + * are like this: * * NOT( $current_a==a AND $current_b==b AND ... ) * The primary key is (a,b,...) */ - if (pTab == pFKey->pFrom && nIncr > 0) { - Expr *pNe; /* Expression (pLeft != pRight) */ - Expr *pLeft; /* Value from parent table row */ - Expr *pRight; /* Column ref to child table */ - - Expr *pEq, *pAll = 0; - Index *pPk = sqlite3PrimaryKeyIndex(pTab); - assert(pIdx != NULL); - uint32_t part_count = pPk->def->key_def->part_count; - for (uint32_t i = 0; i < part_count; i++) { - uint32_t fieldno = pIdx->def->key_def->parts[i].fieldno; - pLeft = exprTableRegister(pParse, pTab, regData, + if (tab->def->id == fkey->child_id && incr_count > 0) { + struct Expr *expr = NULL, *pexpr, *chexpr, *eq; + for (uint32_t i = 0; i < fkey->field_count; i++) { + uint32_t fieldno = fkey->links[i].parent_field; + pexpr = exprTableRegister(parser, tab, reg_data, fieldno); - pRight = exprTableColumn(db, pTab->def, - pSrc->a[0].iCursor, fieldno); - pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); - pAll = sqlite3ExprAnd(db, pAll, pEq); + chexpr = exprTableColumn(db, tab->def, + src->a[0].iCursor, fieldno); + eq = sqlite3PExpr(parser, TK_EQ, pexpr, chexpr); + expr = sqlite3ExprAnd(db, expr, eq); } - pNe = sqlite3PExpr(pParse, TK_NOT, pAll, 0); - pWhere = sqlite3ExprAnd(db, pWhere, pNe); + struct Expr *pNe = sqlite3PExpr(parser, TK_NOT, expr, 0); + where = sqlite3ExprAnd(db, where, pNe); } /* Resolve the references in the WHERE clause. */ - memset(&sNameContext, 0, sizeof(NameContext)); - sNameContext.pSrcList = pSrc; - sNameContext.pParse = pParse; - sqlite3ResolveExprNames(&sNameContext, pWhere); - - /* Create VDBE to loop through the entries in pSrc that match the WHERE - * clause. For each row found, increment either the deferred or immediate - * foreign key constraint counter. + struct NameContext namectx; + memset(&namectx, 0, sizeof(namectx)); + namectx.pSrcList = src; + namectx.pParse = parser; + sqlite3ResolveExprNames(&namectx, where); + + /* + * Create VDBE to loop through the entries in src that + * match the WHERE clause. For each row found, increment + * either the deferred or immediate foreign key constraint + * counter. */ - pWInfo = sqlite3WhereBegin(pParse, pSrc, pWhere, 0, 0, 0, 0); - sqlite3VdbeAddOp2(v, OP_FkCounter, pFKey->isDeferred, nIncr); - if (pWInfo) { - sqlite3WhereEnd(pWInfo); - } + struct WhereInfo *info = + sqlite3WhereBegin(parser, src, where, NULL, NULL, 0, 0); + sqlite3VdbeAddOp2(v, OP_FkCounter, fkey->is_deferred, incr_count); + if (info != NULL) + sqlite3WhereEnd(info); /* Clean up the WHERE clause constructed above. */ - sql_expr_delete(db, pWhere, false); - if (iFkIfZero) - sqlite3VdbeJumpHere(v, iFkIfZero); + sql_expr_delete(db, where, false); + if (fkifzero_label != 0) + sqlite3VdbeJumpHere(v, fkifzero_label); } -/* - * This function returns a linked list of FKey objects (connected by - * FKey.pNextTo) holding all children of table pTab. For example, - * given the following schema: - * - * CREATE TABLE t1(a PRIMARY KEY); - * CREATE TABLE t2(b REFERENCES t1(a); - * - * Calling this function with table "t1" as an argument returns a pointer - * to the FKey structure representing the foreign key constraint on table - * "t2". Calling this function with "t2" as the argument would return a - * NULL pointer (as there are no FK constraints for which t2 is the parent - * table). - */ -FKey * -sqlite3FkReferences(Table * pTab) -{ - return (FKey *) sqlite3HashFind(&pTab->pSchema->fkeyHash, - pTab->def->name); -} - -/* - * The second argument points to an FKey object representing a foreign key - * for which pTab is the child table. An UPDATE statement against pTab - * is currently being processed. For each column of the table that is - * actually updated, the corresponding element in the aChange[] array - * is zero or greater (if a column is unmodified the corresponding element - * is set to -1). - * - * This function returns true if any of the columns that are part of the - * child key for FK constraint *p are modified. +/** + * An UPDATE statement against the table having foreign key with + * definition @a fkey is currently being processed. For each + * updated column of the table the corresponding element in @a + * changes array is zero or greater (if a column is unmodified the + * corresponding element is set to -1). + * + * @param fkey FK constraint definition. + * @param changes Array indicating modified columns. + * @retval true, if any of the columns that are part of the child + * key for FK constraint are modified. */ -static int -fkChildIsModified(FKey * p, /* Foreign key for which pTab is the child */ - int *aChange /* Array indicating modified columns */ - ) +static bool +fkey_child_is_modified(const struct fkey_def *fkey, const int *changes) { - int i; - for (i = 0; i < p->nCol; i++) { - int iChildKey = p->aCol[i].iFrom; - if (aChange[iChildKey] >= 0) - return 1; + for (uint32_t i = 0; i < fkey->field_count; ++i) { + uint32_t child_key = fkey->links[i].child_field; + if (changes[child_key] >= 0) + return true; } - return 0; + return false; } -/* - * The second argument points to an FKey object representing a foreign key - * for which pTab is the parent table. An UPDATE statement against pTab - * is currently being processed. For each column of the table that is - * actually updated, the corresponding element in the aChange[] array - * is zero or greater (if a column is unmodified the corresponding element - * is set to -1). - * - * This function returns true if any of the columns that are part of the - * parent key for FK constraint *p are modified. +/** + * Works the same as fkey_child_is_modified(), but checks are + * provided on parent table. */ -static int -fkParentIsModified(Table * pTab, FKey * p, int *aChange) +static bool +fkey_parent_is_modified(const struct fkey_def *fkey, const int *changes) { - int i; - for (i = 0; i < p->nCol; i++) { - char *zKey = p->aCol[i].zCol; - int iKey; - for (iKey = 0; iKey < (int)pTab->def->field_count; iKey++) { - if (aChange[iKey] >= 0) { - if (zKey) { - if (strcmp(pTab->def->fields[iKey].name, - zKey) == 0) - return 1; - } else if (table_column_is_in_pk(pTab, iKey)) { - return 1; - } - } - } + for (uint32_t i = 0; i < fkey->field_count; i++) { + uint32_t parent_key = fkey->links[i].parent_field; + if (changes[parent_key] >= 0) + return true; } - return 0; + return false; } -/* - * Return true if the parser passed as the first argument is being - * used to code a trigger that is really a "SET NULL" action belonging - * to trigger pFKey. +/** + * Return true if the parser passed as the first argument is + * used to code a trigger that is really a "SET NULL" action. */ -static int -isSetNullAction(Parse * pParse, FKey * pFKey) +static bool +fkey_action_is_set_null(struct Parse *parse_context, const struct fkey *fkey) { - Parse *pTop = sqlite3ParseToplevel(pParse); - if (pTop->pTriggerPrg != NULL) { - struct sql_trigger *trigger = pTop->pTriggerPrg->trigger; - if ((trigger == pFKey->apTrigger[0] && - pFKey->aAction[0] == OE_SetNull) || - (trigger == pFKey->apTrigger[1] - && pFKey->aAction[1] == OE_SetNull)) - return 1; + struct Parse *top_parse = sqlite3ParseToplevel(parse_context); + if (top_parse->pTriggerPrg != NULL) { + struct sql_trigger *trigger = top_parse->pTriggerPrg->trigger; + if ((trigger == fkey->on_delete_trigger && + fkey->def->on_delete == FKEY_ACTION_SET_NULL) || + (trigger == fkey->on_update_trigger && + fkey->def->on_update == FKEY_ACTION_SET_NULL)) + return true; } - return 0; + return false; } -/* - * This function is called when inserting, deleting or updating a row of - * table pTab to generate VDBE code to perform foreign key constraint - * processing for the operation. - * - * For a DELETE operation, parameter regOld is passed the index of the - * first register in an array of (pTab->nCol+1) registers containing the - * PK of the row being deleted, followed by each of the column values - * of the row being deleted, from left to right. Parameter regNew is passed - * zero in this case. - * - * For an INSERT operation, regOld is passed zero and regNew is passed the - * first register of an array of (pTab->nCol+1) registers containing the new - * row data. - * - * For an UPDATE operation, this function is called twice. Once before - * the original record is deleted from the table using the calling convention - * described for DELETE. Then again after the original record is deleted - * but before the new record is inserted using the INSERT convention. - */ void -sqlite3FkCheck(Parse * pParse, /* Parse context */ - Table * pTab, /* Row is being deleted from this table */ - int regOld, /* Previous row data is stored here */ - int regNew, /* New row data is stored here */ - int *aChange /* Array indicating UPDATEd columns (or 0) */ - ) +fkey_emit_check(struct Parse *parser, struct Table *tab, int reg_old, + int reg_new, const int *changed_cols) { - sqlite3 *db = pParse->db; /* Database handle */ - FKey *pFKey; /* Used to iterate through FKs */ + struct sqlite3 *db = parser->db; struct session *user_session = current_session(); - /* Exactly one of regOld and regNew should be non-zero. */ - assert((regOld == 0) != (regNew == 0)); + /* + * Exactly one of reg_old and reg_new should be non-zero. + */ + assert((reg_old == 0) != (reg_new == 0)); - /* If foreign-keys are disabled, this function is a no-op. */ + /* + * If foreign-keys are disabled, this function is a no-op. + */ if ((user_session->sql_flags & SQLITE_ForeignKeys) == 0) return; - /* Loop through all the foreign key constraints for which pTab is the - * child table (the table that the foreign key definition is part of). + /* + * Loop through all the foreign key constraints for which + * tab is the child table. */ - for (pFKey = pTab->pFKey; pFKey; pFKey = pFKey->pNextFrom) { - Table *pTo; /* Parent table of foreign key pFKey */ - Index *pIdx = 0; /* Index on key columns in pTo */ - int *aiFree = 0; - int *aiCol; - int iCol; - int bIgnore = 0; - - if (aChange - && sqlite3_stricmp(pTab->def->name, pFKey->zTo) != 0 - && fkChildIsModified(pFKey, aChange) == 0) { + struct space *space = space_by_id(tab->def->id); + assert(space != NULL); + struct fkey *fk; + rlist_foreach_entry(fk, &space->child_fkey, child_link) { + struct fkey_def *fk_def = fk->def; + if (changed_cols != NULL && !fkey_is_self_referenced(fk_def) && + !fkey_child_is_modified(fk_def, changed_cols)) continue; - } - - /* Find the parent table of this foreign key. Also find a unique index - * on the parent key columns in the parent table. If either of these - * schema items cannot be located, set an error in pParse and return - * early. - */ - pTo = sqlite3LocateTable(pParse, 0, pFKey->zTo); - if (pTo == NULL || sqlite3FkLocateIndex(pParse, pTo, pFKey, - &pIdx, &aiFree) != 0) - return; - assert(pFKey->nCol == 1 || (aiFree && pIdx)); - - if (aiFree) { - aiCol = aiFree; - } else { - iCol = pFKey->aCol[0].iFrom; - aiCol = &iCol; - } - - pParse->nTab++; - - if (regOld != 0) { - /* A row is being removed from the child table. Search for the parent. - * If the parent does not exist, removing the child row resolves an - * outstanding foreign key constraint violation. + parser->nTab++; + struct space *parent = space_by_id(fk_def->parent_id); + assert(parent != NULL); + if (reg_old != 0) { + /* + * A row is being removed from the child + * table. Search for the parent. If the + * parent does not exist, removing the + * child row resolves an outstanding + * foreign key constraint violation. */ - fkLookupParent(pParse, pTo, pIdx, pFKey, aiCol, - regOld, -1, bIgnore); + fkey_lookup_parent(parser, parent, fk_def, fk->index_id, + reg_old, -1); } - if (regNew != 0 && !isSetNullAction(pParse, pFKey)) { - /* A row is being added to the child table. If a parent row cannot - * be found, adding the child row has violated the FK constraint. + if (reg_new != 0 && !fkey_action_is_set_null(parser, fk)) { + /* + * A row is being added to the child + * table. If a parent row cannot be found, + * adding the child row has violated the + * FK constraint. * - * If this operation is being performed as part of a trigger program - * that is actually a "SET NULL" action belonging to this very - * foreign key, then omit this scan altogether. As all child key - * values are guaranteed to be NULL, it is not possible for adding - * this row to cause an FK violation. + * If this operation is being performed as + * part of a trigger program that is + * actually a "SET NULL" action belonging + * to this very foreign key, then omit + * this scan altogether. As all child key + * values are guaranteed to be NULL, it is + * not possible for adding this row to + * cause an FK violation. */ - fkLookupParent(pParse, pTo, pIdx, pFKey, aiCol, - regNew, +1, bIgnore); + fkey_lookup_parent(parser, parent, fk_def, fk->index_id, + reg_new, +1); } - - sqlite3DbFree(db, aiFree); } - - /* Loop through all the foreign key constraints that refer to this table. - * (the "child" constraints) + /* + * Loop through all the foreign key constraints that + * refer to this table. */ - for (pFKey = sqlite3FkReferences(pTab); pFKey; pFKey = pFKey->pNextTo) { - Index *pIdx = 0; /* Foreign key index for pFKey */ - SrcList *pSrc; - int *aiCol = 0; - - if (aChange - && fkParentIsModified(pTab, pFKey, aChange) == 0) { + rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { + struct fkey_def *fk_def = fk->def; + if (changed_cols != NULL && + !fkey_parent_is_modified(fk_def, changed_cols)) continue; - } - - if (!pFKey->isDeferred - && !(user_session->sql_flags & SQLITE_DeferFKs) - && !pParse->pToplevel && !pParse->isMultiWrite) { - assert(regOld == 0 && regNew != 0); - /* Inserting a single row into a parent table cannot cause (or fix) - * an immediate foreign key violation. So do nothing in this case. + if (!fk_def->is_deferred && + (user_session->sql_flags & SQLITE_DeferFKs) == 0 && + parser->pToplevel == NULL && !parser->isMultiWrite) { + assert(reg_old == 0 && reg_new != 0); + /* + * Inserting a single row into a parent + * table cannot cause (or fix) an + * immediate foreign key violation. So do + * nothing in this case. */ continue; } - if (sqlite3FkLocateIndex(pParse, pTab, pFKey, &pIdx, - &aiCol) != 0) - return; - assert(aiCol || pFKey->nCol == 1); - - /* Create a SrcList structure containing the child table. We need the - * child table as a SrcList for sqlite3WhereBegin() + /* + * Create a SrcList structure containing the child + * table. We need the child table as a SrcList for + * sqlite3WhereBegin(). */ - pSrc = sqlite3SrcListAppend(db, 0, 0); - if (pSrc) { - struct SrcList_item *pItem = pSrc->a; - pItem->pTab = pFKey->pFrom; - pItem->zName = pFKey->pFrom->def->name; - pItem->pTab->nTabRef++; - pItem->iCursor = pParse->nTab++; - - if (regNew != 0) { - fkScanChildren(pParse, pSrc, pTab, pIdx, pFKey, - aiCol, regNew, -1); - } - if (regOld != 0) { - int eAction = pFKey->aAction[aChange != 0]; - fkScanChildren(pParse, pSrc, pTab, pIdx, pFKey, - aiCol, regOld, 1); - /* If this is a deferred FK constraint, or a CASCADE or SET NULL - * action applies, then any foreign key violations caused by - * removing the parent key will be rectified by the action trigger. - * So do not set the "may-abort" flag in this case. - * - * Note 1: If the FK is declared "ON UPDATE CASCADE", then the - * may-abort flag will eventually be set on this statement anyway - * (when this function is called as part of processing the UPDATE - * within the action trigger). - * - * Note 2: At first glance it may seem like SQLite could simply omit - * all OP_FkCounter related scans when either CASCADE or SET NULL - * applies. The trouble starts if the CASCADE or SET NULL action - * trigger causes other triggers or action rules attached to the - * child table to fire. In these cases the fk constraint counters - * might be set incorrectly if any OP_FkCounter related scans are - * omitted. - */ - if (!pFKey->isDeferred && eAction != OE_Cascade - && eAction != OE_SetNull) { - sqlite3MayAbort(pParse); - } - } - pItem->zName = 0; - sqlite3SrcListDelete(db, pSrc); - } - sqlite3DbFree(db, aiCol); - } -} - -#define COLUMN_MASK(x) (((x)>31) ? 0xffffffff : ((u32)1<<(x))) - -/* - * This function is called before generating code to update or delete a - * row contained in table pTab. - */ -u32 -sqlite3FkOldmask(Parse * pParse, /* Parse context */ - Table * pTab /* Table being modified */ - ) -{ - u32 mask = 0; - struct session *user_session = current_session(); - - if (user_session->sql_flags & SQLITE_ForeignKeys) { - FKey *p; - for (p = pTab->pFKey; p; p = p->pNextFrom) { - for (int i = 0; i < p->nCol; i++) - mask |= COLUMN_MASK(p->aCol[i].iFrom); + struct SrcList *src = sqlite3SrcListAppend(db, NULL, NULL); + if (src == NULL) + continue; + struct SrcList_item *item = src->a; + struct space *child = space_by_id(fk->def->child_id); + assert(child != NULL); + struct Table *child_tab = sqlite3HashFind(&db->pSchema->tblHash, + child->def->name); + item->pTab = child_tab; + item->zName = sqlite3DbStrDup(db, child->def->name); + item->pTab->nTabRef++; + item->iCursor = parser->nTab++; + + if (reg_new != 0) { + fkey_scan_children(parser, src, tab, fk->def, reg_new, + -1); } - for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) { - Index *pIdx = 0; - sqlite3FkLocateIndex(pParse, pTab, p, &pIdx, 0); - if (pIdx != NULL) { - uint32_t part_count = - pIdx->def->key_def->part_count; - for (uint32_t i = 0; i < part_count; i++) { - mask |= COLUMN_MASK(pIdx->def-> - key_def->parts[i].fieldno); - } - } + if (reg_old != 0) { + enum fkey_action action = fk_def->on_update; + fkey_scan_children(parser, src, tab, fk->def, reg_old, + 1); + /* + * If this is a deferred FK constraint, or + * a CASCADE or SET NULL action applies, + * then any foreign key violations caused + * by removing the parent key will be + * rectified by the action trigger. So do + * not set the "may-abort" flag in this + * case. + * + * Note 1: If the FK is declared "ON + * UPDATE CASCADE", then the may-abort + * flag will eventually be set on this + * statement anyway (when this function is + * called as part of processing the UPDATE + * within the action trigger). + * + * Note 2: At first glance it may seem + * like SQLite could simply omit all + * OP_FkCounter related scans when either + * CASCADE or SET NULL applies. The + * trouble starts if the CASCADE or SET + * NULL action trigger causes other + * triggers or action rules attached to + * the child table to fire. In these cases + * the fk constraint counters might be set + * incorrectly if any OP_FkCounter related + * scans are omitted. + */ + if (!fk_def->is_deferred && + action != FKEY_ACTION_CASCADE && + action != FKEY_ACTION_SET_NULL) + sqlite3MayAbort(parser); } + sqlite3SrcListDelete(db, src); } - return mask; } -/* - * This function is called before generating code to update or delete a - * row contained in table pTab. If the operation is a DELETE, then - * parameter aChange is passed a NULL value. For an UPDATE, aChange points - * to an array of size N, where N is the number of columns in table pTab. - * If the i'th column is not modified by the UPDATE, then the corresponding - * entry in the aChange[] array is set to -1. If the column is modified, - * the value is 0 or greater. - * - * If any foreign key processing will be required, this function returns - * true. If there is no foreign key related processing, this function - * returns false. - */ -int -sqlite3FkRequired(Table * pTab, /* Table being modified */ - int *aChange /* Non-NULL for UPDATE operations */ - ) +bool +fkey_is_required(uint32_t space_id, const int *changes) { struct session *user_session = current_session(); - if (user_session->sql_flags & SQLITE_ForeignKeys) { - if (!aChange) { - /* A DELETE operation. Foreign key processing is required if the - * table in question is either the child or parent table for any - * foreign key constraint. - */ - return (sqlite3FkReferences(pTab) || pTab->pFKey); - } else { - /* This is an UPDATE. Foreign key processing is only required if the - * operation modifies one or more child or parent key columns. - */ - FKey *p; - - /* Check if any child key columns are being modified. */ - for (p = pTab->pFKey; p; p = p->pNextFrom) { - if (fkChildIsModified(p, aChange)) - return 1; - } - - /* Check if any parent key columns are being modified. */ - for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) { - if (fkParentIsModified(pTab, p, aChange)) - return 1; - } - } + if ((user_session->sql_flags & SQLITE_ForeignKeys) == 0) + return false; + struct space *space = space_by_id(space_id); + if (changes == NULL) { + /* + * A DELETE operation. FK processing is required + * if space is child or parent. + */ + return ! rlist_empty(&space->parent_fkey) || + ! rlist_empty(&space->child_fkey); + } + /* + * This is an UPDATE. FK processing is only required if + * the operation modifies one or more child or parent key + * columns. + */ + struct fkey *fk; + rlist_foreach_entry(fk, &space->child_fkey, child_link) { + if (fkey_child_is_modified(fk->def, changes)) + return true; } - return 0; + rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { + if (fkey_parent_is_modified(fk->def, changes)) + return true; + } + return false; } /** * This function is called when an UPDATE or DELETE operation is * being compiled on table pTab, which is the parent table of - * foreign-key pFKey. + * foreign-key fkey. * If the current operation is an UPDATE, then the pChanges * parameter is passed a pointer to the list of columns being * modified. If it is a DELETE, pChanges is passed a NULL pointer. * * It returns a pointer to a sql_trigger structure containing a * trigger equivalent to the ON UPDATE or ON DELETE action - * specified by pFKey. + * specified by fkey. * If the action is "NO ACTION" or "RESTRICT", then a NULL pointer * is returned (these actions require no special handling by the * triggers sub-system, code for them is created by - * fkScanChildren()). + * fkey_scan_children()). * - * For example, if pFKey is the foreign key and pTab is table "p" + * For example, if fkey is the foreign key and pTab is table "p" * in the following schema: * * CREATE TABLE p(pk PRIMARY KEY); @@ -1078,308 +745,214 @@ sqlite3FkRequired(Table * pTab, /* Table being modified */ * * The returned pointer is cached as part of the foreign key * object. It is eventually freed along with the rest of the - * foreign key object by sqlite3FkDelete(). + * foreign key object by fkey_delete(). * * @param pParse Parse context. * @param pTab Table being updated or deleted from. - * @param pFKey Foreign key to get action for. - * @param pChanges Change-list for UPDATE, NULL for DELETE. + * @param fkey Foreign key to get action for. + * @param is_update True if action is on update. * * @retval not NULL on success. * @retval NULL on failure. */ static struct sql_trigger * -fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey, - struct ExprList *pChanges) +fkey_action_trigger(struct Parse *pParse, struct Table *pTab, struct fkey *fkey, + bool is_update) { - sqlite3 *db = pParse->db; /* Database handle */ - int action; /* One of OE_None, OE_Cascade etc. */ - /* Trigger definition to return. */ - struct sql_trigger *trigger; - int iAction = (pChanges != 0); /* 1 for UPDATE, 0 for DELETE */ - struct session *user_session = current_session(); - - action = pFKey->aAction[iAction]; - if (action == OE_Restrict - && (user_session->sql_flags & SQLITE_DeferFKs)) { - return 0; - } - trigger = pFKey->apTrigger[iAction]; - - if (action != ON_CONFLICT_ACTION_NONE && trigger == NULL) { - char const *zFrom; /* Name of child table */ - int nFrom; /* Length in bytes of zFrom */ - Index *pIdx = 0; /* Parent key index for this FK */ - int *aiCol = 0; /* child table cols -> parent key cols */ - TriggerStep *pStep = 0; /* First (only) step of trigger program */ - Expr *pWhere = 0; /* WHERE clause of trigger step */ - ExprList *pList = 0; /* Changes list if ON UPDATE CASCADE */ - Select *pSelect = 0; /* If RESTRICT, "SELECT RAISE(...)" */ - int i; /* Iterator variable */ - Expr *pWhen = 0; /* WHEN clause for the trigger */ - - if (sqlite3FkLocateIndex(pParse, pTab, pFKey, &pIdx, &aiCol)) - return 0; - assert(aiCol || pFKey->nCol == 1); - - for (i = 0; i < pFKey->nCol; i++) { - Token tOld = { "old", 3, false }; /* Literal "old" token */ - Token tNew = { "new", 3, false }; /* Literal "new" token */ - Token tFromCol; /* Name of column in child table */ - Token tToCol; /* Name of column in parent table */ - int iFromCol; /* Idx of column in child table */ - Expr *pEq; /* tFromCol = OLD.tToCol */ - - iFromCol = aiCol ? aiCol[i] : pFKey->aCol[0].iFrom; - assert(iFromCol >= 0); - assert(pIdx != NULL); + struct sqlite3 *db = pParse->db; + struct fkey_def *fk_def = fkey->def; + enum fkey_action action = is_update ? fk_def->on_update : + fk_def->on_delete; + struct sql_trigger *trigger = is_update ? fkey->on_update_trigger : + fkey->on_delete_trigger; + if (action == FKEY_NO_ACTION || trigger != NULL) + return trigger; + struct TriggerStep *step = NULL; + struct Expr *where = NULL, *when = NULL; + struct ExprList *list = NULL; + struct Select *select = NULL; + struct space *child_space = space_by_id(fk_def->child_id); + assert(child_space != NULL); + for (uint32_t i = 0; i < fk_def->field_count; ++i) { + /* Literal "old" token. */ + struct Token t_old = { "old", 3, false }; + /* Literal "new" token. */ + struct Token t_new = { "new", 3, false }; + /* Name of column in child table. */ + struct Token t_from_col; + /* Name of column in parent table. */ + struct Token t_to_col; + struct field_def *child_fields = child_space->def->fields; + + uint32_t pcol = fk_def->links[i].parent_field; + sqlite3TokenInit(&t_to_col, pTab->def->fields[pcol].name); + + uint32_t chcol = fk_def->links[i].child_field; + sqlite3TokenInit(&t_from_col, child_fields[chcol].name); - uint32_t fieldno = pIdx->def->key_def->parts[i].fieldno; - sqlite3TokenInit(&tToCol, - pTab->def->fields[fieldno].name); - sqlite3TokenInit(&tFromCol, - pFKey->pFrom->def->fields[ - iFromCol].name); - - /* Create the expression "OLD.zToCol = zFromCol". It is important - * that the "OLD.zToCol" term is on the LHS of the = operator, so - * that the affinity and collation sequence associated with the - * parent table are used for the comparison. - */ - pEq = sqlite3PExpr(pParse, TK_EQ, - sqlite3PExpr(pParse, TK_DOT, - sqlite3ExprAlloc(db, - TK_ID, - &tOld, - 0), - sqlite3ExprAlloc(db, - TK_ID, - &tToCol, - 0)), - sqlite3ExprAlloc(db, TK_ID, - &tFromCol, 0) - ); - pWhere = sqlite3ExprAnd(db, pWhere, pEq); + /* + * Create the expression "old.to_col = from_col". + * It is important that the "old.to_col" term is + * on the LHS of the = operator, so that the + * affinity and collation sequence associated with + * the parent table are used for the comparison. + */ + struct Expr *to_col = + sqlite3PExpr(pParse, TK_DOT, + sqlite3ExprAlloc(db, TK_ID, &t_old, 0), + sqlite3ExprAlloc(db, TK_ID, &t_to_col, 0)); + struct Expr *from_col = + sqlite3ExprAlloc(db, TK_ID, &t_from_col, 0); + struct Expr *eq = sqlite3PExpr(pParse, TK_EQ, to_col, from_col); + where = sqlite3ExprAnd(db, where, eq); - /* For ON UPDATE, construct the next term of the WHEN clause. - * The final WHEN clause will be like this: - * - * WHEN NOT(old.col1 = new.col1 AND ... AND old.colN = new.colN) - */ - if (pChanges) { - pEq = sqlite3PExpr(pParse, TK_EQ, - sqlite3PExpr(pParse, TK_DOT, - sqlite3ExprAlloc - (db, TK_ID, - &tOld, 0), - sqlite3ExprAlloc - (db, TK_ID, - &tToCol, 0)), - sqlite3PExpr(pParse, TK_DOT, - sqlite3ExprAlloc - (db, TK_ID, - &tNew, 0), - sqlite3ExprAlloc - (db, TK_ID, - &tToCol, 0)) - ); - pWhen = sqlite3ExprAnd(db, pWhen, pEq); - } + /* + * For ON UPDATE, construct the next term of the + * WHEN clause. The final WHEN clause will be like + * this: + * + * WHEN NOT(old.col1 = new.col1 AND ... AND + * old.colN = new.colN) + */ + if (is_update) { + struct Expr *l, *r; + l = sqlite3PExpr(pParse, TK_DOT, + sqlite3ExprAlloc(db, TK_ID, &t_old, 0), + sqlite3ExprAlloc(db, TK_ID, &t_to_col, + 0)); + r = sqlite3PExpr(pParse, TK_DOT, + sqlite3ExprAlloc(db, TK_ID, &t_new, 0), + sqlite3ExprAlloc(db, TK_ID, &t_to_col, + 0)); + eq = sqlite3PExpr(pParse, TK_EQ, l, r); + when = sqlite3ExprAnd(db, when, eq); + } - if (action != OE_Restrict - && (action != OE_Cascade || pChanges)) { - Expr *pNew; - if (action == OE_Cascade) { - pNew = sqlite3PExpr(pParse, TK_DOT, - sqlite3ExprAlloc(db, - TK_ID, - &tNew, - 0), - sqlite3ExprAlloc(db, - TK_ID, - &tToCol, - 0)); - } else if (action == OE_SetDflt) { - Expr *pDflt = - space_column_default_expr( - pFKey->pFrom->def->id, - (uint32_t)iFromCol); - if (pDflt) { - pNew = - sqlite3ExprDup(db, pDflt, - 0); - } else { - pNew = - sqlite3ExprAlloc(db, - TK_NULL, 0, - 0); - } + if (action != FKEY_ACTION_RESTRICT && + (action != FKEY_ACTION_CASCADE || is_update)) { + struct Expr *new, *d; + if (action == FKEY_ACTION_CASCADE) { + new = sqlite3PExpr(pParse, TK_DOT, + sqlite3ExprAlloc(db, TK_ID, + &t_new, 0), + sqlite3ExprAlloc(db, TK_ID, + &t_to_col, + 0)); + } else if (action == FKEY_ACTION_SET_DEFAULT) { + d = child_fields[chcol].default_value_expr; + if (d != NULL) { + new = sqlite3ExprDup(db, d, 0); } else { - pNew = - sqlite3ExprAlloc(db, TK_NULL, 0, 0); + new = sqlite3ExprAlloc(db, TK_NULL, + NULL, 0); } - pList = - sql_expr_list_append(pParse->db, pList, - pNew); - sqlite3ExprListSetName(pParse, pList, &tFromCol, - 0); + } else { + new = sqlite3ExprAlloc(db, TK_NULL, NULL, 0); } + list = sql_expr_list_append(db, list, new); + sqlite3ExprListSetName(pParse, list, &t_from_col, 0); } - sqlite3DbFree(db, aiCol); - - zFrom = pFKey->pFrom->def->name; - nFrom = sqlite3Strlen30(zFrom); - - if (action == OE_Restrict) { - Token tFrom; - Expr *pRaise; + } - tFrom.z = zFrom; - tFrom.n = nFrom; - pRaise = - sqlite3Expr(db, TK_RAISE, - "FOREIGN KEY constraint failed"); - if (pRaise) { - pRaise->affinity = ON_CONFLICT_ACTION_ABORT; - } - pSelect = sqlite3SelectNew(pParse, - sql_expr_list_append(pParse->db, - NULL, - pRaise), - sqlite3SrcListAppend(db, 0, - &tFrom), - pWhere, 0, 0, 0, 0, 0, 0); - pWhere = 0; - } - trigger = (struct sql_trigger *)sqlite3DbMallocZero(db, - sizeof(*trigger)); - if (trigger != NULL) { - size_t step_size = sizeof(TriggerStep) + nFrom + 1; - trigger->step_list = sqlite3DbMallocZero(db, step_size); - pStep = trigger->step_list; - pStep->zTarget = (char *)&pStep[1]; - memcpy(pStep->zTarget, zFrom, nFrom); - pStep->pWhere = - sqlite3ExprDup(db, pWhere, EXPRDUP_REDUCE); - pStep->pExprList = - sql_expr_list_dup(db, pList, EXPRDUP_REDUCE); - pStep->pSelect = - sqlite3SelectDup(db, pSelect, EXPRDUP_REDUCE); - if (pWhen) { - pWhen = sqlite3PExpr(pParse, TK_NOT, pWhen, 0); - trigger->pWhen = - sqlite3ExprDup(db, pWhen, EXPRDUP_REDUCE); - } - } + const char *space_name = child_space->def->name; + uint32_t name_len = strlen(space_name); + + if (action == FKEY_ACTION_RESTRICT) { + struct Token err; + err.z = space_name; + err.n = name_len; + struct Expr *r = sqlite3Expr(db, TK_RAISE, "FOREIGN KEY "\ + "constraint failed"); + if (r != NULL) + r->affinity = ON_CONFLICT_ACTION_ABORT; + select = sqlite3SelectNew(pParse, + sql_expr_list_append(db, NULL, r), + sqlite3SrcListAppend(db, NULL, &err), + where, NULL, NULL, NULL, 0, NULL, + NULL); + where = NULL; + } - sql_expr_delete(db, pWhere, false); - sql_expr_delete(db, pWhen, false); - sql_expr_list_delete(db, pList); - sql_select_delete(db, pSelect); - if (db->mallocFailed == 1) { - sql_trigger_delete(db, trigger); - return 0; + trigger = (struct sql_trigger *) sqlite3DbMallocZero(db, + sizeof(*trigger)); + if (trigger != NULL) { + size_t step_size = sizeof(TriggerStep) + name_len + 1; + trigger->step_list = sqlite3DbMallocZero(db, step_size); + step = trigger->step_list; + step->zTarget = (char *) &step[1]; + memcpy((char *) step->zTarget, space_name, name_len); + + step->pWhere = sqlite3ExprDup(db, where, EXPRDUP_REDUCE); + step->pExprList = sql_expr_list_dup(db, list, EXPRDUP_REDUCE); + step->pSelect = sqlite3SelectDup(db, select, EXPRDUP_REDUCE); + if (when != NULL) { + when = sqlite3PExpr(pParse, TK_NOT, when, 0); + trigger->pWhen = + sqlite3ExprDup(db, when, EXPRDUP_REDUCE); } - assert(pStep != 0); + } - switch (action) { - case OE_Restrict: - pStep->op = TK_SELECT; + sql_expr_delete(db, where, false); + sql_expr_delete(db, when, false); + sql_expr_list_delete(db, list); + sql_select_delete(db, select); + if (db->mallocFailed) { + sql_trigger_delete(db, trigger); + return NULL; + } + assert(step != NULL); + + switch (action) { + case FKEY_ACTION_RESTRICT: + step->op = TK_SELECT; + break; + case FKEY_ACTION_CASCADE: + if (! is_update) { + step->op = TK_DELETE; break; - case OE_Cascade: - if (!pChanges) { - pStep->op = TK_DELETE; - break; - } - FALLTHROUGH; - default: - pStep->op = TK_UPDATE; } - pStep->trigger = trigger; - pFKey->apTrigger[iAction] = trigger; - trigger->op = pChanges ? TK_UPDATE : TK_DELETE; + FALLTHROUGH; + default: + step->op = TK_UPDATE; } + step->trigger = trigger; + if (is_update) { + fkey->on_update_trigger = trigger; + trigger->op = TK_UPDATE; + } else { + fkey->on_delete_trigger = trigger; + trigger->op = TK_DELETE; + } return trigger; } -/* - * This function is called when deleting or updating a row to implement - * any required CASCADE, SET NULL or SET DEFAULT actions. - */ void -sqlite3FkActions(Parse * pParse, /* Parse context */ - Table * pTab, /* Table being updated or deleted from */ - ExprList * pChanges, /* Change-list for UPDATE, NULL for DELETE */ - int regOld, /* Address of array containing old row */ - int *aChange /* Array indicating UPDATEd columns (or 0) */ - ) +fkey_emit_actions(struct Parse *parser, struct Table *tab, int reg_old, + int *changes) { struct session *user_session = current_session(); - /* If foreign-key support is enabled, iterate through all FKs that - * refer to table pTab. If there is an action associated with the FK - * for this operation (either update or delete), invoke the associated - * trigger sub-program. + /* + * If foreign-key support is enabled, iterate through all + * FKs that refer to table tab. If there is an action + * associated with the FK for this operation (either + * update or delete), invoke the associated trigger + * sub-program. */ - if (user_session->sql_flags & SQLITE_ForeignKeys) { - FKey *pFKey; /* Iterator variable */ - for (pFKey = sqlite3FkReferences(pTab); pFKey; - pFKey = pFKey->pNextTo) { - if (aChange == 0 - || fkParentIsModified(pTab, pFKey, aChange)) { - struct sql_trigger *pAct = - fkActionTrigger(pParse, pTab, pFKey, - pChanges); - if (pAct == NULL) - continue; - vdbe_code_row_trigger_direct(pParse, pAct, pTab, - regOld, - ON_CONFLICT_ACTION_ABORT, - 0); - } - } - } -} - -/* - * Free all memory associated with foreign key definitions attached to - * table pTab. Remove the deleted foreign keys from the Schema.fkeyHash - * hash table. - */ -void -sqlite3FkDelete(sqlite3 * db, Table * pTab) -{ - FKey *pFKey; /* Iterator variable */ - FKey *pNext; /* Copy of pFKey->pNextFrom */ - - for (pFKey = pTab->pFKey; pFKey; pFKey = pNext) { - /* Remove the FK from the fkeyHash hash table. */ - if (!db || db->pnBytesFreed == 0) { - if (pFKey->pPrevTo) { - pFKey->pPrevTo->pNextTo = pFKey->pNextTo; - } else { - void *p = (void *)pFKey->pNextTo; - const char *z = - (p ? pFKey->pNextTo->zTo : pFKey->zTo); - sqlite3HashInsert(&pTab->pSchema->fkeyHash, z, - p); - } - if (pFKey->pNextTo) { - pFKey->pNextTo->pPrevTo = pFKey->pPrevTo; - } - } - - /* EV: R-30323-21917 Each foreign key constraint in SQLite is - * classified as either immediate or deferred. - */ - assert(pFKey->isDeferred == 0 || pFKey->isDeferred == 1); - - /* Delete any triggers created to implement actions for this FK. */ - sql_trigger_delete(db, pFKey->apTrigger[0]); - sql_trigger_delete(db, pFKey->apTrigger[1]); - - pNext = pFKey->pNextFrom; - sqlite3DbFree(db, pFKey); + if ((user_session->sql_flags & SQLITE_ForeignKeys) == 0) + return; + struct space *space = space_by_id(tab->def->id); + assert(space != NULL); + struct fkey *fk; + rlist_foreach_entry(fk, &space->parent_fkey, parent_link) { + if (changes != NULL && + !fkey_parent_is_modified(fk->def, changes)) + continue; + struct sql_trigger *pAct = + fkey_action_trigger(parser, tab, fk, changes != NULL); + if (pAct == NULL) + continue; + vdbe_code_row_trigger_direct(parser, pAct, tab, reg_old, + ON_CONFLICT_ACTION_ABORT, 0); } } -#endif /* ifndef SQLITE_OMIT_FOREIGN_KEY */ diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index 432e003c0..ea3ec9abc 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -837,7 +837,7 @@ sqlite3Insert(Parse * pParse, /* Parser context */ iIdxCur, regIns, 0, true, &on_conflict, endOfLoop, &isReplace, 0); - sqlite3FkCheck(pParse, pTab, 0, regIns, 0); + fkey_emit_check(pParse, pTab, 0, regIns, 0); vdbe_emit_insertion_completion(v, iIdxCur, aRegIdx[0], &on_conflict); } @@ -1313,15 +1313,14 @@ sqlite3GenerateConstraintChecks(Parse * pParse, /* The parser context */ (on_error == ON_CONFLICT_ACTION_REPLACE || on_error == ON_CONFLICT_ACTION_IGNORE); bool no_delete_triggers = - (0 == (user_session->sql_flags & - SQLITE_RecTriggers) || - sql_triggers_exist(pTab, TK_DELETE, NULL, NULL) == - NULL); + (user_session->sql_flags & SQLITE_RecTriggers) == 0 || + sql_triggers_exist(pTab, TK_DELETE, NULL, NULL) == NULL; + struct space *space = space_by_id(pTab->def->id); + assert(space != NULL); bool no_foreign_keys = - (0 == (user_session->sql_flags & - SQLITE_ForeignKeys) || - (0 == pTab->pFKey && - 0 == sqlite3FkReferences(pTab))); + (user_session->sql_flags & SQLITE_ForeignKeys) == 0 || + (rlist_empty(&space->child_fkey) && + ! rlist_empty(&space->parent_fkey)); if (no_secondary_indexes && no_foreign_keys && proper_error_action && no_delete_triggers) { @@ -1559,7 +1558,7 @@ sqlite3OpenTableAndIndices(Parse * pParse, /* Parsing context */ if (isUpdate || /* Condition 1 */ IsPrimaryKeyIndex(pIdx) || /* Condition 2 */ - sqlite3FkReferences(pTab) || /* Condition 3 */ + ! rlist_empty(&space->parent_fkey) || /* Condition 4 */ (pIdx->def->opts.is_unique && pIdx->onError != ON_CONFLICT_ACTION_DEFAULT && @@ -1820,10 +1819,11 @@ xferOptimization(Parse * pParse, /* Parser context */ * So the extra complication to make this rule less restrictive is probably * not worth the effort. Ticket [6284df89debdfa61db8073e062908af0c9b6118e] */ - if ((user_session->sql_flags & SQLITE_ForeignKeys) != 0 - && pDest->pFKey != 0) { + struct space *dest = space_by_id(pDest->def->id); + assert(dest != NULL); + if ((user_session->sql_flags & SQLITE_ForeignKeys) != 0 && + !rlist_empty(&dest->child_fkey)) return 0; - } #endif if ((user_session->sql_flags & SQLITE_CountRows) != 0) { return 0; /* xfer opt does not play well with PRAGMA count_changes */ diff --git a/src/box/sql/main.c b/src/box/sql/main.c index ded3b5b26..41979beb4 100644 --- a/src/box/sql/main.c +++ b/src/box/sql/main.c @@ -730,11 +730,6 @@ sqlite3RollbackAll(Vdbe * pVdbe, int tripCode) { sqlite3 *db = pVdbe->db; (void)tripCode; - struct session *user_session = current_session(); - - /* DDL is impossible inside a transaction. */ - assert((user_session->sql_flags & SQLITE_InternChanges) == 0 - || db->init.busy == 1); /* If one has been configured, invoke the rollback-hook callback */ if (db->xRollbackCallback && (!pVdbe->auto_commit)) { diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 0c510f565..1b06c6d87 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -51,6 +51,7 @@ // %include { #include "sqliteInt.h" +#include "box/fkey.h" /* ** Disable all error recovery processing in the parser push-down @@ -281,8 +282,8 @@ ccons ::= UNIQUE onconf(R). {sql_create_index(pParse,0,0,0,R,0,0, SQL_INDEX_TYPE_CONSTRAINT_UNIQUE);} ccons ::= CHECK LP expr(X) RP. {sql_add_check_constraint(pParse,&X);} ccons ::= REFERENCES nm(T) eidlist_opt(TA) refargs(R). - {sqlite3CreateForeignKey(pParse,0,&T,TA,R);} -ccons ::= defer_subclause(D). {sqlite3DeferForeignKey(pParse,D);} + {sql_create_foreign_key(pParse, NULL, NULL, NULL, &T, TA, false, R);} +ccons ::= defer_subclause(D). {fkey_change_defer_mode(pParse, D);} ccons ::= COLLATE id(C). {sqlite3AddCollateType(pParse, &C);} // The optional AUTOINCREMENT keyword @@ -296,19 +297,23 @@ autoinc(X) ::= AUTOINCR. {X = 1;} // check fails. // %type refargs {int} -refargs(A) ::= . { A = ON_CONFLICT_ACTION_NONE*0x0101; /* EV: R-19803-45884 */} +refargs(A) ::= . { A = FKEY_NO_ACTION; } refargs(A) ::= refargs(A) refarg(Y). { A = (A & ~Y.mask) | Y.value; } %type refarg {struct {int value; int mask;}} -refarg(A) ::= MATCH nm. { A.value = 0; A.mask = 0x000000; } +refarg(A) ::= MATCH matcharg(X). { A.value = X<<16; A.mask = 0xff0000; } refarg(A) ::= ON INSERT refact. { A.value = 0; A.mask = 0x000000; } refarg(A) ::= ON DELETE refact(X). { A.value = X; A.mask = 0x0000ff; } refarg(A) ::= ON UPDATE refact(X). { A.value = X<<8; A.mask = 0x00ff00; } +%type matcharg {int} +matcharg(A) ::= SIMPLE. { A = FKEY_MATCH_SIMPLE; } +matcharg(A) ::= PARTIAL. { A = FKEY_MATCH_PARTIAL; } +matcharg(A) ::= FULL. { A = FKEY_MATCH_FULL; } %type refact {int} -refact(A) ::= SET NULL. { A = OE_SetNull; /* EV: R-33326-45252 */} -refact(A) ::= SET DEFAULT. { A = OE_SetDflt; /* EV: R-33326-45252 */} -refact(A) ::= CASCADE. { A = OE_Cascade; /* EV: R-33326-45252 */} -refact(A) ::= RESTRICT. { A = OE_Restrict; /* EV: R-33326-45252 */} -refact(A) ::= NO ACTION. { A = ON_CONFLICT_ACTION_NONE; /* EV: R-33326-45252 */} +refact(A) ::= SET NULL. { A = FKEY_ACTION_SET_NULL; } +refact(A) ::= SET DEFAULT. { A = FKEY_ACTION_SET_DEFAULT; } +refact(A) ::= CASCADE. { A = FKEY_ACTION_CASCADE; } +refact(A) ::= RESTRICT. { A = FKEY_ACTION_RESTRICT; } +refact(A) ::= NO ACTION. { A = FKEY_NO_ACTION; } %type defer_subclause {int} defer_subclause(A) ::= NOT DEFERRABLE init_deferred_pred_opt. {A = 0;} defer_subclause(A) ::= DEFERRABLE init_deferred_pred_opt(X). {A = X;} @@ -334,8 +339,7 @@ tcons ::= CHECK LP expr(E) RP onconf. {sql_add_check_constraint(pParse,&E);} tcons ::= FOREIGN KEY LP eidlist(FA) RP REFERENCES nm(T) eidlist_opt(TA) refargs(R) defer_subclause_opt(D). { - sqlite3CreateForeignKey(pParse, FA, &T, TA, R); - sqlite3DeferForeignKey(pParse, D); + sql_create_foreign_key(pParse, NULL, NULL, FA, &T, TA, D, R); } %type defer_subclause_opt {int} defer_subclause_opt(A) ::= . {A = 0;} @@ -1431,6 +1435,17 @@ cmd ::= ANALYZE nm(X). {sqlite3Analyze(pParse, &X);} cmd ::= ALTER TABLE fullname(X) RENAME TO nm(Z). { sqlite3AlterRenameTable(pParse,X,&Z); } + +cmd ::= ALTER TABLE fullname(X) ADD CONSTRAINT nm(Z) FOREIGN KEY + LP eidlist(FA) RP REFERENCES nm(T) eidlist_opt(TA) refargs(R) + defer_subclause_opt(D). { + sql_create_foreign_key(pParse, X, &Z, FA, &T, TA, D, R); +} + +cmd ::= ALTER TABLE fullname(X) DROP CONSTRAINT nm(Z). { + sql_drop_foreign_key(pParse, X, &Z); +} + /* gh-3075: Commented until ALTER ADD COLUMN is implemeneted. */ /* cmd ::= ALTER TABLE add_column_fullname */ /* ADD kwcolumn_opt columnname(Y) carglist. { */ diff --git a/src/box/sql/pragma.c b/src/box/sql/pragma.c index d427f7844..124961a6a 100644 --- a/src/box/sql/pragma.c +++ b/src/box/sql/pragma.c @@ -32,9 +32,10 @@ /* * This file contains code used to implement the PRAGMA command. */ -#include <box/index.h> -#include <box/box.h> -#include <box/tuple.h> +#include "box/index.h" +#include "box/box.h" +#include "box/tuple.h" +#include "box/fkey.h" #include "box/schema.h" #include "box/coll_id_cache.h" #include "sqliteInt.h" @@ -154,36 +155,6 @@ returnSingleInt(Vdbe * v, i64 value) sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1); } -/* - * Return a human-readable name for a constraint resolution action. - */ -#ifndef SQLITE_OMIT_FOREIGN_KEY -static const char * -actionName(u8 action) -{ - const char *zName; - switch (action) { - case OE_SetNull: - zName = "SET NULL"; - break; - case OE_SetDflt: - zName = "SET DEFAULT"; - break; - case OE_Cascade: - zName = "CASCADE"; - break; - case OE_Restrict: - zName = "RESTRICT"; - break; - default: - zName = "NO ACTION"; - assert(action == ON_CONFLICT_ACTION_NONE); - break; - } - return zName; -} -#endif - /* * Locate a pragma in the aPragmaName[] array. */ @@ -588,206 +559,39 @@ sqlite3Pragma(Parse * pParse, Token * pId, /* First part of [schema.]id field */ case PragTyp_FOREIGN_KEY_LIST:{ if (zRight == NULL) break; - Table *table = sqlite3HashFind(&db->pSchema->tblHash, zRight); - if (table == NULL) - break; - FKey *fkey = table->pFKey; - if (fkey == NULL) + uint32_t space_id = box_space_id_by_name(zRight, + strlen(zRight)); + if (space_id == BOX_ID_NIL) break; + struct space *space = space_by_id(space_id); int i = 0; pParse->nMem = 8; - while (fkey != NULL) { - for (int j = 0; j < fkey->nCol; j++) { - const char *name = - table->def->fields[ - fkey->aCol[j].iFrom].name; + struct fkey *fkey; + rlist_foreach_entry(fkey, &space->child_fkey, child_link) { + struct fkey_def *fdef = fkey->def; + for (uint32_t j = 0; j < fdef->field_count; j++) { + struct space *parent = + space_by_id(fdef->parent_id); + assert(parent != NULL); + uint32_t ch_fl = fdef->links[j].child_field; + const char *child_col = + space->def->fields[ch_fl].name; + uint32_t pr_fl = fdef->links[j].parent_field; + const char *parent_col = + parent->def->fields[pr_fl].name; sqlite3VdbeMultiLoad(v, 1, "iissssss", i, j, - fkey->zTo, name, - fkey->aCol[j].zCol, - actionName( - fkey->aAction[1]), - actionName( - fkey->aAction[0]), + parent->def->name, + child_col, parent_col, + fkey_action_strs[fdef->on_delete], + fkey_action_strs[fdef->on_update], "NONE"); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 8); } ++i; - fkey = fkey->pNextFrom; } break; } #endif /* !defined(SQLITE_OMIT_FOREIGN_KEY) */ - -#ifndef SQLITE_OMIT_FOREIGN_KEY - case PragTyp_FOREIGN_KEY_CHECK:{ - FKey *pFK; /* A foreign key constraint */ - Table *pTab; /* Child table contain "REFERENCES" - * keyword - */ - Table *pParent; /* Parent table that child points to */ - Index *pIdx; /* Index in the parent table */ - int i; /* Loop counter: Foreign key number for pTab */ - int j; /* Loop counter: Field of the foreign key */ - HashElem *k; /* Loop counter: Next table in schema */ - int x; /* result variable */ - int regResult; /* 3 registers to hold a result row */ - int regKey; /* Register to hold key for checking - * the FK - */ - int regRow; /* Registers to hold a row from pTab */ - int addrTop; /* Top of a loop checking foreign keys */ - int addrOk; /* Jump here if the key is OK */ - int *aiCols; /* child to parent column mapping */ - - regResult = pParse->nMem + 1; - pParse->nMem += 4; - regKey = ++pParse->nMem; - regRow = ++pParse->nMem; - k = sqliteHashFirst(&db->pSchema->tblHash); - while (k) { - if (zRight) { - pTab = - sqlite3LocateTable(pParse, 0, - zRight); - k = 0; - } else { - pTab = (Table *) sqliteHashData(k); - k = sqliteHashNext(k); - } - if (pTab == 0 || pTab->pFKey == 0) - continue; - if ((int)pTab->def->field_count + regRow > pParse->nMem) - pParse->nMem = pTab->def->field_count + regRow; - sqlite3OpenTable(pParse, 0, pTab, OP_OpenRead); - sqlite3VdbeLoadString(v, regResult, - pTab->def->name); - for (i = 1, pFK = pTab->pFKey; pFK; - i++, pFK = pFK->pNextFrom) { - pParent = - sqlite3HashFind(&db->pSchema->tblHash, - pFK->zTo); - if (pParent == NULL) - continue; - pIdx = 0; - x = sqlite3FkLocateIndex(pParse, - pParent, pFK, - &pIdx, 0); - if (x != 0) { - k = 0; - break; - } - if (pIdx == NULL) { - sqlite3OpenTable(pParse, i, - pParent, - OP_OpenRead); - continue; - } - struct space *space = - space_cache_find(pIdx->pTable-> - def->id); - assert(space != NULL); - sqlite3VdbeAddOp4(v, OP_OpenRead, i, - pIdx->def->iid, 0, - (void *) space, - P4_SPACEPTR); - - } - assert(pParse->nErr > 0 || pFK == 0); - if (pFK) - break; - if (pParse->nTab < i) - pParse->nTab = i; - addrTop = sqlite3VdbeAddOp1(v, OP_Rewind, 0); - VdbeCoverage(v); - for (i = 1, pFK = pTab->pFKey; pFK; - i++, pFK = pFK->pNextFrom) { - pParent = - sqlite3HashFind(&db->pSchema->tblHash, - pFK->zTo); - pIdx = 0; - aiCols = 0; - if (pParent) { - x = sqlite3FkLocateIndex(pParse, - pParent, - pFK, - &pIdx, - &aiCols); - assert(x == 0); - } - addrOk = sqlite3VdbeMakeLabel(v); - if (pParent && pIdx == 0) { - int iKey = pFK->aCol[0].iFrom; - assert(iKey >= 0 && iKey < - (int)pTab->def->field_count); - sqlite3VdbeAddOp3(v, - OP_Column, - 0, - iKey, - regRow); - sqlite3ColumnDefault(v, - pTab->def, - iKey, - regRow); - sqlite3VdbeAddOp2(v, - OP_IsNull, - regRow, - addrOk); - VdbeCoverage(v); - sqlite3VdbeGoto(v, addrOk); - sqlite3VdbeJumpHere(v, - sqlite3VdbeCurrentAddr - (v) - 2); - } else { - for (j = 0; j < pFK->nCol; j++) { - sqlite3ExprCodeGetColumnOfTable - (v, pTab->def, 0, - aiCols ? aiCols[j] - : pFK->aCol[j]. - iFrom, regRow + j); - sqlite3VdbeAddOp2(v, - OP_IsNull, - regRow - + j, - addrOk); - VdbeCoverage(v); - } - if (pParent) { - sqlite3VdbeAddOp4(v, - OP_MakeRecord, - regRow, - pFK-> - nCol, - regKey, - sqlite3IndexAffinityStr - (db, - pIdx), - pFK-> - nCol); - sqlite3VdbeAddOp4Int(v, - OP_Found, - i, - addrOk, - regKey, - 0); - VdbeCoverage(v); - } - } - sqlite3VdbeMultiLoad(v, regResult + 2, - "si", pFK->zTo, - i - 1); - sqlite3VdbeAddOp2(v, OP_ResultRow, - regResult, 4); - sqlite3VdbeResolveLabel(v, addrOk); - sqlite3DbFree(db, aiCols); - } - sqlite3VdbeAddOp2(v, OP_Next, 0, addrTop + 1); - VdbeCoverage(v); - sqlite3VdbeJumpHere(v, addrTop); - } - break; - } -#endif /* !defined(SQLITE_OMIT_FOREIGN_KEY) */ - #ifndef NDEBUG case PragTyp_PARSER_TRACE:{ if (zRight) { diff --git a/src/box/sql/pragma.h b/src/box/sql/pragma.h index 795c98c6d..4f635b080 100644 --- a/src/box/sql/pragma.h +++ b/src/box/sql/pragma.h @@ -10,7 +10,6 @@ #define PragTyp_CASE_SENSITIVE_LIKE 2 #define PragTyp_COLLATION_LIST 3 #define PragTyp_FLAG 5 -#define PragTyp_FOREIGN_KEY_CHECK 8 #define PragTyp_FOREIGN_KEY_LIST 9 #define PragTyp_INDEX_INFO 10 #define PragTyp_INDEX_LIST 11 @@ -79,8 +78,7 @@ static const char *const pragCName[] = { /* 34 */ "on_update", /* 35 */ "on_delete", /* 36 */ "match", - /* 37 */ "table", - /* Used by: foreign_key_check */ + /* 37 */ "table", /* 38 */ "rowid", /* 39 */ "parent", /* 40 */ "fkid", @@ -135,13 +133,6 @@ static const PragmaName aPragmaName[] = { /* iArg: */ SQLITE_DeferFKs}, #endif #endif -#if !defined(SQLITE_OMIT_FOREIGN_KEY) - { /* zName: */ "foreign_key_check", - /* ePragTyp: */ PragTyp_FOREIGN_KEY_CHECK, - /* ePragFlg: */ PragFlg_NeedSchema, - /* ColNames: */ 37, 4, - /* iArg: */ 0}, -#endif #if !defined(SQLITE_OMIT_FOREIGN_KEY) { /* zName: */ "foreign_key_list", /* ePragTyp: */ PragTyp_FOREIGN_KEY_LIST, diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c index 14239c489..ca6362dbf 100644 --- a/src/box/sql/prepare.c +++ b/src/box/sql/prepare.c @@ -418,6 +418,7 @@ sql_parser_create(struct Parse *parser, sqlite3 *db) { memset(parser, 0, sizeof(struct Parse)); parser->db = db; + rlist_create(&parser->new_fkey); region_create(&parser->region, &cord()->slabc); } @@ -428,6 +429,9 @@ sql_parser_destroy(Parse *parser) sqlite3 *db = parser->db; sqlite3DbFree(db, parser->aLabel); sql_expr_list_delete(db, parser->pConstExpr); + struct fkey_parse *fk; + rlist_foreach_entry(fk, &parser->new_fkey, link) + sql_expr_list_delete(db, fk->selfref_cols); if (db != NULL) { assert(db->lookaside.bDisable >= parser->disableLookaside); diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h index c9923a777..76c35f398 100644 --- a/src/box/sql/sqliteInt.h +++ b/src/box/sql/sqliteInt.h @@ -1472,7 +1472,6 @@ typedef struct Schema Schema; typedef struct Expr Expr; typedef struct ExprList ExprList; typedef struct ExprSpan ExprSpan; -typedef struct FKey FKey; typedef struct FuncDestructor FuncDestructor; typedef struct FuncDef FuncDef; typedef struct FuncDefHash FuncDefHash; @@ -1525,7 +1524,6 @@ typedef int VList; struct Schema { int schema_cookie; /* Database schema version number for this file */ Hash tblHash; /* All tables indexed by name */ - Hash fkeyHash; /* All foreign keys by referenced table name */ }; /* @@ -1912,7 +1910,6 @@ struct Column { struct Table { Column *aCol; /* Information about each column */ Index *pIndex; /* List of SQL indexes on this table. */ - FKey *pFKey; /* Linked list of all foreign keys in this table */ char *zColAff; /* String defining the affinity of each column */ /* ... also used as column name list in a VIEW */ Hash idxHash; /* All (named) indices indexed by name */ @@ -1975,42 +1972,7 @@ sql_space_tuple_log_count(struct Table *tab); * Each REFERENCES clause generates an instance of the following structure * which is attached to the from-table. The to-table need not exist when * the from-table is created. The existence of the to-table is not checked. - * - * The list of all parents for child Table X is held at X.pFKey. - * - * A list of all children for a table named Z (which might not even exist) - * is held in Schema.fkeyHash with a hash key of Z. - */ -struct FKey { - Table *pFrom; /* Table containing the REFERENCES clause (aka: Child) */ - FKey *pNextFrom; /* Next FKey with the same in pFrom. Next parent of pFrom */ - char *zTo; /* Name of table that the key points to (aka: Parent) */ - FKey *pNextTo; /* Next with the same zTo. Next child of zTo. */ - FKey *pPrevTo; /* Previous with the same zTo */ - int nCol; /* Number of columns in this key */ - /* EV: R-30323-21917 */ - u8 isDeferred; /* True if constraint checking is deferred till COMMIT */ - u8 aAction[2]; /* ON DELETE and ON UPDATE actions, respectively */ - /** Triggers for aAction[] actions. */ - struct sql_trigger *apTrigger[2]; - struct sColMap { /* Mapping of columns in pFrom to columns in zTo */ - int iFrom; /* Index of column in pFrom */ - char *zCol; /* Name of column in zTo. If NULL use PRIMARY KEY */ - } aCol[1]; /* One entry for each of nCol columns */ -}; - -/* - * RESTRICT, SETNULL, and CASCADE actions apply only to foreign keys. - * RESTRICT is the same as ABORT for IMMEDIATE foreign keys and the - * same as ROLLBACK for DEFERRED keys. SETNULL means that the foreign - * key is set to NULL. CASCADE means that a DELETE or UPDATE of the - * referenced table row is propagated into the row that holds the - * foreign key. */ -#define OE_Restrict 6 /* OE_Abort for IMMEDIATE, OE_Rollback for DEFERRED */ -#define OE_SetNull 7 /* Set the foreign key value to NULL */ -#define OE_SetDflt 8 /* Set the foreign key value to its default */ -#define OE_Cascade 9 /* Cascade the changes */ /* * This object holds a record which has been parsed out into individual @@ -2844,6 +2806,33 @@ enum ast_type { ast_type_MAX }; +/** + * Structure representing foreign keys constraints appeared + * within CREATE TABLE statement. Used only during parsing. + */ +struct fkey_parse { + /** + * Foreign keys constraint declared in <CREATE TABLE ...> + * statement. They must be coded after space creation. + */ + struct fkey_def *fkey; + /** + * If inside CREATE TABLE statement we want to declare + * self-referenced FK constraint, we must delay their + * resolution until the end of parsing of all columns. + * E.g.: CREATE TABLE t1(id REFERENCES t1(b), b); + */ + struct ExprList *selfref_cols; + /** + * Still, self-referenced columns might be NULL, if + * we declare FK constraints referencing PK: + * CREATE TABLE t1(id REFERENCES t1) - it is a valid case. + */ + bool is_self_referenced; + /** Organize these structs into linked list. */ + struct rlist link; +}; + /* * An SQL parser context. A copy of this structure is passed through * the parser and down into all the parser action routine in order to @@ -2936,7 +2925,15 @@ struct Parse { TriggerPrg *pTriggerPrg; /* Linked list of coded triggers */ With *pWith; /* Current WITH clause, or NULL */ With *pWithToFree; /* Free this WITH object at the end of the parse */ - + /** + * Number of FK constraints declared within + * CREATE TABLE statement. + */ + uint32_t fkey_count; + /** + * Foreign key constraint appeared in CREATE TABLE stmt. + */ + struct rlist new_fkey; bool initiateTTrans; /* Initiate Tarantool transaction */ /** If set - do not emit byte code at all, just parse. */ bool parse_only; @@ -4240,8 +4237,57 @@ sql_trigger_colmask(Parse *parser, struct sql_trigger *trigger, #define sqlite3IsToplevel(p) ((p)->pToplevel==0) int sqlite3JoinType(Parse *, Token *, Token *, Token *); -void sqlite3CreateForeignKey(Parse *, ExprList *, Token *, ExprList *, int); -void sqlite3DeferForeignKey(Parse *, int); + +/** + * Change defer mode of last FK constraint processed during + * <CREATE TABLE> statement. + * + * @param parse_context Current parsing context. + * @param is_deferred Change defer mode to this value. + */ +void +fkey_change_defer_mode(struct Parse *parse_context, bool is_deferred); + +/** + * Function called from parser to handle + * <ALTER TABLE child ADD CONSTRAINT constraint + * FOREIGN KEY (child_cols) REFERENCES parent (parent_cols)> + * OR to handle <CREATE TABLE ...> + * + * @param parse_context Parsing context. + * @param child Name of table to be altered. NULL on CREATE TABLE + * statement processing. + * @param constraint Name of the constraint to be created. May be + * NULL on CREATE TABLE statement processing. + * Then, auto-generated name is used. + * @param child_cols Columns of child table involved in FK. + * May be NULL on CREATE TABLE statement processing. + * If so, the last column added is used. + * @param parent Name of referenced table. + * @param parent_cols List of referenced columns. If NULL, columns + * which make up PK of referenced table are used. + * @param is_deferred Is FK constraint initially deferred. + * @param actions ON DELETE, UPDATE and INSERT resolution + * algorithms (e.g. CASCADE, RESTRICT etc). + */ +void +sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child, + struct Token *constraint, struct ExprList *child_cols, + struct Token *parent, struct ExprList *parent_cols, + bool is_deferred, int actions); + +/** + * Function called from parser to handle + * <ALTER TABLE table DROP CONSTRAINT constraint> SQL statement. + * + * @param parse_context Parsing context. + * @param table Table to be altered. + * @param constraint Name of constraint to be dropped. + */ +void +sql_drop_foreign_key(struct Parse *parse_context, struct SrcList *table, + struct Token *constraint); + void sqlite3Detach(Parse *, Expr *); void sqlite3FixInit(DbFixer *, Parse *, const char *, const Token *); int sqlite3FixSrcList(DbFixer *, SrcList *); @@ -4517,8 +4563,6 @@ sqlite3ColumnDefault(Vdbe *v, struct space_def *def, int i, int ireg); void sqlite3AlterFinishAddColumn(Parse *, Token *); void sqlite3AlterBeginAddColumn(Parse *, SrcList *); char* rename_table(sqlite3 *, const char *, const char *, bool *); -char* rename_parent_table(sqlite3 *, const char *, const char *, const char *, - uint32_t *, uint32_t *); char* rename_trigger(sqlite3 *, char const *, char const *, bool *); /** * Find a collation by name. Set error in @a parser if not found. @@ -4661,32 +4705,67 @@ void sqlite3WithPush(Parse *, With *, u8); #define sqlite3WithDelete(x,y) #endif -/* Declarations for functions in fkey.c. All of these are replaced by - * no-op macros if OMIT_FOREIGN_KEY is defined. In this case no foreign - * key functionality is available. If OMIT_TRIGGER is defined but - * OMIT_FOREIGN_KEY is not, only some of the functions are no-oped. In - * this case foreign keys are parsed, but no other functionality is - * provided (enforcement of FK constraints requires the triggers sub-system). - */ -#if !defined(SQLITE_OMIT_FOREIGN_KEY) -void sqlite3FkCheck(Parse *, Table *, int, int, int *); -void sqlite3FkActions(Parse *, Table *, ExprList *, int, int *); -int sqlite3FkRequired(Table *, int *); -u32 sqlite3FkOldmask(Parse *, Table *); -FKey *sqlite3FkReferences(Table *); -#else -#define sqlite3FkActions(a,b,c,d,e) -#define sqlite3FkCheck(a,b,c,d,e,f) -#define sqlite3FkOldmask(a,b) 0 -#define sqlite3FkRequired(b,c) 0 -#endif -#ifndef SQLITE_OMIT_FOREIGN_KEY -void sqlite3FkDelete(sqlite3 *, Table *); -int sqlite3FkLocateIndex(Parse *, Table *, FKey *, Index **, int **); -#else -#define sqlite3FkDelete(a,b) -#define sqlite3FkLocateIndex(a,b,c,d,e) -#endif +/* + * This function is called when inserting, deleting or updating a + * row of table tab to generate VDBE code to perform foreign key + * constraint processing for the operation. + * + * For a DELETE operation, parameter reg_old is passed the index + * of the first register in an array of (tab->def->field_count + + * 1) registers containing the PK of the row being deleted, + * followed by each of the column values of the row being deleted, + * from left to right. Parameter reg_new is passed zero in this + * case. + * + * For an INSERT operation, reg_old is passed zero and reg_new is + * passed the first register of an array of + * (tab->def->field_count + 1) registers containing the new row + * data. + * + * For an UPDATE operation, this function is called twice. Once + * before the original record is deleted from the table using the + * calling convention described for DELETE. Then again after the + * original record is deleted but before the new record is + * inserted using the INSERT convention. + * + * @param parser SQL parser. + * @param tab Table from which the row is deleted. + * @param reg_old Register with deleted row. + * @param reg_new Register with inserted row. + * @param changed_cols Array of updated columns. Can be NULL. + */ +void +fkey_emit_check(struct Parse *parser, struct Table *tab, int reg_old, + int reg_new, const int *changed_cols); + +/** + * Emit VDBE code to do CASCADE, SET NULL or SET DEFAULT actions + * when deleting or updating a row. + * @param parser SQL parser. + * @param tab Table being updated or deleted from. + * @param reg_old Register of the old record. + * param changes Array of numbers of changed columns. + */ +void +fkey_emit_actions(struct Parse *parser, struct Table *tab, int reg_old, + int *changes); + +/** + * This function is called before generating code to update or + * delete a row contained in given space. If the operation is + * a DELETE, then parameter changes is passed a NULL value. + * For an UPDATE, changes points to an array of size N, where N + * is the number of columns in table. If the i'th column is not + * modified by the UPDATE, then the corresponding entry in the + * changes[] array is set to -1. If the column is modified, + * the value is 0 or greater. + * + * @param space_id Id of space to be modified. + * @param changes Array of modified fields for UPDATE. + * @retval True, if any foreign key processing will be required. + */ +bool +fkey_is_required(uint32_t space_id, const int *changes); /* * Available fault injectors. Should be numbered beginning with 0. diff --git a/src/box/sql/status.c b/src/box/sql/status.c index 5bb1f8f14..209ed8571 100644 --- a/src/box/sql/status.c +++ b/src/box/sql/status.c @@ -244,13 +244,8 @@ sqlite3_db_status(sqlite3 * db, /* The database connection whose status is desir Schema *pSchema = db->pSchema; if (ALWAYS(pSchema != 0)) { HashElem *p; - - nByte += - ROUND8(sizeof(HashElem)) * - (pSchema->tblHash.count + - pSchema->fkeyHash.count); - nByte += sqlite3_msize(pSchema->tblHash.ht); - nByte += sqlite3_msize(pSchema->fkeyHash.ht); + nByte += ROUND8(sizeof(HashElem)) * + pSchema->tblHash.count; for (p = sqliteHashFirst(&pSchema->tblHash); p; p = sqliteHashNext(p)) { diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h index bc61e8426..2850b511e 100644 --- a/src/box/sql/tarantoolInt.h +++ b/src/box/sql/tarantoolInt.h @@ -91,11 +91,6 @@ sql_rename_table(uint32_t space_id, const char *new_name, char **sql_stmt); int tarantoolSqlite3RenameTrigger(const char *zTriggerName, const char *zOldName, const char *zNewName); -/* Alter create table statement of child foreign key table by - * replacing parent table name in create table statement.*/ -int tarantoolSqlite3RenameParentTable(int iTab, const char *zOldParentName, - const char *zNewParentName); - /* Interface for ephemeral tables. */ int tarantoolSqlite3EphemeralCreate(BtCursor * pCur, uint32_t filed_count, struct key_def *def); @@ -154,6 +149,23 @@ int tarantoolSqlite3MakeTableFormat(Table * pTable, void *buf); */ int tarantoolSqlite3MakeTableOpts(Table * pTable, const char *zSql, char *buf); +/** + * Encode links of given foreign key constraint into MsgPack. + * Note: this function is adapted to encode only members of + * struct field_link since it uses offset of (sizeof(field_link)) + * to fetch next member. + * + * @param links Array of unsigned number representing parent or + * child field numbers. + * @param link_count Number of members in @links. + * @param buf Buffer to hold encoded links. Can be NULL. + * In this case function would simply calculate + * memory required for such buffer. + * @retval Length of encoded array. + */ +int +fkey_encode_links(uint32_t *links, uint32_t link_count, char *buf); + /* * Format "parts" array for _index entry. * Returns result size. diff --git a/src/box/sql/update.c b/src/box/sql/update.c index d51a05cad..07396e1a9 100644 --- a/src/box/sql/update.c +++ b/src/box/sql/update.c @@ -229,7 +229,7 @@ sqlite3Update(Parse * pParse, /* The parser context */ */ pTabList->a[0].colUsed = 0; - hasFK = sqlite3FkRequired(pTab, aXRef); + hasFK = fkey_is_required(pTab->def->id, aXRef); /* There is one entry in the aRegIdx[] array for each index on the table * being updated. Fill in aRegIdx[] with a register number that will hold @@ -433,7 +433,9 @@ sqlite3Update(Parse * pParse, /* The parser context */ * information is needed */ if (chngPk != 0 || hasFK != 0 || trigger != NULL) { - u32 oldmask = (hasFK ? sqlite3FkOldmask(pParse, pTab) : 0); + struct space *space = space_by_id(pTab->def->id); + assert(space != NULL); + u32 oldmask = hasFK ? space->fkey_mask : 0; oldmask |= sql_trigger_colmask(pParse, trigger, pChanges, 0, TRIGGER_BEFORE | TRIGGER_AFTER, pTab, on_error); @@ -545,9 +547,8 @@ sqlite3Update(Parse * pParse, /* The parser context */ aXRef); /* Do FK constraint checks. */ - if (hasFK) { - sqlite3FkCheck(pParse, pTab, regOldPk, 0, aXRef); - } + if (hasFK) + fkey_emit_check(pParse, pTab, regOldPk, 0, aXRef); /* Delete the index entries associated with the current record. */ if (bReplace || chngPk) { @@ -583,9 +584,8 @@ sqlite3Update(Parse * pParse, /* The parser context */ sqlite3VdbeJumpHere(v, addr1); } - if (hasFK) { - sqlite3FkCheck(pParse, pTab, 0, regNewPk, aXRef); - } + if (hasFK) + fkey_emit_check(pParse, pTab, 0, regNewPk, aXRef); /* Insert the new index entries and the new record. */ vdbe_emit_insertion_completion(v, iIdxCur, aRegIdx[0], @@ -595,9 +595,8 @@ sqlite3Update(Parse * pParse, /* The parser context */ * handle rows (possibly in other tables) that refer via a foreign key * to the row just updated. */ - if (hasFK) { - sqlite3FkActions(pParse, pTab, pChanges, regOldPk, aXRef); - } + if (hasFK) + fkey_emit_actions(pParse, pTab, regOldPk, aXRef); } /* Increment the row counter diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c index 8e6e14f5d..46ad9af58 100644 --- a/src/box/sql/vdbe.c +++ b/src/box/sql/vdbe.c @@ -39,6 +39,7 @@ * in this file for details. If in doubt, do not deviate from existing * commenting and indentation practices when changing or adding code. */ +#include "box/fkey.h" #include "box/txn.h" #include "box/session.h" #include "sqliteInt.h" @@ -4642,7 +4643,6 @@ case OP_RenameTable: { const char *zOldTableName; const char *zNewTableName; Table *pTab; - FKey *pFKey; struct init_data init; char *zSqlStmt; @@ -4661,20 +4661,6 @@ case OP_RenameTable: { rc = sql_rename_table(space_id, zNewTableName, &zSqlStmt); if (rc) goto abort_due_to_error; - /* If it is parent table, all children statements should be updated. */ - for (pFKey = sqlite3FkReferences(pTab); pFKey; pFKey = pFKey->pNextTo) { - assert(pFKey->zTo != NULL); - assert(pFKey->pFrom != NULL); - rc = tarantoolSqlite3RenameParentTable(pFKey->pFrom->def->id, - pFKey->zTo, - zNewTableName); - if (rc) goto abort_due_to_error; - pFKey->zTo = sqlite3DbStrNDup(db, zNewTableName, - sqlite3Strlen30(zNewTableName)); - sqlite3HashInsert(&db->pSchema->fkeyHash, zOldTableName, 0); - sqlite3HashInsert(&db->pSchema->fkeyHash, zNewTableName, pFKey); - } - sqlite3UnlinkAndDeleteTable(db, pTab->def->name); init.db = db; diff --git a/test/sql-tap/alter.test.lua b/test/sql-tap/alter.test.lua index a1f6a24b4..db87c7003 100755 --- a/test/sql-tap/alter.test.lua +++ b/test/sql-tap/alter.test.lua @@ -313,8 +313,8 @@ test:do_execsql_test( DROP TABLE IF EXISTS t1; DROP TABLE IF EXISTS t2; DROP TABLE IF EXISTS t3; - CREATE TABLE t2(id INT PRIMARY KEY); - CREATE TABLE t3(id INT PRIMARY KEY); + CREATE TABLE t2(id PRIMARY KEY); + CREATE TABLE t3(id PRIMARY KEY); CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id)); INSERT INTO t2 VALUES(1); INSERT INTO t3 VALUES(2); diff --git a/test/sql-tap/alter2.test.lua b/test/sql-tap/alter2.test.lua new file mode 100755 index 000000000..94a61ebf4 --- /dev/null +++ b/test/sql-tap/alter2.test.lua @@ -0,0 +1,229 @@ +#!/usr/bin/env tarantool +test = require("sqltester") +test:plan(18) + +-- This suite is aimed to test ALTER TABLE ADD CONSTRAINT statement. +-- + +test:do_catchsql_test( + "alter2-1.1", + [[ + CREATE TABLE t1(id PRIMARY KEY, a, b); + ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY (a) REFERENCES t1(id); + ALTER TABLE t1 ADD CONSTRAINT fk2 FOREIGN KEY (a) REFERENCES t1; + INSERT INTO t1 VALUES(1, 1, 2); + ]], { + -- <alter2-1.1> + 0 + -- </alter2-1.1> + }) + +test:do_catchsql_test( + "alter2-1.2", + [[ + INSERT INTO t1 VALUES(2, 3, 2); + ]], { + -- <alter2-1.2> + 1, "FOREIGN KEY constraint failed" + -- </alter2-1.2> + }) + +test:do_catchsql_test( + "alter2-1.3", + [[ + DELETE FROM t1; + ]], { + -- <alter2-1.3> + 0 + -- </alter2-1.3> + }) + +test:do_catchsql_test( + "alter2-1.4", + [[ + ALTER TABLE t1 DROP CONSTRAINT fk1; + INSERT INTO t1 VALUES(2, 3, 2); + ]], { + -- <alter2-1.4> + 1, "FOREIGN KEY constraint failed" + -- </alter2-1.4> + }) + +test:do_execsql_test( + "alter2-1.5", + [[ + ALTER TABLE t1 DROP CONSTRAINT fk2; + INSERT INTO t1 VALUES(2, 3, 2); + SELECT * FROM t1; + ]], { + -- <alter2-1.5> + 2, 3, 2 + -- </alter2-1.5> + }) + +test:do_catchsql_test( + "alter2-1.6", + [[ + DELETE FROM t1; + CREATE UNIQUE INDEX i1 ON t1(b, a); + ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY (a, b) REFERENCES t1(b, a); + INSERT INTO t1 VALUES(3, 1, 1); + INSERT INTO t1 VALUES(4, 2, 1); + ]], { + -- <alter2-1.6> + 1, "FOREIGN KEY constraint failed" + -- </alter2-1.6> + }) + +test:do_execsql_test( + "alter2-1.7", + [[ + ALTER TABLE t1 DROP CONSTRAINT fk1; + INSERT INTO t1 VALUES(5, 2, 1); + SELECT * FROM t1; + ]], { + -- <alter2-1.7> + 3, 1, 1, 5, 2, 1 + -- </alter2-1.7> + }) + +test:do_test( + "alter2-1.7.1", + function() + test:execsql([[DELETE FROM t1;]]) + t1 = box.space.T1 + if t1.engine ~= 'vinyl' then + return + end + box.snapshot() + end, { + -- <alter2-1.7.1> + -- </alter2-1.7.1> + }) + +test:do_catchsql_test( + "alter2-1.8", + [[ + ALTER TABLE t1 ADD CONSTRAINT fk1 FOREIGN KEY (a) REFERENCES t1(id); + ALTER TABLE t1 ADD CONSTRAINT fk2 FOREIGN KEY (a, b) REFERENCES t1(b, a); + DROP TABLE t1; + ]], { + -- <alter2-1.8> + 0 + -- </alter2-1.8> + }) + +test:do_execsql_test( + "alter2-1.9", + [[ + SELECT * FROM "_fk_constraint"; + ]], { + -- <alter2-1.9> + -- </alter2-1.9> + }) + +test:do_catchsql_test( + "alter2-2.1", + [[ + CREATE TABLE child (id PRIMARY KEY, a, b); + CREATE TABLE parent (id PRIMARY KEY, c UNIQUE, d); + ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent(c); + ALTER TABLE parent ADD CONSTRAINT fk FOREIGN KEY (c) REFERENCES parent; + INSERT INTO parent VALUES(1, 2, 3); + ]], { + -- <alter2-2.1> + 1, "FOREIGN KEY constraint failed" + -- </alter2-2.1> + }) + +test:do_catchsql_test( + "alter2-2.2", + [[ + INSERT INTO parent VALUES(1, 1, 2); + INSERT INTO child VALUES(2, 1, 1); + ]], { + -- <alter2-2.2> + 1, "FOREIGN KEY constraint failed" + -- </alter2-2.2> + }) + +test:do_catchsql_test( + "alter2-2.3", + [[ + ALTER TABLE child DROP CONSTRAINT fk; + INSERT INTO parent VALUES(3, 4, 2); + ]], { + -- <alter2-2.3> + 1, "FOREIGN KEY constraint failed" + -- </alter2-2.3> + }) + +test:do_execsql_test( + "alter2-2.4", + [[ + ALTER TABLE parent DROP CONSTRAINT fk; + INSERT INTO parent VALUES(3, 4, 2); + SELECT * FROM parent; + ]], { + -- <alter2-2.4> + 1, 1, 2, 3, 4, 2 + -- </alter2-2.4> + }) + +test:do_execsql_test( + "alter2-3.1", + [[ + DROP TABLE child; + DROP TABLE parent; + CREATE TABLE child (id PRIMARY KEY, a, b); + CREATE TABLE parent (id PRIMARY KEY, c, d); + ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent ON DELETE CASCADE MATCH FULL; + INSERT INTO parent VALUES(1, 2, 3), (3, 4, 5), (6, 7, 8); + INSERT INTO child VALUES(1, 1, 1), (3, 2, 2); + DELETE FROM parent WHERE id = 1; + SELECT * FROM CHILD; + ]], { + -- <alter2-3.1> + 3, 2, 2 + -- </alter2-3.1> + }) + +test:do_execsql_test( + "alter2-3.2", + [[ + DROP TABLE child; + DROP TABLE parent; + CREATE TABLE child (id PRIMARY KEY, a, b); + CREATE TABLE parent (id PRIMARY KEY, c, d); + ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent ON UPDATE CASCADE MATCH PARTIAL; + INSERT INTO parent VALUES(1, 2, 3), (3, 4, 5), (6, 7, 8); + INSERT INTO child VALUES(1, 1, 1), (3, 2, 2); + UPDATE parent SET id = 5 WHERE id = 1; + SELECT * FROM CHILD; + ]], { + -- <alter2-3.2> + 3, 2, 2, 5, 1, 1 + -- </alter2-3.2> + }) + +test:do_catchsql_test( + "alter2-4.1", + [[ + ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY REFERENCES child; + ]], { + -- <alter2-4.1> + 1, "near \"REFERENCES\": syntax error" + -- </alter2-4.1> + }) + +test:do_catchsql_test( + "alter2-4.2", + [[ + ALTER TABLE child ADD CONSTRAINT fk () FOREIGN KEY REFERENCES child; + ]], { + -- <alter2-4.1> + 1, "near \"(\": syntax error" + -- </alter2-4.2> + }) + +test:finish_test() diff --git a/test/sql-tap/fkey1.test.lua b/test/sql-tap/fkey1.test.lua index 494af4b4a..3c29b097d 100755 --- a/test/sql-tap/fkey1.test.lua +++ b/test/sql-tap/fkey1.test.lua @@ -1,13 +1,13 @@ #!/usr/bin/env tarantool test = require("sqltester") -test:plan(19) +test:plan(18) -- This file implements regression tests for foreign keys. test:do_execsql_test( "fkey1-1.1", [[ - CREATE TABLE t2(x PRIMARY KEY, y TEXT); + CREATE TABLE t2(x PRIMARY KEY, y TEXT, UNIQUE (x, y)); ]], { -- <fkey1-1.1> -- </fkey1-1.1> @@ -17,10 +17,10 @@ test:do_execsql_test( "fkey1-1.2", [[ CREATE TABLE t1( - a INTEGER PRIMARY KEY, + a PRIMARY KEY, b INTEGER REFERENCES t1 ON DELETE CASCADE - REFERENCES t2, + REFERENCES t2 (x), c TEXT, FOREIGN KEY (b, c) REFERENCES t2(x, y) ON UPDATE CASCADE); ]], { @@ -32,7 +32,7 @@ test:do_execsql_test( "fkey1-1.3", [[ CREATE TABLE t3( - a INTEGER PRIMARY KEY REFERENCES t2, + a PRIMARY KEY REFERENCES t2, b INTEGER REFERENCES t1, FOREIGN KEY (a, b) REFERENCES t2(x, y)); ]], { @@ -64,13 +64,13 @@ test:do_execsql_test( test:do_execsql_test( "fkey1-3.1", [[ - CREATE TABLE t5(a INTEGER PRIMARY KEY, b, c); + CREATE TABLE t5(a PRIMARY KEY, b, c UNIQUE, UNIQUE(a, b)); CREATE TABLE t6(d REFERENCES t5, e PRIMARY KEY REFERENCES t5(c)); PRAGMA foreign_key_list(t6); ]], { -- <fkey1-3.1> - 0, 0, 'T5', 'E', 'C', 'NO ACTION', 'NO ACTION', 'NONE', - 1, 0, 'T5', 'D', '', 'NO ACTION', 'NO ACTION', 'NONE' + 0, 0, 'T5', 'D', 'A', 'no_action', 'no_action', 'NONE', + 1, 0, 'T5', 'E', 'C', 'no_action', 'no_action', 'NONE' -- </fkey1-3.1> }) @@ -81,8 +81,8 @@ test:do_execsql_test( PRAGMA foreign_key_list(t7); ]], { -- <fkey1-3.2> - 0, 0, 'T5', 'D', 'A', 'NO ACTION', 'NO ACTION', 'NONE', - 0, 1, 'T5', 'E', 'B', 'NO ACTION', 'NO ACTION', 'NONE' + 0, 0, 'T5', 'D', 'A', 'no_action', 'no_action', 'NONE', + 0, 1, 'T5', 'E', 'B', 'no_action', 'no_action', 'NONE' -- </fkey1-3.2> }) @@ -91,12 +91,12 @@ test:do_execsql_test( [[ CREATE TABLE t8( d PRIMARY KEY, e, f, - FOREIGN KEY (d, e) REFERENCES t5 ON DELETE CASCADE ON UPDATE SET NULL); + FOREIGN KEY (d, e) REFERENCES t5(a, b) ON DELETE CASCADE ON UPDATE SET NULL); PRAGMA foreign_key_list(t8); ]], { -- <fkey1-3.3> - 0, 0, 'T5', 'D', '', 'SET NULL', 'CASCADE', 'NONE', - 0, 1, 'T5', 'E', '', 'SET NULL', 'CASCADE', 'NONE' + 0, 0, 'T5', 'D', 'A', 'cascade', 'set_null', 'NONE', + 0, 1, 'T5', 'E', 'B', 'cascade', 'set_null', 'NONE' -- </fkey1-3.3> }) @@ -105,12 +105,12 @@ test:do_execsql_test( [[ CREATE TABLE t9( d PRIMARY KEY, e, f, - FOREIGN KEY (d, e) REFERENCES t5 ON DELETE CASCADE ON UPDATE SET DEFAULT); + FOREIGN KEY (d, e) REFERENCES t5(a, b) ON DELETE CASCADE ON UPDATE SET DEFAULT); PRAGMA foreign_key_list(t9); ]], { -- <fkey1-3.4> - 0, 0, 'T5', 'D', '', 'SET DEFAULT', 'CASCADE', 'NONE', - 0, 1, 'T5', 'E', '', 'SET DEFAULT', 'CASCADE', 'NONE' + 0, 0, 'T5', 'D', 'A', 'cascade', 'set_default', 'NONE', + 0, 1, 'T5', 'E', 'B', 'cascade', 'set_default', 'NONE' -- </fkey1-3.4> }) @@ -144,7 +144,7 @@ test:do_execsql_test( "fkey1-5.1", [[ CREATE TABLE t11( - x INTEGER PRIMARY KEY, + x PRIMARY KEY, parent REFERENCES t11 ON DELETE CASCADE); INSERT INTO t11 VALUES(1, NULL), (2, 1), (3, 2); ]], { @@ -176,7 +176,7 @@ test:do_execsql_test( "fkey1-5.4", [[ CREATE TABLE Foo ( - Id INTEGER PRIMARY KEY, + Id PRIMARY KEY, ParentId INTEGER REFERENCES Foo(Id) ON DELETE CASCADE, C1); INSERT OR REPLACE INTO Foo(Id, ParentId, C1) VALUES (1, null, 'A'); @@ -208,7 +208,7 @@ test:do_execsql_test( -- </fkey1-5.6> }) -test:do_execsql_test( +test:do_catchsql_test( "fkey1-6.1", [[ CREATE TABLE p1(id PRIMARY KEY, x, y); @@ -217,23 +217,16 @@ test:do_execsql_test( CREATE TABLE c1(a PRIMARY KEY REFERENCES p1(x)); ]], { -- <fkey1-6.1> + 1, "Failed to create foreign key constraint 'FK_CONSTRAINT_1_C1': referenced fields don't compose unique index" -- </fkey1-6.1> }) -test:do_catchsql_test( - "fkey1-6.2", - [[ - INSERT INTO c1 VALUES(1); - ]], { - -- <fkey1-6.2> - 1, "foreign key mismatch - \"C1\" referencing \"P1\"" - -- </fkey1-6.2> - }) - test:do_execsql_test( "fkey1-6.3", [[ CREATE UNIQUE INDEX p1x2 ON p1(x); + DROP TABLE IF EXISTS c1; + CREATE TABLE c1(a PRIMARY KEY REFERENCES p1(x)); INSERT INTO c1 VALUES(1); ]], { -- <fkey1-6.3> diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 523340f6b..611e7cba0 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -1,6 +1,6 @@ #!/usr/bin/env tarantool test = require("sqltester") -test:plan(121) +test:plan(116) -- This file implements regression tests for foreign keys. @@ -14,7 +14,7 @@ test:do_execsql_test( CREATE TABLE t4(c PRIMARY KEY REFERENCES t3, d); CREATE TABLE t7(a, b INTEGER PRIMARY KEY); - CREATE TABLE t8(c PRIMARY KEY REFERENCES t7, d); + CREATE TABLE t8(c INTEGER PRIMARY KEY REFERENCES t7, d); ]], { -- <fkey2-1.1> -- </fkey2-1.1> @@ -317,13 +317,13 @@ test:do_execsql_test( "fkey2-2.1", [[ CREATE TABLE i(i INTEGER PRIMARY KEY); - CREATE TABLE j(j PRIMARY KEY REFERENCES i); + CREATE TABLE j(j INT PRIMARY KEY REFERENCES i); INSERT INTO i VALUES(35); - INSERT INTO j VALUES('35.0'); + INSERT INTO j VALUES(35); SELECT j, typeof(j) FROM j; ]], { -- <fkey2-2.1> - "35.0", "text" + 35, "integer" -- </fkey2-2.1> }) @@ -524,7 +524,7 @@ test:do_execsql_test( [[ DROP TABLE IF EXISTS t1; DROP TABLE IF EXISTS t2; - CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t1(a INTEGER PRIMARY KEY, b); CREATE TABLE t2(c INTEGER PRIMARY KEY REFERENCES t1, b); ]], { -- <fkey2-5.1> @@ -600,10 +600,10 @@ test:do_execsql_test( [[ DROP TABLE IF EXISTS t2; DROP TABLE IF EXISTS t1; - CREATE TABLE t1(a INTEGER PRIMARY KEY, b); + CREATE TABLE t1(a PRIMARY KEY, b); CREATE TABLE t2( c INTEGER PRIMARY KEY, - d INTEGER DEFAULT 1 REFERENCES t1 ON DELETE SET DEFAULT); + d DEFAULT 1 REFERENCES t1 ON DELETE SET DEFAULT); DELETE FROM t1; ]], { -- <fkey2-6.1> @@ -714,24 +714,20 @@ test:do_catchsql_test( [[ CREATE TABLE p(a PRIMARY KEY, b); CREATE TABLE c(x PRIMARY KEY REFERENCES p(c)); - INSERT INTO c DEFAULT VALUES; ]], { -- <fkey2-7.1> - 1, "foreign key mismatch - \"C\" referencing \"P\"" + 1, "Failed to create foreign key constraint 'fk_constraint_1_C': foreign key refers to nonexistent field C" -- </fkey2-7.1> }) test:do_catchsql_test( "fkey2-7.2", [[ - DROP TABLE IF EXISTS c; - DROP TABLE IF EXISTS p; - CREATE VIEW v AS SELECT x AS y FROM c; + CREATE VIEW v AS SELECT b AS y FROM p; CREATE TABLE c(x PRIMARY KEY REFERENCES v(y)); - INSERT INTO c DEFAULT VALUES; ]], { -- <fkey2-7.2> - 1, "no such table: C" + 1, "referenced table can't be view" -- </fkey2-7.2> }) @@ -740,13 +736,13 @@ test:do_catchsql_test( [[ DROP VIEW v; DROP TABLE IF EXISTS c; - CREATE TABLE p(a COLLATE binary, b PRIMARY KEY); - CREATE UNIQUE INDEX idx ON p(a COLLATE "unicode_ci"); + DROP TABLE IF EXISTS p; + CREATE TABLE p(a COLLATE "unicode_ci", b PRIMARY KEY); + CREATE UNIQUE INDEX idx ON p(a); CREATE TABLE c(x PRIMARY KEY REFERENCES p(a)); - INSERT INTO c DEFAULT VALUES; ]], { -- <fkey2-7.3> - 1, "no such view: V" + 1, "Failed to create foreign key constraint 'FK_CONSTRAINT_1_C': field collation mismatch" -- </fkey2-7.3> }) @@ -757,10 +753,9 @@ test:do_catchsql_test( DROP TABLE IF EXISTS p; CREATE TABLE p(a, b, PRIMARY KEY(a, b)); CREATE TABLE c(x PRIMARY KEY REFERENCES p); - INSERT INTO c DEFAULT VALUES; ]], { -- <fkey2-7.4> - 1, "foreign key mismatch - \"C\" referencing \"P\"" + 1, "Failed to create foreign key constraint 'fk_constraint_1_C': number of columns in foreign key does not match the number of columns in the primary index of referenced table" -- </fkey2-7.4> }) @@ -771,7 +766,7 @@ test:do_execsql_test( "fkey2-8.1", [[ CREATE TABLE t1(a INTEGER PRIMARY KEY, b); - CREATE TABLE t2(c PRIMARY KEY, d, FOREIGN KEY(c) REFERENCES t1(a) ON UPDATE CASCADE); + CREATE TABLE t2(c INTEGER PRIMARY KEY, d, FOREIGN KEY(c) REFERENCES t1(a) ON UPDATE CASCADE); INSERT INTO t1 VALUES(10, 100); INSERT INTO t2 VALUES(10, 100); @@ -794,8 +789,7 @@ test:do_execsql_test( DROP TABLE IF EXISTS t1; CREATE TABLE t1(a, b PRIMARY KEY); CREATE TABLE t2( - x PRIMARY KEY REFERENCES t1 - ON UPDATE RESTRICT DEFERRABLE INITIALLY DEFERRED); + x PRIMARY KEY REFERENCES t1 ON UPDATE RESTRICT); INSERT INTO t1 VALUES(1, 'one'); INSERT INTO t1 VALUES(2, 'two'); INSERT INTO t1 VALUES(3, 'three'); @@ -847,7 +841,7 @@ test:do_execsql_test( BEGIN INSERT INTO t1 VALUES(old.x); END; - CREATE TABLE t2(y PRIMARY KEY REFERENCES t1); + CREATE TABLE t2(y COLLATE "unicode_ci" PRIMARY KEY REFERENCES t1); INSERT INTO t1 VALUES('A'); INSERT INTO t1 VALUES('B'); INSERT INTO t2 VALUES('A'); @@ -875,7 +869,7 @@ test:do_execsql_test( "fkey2-9.7", [[ DROP TABLE t2; - CREATE TABLE t2(y PRIMARY KEY REFERENCES t1 ON DELETE RESTRICT); + CREATE TABLE t2(y COLLATE "unicode_ci" PRIMARY KEY REFERENCES t1 ON DELETE RESTRICT); INSERT INTO t2 VALUES('A'); INSERT INTO t2 VALUES('B'); ]], { @@ -1053,7 +1047,7 @@ test:do_catchsql_test( CREATE TABLE t1(a PRIMARY KEY, b REFERENCES nosuchtable); ]], { -- <fkey2-10.6> - 1, "foreign key constraint references nonexistent table: NOSUCHTABLE" + 1, "Space 'NOSUCHTABLE' does not exist" -- </fkey2-10.6> }) @@ -1083,7 +1077,7 @@ test:do_catchsql_test( test:do_execsql_test( "fkey2-10.9", [[ - DELETE FROM t2; + DROP TABLE t2; DROP TABLE t1; ]], { -- <fkey2-10.9> @@ -1091,47 +1085,6 @@ test:do_execsql_test( }) test:do_catchsql_test( - "fkey2-10.10", - [[ - INSERT INTO t2 VALUES('x'); - ]], { - -- <fkey2-10.10> - 1, "no such table: T1" - -- </fkey2-10.10> - }) - -test:do_execsql_test( - "fkey2-10.11", - [[ - CREATE TABLE t1(x PRIMARY KEY); - INSERT INTO t1 VALUES('x'); - INSERT INTO t2 VALUES('x'); - ]], { - -- <fkey2-10.11> - -- </fkey2-10.11> - }) - -test:do_catchsql_test( - "fkey2-10.12", - [[ - DROP TABLE t1; - ]], { - -- <fkey2-10.12> - 1, "FOREIGN KEY constraint failed" - -- </fkey2-10.12> - }) - -test:do_execsql_test( - "fkey2-10.13", - [[ - DROP TABLE t2; - DROP TABLE t1; - ]], { - -- <fkey2-10.13> - -- </fkey2-10.13> - }) - -test:do_execsql_test( "fkey2-10.14", [[ DROP TABLE IF EXISTS cc; @@ -1140,23 +1093,13 @@ test:do_execsql_test( CREATE TABLE cc(a PRIMARY KEY, b, FOREIGN KEY(a, b) REFERENCES pp(x, z)); ]], { -- <fkey2-10.14> + 1, "Failed to create foreign key constraint 'fk_constraint_1_CC': foreign key refers to nonexistent field Z" -- </fkey2-10.14> }) -test:do_catchsql_test( - "fkey2-10.15", - [[ - INSERT INTO cc VALUES(1, 2); - ]], { - -- <fkey2-10.15> - 1, "foreign key mismatch - \"CC\" referencing \"PP\"" - -- </fkey2-10.15> - }) - test:do_execsql_test( "fkey2-10.16", [[ - DROP TABLE cc; CREATE TABLE cc( a PRIMARY KEY, b, FOREIGN KEY(a, b) REFERENCES pp DEFERRABLE INITIALLY DEFERRED); @@ -1181,7 +1124,7 @@ test:do_execsql_test( -- </fkey2-10.17> }) -test:do_execsql_test( +test:do_catchsql_test( "fkey2-10.18", [[ CREATE TABLE b1(a PRIMARY KEY, b); @@ -1193,7 +1136,7 @@ test:do_execsql_test( -- </fkey2-10.18> }) -test:do_execsql_test( +test:do_catchsql_test( "fkey2-10.19", [[ CREATE TABLE b3(a PRIMARY KEY, b REFERENCES b2 DEFERRABLE INITIALLY DEFERRED); @@ -1204,15 +1147,15 @@ test:do_execsql_test( -- </fkey2-10.19> }) -test:do_execsql_test( +test:do_catchsql_test( "fkey2-10.20", [[ DROP VIEW IF EXISTS v; - CREATE VIEW v AS SELECT * FROM t1; + CREATE VIEW v AS SELECT * FROM b1; CREATE TABLE t1(x PRIMARY KEY REFERENCES v); - DROP VIEW v; ]], { -- <fkey2-10.20> + 1, "referenced table can't be view" -- </fkey2-10.20> }) @@ -1224,7 +1167,7 @@ test:do_execsql_test( test:do_execsql_test( "fkey2-11.1", [[ - CREATE TABLE self(a INTEGER PRIMARY KEY, b REFERENCES self(a)); + CREATE TABLE self(a PRIMARY KEY, b REFERENCES self(a)); INSERT INTO self VALUES(13, 13); UPDATE self SET a = 14, b = 14; ]], { @@ -1294,7 +1237,7 @@ test:do_execsql_test( "fkey2-11.8", [[ DROP TABLE IF EXISTS self; - CREATE TABLE self(a UNIQUE, b INTEGER PRIMARY KEY REFERENCES self(a)); + CREATE TABLE self(a UNIQUE, b PRIMARY KEY REFERENCES self(a)); INSERT INTO self VALUES(13, 13); UPDATE self SET a = 14, b = 14; ]], { @@ -1366,7 +1309,7 @@ test:do_catchsql_test( test:do_execsql_test( "fkey2-12.1", [[ - CREATE TABLE tdd08(a INTEGER PRIMARY KEY, b); + CREATE TABLE tdd08(a PRIMARY KEY, b); CREATE UNIQUE INDEX idd08 ON tdd08(a,b); INSERT INTO tdd08 VALUES(200,300); @@ -1430,7 +1373,7 @@ test:do_catchsql_test( test:do_execsql_test( "fkey2-13.1", [[ - CREATE TABLE tce71(a INTEGER PRIMARY KEY, b); + CREATE TABLE tce71(a PRIMARY KEY, b); CREATE UNIQUE INDEX ice71 ON tce71(a,b); INSERT INTO tce71 VALUES(100,200); CREATE TABLE tce72(w PRIMARY KEY, x, y, FOREIGN KEY(x,y) REFERENCES tce71(a,b)); @@ -1466,9 +1409,9 @@ test:do_catchsql_test( test:do_execsql_test( "fkey2-14.1", [[ - CREATE TABLE tce73(a INTEGER PRIMARY KEY, b, UNIQUE(a,b)); + CREATE TABLE tce73(a PRIMARY KEY, b, UNIQUE(a,b)); INSERT INTO tce73 VALUES(100,200); - CREATE TABLE tce74(w INTEGER PRIMARY KEY, x, y, FOREIGN KEY(x,y) REFERENCES tce73(a,b)); + CREATE TABLE tce74(w PRIMARY KEY, x, y, FOREIGN KEY(x,y) REFERENCES tce73(a,b)); INSERT INTO tce74 VALUES(300,100,200); UPDATE tce73 set b = 200 where a = 100; SELECT * FROM tce73, tce74; diff --git a/test/sql-tap/fkey3.test.lua b/test/sql-tap/fkey3.test.lua index d7055b096..84997dd35 100755 --- a/test/sql-tap/fkey3.test.lua +++ b/test/sql-tap/fkey3.test.lua @@ -158,9 +158,8 @@ test:do_catchsql_test( test:do_execsql_test( "fkey3-3.6", [[ - CREATE TABLE t6(a INTEGER PRIMARY KEY, b, c, d, + CREATE TABLE t6(a PRIMARY KEY, b, c, d, UNIQUE (a, b), FOREIGN KEY(c, d) REFERENCES t6(a, b)); - CREATE UNIQUE INDEX t6i ON t6(b, a); INSERT INTO t6 VALUES(1, 'a', 1, 'a'); INSERT INTO t6 VALUES(2, 'a', 2, 'a'); INSERT INTO t6 VALUES(3, 'a', 1, 'a'); @@ -206,9 +205,8 @@ test:do_execsql_test( test:do_execsql_test( "fkey3-3.10", [[ - CREATE TABLE t7(a, b, c, d INTEGER PRIMARY KEY, + CREATE TABLE t7(a, b, c, d PRIMARY KEY, UNIQUE(a, b), FOREIGN KEY(c, d) REFERENCES t7(a, b)); - CREATE UNIQUE INDEX t7i ON t7(a, b); INSERT INTO t7 VALUES('x', 1, 'x', 1); INSERT INTO t7 VALUES('x', 2, 'x', 2); ]], { @@ -239,9 +237,10 @@ test:do_catchsql_test( test:do_execsql_test( "fkey3-6.1", [[ - CREATE TABLE t8(a PRIMARY KEY, b, c, d, e, FOREIGN KEY(c, d) REFERENCES t8(a, b)); + CREATE TABLE t8(a PRIMARY KEY, b, c, d, e); CREATE UNIQUE INDEX t8i1 ON t8(a, b); CREATE UNIQUE INDEX t8i2 ON t8(c); + ALTER TABLE t8 ADD CONSTRAINT fk1 FOREIGN KEY (c, d) REFERENCES t8(a, b); INSERT INTO t8 VALUES(1, 1, 1, 1, 1); ]], { -- <fkey3-6.1> @@ -272,12 +271,12 @@ test:do_catchsql_test( "fkey3-6.4", [[ CREATE TABLE TestTable ( - id INTEGER PRIMARY KEY, + id PRIMARY KEY, name TEXT, source_id INTEGER NOT NULL, - parent_id INTEGER, - FOREIGN KEY(source_id, parent_id) REFERENCES TestTable(source_id, id)); + parent_id INTEGER); CREATE UNIQUE INDEX testindex on TestTable(source_id, id); + ALTER TABLE TestTable ADD CONSTRAINT fk1 FOREIGN KEY (source_id, parent_id) REFERENCES TestTable(source_id, id); INSERT INTO TestTable VALUES (1, 'parent', 1, null); INSERT INTO TestTable VALUES (2, 'child', 1, 1); UPDATE TestTable SET parent_id=1000 WHERE id=2; diff --git a/test/sql-tap/orderby1.test.lua b/test/sql-tap/orderby1.test.lua index e0ea3698d..1cc104bfc 100755 --- a/test/sql-tap/orderby1.test.lua +++ b/test/sql-tap/orderby1.test.lua @@ -29,7 +29,7 @@ test:do_test( function() return test:execsql [[ CREATE TABLE album( - aid INTEGER PRIMARY KEY, + aid PRIMARY KEY, title TEXT UNIQUE NOT NULL ); CREATE TABLE track( @@ -417,7 +417,7 @@ test:do_test( DROP TABLE track; DROP TABLE album; CREATE TABLE album( - aid INTEGER PRIMARY KEY, + aid PRIMARY KEY, title TEXT UNIQUE NOT NULL ); CREATE TABLE track( @@ -664,7 +664,7 @@ test:do_test( 4.0, function() return test:execsql [[ - CREATE TABLE t41(a INTEGER PRIMARY KEY, b INT NOT NULL); + CREATE TABLE t41(a PRIMARY KEY, b INT NOT NULL); CREATE INDEX t41ba ON t41(b,a); CREATE TABLE t42(id INTEGER PRIMARY KEY, x INT NOT NULL REFERENCES t41(a), y INT NOT NULL); CREATE UNIQUE INDEX t42xy ON t42(x,y); diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 3f8182fc4..52b85baa3 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -731,7 +731,7 @@ test:do_catchsql_test( [[ DROP TABLE t6; CREATE TABLE t4(a INT PRIMARY KEY); - CREATE TABLE t6(a REFERENCES t4(a) MATCH PARTIAL primary key); + CREATE TABLE t6(a INTEGER REFERENCES t4(a) MATCH PARTIAL primary key); ]], { -- <table-10.2> 0 @@ -742,7 +742,7 @@ test:do_catchsql_test( "table-10.3", [[ DROP TABLE t6; - CREATE TABLE t6(a REFERENCES t4 MATCH FULL ON DELETE SET NULL NOT NULL primary key); + CREATE TABLE t6(a INTEGER REFERENCES t4 MATCH FULL ON DELETE SET NULL NOT NULL primary key); ]], { -- <table-10.3> 0 @@ -753,7 +753,7 @@ test:do_catchsql_test( "table-10.4", [[ DROP TABLE t6; - CREATE TABLE t6(a REFERENCES t4 MATCH FULL ON UPDATE SET DEFAULT DEFAULT 1 primary key); + CREATE TABLE t6(a INT REFERENCES t4 MATCH FULL ON UPDATE SET DEFAULT DEFAULT 1 primary key); ]], { -- <table-10.4> 0 @@ -791,14 +791,16 @@ test:do_catchsql_test( ); ]], { -- <table-10.7> - 0 + 1, "Failed to create foreign key constraint 'fk_constraint_1_T6': foreign key refers to nonexistent field B" -- </table-10.7> }) test:do_catchsql_test( "table-10.8", [[ - DROP TABLE t6; + DROP TABLE IF EXISTS t6; + DROP TABLE IF EXISTS t4; + CREATE TABLE t4(x UNIQUE, y, PRIMARY KEY (x, y)); CREATE TABLE t6(a primary key,b,c, FOREIGN KEY (b,c) REFERENCES t4(x,y) MATCH PARTIAL ON UPDATE SET NULL ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED @@ -818,7 +820,7 @@ test:do_catchsql_test( ); ]], { -- <table-10.9> - 1, "number of columns in foreign key does not match the number of columns in the referenced table" + 1, "Failed to create foreign key constraint 'fk_constraint_1_T6': number of columns in foreign key does not match the number of columns in the primary index of referenced table" -- </table-10.9> }) @@ -833,7 +835,7 @@ test:do_test( ]] end, { -- <table-10.10> - 1, "number of columns in foreign key does not match the number of columns in the referenced table" + 1, "Failed to create foreign key constraint 'fk_constraint_1_T6': number of columns in foreign key does not match the number of columns in the primary index of referenced table" -- </table-10.10> }) @@ -846,7 +848,7 @@ test:do_test( ]] end, { -- <table-10.11> - 1, "foreign key on C should reference only one column of table t4" + 1, "Failed to create foreign key constraint 'fk_constraint_1_T6': number of columns in foreign key does not match the number of columns in the primary index of referenced table" -- </table-10.11> }) @@ -861,7 +863,7 @@ test:do_test( ]] end, { -- <table-10.12> - 1, [[unknown column "X" in foreign key definition]] + 1, [[Failed to create foreign key constraint 'fk_constraint_1_T6': unknown column X in foreign key definition]] -- </table-10.12> }) @@ -876,7 +878,7 @@ test:do_test( ]] end, { -- <table-10.13> - 1, [[unknown column "X" in foreign key definition]] + 1, [[Failed to create foreign key constraint 'fk_constraint_1_T6': unknown column X in foreign key definition]] -- </table-10.13> }) diff --git a/test/sql-tap/tkt-b1d3a2e531.test.lua b/test/sql-tap/tkt-b1d3a2e531.test.lua index e140cf82a..85b0f46d7 100755 --- a/test/sql-tap/tkt-b1d3a2e531.test.lua +++ b/test/sql-tap/tkt-b1d3a2e531.test.lua @@ -65,7 +65,7 @@ test:do_execsql_test( test:do_execsql_test( 2.1, [[ - CREATE TABLE pp(x PRIMARY KEY); + CREATE TABLE pp(x INTEGER PRIMARY KEY); CREATE TABLE cc( y INTEGER PRIMARY KEY REFERENCES pp DEFERRABLE INITIALLY DEFERRED ); @@ -83,7 +83,7 @@ test:do_execsql_test( test:do_execsql_test( 2.3, [[ - CREATE TABLE pp(x PRIMARY KEY); + CREATE TABLE pp(x INTEGER PRIMARY KEY); CREATE TABLE cc( y INTEGER PRIMARY KEY REFERENCES pp DEFERRABLE INITIALLY DEFERRED ); diff --git a/test/sql-tap/triggerC.test.lua b/test/sql-tap/triggerC.test.lua index e58072e2f..d1fc82842 100755 --- a/test/sql-tap/triggerC.test.lua +++ b/test/sql-tap/triggerC.test.lua @@ -1150,7 +1150,7 @@ test:do_execsql_test( PRAGMA foreign_keys='false'; PRAGMA recursive_triggers = 1; CREATE TABLE node( - id int not null primary key, + id not null primary key, pid int not null default 0 references node, key varchar not null, path varchar default '', diff --git a/test/sql-tap/whereG.test.lua b/test/sql-tap/whereG.test.lua index 13cef16c8..ded983975 100755 --- a/test/sql-tap/whereG.test.lua +++ b/test/sql-tap/whereG.test.lua @@ -23,11 +23,11 @@ test:do_execsql_test( "whereG-1.0", [[ CREATE TABLE composer( - cid INTEGER PRIMARY KEY, + cid PRIMARY KEY, cname TEXT ); CREATE TABLE album( - aid INTEGER PRIMARY KEY, + aid PRIMARY KEY, aname TEXT ); CREATE TABLE track( diff --git a/test/sql-tap/with1.test.lua b/test/sql-tap/with1.test.lua index 6db8d130c..c6a895875 100755 --- a/test/sql-tap/with1.test.lua +++ b/test/sql-tap/with1.test.lua @@ -397,7 +397,7 @@ test:do_catchsql_test("5.6.7", [[ -- test:do_execsql_test(6.1, [[ CREATE TABLE f( - id INTEGER PRIMARY KEY, parentid REFERENCES f, name TEXT + id PRIMARY KEY, parentid REFERENCES f, name TEXT ); INSERT INTO f VALUES(0, NULL, ''); diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result index c2ec429c3..f33b49a03 100644 --- a/test/sql/foreign-keys.result +++ b/test/sql/foreign-keys.result @@ -332,5 +332,29 @@ box.space.CHILD:drop() box.space.PARENT:drop() --- ... --- Clean-up SQL DD hash. -test_run:cmd('restart server default with cleanup=1') +-- Check that parser correctly handles MATCH, ON DELETE and +-- ON UPDATE clauses. +-- +box.sql.execute('CREATE TABLE tp (id INT PRIMARY KEY, a INT UNIQUE)') +--- +... +box.sql.execute('CREATE TABLE tc (id INT PRIMARY KEY, a INT REFERENCES tp(a) ON DELETE SET NULL MATCH FULL)') +--- +... +box.sql.execute('ALTER TABLE tc ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES tp(id) MATCH PARTIAL ON DELETE CASCADE ON UPDATE SET NULL') +--- +... +box.space._fk_constraint:select{} +--- +- - ['FK1', 518, 517, false, 'partial', 'cascade', 'set_null', [0], [0]] + - ['FK_CONSTRAINT_1_TC', 518, 517, false, 'full', 'set_null', 'no_action', [1], + [1]] +... +box.sql.execute('DROP TABLE tc') +--- +... +box.sql.execute('DROP TABLE tp') +--- +... +--- Clean-up SQL DD hash. +-test_run:cmd('restart server default with cleanup=1') diff --git a/test/sql/foreign-keys.test.lua b/test/sql/foreign-keys.test.lua index a7a242bc2..8d27aa00e 100644 --- a/test/sql/foreign-keys.test.lua +++ b/test/sql/foreign-keys.test.lua @@ -150,5 +150,15 @@ box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred'] box.space.CHILD:drop() box.space.PARENT:drop() --- Clean-up SQL DD hash. -test_run:cmd('restart server default with cleanup=1') +-- Check that parser correctly handles MATCH, ON DELETE and +-- ON UPDATE clauses. +-- +box.sql.execute('CREATE TABLE tp (id INT PRIMARY KEY, a INT UNIQUE)') +box.sql.execute('CREATE TABLE tc (id INT PRIMARY KEY, a INT REFERENCES tp(a) ON DELETE SET NULL MATCH FULL)') +box.sql.execute('ALTER TABLE tc ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES tp(id) MATCH PARTIAL ON DELETE CASCADE ON UPDATE SET NULL') +box.space._fk_constraint:select{} +box.sql.execute('DROP TABLE tc') +box.sql.execute('DROP TABLE tp') + +--- Clean-up SQL DD hash. +-test_run:cmd('restart server default with cleanup=1') -- 2.15.1
next prev parent reply other threads:[~2018-08-01 20:54 UTC|newest] Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top 2018-07-13 2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik 2018-07-13 2:04 ` [tarantool-patches] [PATCH 1/5] sql: prohibit creation of FK on unexisting tables Nikita Pettik 2018-07-17 21:05 ` [tarantool-patches] " Vladislav Shpilevoy 2018-07-25 10:03 ` n.pettik 2018-07-26 20:12 ` Vladislav Shpilevoy 2018-08-01 20:54 ` n.pettik 2018-08-02 22:15 ` Vladislav Shpilevoy 2018-08-06 0:27 ` n.pettik 2018-07-13 2:04 ` [tarantool-patches] [PATCH 2/5] schema: add new system space for FK constraints Nikita Pettik 2018-07-17 21:05 ` [tarantool-patches] " Vladislav Shpilevoy 2018-07-25 10:03 ` n.pettik 2018-07-26 20:12 ` Vladislav Shpilevoy 2018-08-01 20:54 ` n.pettik 2018-08-02 22:15 ` Vladislav Shpilevoy 2018-08-06 0:28 ` n.pettik 2018-08-06 18:24 ` Vladislav Shpilevoy 2018-07-13 2:04 ` [tarantool-patches] [PATCH 3/5] sql: introduce ADD CONSTRAINT statement Nikita Pettik 2018-07-17 21:05 ` [tarantool-patches] " Vladislav Shpilevoy 2018-07-25 10:03 ` n.pettik 2018-07-26 20:12 ` Vladislav Shpilevoy 2018-08-01 20:54 ` n.pettik [this message] 2018-08-02 22:15 ` Vladislav Shpilevoy 2018-08-06 0:28 ` n.pettik 2018-08-06 18:24 ` Vladislav Shpilevoy 2018-08-06 23:43 ` n.pettik 2018-07-13 2:04 ` [tarantool-patches] [PATCH 4/5] sql: display error on FK creation and drop failure Nikita Pettik 2018-07-17 21:04 ` [tarantool-patches] " Vladislav Shpilevoy 2018-07-25 10:03 ` n.pettik 2018-07-26 20:11 ` Vladislav Shpilevoy 2018-07-13 2:04 ` [tarantool-patches] [PATCH 5/5] sql: remove SQLITE_OMIT_FOREIGN_KEY define guard Nikita Pettik 2018-07-17 21:04 ` [tarantool-patches] Re: [PATCH 0/5] Move FK constraints to server Vladislav Shpilevoy 2018-08-07 14:57 ` Kirill Yukhin
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to=D9D14B81-D00C-43ED-A2DE-3866CA0F1542@tarantool.org \ --to=korablev@tarantool.org \ --cc=tarantool-patches@freelists.org \ --cc=v.shpilevoy@tarantool.org \ --subject='[tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement' \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox