[tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement

  • From: Vladislav Shpilevoy <v.shpilevoy@xxxxxxxxxxxxx>
  • To: tarantool-patches@xxxxxxxxxxxxx, Nikita Pettik <korablev@xxxxxxxxxxxxx>
  • Date: Wed, 18 Jul 2018 00:05:00 +0300

Thanks for the patch! See 32 comments below. Also see other
fixes on the branch in a separate commit.

1. Typos below: paren, refrencing.

On 13/07/2018 05:04, Nikita Pettik wrote:

After introducing separate space for persisting foreign key
constraints, nothing prevents us from adding ALTER TABLE statement to
add or drop named constraints. According to ANSI syntax is following:

ALTER TABLE <referencing table> ADD CONSTRAINT
   <referential constraint name> FOREIGN KEY
   <left parent> <referencing columns> <right paren> REFERENCES

2. Can you give an example what is <left/right parent>? Or maybe you meant
parentheses?

   <referenced table> [ <referenced columns> ] [ MATCH <match type> ]
   [ <referential triggered action> ] [ <constraint check time> ]

ALTER TABLE <referencing table> DROP CONSTRAINT <constrain name>

In our terms it looks like:

ALTER TABLE t1 ADD CONSTRAINT f1 FOREIGN KEY(id, a)
     REFERENCES t2 (id, b) MATCH FULL;
ALTER TABLE t1 DROP CONSTRAINT f1;

FK constraints which come with CREATE TABLE statement are also
persisted with auto-generated name. They are coded after space and its
indexes.

Moreover, we don't use original SQLite foreign keys anymore: those
obsolete structs have been removed alongside FK hash. Now FK constraints
are stored only in space.

Since types of refrencing and referenced fields must match, and now in
SQL only PK is allowed to feature INT (other fields are always SCALAR),
some tests have been corrected to obey this rule.

Part of #3271
---
  extra/mkkeywordhash.c                |   3 +
  src/box/fkey.h                       |   6 +
  src/box/sql.c                        |  16 +
  src/box/sql/alter.c                  |   7 -
  src/box/sql/build.c                  | 581 +++++++++++++++------
  src/box/sql/callback.c               |   2 -
  src/box/sql/delete.c                 |   6 +-
  src/box/sql/expr.c                   |  10 +-
  src/box/sql/fkey.c                   | 974 +++++++++++------------------------
  src/box/sql/insert.c                 |  19 +-
  src/box/sql/main.c                   |   5 -
  src/box/sql/parse.y                  |  37 +-
  src/box/sql/pragma.c                 | 240 +--------
  src/box/sql/pragma.h                 |   7 -
  src/box/sql/prepare.c                |   5 +
  src/box/sql/sqliteInt.h              | 167 +++---
  src/box/sql/status.c                 |   5 +-
  src/box/sql/tarantoolInt.h           |  12 +
  src/box/sql/update.c                 |   4 +-
  src/box/sql/vdbe.c                   |  16 +-
  test/sql-tap/alter.test.lua          |   4 +-
  test/sql-tap/alter2.test.lua         | 196 +++++++
  test/sql-tap/fkey1.test.lua          |  51 +-
  test/sql-tap/fkey2.test.lua          | 131 ++---
  test/sql-tap/fkey3.test.lua          |  19 +-
  test/sql-tap/fkey4.test.lua          |   2 +-
  test/sql-tap/orderby1.test.lua       |   6 +-
  test/sql-tap/table.test.lua          |  18 +-
  test/sql-tap/tkt-b1d3a2e531.test.lua |   6 +-
  test/sql-tap/triggerC.test.lua       |   2 +-
  test/sql-tap/whereG.test.lua         |   4 +-
  test/sql-tap/with1.test.lua          |   2 +-
  32 files changed, 1226 insertions(+), 1337 deletions(-)
  create mode 100755 test/sql-tap/alter2.test.lua

diff --git a/src/box/fkey.h b/src/box/fkey.h
index 1b6ea71d9..939773ef2 100644
--- a/src/box/fkey.h
+++ b/src/box/fkey.h
@@ -141,6 +141,12 @@ fkey_is_self_referenced(const struct fkey_def *fkey)
        return fkey->child_id == fkey->parent_id;
  }
+static inline bool
+space_fkey_check_references(const struct space *space)
+{
+       return space->parent_fkey != NULL;
+}

3. Are you sure that you need this one-line function for the
single place of usage? And in this place you can remove it and
nothing would change. See the code:

        if (space_fkey_check_references(space)) {
                for (struct fkey *fk = space->parent_fkey; fk != NULL;
                     fk = fk->fkey_parent_next) {

If here space->parent_key == NULL, then the cycle just won't start. It
is not?

(I have fixed this comment in my commit).

+
  /**
   * The second argument is a Trigger structure allocated by the
   * fkActionTrigger() routine.This function deletes the Trigger
diff --git a/src/box/sql/alter.c b/src/box/sql/alter.c
index fe54e5531..e81113f58 100644
--- a/src/box/sql/alter.c
+++ b/src/box/sql/alter.c
@@ -189,12 +188,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;
-       }

4. Why did you remove this?

        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 0c762fac9..c2d3cd035 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -373,9 +374,6 @@ deleteTable(sqlite3 * db, Table * pTable)
                freeIndex(db, pIndex);
        }
- /* Delete any foreign keys attached to this table. */
-       sqlite3FkDelete(db, pTable);

5. I still see sqlite3FkDelete in one comment. Please, remove. Maybe
the comment is obsolete.

-
        /* Delete the Table structure itself.
         */
        sqlite3HashClear(&pTable->idxHash);
@@ -1743,6 +1741,95 @@ emitNewSysSpaceSequenceRecord(Parse *pParse, int 
space_id, const char reg_seq_id
        return first_col;
  }
+/**
+ * Generate opcodes to serialize foreign key into MgsPack and
+ * insert produced tuple into _fk_constraint space.
+ *
+ * @param parse_context Parsing context.
+ * @param fk Foreign key to be created.
+ */
+static void
+vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)

6. How about vdbe_emit_fkey_create? As I remember, we have decided to use
_emit for functions generating opcodes.

+{
+       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, 9);
+       const 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_links_sz = fkey_encode_links(fk, NULL) + 1;
+       char *encoded_links = sqlite3DbMallocRaw(parse_context->db,
+                                                encoded_links_sz);
+       if (encoded_links == NULL) {
+               free((void *) name_copy);

7. name_copy is allocated on Db, but freed with libc. It is a path to
the dark side.

+               return;
+       }
+       size_t real_links_sz = fkey_encode_links(fk, encoded_links);
+       encoded_links[real_links_sz] = '\0';

8. Why do you need zero-termination? Encoded_links is MessagePack. It
can contain any number of zeros inside and can be non-terminated.

+       sqlite3VdbeAddOp4(vdbe, OP_Blob, real_links_sz, constr_tuple_reg + 7,
+                         SQL_SUBTYPE_MSGPACK, encoded_links, P4_DYNAMIC);
+       sqlite3VdbeAddOp3(vdbe, OP_MakeRecord, constr_tuple_reg, 8,
+                         constr_tuple_reg + 8);
+       sqlite3VdbeAddOp2(vdbe, OP_SInsert, BOX_FK_CONSTRAINT_ID,
+                         constr_tuple_reg + 8);
+       sqlite3VdbeChangeP5(vdbe, OPFLAG_NCHANGE);
+       sqlite3ReleaseTempRange(parse_context, constr_tuple_reg, 9);
+}
+
+static int
+resolve_link(struct Parse *parse_context, const struct space_def *def,
+            const char *field_name, uint32_t *link)
+{
+       assert(link != NULL);
+       uint32_t j;
+       for (j = 0; j < def->field_count; ++j) {
+               if (strcmp(field_name, def->fields[j].name) == 0) {
+                       *link = j;
+                       break;
+               }
+       }
+       if (j == def->field_count) {
+               sqlite3ErrorMsg(parse_context, "no such column %s", field_name);
+               return -1;
+       }
+       return 0;
+}

9. How about create tuple_dictionary on CREATE TABLE in table->def and use
its method tuple_fieldno_by_name?

+
  /*
   * This routine is called to report the final ")" that terminates
   * a CREATE TABLE statement.
@@ -1913,6 +2000,39 @@ 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 = pParse->new_fkey;
+               while (fk_parse != NULL) {
+                       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) {

10. Why do you iterate for fk->field_count, but access cols->a? Is it
guaranteed that fk->field_count == cols->nExpr?

+                                       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->nColumn != fk->field_count) {
+                                       sqlite3ErrorMsg(pParse,
+                                                       "number of columns in 
foreign key does "
+                                                       "not match the number of 
columns in "
+                                                       "the referenced table");

11. ER_CREATE_FK_CONSTRAINT? Or ER_CREATE_SPACE.

+                                       return;
+                               }
+                               for (uint32_t i = 0; i < fk->field_count; ++i) {
+                                       fk->links[i].parent_field =
+                                               pk->aiColumn[i];
+                               }
+                               fk->parent_id = iSpaceId;
+                       }
+                       fk->child_id = iSpaceId;
+                       vdbe_fkey_code_creation(pParse, fk);
+                       fk_parse = fk_parse->next;

12. You can use stailq/rlist to link fkey_parse objects and use
here (not only here) rlist_foreach_entry.

+               }
        }
/* Add the table to the in-memory representation of the database.
@@ -2085,6 +2205,32 @@ sql_clear_stat_spaces(Parse *parse, const char 
*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_fkey_code_drop(struct Parse *parse_context, const char *constraint_name,
+                   uint32_t child_id)

13. vdbe_emit_fkey_drop?

+{
+       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,
@@ -2270,176 +2429,276 @@ 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.
   *
- * 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)
  {
-       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 *normilized_name = strndup(pTo->z, pTo->n);
-       if (normilized_name == NULL) {
-               diag_set(OutOfMemory, pTo->n, "strndup", "normalized name");
-               goto fk_end;
-       }
-       sqlite3NormalizeName(normilized_name);
-       uint32_t parent_id = box_space_id_by_name(normilized_name,
-                                                 strlen(normilized_name));
-       if (parent_id == BOX_ID_NIL &&
-           strcmp(normilized_name, p->def->name) != 0) {
-               sqlite3ErrorMsg(pParse, "foreign key constraint references "\
-                               "nonexistent table: %s", normilized_name);
-               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", normilized_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) {
+               sqlite3ErrorMsg(parse_context,
+                               "table \"%s\" doesn't feature column %s",
+                               space->def->name, column_name);
+               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;
-               }
-               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;
+       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;

14. How is it possible? That sql_create_foreign_key is called twice. I
see that it is called from the parser only. But when I removed it, I got
a lot of errors.

+       /*
+        * 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) {
+               if (is_alter) {
+                       sqlite3ErrorMsg(parse_context,
+                                       "referencing columns are not 
specified");
+                       goto exit_create_fk;

15. No test. Can not grep this message anywhere.

+               }
+               child_cols_count = 1;
        } else {
-               nCol = pFromCol->nExpr;
-       }
-       nByte = sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) +
-               strlen(normilized_name) + 1;
-       if (pToCol) {
-               for (i = 0; i < pToCol->nExpr; i++) {
-                       nByte += sqlite3Strlen30(pToCol->a[i].zName) + 1;
-               }
-       }
-       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, normilized_name, strlen(normilized_name) + 1);
-       z += strlen(normilized_name) + 1;
-       pFKey->nCol = nCol;
-       if (pFromCol == 0) {
-               pFKey->aCol[0].iFrom = p->def->field_count - 1;
+               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;
+               }
+               child_space = space_by_id(child_id);
+               assert(child_space != NULL);
        } 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;
-                       }
+               struct fkey_parse *fk = region_alloc(&parse_context->region,
+                                                    sizeof(*fk));
+               if (fk == NULL) {
+                       diag_set(OutOfMemory, sizeof(*fk), "region",
+                                "struct fkey_parse");
+                       parse_context->rc = SQL_TARANTOOL_ERROR;
+                       parse_context->nErr++;
+                       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;
+               memset(fk, 0, sizeof(*fk));
+               struct fkey_parse *last_fk = parse_context->new_fkey;
+               parse_context->new_fkey = fk;
+               fk->next = last_fk;
+       }
+       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 ? false :
+                            !strcmp(parent_name, new_tab->def->name);
+       if (parent_id == BOX_ID_NIL) {
+               if (is_self_referenced) {
+                       parse_context->new_fkey->selfref_cols = parent_cols;
+                       parse_context->new_fkey->is_self_referenced = true;
+               } else {
+                       diag_set(ClientError, ER_NO_SUCH_SPACE, parent_name);;
+                       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;
-       }
-       if (pNextTo) {
-               assert(pNextTo->pPrevTo == 0);
-               pFKey->pNextTo = pNextTo;
-               pNextTo->pPrevTo = pFKey;
+       struct space *parent_space = space_by_id(parent_id);
+       if (parent_space != NULL && parent_space->def->opts.is_view) {
+               sqlite3ErrorMsg(parse_context,
+                               "referenced table can't be view");
+               goto exit_create_fk;
+       }
+       if (parent_cols != NULL) {
+               if (parent_cols->nExpr != (int) child_cols_count) {
+                       sqlite3ErrorMsg(parse_context,
+                                       "number of columns in foreign key does "
+                                       "not match the number of columns in "
+                                       "the referenced table");

16. This message appears 3 times. I think, it is worth to create a
separate error code. Or at least remember this string somewhere in a
variable and use it with ER_CREATE_FK_CONSTRAINT.

Or add a separate label with this error and do goto when occurs.

+                       goto exit_create_fk;
+               }
+       } 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) {
+                       sqlite3ErrorMsg(parse_context,
+                                       "number of columns in foreign key does "
+                                       "not match the number of columns in "
+                                       "the referenced table");
+                       goto exit_create_fk;
+               }
        }
-
-       /* Link the foreign key to the table as the last step.
+       if (constraint == NULL && !is_alter) {
+               if (parse_context->constraintName.n == 0) {
+                       uint32_t fk_count = 0;
+                       for (struct fkey_parse *fk = parse_context->new_fkey;
+                            fk != NULL; fk = fk->next, fk_count++);

17. How about store fk count in fkey_parse?

diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 3183e3dc7..ad19759e2 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -4835,12 +4835,12 @@ sqlite3ExprIfFalse(Parse * pParse, Expr * pExpr, int 
dest, int jumpIfNull)
         * Assert()s verify that the computation is correct.
         */
- op = ((pExpr->op + (TK_ISNULL & 1)) ^ 1) - (TK_ISNULL & 1);
+       if (pExpr->op >= TK_NE && pExpr->op <= TK_GE)

18. Why from NE to GE? In the table above I see the range [NE, LT],
that includes [NE, GE]. Why this hunk is needed? I know about
dependecy of opcode and token values, but why your patch breaks it?
Can you add words in such way that they will not break parity?

+               op = ((pExpr->op + (TK_NE & 1)) ^ 1) - (TK_NE & 1);
+       if (pExpr->op == TK_ISNULL || pExpr->op == TK_NOTNULL)
+               op = ((pExpr->op + (TK_ISNULL & 1)) ^ 1) - (TK_ISNULL & 1);
- /*
-        * Verify correct alignment of TK_ and OP_ constants.
-        * Tokens TK_ISNULL and TK_NE shoud have the same parity.
-        */
+       /* Verify correct alignment of TK_ and OP_ constants. */
        assert(pExpr->op != TK_NE || op == OP_Eq);
        assert(pExpr->op != TK_EQ || op == OP_Ne);
        assert(pExpr->op != TK_LT || op == OP_Ge);
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 016ded8d0..1eebf6b10 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -39,8 +39,9 @@
  #include "box/schema.h"
  #include "box/session.h"
  #include "tarantoolInt.h"
+#include "vdbeInt.h"
-#ifndef SQLITE_OMIT_FOREIGN_KEY
+#ifndef SQLITE_OMIT_TRIGGER

19. Why?

@@ -366,150 +187,116 @@ sqlite3FkLocateIndex(Parse * pParse,    /* Parse 
context to store any error in */
   *
   *   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".
+ * These operations are identified in the comment at the top of
+ * this file (fkey.c) as "I.1" and "D.1".
+ *
+ * @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.
+ * @param is_ignore If true, pretend parent contains all NULLs.
   */
  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 */
-    )
+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, bool is_ignore)
  {
-       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.
+       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.
         *
-        * 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.
+        * 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 (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);
-                       }
+       if (incr_count < 0)
+               sqlite3VdbeAddOp2(v, OP_FkIfZero, fk_def->is_deferred,
+                                 ok_label);
- } else {
-                       int nCol = pFKey->nCol;
-                       int regTemp = sqlite3GetTempRange(pParse, nCol);
-                       int regRec = sqlite3GetTempReg(pParse);
-                       struct space *space =
-                               
space_by_id(SQLITE_PAGENO_TO_SPACEID(pIdx->tnum));
-                       vdbe_emit_open_cursor(pParse, iCur, pIdx->tnum, 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;
-                               for (i = 0; i < nCol; i++) {
-                                       int iChild = aiCol[i] + 1 + regData;
-                                       int iParent =
-                                           pIdx->aiColumn[i] + 1 + regData;
-                                       assert(pIdx->aiColumn[i] >= 0);
-                                       assert(aiCol[i] != pTab->iPKey);
-                                       if (pIdx->aiColumn[i] == pTab->iPKey) {
-                                               /* The parent key is a 
composite key that includes the IPK column */
-                                               iParent = regData;
-                                       }
-                                       sqlite3VdbeAddOp3(v, OP_Ne, iChild,
-                                                         iJump, iParent);
-                                       VdbeCoverage(v);
-                                       sqlite3VdbeChangeP5(v,
-                                                           SQLITE_JUMPIFNULL);
-                               }
-                               sqlite3VdbeGoto(v, iOk);
+       for (uint32_t i = 0; i < fk_def->field_count; i++) {
+               int iReg = fk_def->links[i].child_field + reg_data + 1;
+               sqlite3VdbeAddOp2(v, OP_IsNull, iReg, ok_label);
+       }
+       if (is_ignore == 0) {
+               uint32_t field_count = fk_def->field_count;
+               int temp_regs = sqlite3GetTempRange(parse_context, field_count);
+               int rec_reg = sqlite3GetTempReg(parse_context);
+               uint32_t id =
+                       
SQLITE_PAGENO_FROM_SPACEID_AND_INDEXID(fk_def->parent_id,
+                                                              referenced_idx);

20. Vdbe_emit_open_cursor takes exactly index id, not pageno, on 2.0, so please,
rebase on the latest version.

+               vdbe_emit_open_cursor(parse_context, cursor, id, parent);
+               for (uint32_t i = 0; i < field_count; ++i) {
+                       sqlite3VdbeAddOp2(v, OP_Copy,
+                                         fk_def->links[i].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.
+                *
+                * 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 (parent->def->id == fk_def->child_id && incr_count == 1) {

21. What about fkey_is_self_referenced(fk_def)? Is it the same?

+                       int jump = sqlite3VdbeCurrentAddr(v) + field_count + 1;
+                       for (uint32_t i = 0; i < field_count; i++) {
+                               int child_col = fk_def->links[i].child_field +
+                                               1 + reg_data;
+                               int parent_col = fk_def->links[i].parent_field +
+                                                1 + reg_data;
+                               sqlite3VdbeAddOp3(v, OP_Ne, child_col, jump,
+                                                 parent_col);
+                               sqlite3VdbeChangeP5(v, SQLITE_JUMPIFNULL);
                        }
-
-                       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);
+                       sqlite3VdbeGoto(v, ok_label);
                }
+               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(v->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);
        }
-
-       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
+       struct session *user_session = current_session();
+       if (!fk_def->is_deferred &&
+           !(user_session->sql_flags & SQLITE_DeferFKs) &&

22. Why do we check session flags here? They are runtime and I
can change DeferFKs after parsing but before executing. DeferFKs is
checked both on runtime and on parsing for unknown reason.

This is not a single place of this strange thing.

@@ -844,59 +586,31 @@ sqlite3FkCheck(Parse * pParse,    /* Parse context */
        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
+        * pTab 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 i;
+       struct space *space = space_by_id(pTab->def->id);
+       assert(space != NULL);
+       for (struct fkey *fk = space->child_fkey; fk != NULL;
+            fk = fk->fkey_child_next) {
+               struct fkey_def *fk_def = fk->def;
                int bIgnore = 0;
-
-               if (aChange
-                   && sqlite3_stricmp(pTab->def->name, pFKey->zTo) != 0
-                   && fkChildIsModified(pFKey, aChange) == 0) {
+               if (aChange != NULL && space->def->id != fk_def->parent_id &&

23. fkey_is_self_referenced?

+                   !fkey_child_is_modified(fk_def, aChange))
                        continue;
-               }
-
@@ -977,100 +686,74 @@ sqlite3FkCheck(Parse * pParse,   /* Parse context */
                                 * might be set incorrectly if any OP_FkCounter 
related scans are
                                 * omitted.
                                 */
-                               if (!pFKey->isDeferred && eAction != OE_Cascade
-                                   && eAction != OE_SetNull) {
+                               if (!fk_def->is_deferred &&
+                                   action != FKEY_ACTION_CASCADE &&
+                                   action != FKEY_ACTION_SET_NULL) {
                                        sqlite3MayAbort(pParse);
                                }
                        }
-                       pItem->zName = 0;
                        sqlite3SrcListDelete(db, pSrc);
                }
-               sqlite3DbFree(db, aiCol);
        }
  }
#define COLUMN_MASK(x) (((x)>31) ? 0xffffffff : ((u32)1<<(x)))

24. Lets use 64 bitmask and utilities from column_mask.h.

-/*
- * 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.

  {
-       u32 mask = 0;
+       uint32_t mask = 0;
        struct session *user_session = current_session();
-
        if (user_session->sql_flags & SQLITE_ForeignKeys) {
-               FKey *p;
-               int i;
-               for (p = pTab->pFKey; p; p = p->pNextFrom) {
-                       for (i = 0; i < p->nCol; i++)
-                               mask |= COLUMN_MASK(p->aCol[i].iFrom);
+               struct space *space = space_by_id(space_id);
+               for (struct fkey *fk = space->child_fkey; fk != NULL;
+                    fk = fk->fkey_child_next) {
+                       struct fkey_def *def = fk->def;
+                       for (uint32_t i = 0; i < def->field_count; ++i)
+                               mask |=COLUMN_MASK(def->links[i].child_field);
                }
-               for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) {
-                       Index *pIdx = 0;
-                       sqlite3FkLocateIndex(pParse, pTab, p, &pIdx, 0);
-                       if (pIdx) {
-                               int nIdxCol = index_column_count(pIdx);
-                               for (i = 0; i < nIdxCol; i++) {
-                                       assert(pIdx->aiColumn[i] >= 0);
-                                       mask |= COLUMN_MASK(pIdx->aiColumn[i]);
-                               }
-                       }
+               for (struct fkey *fk = space->parent_fkey; fk != NULL;
+                    fk = fk->fkey_parent_next) {
+                       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;
  }
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index 00dc7a631..618cdc420 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);

26. Why?

/* 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 b2940b7c4..1b84dbcaa 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -300,19 +301,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; }

27. Why exactly 16? Why can not remain 0, or be << 2, or << 4?

  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; }
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 5c5369aeb..2489b31b2 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h> @@ -4280,8 +4271,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.

28. 'CREATE and ALTER', it is not?

+ *
+ * @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);
+
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
@@ -17,10 +17,10 @@ test:do_execsql_test(
      "fkey1-1.2",
      [[
          CREATE TABLE t1(
-            a INTEGER PRIMARY KEY,
+            a PRIMARY KEY,

29. Why not INTEGER? As I know, we are going to forbid type omitting.
Same for other tests (for example: fkey3.test.lua, orderby1.test.lua).

              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);
      ]], {
diff --git a/test/sql-tap/fkey4.test.lua b/test/sql-tap/fkey4.test.lua
index 9415b62cb..9810ce22f 100755
--- a/test/sql-tap/fkey4.test.lua
+++ b/test/sql-tap/fkey4.test.lua
@@ -186,7 +186,7 @@ test:do_execsql_test(
          DROP TABLE IF EXISTS c1;
          DROP TABLE IF EXISTS p1;
          CREATE TABLE p1(a PRIMARY KEY, b);
-        CREATE TABLE c1(x PRIMARY KEY REFERENCES p1 DEFERRABLE INITIALLY 
DEFERRED);
+        CREATE TABLE c1(x PRIMARY KEY REFERENCES p1);

30. Why?

          INSERT INTO p1 VALUES (1, 'one');
          INSERT INTO p1 VALUES (2, 'two');
          INSERT INTO c1 VALUES (1);
diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua
index 6aa290742..24f494852 100755
--- a/test/sql-tap/table.test.lua
+++ b/test/sql-tap/table.test.lua
@@ -791,14 +791,16 @@ test:do_catchsql_test(
          );
      ]], {
          -- <table-10.7>
-        0
+        1, "table \"T4\" doesn't feature column B"
          -- </table-10.7>
      })
test:do_catchsql_test(
      "table-10.8",
      [[
-        DROP TABLE t6;
+        DROP TABLE IF EXISTS t6;
+       DROP TABLE IF EXISTS t4;

31. Indentation.

+        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
@@ -861,7 +863,7 @@ test:do_test(
          ]]
      end, {
          -- <table-10.12>
-        1, [[unknown column "X" in foreign key definition]]
+        1, [[no such column X]]

32. Can you keep the old message?

          -- </table-10.12>
      })


Other related posts: