Tarantool development patches archive
 help / color / mirror / Atom feed
* [tarantool-patches] [PATCH 0/5] Move FK constraints to server
@ 2018-07-13  2:04 Nikita Pettik
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 1/5] sql: prohibit creation of FK on unexisting tables Nikita Pettik
                   ` (6 more replies)
  0 siblings, 7 replies; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

Branch: https://github.com/tarantool/tarantool/commits/np/move-fk-to-server
Issue: https://github.com/tarantool/tarantool/issues/3271

The aim of this patch-set is to move foreign key constraint to server
and them be closer to ANSI specification.

First patch is preliminary and enables additional restrictions
for FK constraints (ban opportunity to drop space referenced space,
create FK referencing VIEW etc).

In original SQLite FK constraints may appear only during CREATE TABLE
statement. Thus, it was enough to hold string of CREATE TABLE statement
and reparse it once on instance loading. This approach defers all
resolutions until FK usage. For instance:

CREATE TABLE t1(id PRIMARY KEY REFERENCES t2);
CREATE TABLE t2(id PRIMARY KEY);

We decided to use another approach - where FK constraints are always consistent
and all DD links are kept up. For instance, if we attempted to satisfy all
restrictions using SQLite schema - we wouldn't be able to create circular
dependencies. To support circular dependecies, we must allow to create them
after space itself. In turn, to create FK constraints outside CREATE STATEMENT,
we must persist them.  To implement these steps, firstly _fk_constraint system
space is added - it contains tuples describing FK. Then, separate SQL statement
<ALTER TABLE name ADD CONSTRAINT ...> is introduced which processes insertion
and deletion from this space. Finally, FK processing has been refactored to
rely on new DD in server (struct fkey and struct fkey_def). It seems that
perfomance of FK handling has become a little bit better: now we don't need
find suitable index on each FK invocation - its id is held into FK struct
itself.

The last patch is simple follow-up which removes obsolete define guard
for FK constraints.

Nikita Pettik (5):
  sql: prohibit creation of FK on unexisting tables
  schema: add new system space for FK constraints
  sql: introduce ADD CONSTRAINT statement
  sql: display error on FK creation and drop failure
  sql: remove SQLITE_OMIT_FOREIGN_KEY define guard

 extra/mkkeywordhash.c                |    9 +-
 src/box/CMakeLists.txt               |    1 +
 src/box/alter.cc                     |  432 +++++++++++++-
 src/box/alter.h                      |    1 +
 src/box/bootstrap.snap               |  Bin 1704 -> 1798 bytes
 src/box/errcode.h                    |    6 +
 src/box/fkey.c                       |   69 +++
 src/box/fkey.h                       |  169 ++++++
 src/box/lua/schema.lua               |    6 +
 src/box/lua/space.cc                 |    2 +
 src/box/lua/upgrade.lua              |   16 +
 src/box/schema.cc                    |   16 +
 src/box/schema_def.h                 |   14 +
 src/box/space.c                      |    2 +
 src/box/space.h                      |    3 +
 src/box/sql.c                        |   24 +
 src/box/sql/alter.c                  |    7 -
 src/box/sql/build.c                  |  601 ++++++++++++++-----
 src/box/sql/callback.c               |    2 -
 src/box/sql/delete.c                 |    6 +-
 src/box/sql/expr.c                   |   10 +-
 src/box/sql/fkey.c                   | 1077 ++++++++++------------------------
 src/box/sql/insert.c                 |   21 +-
 src/box/sql/main.c                   |    5 -
 src/box/sql/parse.y                  |   37 +-
 src/box/sql/pragma.c                 |  242 +-------
 src/box/sql/pragma.h                 |   13 -
 src/box/sql/prepare.c                |    5 +
 src/box/sql/sqliteInt.h              |  185 +++---
 src/box/sql/status.c                 |    5 +-
 src/box/sql/tarantoolInt.h           |   13 +
 src/box/sql/trigger.c                |   24 +-
 src/box/sql/update.c                 |    4 +-
 src/box/sql/vdbe.c                   |   51 +-
 src/box/sql/vdbeInt.h                |    4 -
 src/box/sql/vdbeaux.c                |    4 -
 test/box/access_misc.result          |    5 +
 test/box/access_sysview.result       |    6 +-
 test/box/alter.result                |    5 +-
 test/box/misc.result                 |    4 +
 test/engine/iterator.result          |    2 +-
 test/sql-tap/alter.test.lua          |    2 +-
 test/sql-tap/alter2.test.lua         |  219 +++++++
 test/sql-tap/fkey1.test.lua          |   65 +-
 test/sql-tap/fkey2.test.lua          |  148 ++---
 test/sql-tap/fkey3.test.lua          |   19 +-
 test/sql-tap/fkey4.test.lua          |    2 +-
 test/sql-tap/orderby1.test.lua       |    6 +-
 test/sql-tap/suite.ini               |    1 +
 test/sql-tap/table.test.lua          |   19 +-
 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 +-
 test/sql/foreign-keys.result         |  316 ++++++++++
 test/sql/foreign-keys.test.lua       |  144 +++++
 56 files changed, 2534 insertions(+), 1529 deletions(-)
 create mode 100644 src/box/fkey.c
 create mode 100644 src/box/fkey.h
 create mode 100755 test/sql-tap/alter2.test.lua
 create mode 100644 test/sql/foreign-keys.result
 create mode 100644 test/sql/foreign-keys.test.lua

-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-07-13  2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik
@ 2018-07-13  2:04 ` Nikita Pettik
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 2/5] schema: add new system space for FK constraints Nikita Pettik
                   ` (5 subsequent siblings)
  6 siblings, 1 reply; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

Originally, SQLite allows to create table with foreign keys contraint
which refers to yet not created parent table. For instance:

CREATE TABLE child(id INT PRIMARY KEY REFERENCES parent);
CREATE TABLE parent(id INT PRIMARY KEY);

This patch bans such ability since it contradicts SQL ANSI.
Moreover, SQLite allows to drop parent table if deletion of all rows
wouldn't result in FK contraint violations. This feature has been
removed since in such situation child table would become inconsistent.

Finally, within current patch ability to create FK contraints on VIEWs
is banned as well.

Part of #3271
---
 src/box/sql/build.c                  | 41 ++++++++++++----
 src/box/sql/fkey.c                   | 95 +++---------------------------------
 src/box/sql/sqliteInt.h              |  1 -
 src/box/sql/vdbe.c                   | 30 ------------
 test/sql-tap/alter.test.lua          |  6 +--
 test/sql-tap/fkey1.test.lua          | 18 +++----
 test/sql-tap/fkey2.test.lua          | 25 ++++------
 test/sql-tap/fkey3.test.lua          |  4 +-
 test/sql-tap/suite.ini               |  1 +
 test/sql-tap/table.test.lua          |  1 +
 test/sql-tap/tkt-b1d3a2e531.test.lua |  2 +-
 11 files changed, 67 insertions(+), 157 deletions(-)

diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 0072f842e..0c762fac9 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -2256,10 +2256,14 @@ 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.
 	 */
-
-	sql_clear_stat_spaces(parse_context, space_name, NULL);
 	struct Table *tab = sqlite3HashFind(&db->pSchema->tblHash, space_name);
-	sqlite3FkDropTable(parse_context, table_name_list, tab);
+	struct FKey *fk = sqlite3FkReferences(tab);
+	if (fk != NULL && strcmp(fk->pFrom->def->name, tab->def->name) != 0) {
+		sqlite3ErrorMsg(parse_context, "can't drop parent table %s when "
+				"child table refers to it", space_name);
+		goto exit_drop_table;
+	}
+	sql_clear_stat_spaces(parse_context, space_name, NULL);
 	sql_code_drop_table(parse_context, space, is_view);
 
  exit_drop_table:
@@ -2301,6 +2305,26 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
 	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;
+	}
 	if (p == 0)
 		goto fk_end;
 	if (pFromCol == 0) {
@@ -2322,8 +2346,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
 	} else {
 		nCol = pFromCol->nExpr;
 	}
-	nByte =
-	    sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) + pTo->n + 1;
+	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;
@@ -2337,10 +2361,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
 	pFKey->pNextFrom = p->pFKey;
 	z = (char *)&pFKey->aCol[nCol];
 	pFKey->zTo = z;
-	memcpy(z, pTo->z, pTo->n);
-	z[pTo->n] = 0;
-	sqlite3NormalizeName(z);
-	z += pTo->n + 1;
+	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;
@@ -2394,6 +2416,7 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
 
  fk_end:
 	sqlite3DbFree(db, pFKey);
+	free(normilized_name);
 #endif				/* !defined(SQLITE_OMIT_FOREIGN_KEY) */
 	sql_expr_list_delete(db, pFromCol);
 	sql_expr_list_delete(db, pToCol);
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 6c75c4772..be080324f 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -330,13 +330,10 @@ sqlite3FkLocateIndex(Parse * pParse,	/* Parse context to store any error in */
 			}
 		}
 	}
-
 	if (!pIdx) {
-		if (!pParse->disableTriggers) {
-			sqlite3ErrorMsg(pParse,
-					"foreign key mismatch - \"%w\" referencing \"%w\"",
-					pFKey->pFrom->def->name, pFKey->zTo);
-		}
+		sqlite3ErrorMsg(pParse, "foreign key mismatch - "\
+					"\"%w\" referencing \"%w\"",
+				pFKey->pFrom->def->name, pFKey->zTo);
 		sqlite3DbFree(pParse->db, aiCol);
 		return 1;
 	}
@@ -754,46 +751,6 @@ sql_fk_trigger_delete(struct sqlite3 *db, struct sql_trigger *trigger)
 	sqlite3DbFree(db, trigger);
 }
 
-/**
- * This function is called to generate code that runs when table
- * pTab is being dropped from the database. The SrcList passed as
- * the second argument to this function contains a single entry
- * guaranteed to resolve to table pTab.
- *
- * Normally, no code is required. However, if the table is
- * parent table of a FK constraint, then the equivalent
- * of "DELETE FROM <tbl>" is executed in a single transaction
- * before dropping the table from the database. If any FK
- * violations occur, rollback transaction and halt VDBE. Triggers
- * are disabled while running this DELETE, but foreign key
- * actions are not.
- */
-void
-sqlite3FkDropTable(Parse *parser, SrcList *name, Table *table)
-{
-	struct session *user_session = current_session();
-	if ((user_session->sql_flags & SQLITE_ForeignKeys) == 0 ||
-	    table->def->opts.is_view || sqlite3FkReferences(table) == NULL)
-		return;
-	struct Vdbe *v = sqlite3GetVdbe(parser);
-	assert(v != NULL);
-	parser->disableTriggers = 1;
-	/* Staring new transaction before DELETE FROM <tbl> */
-	sqlite3VdbeAddOp0(v, OP_TTransaction);
-	sql_table_delete_from(parser, sqlite3SrcListDup(parser->db, name, 0),
-			      NULL);
-	parser->disableTriggers = 0;
-	/*
-	 * If the DELETE has generated immediate foreign key
-	 * constraint violations, rollback, halt the VDBE and
-	 * return an error at this point, before any modifications
-	 * of the _space and _index spaces. This is because these
-	 * spaces don't support multistatement transactions.
-	 * Otherwise, just commit changes.
-	 */
-	sqlite3VdbeAddOp0(v, OP_FkCheckCommit);
-}
-
 /*
  * The second argument points to an FKey object representing a foreign key
  * for which pTab is the child table. An UPDATE statement against pTab
@@ -902,7 +859,6 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 {
 	sqlite3 *db = pParse->db;	/* Database handle */
 	FKey *pFKey;		/* Used to iterate through FKs */
-	int isIgnoreErrors = pParse->disableTriggers;
 	struct session *user_session = current_session();
 
 	/* Exactly one of regOld and regNew should be non-zero. */
@@ -935,42 +891,10 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 		 * schema items cannot be located, set an error in pParse and return
 		 * early.
 		 */
-		if (pParse->disableTriggers) {
-			pTo = sqlite3HashFind(&db->pSchema->tblHash,
-					      pFKey->zTo);
-		} else {
-			pTo = sqlite3LocateTable(pParse, 0, pFKey->zTo);
-		}
-		if (!pTo
-		    || sqlite3FkLocateIndex(pParse, pTo, pFKey, &pIdx,
-					    &aiFree)) {
-			assert(isIgnoreErrors == 0
-			       || (regOld != 0 && regNew == 0));
-			if (!isIgnoreErrors || db->mallocFailed)
+		pTo = sqlite3LocateTable(pParse, 0, pFKey->zTo);
+		if (!pTo || sqlite3FkLocateIndex(pParse, pTo, pFKey, &pIdx,
+					    &aiFree))
 				return;
-			if (pTo == 0) {
-				/* If isIgnoreErrors is true, then a table is being dropped. In this
-				 * case SQLite runs a "DELETE FROM xxx" on the table being dropped
-				 * before actually dropping it in order to check FK constraints.
-				 * If the parent table of an FK constraint on the current table is
-				 * missing, behave as if it is empty. i.e. decrement the relevant
-				 * FK counter for each row of the current table with non-NULL keys.
-				 */
-				Vdbe *v = sqlite3GetVdbe(pParse);
-				int iJump =
-				    sqlite3VdbeCurrentAddr(v) + pFKey->nCol + 1;
-				for (i = 0; i < pFKey->nCol; i++) {
-					int iReg =
-					    pFKey->aCol[i].iFrom + regOld + 1;
-					sqlite3VdbeAddOp2(v, OP_IsNull, iReg,
-							  iJump);
-					VdbeCoverage(v);
-				}
-				sqlite3VdbeAddOp2(v, OP_FkCounter,
-						  pFKey->isDeferred, -1);
-			}
-			continue;
-		}
 		assert(pFKey->nCol == 1 || (aiFree && pIdx));
 
 		if (aiFree) {
@@ -1036,11 +960,8 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 			continue;
 		}
 
-		if (sqlite3FkLocateIndex(pParse, pTab, pFKey, &pIdx, &aiCol)) {
-			if (!isIgnoreErrors || db->mallocFailed)
-				return;
-			continue;
-		}
+		if (sqlite3FkLocateIndex(pParse, pTab, pFKey, &pIdx, &aiCol))
+			return;
 		assert(aiCol || pFKey->nCol == 1);
 
 		/* Create a SrcList structure containing the child table.  We need the
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 8b75ae888..5c5369aeb 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -2914,7 +2914,6 @@ struct Parse {
 	u32 newmask;		/* Mask of new.* columns referenced */
 	u8 eTriggerOp;		/* TK_UPDATE, TK_INSERT or TK_DELETE */
 	u8 eOrconf;		/* Default ON CONFLICT policy for trigger steps */
-	u8 disableTriggers;	/* True to disable triggers */
 	/** Region to make SQL temp allocations. */
 	struct region region;
 
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 7a4d37602..2c6bd2ba8 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -2959,36 +2959,6 @@ case OP_Savepoint: {
 	break;
 }
 
-/* Opcode: FkCheckCommit * * * * *
- *
- * This opcode is used and required by DROP TABLE statement,
- * since deleted rows should be rollbacked in case of foreign keys
- * constraint violations. In case of rollback, instruction
- * also causes the VM to halt, because it makes no sense to continue
- * execution with FK violations. If there is no FK violations, then
- * just commit changes - deleted rows.
- *
- * Do not use this instruction in any statement implementation
- * except for DROP TABLE!
- */
-case OP_FkCheckCommit: {
-	if (!box_txn()) {
-		sqlite3VdbeError(p, "cannot commit or rollback - " \
-			"no transaction is active");
-		rc = SQLITE_ERROR;
-		goto abort_due_to_error;
-	}
-	if ((rc = sqlite3VdbeCheckFk(p, 0) != SQLITE_OK)) {
-		box_txn_rollback();
-		sqlite3VdbeHalt(p);
-		goto vdbe_return;
-	} else {
-		rc = box_txn_commit() == 0 ? SQLITE_OK : SQL_TARANTOOL_ERROR;
-		if (rc) goto abort_due_to_error;
-	}
-	break;
-}
-
 /* Opcode: CheckViewReferences P1 * * * *
  * Synopsis: r[P1] = space id
  *
diff --git a/test/sql-tap/alter.test.lua b/test/sql-tap/alter.test.lua
index cfe280121..3e5c6102b 100755
--- a/test/sql-tap/alter.test.lua
+++ b/test/sql-tap/alter.test.lua
@@ -313,9 +313,9 @@ test:do_execsql_test(
         DROP TABLE IF EXISTS t1;
         DROP TABLE IF EXISTS t2;
         DROP TABLE IF EXISTS t3;
-        CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));
-        CREATE TABLE t2(id PRIMARY KEY);
-        CREATE TABLE t3(id PRIMARY KEY);
+        CREATE TABLE t2(id INT PRIMARY KEY);
+	CREAte TABLE t3(id INT 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);
         INSERT INTO t1 VALUES(1, 1, 2);
diff --git a/test/sql-tap/fkey1.test.lua b/test/sql-tap/fkey1.test.lua
index bca82d93d..494af4b4a 100755
--- a/test/sql-tap/fkey1.test.lua
+++ b/test/sql-tap/fkey1.test.lua
@@ -6,6 +6,15 @@ test:plan(19)
 
 test:do_execsql_test(
     "fkey1-1.1",
+    [[
+        CREATE TABLE t2(x PRIMARY KEY, y TEXT);
+    ]], {
+        -- <fkey1-1.1>
+        -- </fkey1-1.1>
+    })
+
+test:do_execsql_test(
+    "fkey1-1.2",
     [[
         CREATE TABLE t1(
             a INTEGER PRIMARY KEY,
@@ -19,15 +28,6 @@ test:do_execsql_test(
         -- </fkey1-1.1>
     })
 
-test:do_execsql_test(
-    "fkey1-1.2",
-    [[
-        CREATE TABLE t2(x PRIMARY KEY, y TEXT);
-    ]], {
-        -- <fkey1-1.2>
-        -- </fkey1-1.2>
-    })
-
 test:do_execsql_test(
     "fkey1-1.3",
     [[
diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua
index 9d04a04b0..89a9279da 100755
--- a/test/sql-tap/fkey2.test.lua
+++ b/test/sql-tap/fkey2.test.lua
@@ -15,9 +15,6 @@ test:do_execsql_test(
 
         CREATE TABLE t7(a, b INTEGER PRIMARY KEY);
         CREATE TABLE t8(c PRIMARY KEY REFERENCES t7, d);
-
-        CREATE TABLE t9(a PRIMARY KEY REFERENCES nosuchtable, b);
-        CREATE TABLE t10(a PRIMARY KEY REFERENCES t9(c), b);
     ]], {
         -- <fkey2-1.1>
         -- </fkey2-1.1>
@@ -301,21 +298,19 @@ test:do_catchsql_test(
 test:do_catchsql_test(
     "fkey2-1.29",
     [[
-        INSERT INTO t9 VALUES(1, 3);
+        CREATE TABLE t9(a PRIMARY KEY REFERENCES nosuchtable, b);
     ]], {
-        -- <fkey2-1.29>
-        1, "no such table: NOSUCHTABLE"
-        -- </fkey2-1.29>
+        1, "foreign key constraint references nonexistent table: NOSUCHTABLE"
     })
 
 test:do_catchsql_test(
     "fkey2-1.30",
     [[
-        INSERT INTO t10 VALUES(1, 3);
+        INSERT INTO t9 VALUES(1, 3);
     ]], {
-        -- <fkey2-1.30>
-        1, "foreign key mismatch - \"T10\" referencing \"T9\""
-        -- </fkey2-1.30>
+        -- <fkey2-1.29>
+        1, "no such table: T9"
+        -- </fkey2-1.29>
     })
 
 test:do_execsql_test(
@@ -731,8 +726,8 @@ test:do_catchsql_test(
     [[
         DROP TABLE IF EXISTS c;
         DROP TABLE IF EXISTS p;
-        CREATE TABLE c(x PRIMARY KEY REFERENCES v(y));
         CREATE VIEW v AS SELECT x AS y FROM c;
+        CREATE TABLE c(x PRIMARY KEY REFERENCES v(y));
         INSERT INTO c DEFAULT VALUES;
     ]], {
         -- <fkey2-7.2>
@@ -1050,15 +1045,15 @@ test:do_execsql_test(
 --         -- </fkey2-10.5>
 --     })
 
-test:do_execsql_test(
+test:do_catchsql_test(
     "fkey2-10.6",
     [[
         DROP TABLE IF EXISTS t2;
         DROP TABLE IF EXISTS t1;
         CREATE TABLE t1(a PRIMARY KEY, b REFERENCES nosuchtable);
-        DROP TABLE t1;
     ]], {
         -- <fkey2-10.6>
+        1, "foreign key constraint references nonexistent table: NOSUCHTABLE
         -- </fkey2-10.6>
     })
 
@@ -1211,8 +1206,8 @@ test:do_execsql_test(
     "fkey2-10.20",
     [[
         DROP VIEW IF EXISTS v;
-        CREATE TABLE t1(x PRIMARY KEY REFERENCES v);
         CREATE VIEW v AS SELECT * FROM t1;
+        CREATE TABLE t1(x PRIMARY KEY REFERENCES v);
         DROP VIEW v;
     ]], {
         -- <fkey2-10.20>
diff --git a/test/sql-tap/fkey3.test.lua b/test/sql-tap/fkey3.test.lua
index 82796ba33..2532ec6a0 100755
--- a/test/sql-tap/fkey3.test.lua
+++ b/test/sql-tap/fkey3.test.lua
@@ -36,7 +36,7 @@ test:do_catchsql_test(
         DROP TABLE t1;
     ]], {
         -- <fkey3-1.3.1>
-        1, "FOREIGN KEY constraint failed"
+        1, "can't drop parent table T1 when child table refers to it"
         -- </fkey3-1.3.1>
     })
 
@@ -46,7 +46,7 @@ test:do_catchsql_test(
         DROP TABLE t1;
     ]], {
         -- <fkey3-1.3.2>
-        1, "FOREIGN KEY constraint failed"
+        1, "can't drop parent table T1 when child table refers to it"
         -- </fkey3-1.3.2>
     })
 
diff --git a/test/sql-tap/suite.ini b/test/sql-tap/suite.ini
index 0637cffc1..e9c3d65ed 100644
--- a/test/sql-tap/suite.ini
+++ b/test/sql-tap/suite.ini
@@ -3,6 +3,7 @@ core = app
 description = Database tests with #! using TAP
 disabled =
 	reindex.test.lua ; This test is banned in scope of #2174
+	gh-2953-drop-table-with-FK.test.lua 
 lua_libs = lua/sqltester.lua ../sql/lua/sql_tokenizer.lua ../box/lua/identifier.lua
 is_parallel = True
 release_disabled = debug_mode_only.test.lua
diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua
index 31330a5a0..6aa290742 100755
--- a/test/sql-tap/table.test.lua
+++ b/test/sql-tap/table.test.lua
@@ -730,6 +730,7 @@ test:do_catchsql_test(
     "table-10.2",
     [[
         DROP TABLE t6;
+	CREATE TABLE t4(a INT PRIMARY KEY);
         CREATE TABLE t6(a REFERENCES t4(a) MATCH PARTIAL primary key);
     ]], {
         -- <table-10.2>
diff --git a/test/sql-tap/tkt-b1d3a2e531.test.lua b/test/sql-tap/tkt-b1d3a2e531.test.lua
index 5cfa2e12b..951299dbd 100755
--- a/test/sql-tap/tkt-b1d3a2e531.test.lua
+++ b/test/sql-tap/tkt-b1d3a2e531.test.lua
@@ -124,7 +124,7 @@ test:do_catchsql_test(
           DROP TABLE cc1;
     ]], {
         -- <3.2>
-        1, "FOREIGN KEY constraint failed"
+        1, "can't drop parent table PP1 when child table refers to it"
         -- </3.2>
     })
 
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] [PATCH 2/5] schema: add new system space for FK constraints
  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-13  2:04 ` Nikita Pettik
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 3/5] sql: introduce ADD CONSTRAINT statement Nikita Pettik
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

This patch introduces new system space to persist foreign keys
contraints. Format of the space:

_fk_constraint (space id = 350)

[<contraint name> STR, <parent id> UINT, <child id> UINT,
 <is deferred> BOOL, <match> STR, <on delete action> STR,
 <on update action> STR, <field links> ARRAY<MAP< UINT : UINT >>]

FK constraint is local to space, so every pair <FK name, child id>
is unique (and it is PK in _fk_constraint space).

After insertion into this space, new instance describing FK constraint
is created. FK constraints are held in data-dictionary as two lists
(for child and parent constraints) in struct space.

There is a list of FK restrictions:
 - At the time of FK creation parent and chils spaces must exist;
 - VIEWs can't be involved into FK processing;
 - Child space must be empty;
 - Types of referencing and referenced fields must match;
 - Collations of referencing and referenced fields must match;
 - Referenced fields must compose unique index;

Until space (child) features FK constraints it isn't allowed to be
dropped. Implicitly referenced index also can't be dropped
(and that is why parent space can't be dropped). But :drop() method
of child space firstly deletes all FK constraint (the same as SQL
triggers, indexes etc) and then removes entry from _space.

Part of #3271
---
 src/box/CMakeLists.txt         |   1 +
 src/box/alter.cc               | 432 ++++++++++++++++++++++++++++++++++++++++-
 src/box/alter.h                |   1 +
 src/box/bootstrap.snap         | Bin 1704 -> 1798 bytes
 src/box/errcode.h              |   4 +
 src/box/fkey.c                 |  69 +++++++
 src/box/fkey.h                 | 163 ++++++++++++++++
 src/box/lua/schema.lua         |   6 +
 src/box/lua/space.cc           |   2 +
 src/box/lua/upgrade.lua        |  16 ++
 src/box/schema.cc              |  16 ++
 src/box/schema_def.h           |  14 ++
 src/box/space.c                |   2 +
 src/box/space.h                |   3 +
 src/box/sql.c                  |   8 +
 src/box/sql/fkey.c             |  32 +--
 src/box/sql/tarantoolInt.h     |   1 +
 test/box/access_misc.result    |   5 +
 test/box/access_sysview.result |   6 +-
 test/box/alter.result          |   5 +-
 test/box/misc.result           |   2 +
 test/engine/iterator.result    |   2 +-
 test/sql/foreign-keys.result   | 316 ++++++++++++++++++++++++++++++
 test/sql/foreign-keys.test.lua | 144 ++++++++++++++
 24 files changed, 1214 insertions(+), 36 deletions(-)
 create mode 100644 src/box/fkey.c
 create mode 100644 src/box/fkey.h
 create mode 100644 test/sql/foreign-keys.result
 create mode 100644 test/sql/foreign-keys.test.lua

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 61e3eb637..7ba8a3139 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -93,6 +93,7 @@ add_library(box STATIC
     space.c
     space_def.c
     sequence.c
+    fkey.c
     func.c
     func_def.c
     alter.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 89b11dcd3..aaa56bd21 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -33,6 +33,7 @@
 #include "user.h"
 #include "space.h"
 #include "index.h"
+#include "fkey.h"
 #include "func.h"
 #include "coll_id_cache.h"
 #include "coll_id_def.h"
@@ -559,6 +560,18 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
 	old_space->sql_triggers = new_value;
 }
 
+/** The same as for triggers - swap lists of FK constraints. */
+static void
+space_swap_fkeys(struct space *new_space, struct space *old_space)
+{
+	struct fkey *child_fkey = new_space->child_fkey;
+	struct fkey *parent_fkey = new_space->parent_fkey;
+	new_space->child_fkey = old_space->child_fkey;
+	new_space->parent_fkey = old_space->parent_fkey;
+	old_space->child_fkey = child_fkey;
+	old_space->parent_fkey = parent_fkey;
+}
+
 /**
  * True if the space has records identified by key 'uid'.
  * Uses 'iid' index.
@@ -769,9 +782,10 @@ alter_space_rollback(struct trigger *trigger, void * /* event */)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	struct space *new_space = space_cache_replace(alter->old_space);
 	assert(new_space == alter->new_space);
 	(void) new_space;
@@ -867,9 +881,10 @@ alter_space_do(struct txn *txn, struct alter_space *alter)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	/*
 	 * The new space is ready. Time to update the space
 	 * cache with it.
@@ -1703,6 +1718,18 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 				  space_name(old_space),
 				  "other views depend on this space");
 		}
+		/*
+		 * No need to check existence of parent keys,
+		 * since if we went so far, space would'n have
+		 * any indexes. But referenced space has at least
+		 * one referenced index which can't be dropped
+		 * before constraint itself.
+		 */
+		if (old_space->child_fkey != NULL) {
+			tnt_raise(ClientError, ER_DROP_SPACE,
+				  space_name(old_space),
+				  "the space has foreign key constraints");
+		}
 		/**
 		 * The space must be deleted from the space
 		 * cache right away to achieve linearisable
@@ -1889,6 +1916,22 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 			  "can not add a secondary key before primary");
 	}
 
+	/*
+	 * Can't drop index if foreign key constraints references
+	 * this index.
+	 */
+	if (new_tuple == NULL) {
+		struct fkey *fk = old_space->parent_fkey;
+		while (fk != NULL) {
+			if (old_space->parent_fkey->index_id == iid) {
+				tnt_raise(ClientError, ER_ALTER_SPACE,
+					  space_name(old_space),
+					  "can not drop referenced index");
+			}
+			fk = fk->fkey_parent_next;
+		}
+	}
+
 	struct alter_space *alter = alter_space_new(old_space);
 	auto scoped_guard =
 		make_scoped_guard([=] { alter_space_delete(alter); });
@@ -3404,6 +3447,387 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 	txn_on_commit(txn, on_commit);
 }
 
+/**
+ * Decode MsgPack array of links. It consists from maps:
+ * {parent_id (UINT) : child_id (UINT)}.
+ *
+ * @param data MsgPack array of links.
+ * @param[out] out_count Count of links.
+ * @param constraint_name Constraint name to use in error
+ *			  messages.
+ * @param constraint_len Length of constraint name.
+ * @param errcode Errcode for client errors.
+ * @retval Array of links.
+ */
+static struct field_link *
+fkey_links_decode(const char *data, uint32_t *out_count,
+		  const char *constraint_name, uint32_t constraint_len,
+		  uint32_t errcode)
+{
+	assert(mp_typeof(*data) == MP_ARRAY);
+	uint32_t count = mp_decode_array(&data);
+	if (count == 0) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "at least one link must be specified");
+	}
+	*out_count = count;
+	size_t size = count * sizeof(struct field_link);
+	struct field_link *region_links =
+		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
+	memset(region_links, 0, size);
+	const char **map = &data;
+	for (uint32_t i = 0; i < count; ++i) {
+		uint32_t map_sz = mp_decode_map(map);
+		if (map_sz != 2) {
+			tnt_raise(ClientError, errcode,
+				  tt_cstr(constraint_name, constraint_len),
+				  tt_sprintf("link must be map with 2 fields"));
+		}
+		if (mp_typeof(**map) != MP_STR) {
+			tnt_raise(ClientError, errcode,
+				  tt_cstr(constraint_name, constraint_len),
+				  tt_sprintf("link %d is not map "\
+					     "with string keys", i));
+		}
+		for (uint8_t j = 0; j < map_sz; ++j) {
+			uint32_t key_len;
+			const char *key = mp_decode_str(map, &key_len);
+			if (key_len == 6 &&
+			    memcmp(key, "parent", key_len) == 0) {
+				region_links[i].parent_field =
+					mp_decode_uint(map);
+			} else if (key_len == 5 &&
+				   memcmp(key, "child", key_len) == 0) {
+				region_links[i].child_field =
+					mp_decode_uint(map);
+			} else {
+				char *errmsg = tt_static_buf();
+				snprintf(errmsg, TT_STATIC_BUF_LEN,
+					 "unexpected key of link %d '%.*s'", i,
+					 key_len, key);
+				tnt_raise(ClientError, errcode,
+					  tt_cstr(constraint_name,
+						  constraint_len), errmsg);
+			}
+		}
+	}
+	return region_links;
+}
+
+/** Create an instance of foreign key def constraint from tuple. */
+static struct fkey_def *
+fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
+{
+	uint32_t name_len;
+	const char *name = tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_NAME,
+					      &name_len);
+	if (name_len > BOX_NAME_MAX) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(name, BOX_INVALID_NAME_MAX),
+			  "constraint name is too long");
+	}
+	identifier_check_xc(name, name_len);
+	const char *links_raw =
+		tuple_field_with_type_xc(tuple, BOX_FK_CONSTRAINT_FIELD_LINKS,
+					 MP_ARRAY);
+	uint32_t link_count;
+	struct field_link *links = fkey_links_decode(links_raw, &link_count,
+						     name, name_len, errcode);
+	size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
+	struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
+	if (fk_def == NULL)
+		tnt_raise(OutOfMemory, fkey_sz, "malloc", "fkey_def");
+	auto def_guard = make_scoped_guard([=] { free(fk_def); });
+	memset(fk_def, 0, sizeof(*fk_def));
+	memcpy(fk_def->name, name, name_len);
+	fk_def->name[name_len] = '\0';
+	fk_def->links = (struct field_link *)((char *)&fk_def->name +
+					      name_len + 1);
+	memcpy(fk_def->links, links, link_count * sizeof(struct field_link));
+	fk_def->field_count = link_count;
+	fk_def->child_id = tuple_field_u32_xc(tuple,
+					      BOX_FK_CONSTRAINT_FIELD_CHILD_ID);
+	fk_def->parent_id = tuple_field_u32_xc(tuple,
+					   BOX_FK_CONSTRAINT_FIELD_PARENT_ID);
+	fk_def->is_deferred = tuple_field_bool_xc(tuple,
+					      BOX_FK_CONSTRAINT_FIELD_DEFERRED);
+	const char *match = tuple_field_str_xc(tuple,
+					       BOX_FK_CONSTRAINT_FIELD_MATCH,
+					       &name_len);
+	fk_def->match = fkey_match_by_name(match, name_len);
+	if (fk_def->match == fkey_match_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown MATCH clause");
+	}
+	const char *on_delete_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_DELETE,
+				   &name_len);
+	fk_def->on_delete = fkey_action_by_name(on_delete_action, name_len);
+	if (fk_def->on_delete == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON DELETE action");
+	}
+	const char *on_update_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_UPDATE,
+				   &name_len);
+	fk_def->on_update = fkey_action_by_name(on_update_action, name_len);
+	if (fk_def->on_update == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON UPDATE action");
+	}
+	def_guard.is_active = false;
+	return fk_def;
+}
+
+/**
+ * Replace entry in child's and parent's lists of
+ * FK constraints.
+ *
+ * @param child Child space of FK constraint.
+ * @param parent Parent space of FK constraint.
+ * @param new_fkey Constraint to be added to child and parent.
+ * @param[out] old_fkey Constraint to be found and replaced.
+ */
+static void
+fkey_list_replace(struct space *child, struct space *parent, const char *name,
+		  struct fkey *new_fkey, struct fkey **old_fkey)
+{
+	*old_fkey = NULL;
+	struct fkey **fk = &parent->parent_fkey;
+	while (*fk != NULL && !(strcmp((*fk)->def->name, name) == 0 &&
+			        (*fk)->def->child_id == child->def->id))
+		fk = &((*fk)->fkey_parent_next);
+	if (*fk != NULL) {
+		*old_fkey = *fk;
+		*fk = (*fk)->fkey_parent_next;
+	}
+	if (new_fkey != NULL) {
+		new_fkey->fkey_parent_next = parent->parent_fkey;
+		parent->parent_fkey = new_fkey;
+	}
+	fk = &child->child_fkey;
+	/* In child's list all constraints are unique by name. */
+	while (*fk != NULL && strcmp((*fk)->def->name, name) != 0)
+		fk = &((*fk)->fkey_child_next);
+	if (*fk != NULL) {
+		assert(*old_fkey == *fk);
+		*fk = (*fk)->fkey_child_next;
+	}
+	if (new_fkey != NULL) {
+		new_fkey->fkey_child_next = child->child_fkey;
+		child->child_fkey = new_fkey;
+	}
+}
+
+/**
+ * On rollback of creation we remove FK constraint from DD, i.e.
+ * from parent's and child's lists of constraints and
+ * release memory.
+ */
+static void
+on_create_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk->def->parent_id);
+	struct space *child = space_by_id(fk->def->child_id);
+	struct fkey *fkey = NULL;
+	fkey_list_replace(child, parent, fk->def->name, NULL, &fkey);
+	fkey_delete(fkey);
+}
+
+/** Return old FK and release memory for the new one. */
+static void
+on_replace_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk->def->parent_id);
+	struct space *child = space_by_id(fk->def->child_id);
+	struct fkey *old_fkey = NULL;
+	fkey_list_replace(child, parent, fk->def->name, fk, &old_fkey);
+	fkey_delete(old_fkey);
+}
+
+/** Release memory for old foreign key. */
+static void
+on_replace_fkey_commit(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	fkey_delete(fk);
+}
+
+/** On rollback of drop simply return back FK to DD. */
+static void
+on_drop_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk_to_restore = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk_to_restore->def->parent_id);
+	struct space *child = space_by_id(fk_to_restore->def->child_id);
+	struct fkey *old_fk;
+	fkey_list_replace(child, parent, fk_to_restore->def->name, fk_to_restore,
+			  &old_fk);
+	assert(old_fk == NULL);
+}
+
+/**
+ * On commit of drop we have already deleted foreign key from
+ * both (parent's and child's) lists, so just release memory.
+ */
+static void
+on_drop_fkey_commit(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	fkey_delete(fk);
+}
+
+/** A trigger invoked on replace in the _fk_constraint space. */
+static void
+on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
+{
+	struct txn *txn = (struct txn *) event;
+	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct tuple *old_tuple = stmt->old_tuple;
+	struct tuple *new_tuple = stmt->new_tuple;
+	if (new_tuple != NULL) {
+		/* Create or replace foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(new_tuple,
+						ER_CREATE_FK_CONSTRAINT);
+		auto fkey_def_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		if (child_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space can't be VIEW");
+		}
+		struct space *parent_space =
+			space_cache_find_xc(fk_def->parent_id);
+		if (parent_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referenced space can't be VIEW");
+		}
+		/*
+		 * FIXME: until SQL triggers are completely
+		 * integrated into server (i.e. we are able to
+		 * invoke triggers even if DML occurred via Lua
+		 * interface), it makes no sense to provide any
+		 * checks on existing data in space.
+		 */
+		struct index *pk = space_index(child_space, 0);
+		if (index_count(pk, ITER_ALL, NULL, 0) > 0) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space must be empty");
+		}
+		/* Check types of referenced fields. */
+		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+			uint32_t child_fieldno = fk_def->links[i].child_field;
+			uint32_t parent_fieldno = fk_def->links[i].parent_field;
+			if (child_fieldno >= child_space->def->field_count ||
+			    parent_fieldno >= parent_space->def->field_count) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "foreign key refers to "
+						        "nonexistent field");
+			}
+			if (child_space->def->fields[child_fieldno].type !=
+			    parent_space->def->fields[parent_fieldno].type) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "field type mismatch");
+			}
+			if (child_space->def->fields[child_fieldno].coll_id !=
+			    parent_space->def->fields[parent_fieldno].coll_id) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name,
+					  "field collation mismatch");
+			}
+		}
+		/*
+		 * Search for suitable index in parent space:
+		 * it must be unique and consist exactly from
+		 * referenced columns (but order may be different).
+		 */
+		struct index *fk_index = NULL;
+		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
+			struct index *idx = space_index(parent_space, i);
+			if (!idx->def->opts.is_unique)
+				continue;
+			if (idx->def->key_def->part_count !=
+			    fk_def->field_count)
+				continue;
+			uint32_t j;
+			for (j = 0; j < fk_def->field_count; ++j) {
+				if (idx->def->key_def->parts[j].fieldno !=
+				    fk_def->links[j].parent_field)
+					break;
+			}
+			if (j != fk_def->field_count)
+				continue;
+			fk_index = idx;
+			break;
+		}
+		if (fk_index == NULL) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name, "referenced fields don't "
+					      "compose unique index");
+		}
+		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
+		if (fkey == NULL)
+			tnt_raise(OutOfMemory, sizeof(*fkey), "malloc", "fkey");
+		auto fkey_guard = make_scoped_guard([=] { free(fkey); });
+		memset(fkey, 0, sizeof(*fkey));
+		fkey->def = fk_def;
+		fkey->index_id = fk_index->def->iid;
+		struct fkey *old_fk;
+		fkey_list_replace(child_space, parent_space, fk_def->name,
+				  fkey, &old_fk);
+		if (old_tuple == NULL) {
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_create_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+			assert(old_fk == NULL);
+		} else {
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_replace_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+			struct trigger *on_commit =
+				txn_alter_trigger_new(on_replace_fkey_commit,
+						      old_fk);
+			txn_on_commit(txn, on_commit);
+		}
+		fkey_def_guard.is_active = false;
+		fkey_guard.is_active = false;
+	} else if (new_tuple == NULL && old_tuple != NULL) {
+		/* Drop foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(old_tuple,
+						ER_DROP_FK_CONSTRAINT);
+		auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *parent_space =
+			space_cache_find_xc(fk_def->parent_id);
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		struct fkey *old_fkey = NULL;
+		fkey_list_replace(child_space, parent_space, fk_def->name, NULL,
+				  &old_fkey);
+		struct trigger *on_commit =
+			txn_alter_trigger_new(on_drop_fkey_commit, old_fkey);
+		txn_on_commit(txn, on_commit);
+		struct trigger *on_rollback =
+			txn_alter_trigger_new(on_drop_fkey_rollback, old_fkey);
+		txn_on_rollback(txn, on_rollback);
+	}
+}
+
 struct trigger alter_space_on_replace_space = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL
 };
@@ -3468,4 +3892,8 @@ struct trigger on_replace_trigger = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_trigger, NULL, NULL
 };
 
+struct trigger on_replace_fk_constraint = {
+	RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL
+};
+
 /* vim: set foldmethod=marker */
diff --git a/src/box/alter.h b/src/box/alter.h
index 8ea29c77b..4108fa47c 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -45,6 +45,7 @@ extern struct trigger on_replace_sequence;
 extern struct trigger on_replace_sequence_data;
 extern struct trigger on_replace_space_sequence;
 extern struct trigger on_replace_trigger;
+extern struct trigger on_replace_fk_constraint;
 extern struct trigger on_stmt_begin_space;
 extern struct trigger on_stmt_begin_index;
 extern struct trigger on_stmt_begin_truncate;
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index a8a00ec29e7106afbd06294cf6ffe291f90a2e10..44871cb62de38c37851db1cc6c16a59cafbeb106 100644
GIT binary patch
delta 1793
zcmV+c2mbh|4TcVo8Gkl3I4x&3H(@m}GdVeB3Q2BrbYX5|WjY`?GB#pmVmC4^VmUE2
zEi_{_F)d*;H8L$>V=ypeG&W>0F*0EaRzqxWV{1AfdwmKD)w&D1%?67A&KEt-<)r`s
z0000ewJ-euP!%))N=I-HNzfRn003V+z!wi#2Qap%9}e*e1%I%c5-D@E)qX&4RO>G6
zmMO_fNvuoT@=KEP*hnbWQq$GW7ix*VvzuOPTHs$rR}01CWlAaC0N4QP0G68Mebsm;
zN1J%@DBj3e->-ef6MWsuT$ed8I{~8UG50HqBmRc!i-68jv&xICD~hH5cqaN%ys$H-
zZ_SK7k=nnmwtuyB-ksl`jCC83tO)(-uYaBQ^)AdmTy?2MO1x2k&QdeB?<;Q-=jtu}
zyYrlpjK3iZptIC0(gX*J7Pb1|h_fznmFB&4ouwwNR*#wQhvAZRmYQ2?5tzT9`?}T6
zQWLYl)9R!Oac;sC>9R^H(mzI#@J;DSV3<OMDO89-%YXjPOA+H_o=c_liVM&Q8tX_5
zELn7{0I(D&{S&G@5IRfE9FkTm@BZcL&V$ZUvy^?gn)3q7n_M~QEH%INVus~S3Vj{_
z5Y|ZaKX!e03kS=a{LZRsFx$^d6NAoD)78s6+t17ItR<HuSeGH}yVaTnouy{kpC`e(
z48OC|D1V@N3HBw=T%-LwsPlKLGYFW#zM(2}pkMHVQYjedEH(9gaU5A9j=c+CeD-}i
zKYy;yuiF0Bcb#$McWz^%zwCMb3DuYaKA&Mc#&NI@S78X;z3+Kndkk-_D~4y+<%mB}
z{2UVL+|qwj^7xYcyVcQvE+}fwi5#6(&dS89gn#U2RV1itL}#hl$&jc-m_nFVCRGWi
z1frCxLDk#@)tqQdG^Nf`Gl=GcXCw1SGah6!9b`Dj)X31d_;pd(5ThX`LktF(3osU7
zJZncYRTf00jw*~~YNgOwYQDr!Sx}WPd$KNXM*erVQI}do^JXRx(OEn%NS2=-h5CyR
zet&uta^Zeo=iVPanA5*6DFlkyg_m@l@9tcW_#+a2P-+kfou%f!;>G0a@*61XTF>hm
z4Fv?IbK4J?Ya*-HO#OELi9P%qsy{~H_q$(z>*M(eQIvWkLT9Nd>lf>C2lr$$fgn&|
zse~`-l)AYjQwpO<DbtEYI^nRM1>%H*L4Qk?c{xHVd^Jc;2gLIHSotvZsNv!5kkL_L
za&(rO_N5k`7Dv@%?pT*$&eoZyy0K-;mMhgpXQ?@&&-cT!E^{?HOHBZl)|scGv7apY
zSu4!IStNfG)lSBX<F?@}vP$w+?aV91(D?=4vsM_Lr6!41=ptiW)N!!_cc9VZeScyx
zdWm%5^Dq}|is`yIiM%c>WMn@tZJmGFIxC%dr5By0rVfr5wK|46+wAA1jm^)<l2Yr;
zqj3?0Cx8qIkQTBZJGsi@#tI%4btlD6fm*2HP-KuAm;nF)0Z;((1qUhTiUtyZusDjN
zFphy3h(i#JA`@poASeh4G(fdrG=EdfAiA@++AP~V{hddL@nhsXI$)fgowsJT+~Igs
zv9S=f94`yGKp;SN!jRC9<<pg5Jpb$>#CjBVR2+}`*^9-e4Y8R~fX3aWTSE&zHZ2Gy
z0H+uPonDgeNE36&!f(?YwuTK~_Au&={S&bB^`s{g8#WOF@NtCJ6Vc)z%6}?eA{4Te
z%v0Y+w;o?@A{asAuY(U)`LKYmh#uZ>YMe&vj*~-9=cA*Gig+vj&N*+))C1?dF;g#K
zYs`Zae(kLXy1bs8cG+CZ2q4vNoEhh$mmGn%a)Va8!Po{`^ci@Ngblg){nM{9<ewSl
z%Yc7om><SfwdG(xM*_?P8h<`w^y8wzJBT5}pRnwVP`=RJ5r7=-YGG7A1a<NiX)0Y-
zt)8m{G3c*86fX9nr6i|T=Erp{I~}W8RhQDWUHFi2C|8Fx+Ah1hfxXu@D~UX70*}jC
zDl){K9UN@zOLtUwUKUIQ@Q;mDQ9g!<KXYJ5m_+D2Wc#)Er#h45ZGWc)RQ*NjqcsKB
zaS|g}%!8|maX}+&47Uh?v9M?Sa+I8-4=ZBCf3o|_Z6W-9A!IQ~>@R&7o5_5!Mvf9L
zpB5>ZKMHMOIC^w#l2PN(Z<hXa+5{M9x>;MLxGr111yHte_S)8iotfvH?i~ZFJ*)1x
zD`&{rTDYDhH=M{=KzhJf;;%cw=Y#s|aN%==<=ld>BAPz(wu-?+6LlutXrmW$+RMOg
z^2#0a|Bn;kk#{T-ljvQXL=05>gT!A8c3u65>RfM_K^LK5LQ8?+@T)acRXZkU*u|B-
jgR)jr@;tTBKTzvJa;RZ})DfZhDp^kwhA0Qs5UuSB(zRu&

delta 1699
zcmV;U23+}u4yX-~8Gki5FfC^@G-fw8H#rJPZgX^DZewLSAYx=OVKg#1V=Xo^Ff=VR
zH#adYIA$<6Eiz^~VK^`|F)=qdI0{xnY;R+0Iv{&}3JTS_3%bn)FaXY~@A2EE00000
z04TLD{QyusF#sw<untMk7&id`Up&AM2ap_8K2ct%)hPuc5r2u%1Dtdthk@FZnGtQL
zq{+<`lO*YB%N4E_fREI4wd=GkVz`4fdcG}`x2rRP>+&h36w?690OJ7SjC}R|`e!^<
zufxoBSySaEK}34Y9gE@!en$00z_rw*@?r{$VyQo#kG>SK>wIY(Gha_6_ARVsq0YPW
z*^{p>3xXA)K!3gUtrNGN1@ezpT`H224JN>~)O>B+%A2IIdZ>SQUNe%x&&UF}mYPMF
z<VfKnR$n!OS(mg*w7uMGsY$EVW9Iu^v?SM3GpQn}@&|-lvwAHxzsossplA9N4p%a9
zo>%g6a!e(V!@(sGEAUSy(+NYC{hgO0NP}%ImC`FNL4Q!RY*}%n3YHx!04xm(FRr{v
zl!I%j`QeM%l{Z=Rb?}Fz#-snSYrI36D{t~UtJh#?KQB85*HV+!OFXonm)}`Amn5Jr
zSFUeXZ5CWh&9XmF0_t-0owY^*(UxFi^2{+JpjYYq%_<FoC9cn?&K&4i^*yN+3|vc1
zjb9wclz#}mo&{p?>>GD}0%4t7vHjt9ooVEEE?c6%^m+ajRha@mpI!XLX|9h}V+i2A
z@rhe|T+Ko(uFkH@5&Tr*=hczUq5heY2QkUNS%n#JEj3Bpit$d4G7*DgL3m(5TGT2F
z94mTMRGt((D5}nmjt=Ss$^@zeY-T+Y)EjXvHGeM*i6^2%bzXPkLU15BlTD5dj*v}P
z!_|DamYPB}H(Lp|d7$Y4qv0@PGgC9OfyFV4f(DrkG8kko#8`;gVrZ;|sZe1;Q!301
z424=>WUi&=%LED&qVlCr*5%B||L!vCQjv%@8VT1@b7K*MWDE0|O4Ko**D)efkeJS8
zK!2(n6H`6bb=>(U`tZ-F0)bI|$NTk%AJ0yXpi~?YuB9fdV^Ej7e?_$i!nM?_Aqm60
z`&X+x53Z$V3HxeQC!Tlex*X2Ks1;7>#I3wicRHC$CJrY=2}I$TaHmRP$WmdOj*tpL
zDiEFf!}j<<_2_um?AUb7<ft(?uBE1ZseeePy;1d;JE+T5&DNpKvyqgPbfwm~mYOj1
z`F>Z{WsSzQ)C6E@9ooDZ`^l1@wSo*Vi{$U4dSkpeZW)-xR7uXN9oje;I=58#to6mU
z)GQIMSmcX~J1<y(9cuXapO=izkuQGU#d1mMFpHN+%mRyy?B}Jd^Do1*(xHv(xqp_L
zJ2_y)>bTPRW<M`)Zh*#Alv;<j8W#azNCJUKsNq;-5DClx0Du4}0PzI}Dd&m?5`eHc
zio-CDff$BE0E|KtM?fGb2njSmwO}-3>|otvVxbn$nEy|s!{{(Nj1CZ|XWOmW^1O_Q
z#C9R1B6~4P0)Yit2t#sze0O3}a)0*OYw7YYtf)91`JUH`Pa9%0YaYejkXxhcI3*z3
zdE&uuxjDLwcwchr;ms`(kQeo&CcC+u2o3ol@WqTwal~b%ln5QO6oL_ZE4HeMEh(My
z_v_fe^><ak7er6BFLY%562{3Rr^e{WMg>zAXfXbi)iZOl46U*mTj66G#((TQBk0n5
z@)<oY4g-ONQth~(3rms{8<`ta`P_f=wD_@mkOU1mHT1wuY%u<m)iZOl3{AjWf3|$~
zt8m|QXkeD0hZ@Msd-hKiZ0KL;Q@khOrOoi`UOgQ>poLS)q+!uN;jpegcC;P#`N7`U
znw9D$>kp61-&FJnXm;?~*ng4k=;HZ8FhRgSHd00X7<B%WiF{!a-SZ&o*D9aNO_EGI
zB{7vRtd9oyxDE<2a=|>f+87shpvG{^Ol^j3&MJS%IeJ)8Cti{*Ub%&E>I-oWh=lIa
zrQD!Ql+Ehs<+5HPA%914?C?i#t&Krytl`a4o=ls7UrZNkEfZHnq<_5tDm2be+j_v6
zk)6|HU_kR{(H-|;3^`j1=aVc3C;Vs%FuC|kE%^FTe<LpZkFbK<maIKZW_(1&;Gu~+
zlWeroE4jI4;5K>1vid)s6X20|EK)(~T|9{xS1W;}zgF#f{gJ7;9%W`;4+U&31%|V|
tLZAw@V?4uhT=^Z8O&m3K=&6$aL0H$8dkzbvjtIq9$$FA7L^;(Et?e54Em;5n

diff --git a/src/box/errcode.h b/src/box/errcode.h
index c76018cbf..1558cfae8 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -215,6 +215,10 @@ struct errcode_record {
 	/*160 */_(ER_ACTION_MISMATCH,		"Field %d contains %s on conflict action, but %s in index parts") \
 	/*161 */_(ER_VIEW_MISSING_SQL,		"Space declared as a view must have SQL statement") \
 	/*162 */_(ER_FOREIGN_KEY_CONSTRAINT,	"Can not commit transaction: deferred foreign keys violations are not resolved") \
+	/*163 */_(ER_CREATE_FK_CONSTRAINT,	"Failed to create foreign key constraint '%s': %s") \
+	/*164 */_(ER_DROP_FK_CONSTRAINT,	"Failed to drop foreign key constraint '%s': %s") \
+
+
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/fkey.c b/src/box/fkey.c
new file mode 100644
index 000000000..e45889a0d
--- /dev/null
+++ b/src/box/fkey.c
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "fkey.h"
+#include "sql.h"
+#include "sqliteInt.h"
+
+const char *fkey_action_strs[] = {
+	/* [FKEY_ACTION_RESTRICT]    = */ "no_action",
+	/* [FKEY_ACTION_SET_NULL]    = */ "set_null",
+	/* [FKEY_ACTION_SET_DEFAULT] = */ "set_default",
+	/* [FKEY_ACTION_CASCADE]     = */ "cascade",
+	/* [FKEY_ACTION_NO_ACTION]   = */ "restrict"
+};
+
+const char *fkey_match_strs[] = {
+	/* [FKEY_MATCH_SIMPLE]  = */ "simple",
+	/* [FKEY_MATCH_PARTIAL] = */ "partial",
+	/* [FKEY_MATCH_FULL]    = */ "full"
+};
+
+void
+fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
+{
+	if (p != NULL) {
+		struct TriggerStep *step = p->step_list;
+		sql_expr_delete(db, step->pWhere, false);
+		sql_expr_list_delete(db, step->pExprList);
+		sql_select_delete(db, step->pSelect);
+		sql_expr_delete(db, p->pWhen, false);
+		sqlite3DbFree(db, p);
+	}
+}
+
+void
+fkey_delete(struct fkey *fkey)
+{
+	fkey_trigger_delete(sql_get(), fkey->on_delete_trigger);
+	fkey_trigger_delete(sql_get(), fkey->on_update_trigger);
+	free(fkey->def);
+	free(fkey);
+}
diff --git a/src/box/fkey.h b/src/box/fkey.h
new file mode 100644
index 000000000..1b6ea71d9
--- /dev/null
+++ b/src/box/fkey.h
@@ -0,0 +1,163 @@
+#ifndef TARANTOOL_BOX_FKEY_H_INCLUDED
+#define TARANTOOL_BOX_FKEY_H_INCLUDED
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "space.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct sqlite3;
+
+enum fkey_action {
+	FKEY_NO_ACTION = 0,
+	FKEY_ACTION_SET_NULL,
+	FKEY_ACTION_SET_DEFAULT,
+	FKEY_ACTION_CASCADE,
+	FKEY_ACTION_RESTRICT,
+	fkey_action_MAX
+};
+
+enum fkey_match {
+	FKEY_MATCH_SIMPLE = 0,
+	FKEY_MATCH_PARTIAL,
+	FKEY_MATCH_FULL,
+	fkey_match_MAX
+};
+
+extern const char *fkey_action_strs[];
+
+extern const char *fkey_match_strs[];
+
+/** Structure describing field dependencies for foreign keys. */
+struct field_link {
+	uint32_t parent_field;
+	uint32_t child_field;
+};
+
+/** Definition of foreign key constraint. */
+struct fkey_def {
+	/** Id of space containing the REFERENCES clause (child). */
+	uint32_t child_id;
+	/** Id of space that the key points to (parent). */
+	uint32_t parent_id;
+	/** Number of fields in this key. */
+	uint32_t field_count;
+	/** True if constraint checking is deferred till COMMIT. */
+	bool is_deferred;
+	/** Match condition for foreign key. SIMPLE by default. */
+	enum fkey_match match;
+	/** ON DELETE action. NO ACTION by default. */
+	enum fkey_action on_delete;
+	/** ON UPDATE action. NO ACTION by default. */
+	enum fkey_action on_update;
+	/** Mapping of fields in child to fields in parent. */
+	struct field_link *links;
+	/** Name of the constraint. */
+	char name[0];
+};
+
+/** Structure representing foreign key relationship. */
+struct fkey {
+	struct fkey_def *def;
+	/** Index id of referenced index in parent space. */
+	uint32_t index_id;
+	/** Triggers for actions. */
+	struct sql_trigger *on_delete_trigger;
+	struct sql_trigger *on_update_trigger;
+	/** Linked lists */
+	struct fkey *fkey_parent_next;
+	struct fkey *fkey_child_next;
+};
+
+/**
+ * Alongside with struct fkey_def itself, we reserve memory for
+ * string containing its name and for array of links.
+ * Memory layout:
+ * +-------------------------+ <- Allocated memory starts here
+ * |     struct fkey_def     |
+ * |-------------------------|
+ * |        name + \0        |
+ * |-------------------------|
+ * |          links          |
+ * +-------------------------+
+ */
+static inline size_t
+fkey_def_sizeof(uint32_t links_count, uint32_t name_len)
+{
+	return sizeof(struct fkey) + links_count * sizeof(struct field_link) +
+	       name_len + 1;
+}
+
+static inline enum fkey_action
+fkey_action_by_name(const char *action_str, size_t len)
+{
+	return (enum fkey_action) strnindex(fkey_action_strs, action_str, len,
+					    fkey_action_MAX);
+}
+
+static inline enum fkey_match
+fkey_match_by_name(const char *match_str, size_t len)
+{
+	return (enum fkey_match) strnindex(fkey_match_strs, match_str, len,
+					   fkey_match_MAX);
+}
+
+static inline bool
+fkey_is_self_referenced(const struct fkey_def *fkey)
+{
+	return fkey->child_id == fkey->parent_id;
+}
+
+/**
+ * The second argument is a Trigger structure allocated by the
+ * fkActionTrigger() routine.This function deletes the Trigger
+ * structure and all of its sub-components.
+ *
+ * @param db Database handler.
+ * @param p Trigger to be freed.
+ */
+void
+fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p);
+
+/** Release memory for foreign key and its triggers, if any. */
+void
+fkey_delete(struct fkey *fkey);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* TARANTOOL_BOX_FKEY_H_INCLUDED */
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 87c79bdde..30d8b0081 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -506,6 +506,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _vindex = box.space[box.schema.VINDEX_ID]
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
+    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
     if sequence_tuple ~= nil and sequence_tuple[3] == true then
         -- Delete automatically generated sequence.
@@ -519,6 +520,11 @@ box.schema.space.drop = function(space_id, space_name, opts)
         local v = keys[i]
         _index:delete{v[1], v[2]}
     end
+    for _, t in _fk_constraint.index.primary:pairs() do
+        if t.child_id == space_id then
+            _fk_constraint:delete{t.name, t.child_id}
+        end
+    end
     revoke_object_privs('space', space_id)
     _truncate:delete{space_id}
     if _space:delete{space_id} == nil then
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index b44bed451..ea744dd4e 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -545,6 +545,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "SQL_STAT1_ID");
 	lua_pushnumber(L, BOX_SQL_STAT4_ID);
 	lua_setfield(L, -2, "SQL_STAT4_ID");
+	lua_pushnumber(L, BOX_FK_CONSTRAINT_ID);
+	lua_setfield(L, -2, "FK_CONSTRAINT_ID");
 	lua_pushnumber(L, BOX_TRUNCATE_ID);
 	lua_setfield(L, -2, "TRUNCATE_ID");
 	lua_pushnumber(L, BOX_SEQUENCE_ID);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index f112a93ae..772f55cb2 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -509,6 +509,22 @@ local function upgrade_to_2_1_0()
                   {unique = true}, {{0, 'string'}, {1, 'string'},
                                     {5, 'scalar'}}}
 
+    local fk_constr_ft = {{name='name', type='string'},
+                          {name='child_id', type='unsigned'},
+                          {name='parent_id', type='unsigned'},
+                          {name='deferred', type='boolean'},
+                          {name='match', type='string'},
+                          {name='on_delete', type='string'},
+                          {name='on_update', type='string'},
+                          {name='links', type='array'}}
+    log.info("create space _fk_constraint")
+    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
+                  0, setmap({}), fk_constr_ft}
+
+    log.info("create index primary on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
+                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
+
     -- Nullability wasn't skipable. This was fixed in 1-7.
     -- Now, abscent field means NULL, so we can safely set second
     -- field in format, marking it nullable.
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 0377e4dda..5cd79267c 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -404,6 +404,22 @@ schema_init()
 			 COLL_NONE, SORT_ORDER_ASC);
 	/* _sql_stat4 - extensive statistics on space, seen in SQL. */
 	sc_space_new(BOX_SQL_STAT4_ID, "_sql_stat4", key_def, NULL, NULL);
+
+	key_def_delete(key_def);
+	key_def = key_def_new(2);
+	if (key_def == NULL)
+		diag_raise();
+	/* Constraint name. */
+	key_def_set_part(key_def, 0, BOX_FK_CONSTRAINT_FIELD_NAME,
+			 FIELD_TYPE_STRING, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* Child space. */
+	key_def_set_part(key_def, 1, BOX_FK_CONSTRAINT_FIELD_CHILD_ID,
+			 FIELD_TYPE_UNSIGNED, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* _fk_сonstraint - foreign keys constraints. */
+	sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_def,
+		     &on_replace_fk_constraint, NULL);
 }
 
 void
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 5ab4bb002..22621fc11 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -107,6 +107,8 @@ enum {
 	/** Space ids for SQL statictics. */
 	BOX_SQL_STAT1_ID = 348,
 	BOX_SQL_STAT4_ID = 349,
+	/** Space id of _fk_constraint. */
+	BOX_FK_CONSTRAINT_ID = 350,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -224,6 +226,18 @@ enum {
 	BOX_TRIGGER_FIELD_OPTS = 2,
 };
 
+/** _fk_constraint fields. */
+enum {
+	BOX_FK_CONSTRAINT_FIELD_NAME = 0,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_ID = 1,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_ID = 2,
+	BOX_FK_CONSTRAINT_FIELD_DEFERRED = 3,
+	BOX_FK_CONSTRAINT_FIELD_MATCH = 4,
+	BOX_FK_CONSTRAINT_FIELD_ON_DELETE = 5,
+	BOX_FK_CONSTRAINT_FIELD_ON_UPDATE = 6,
+	BOX_FK_CONSTRAINT_FIELD_LINKS = 7,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/space.c b/src/box/space.c
index 64aa31217..98aa1d8b7 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -216,6 +216,8 @@ space_delete(struct space *space)
 	 * on_replace_dd_trigger on deletion from _trigger.
 	 */
 	assert(space->sql_triggers == NULL);
+	assert(space->child_fkey == NULL);
+	assert(space->parent_fkey == NULL);
 	space->vtab->destroy(space);
 }
 
diff --git a/src/box/space.h b/src/box/space.h
index 7da2ee51f..fc5e8046f 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -183,6 +183,9 @@ struct space {
 	 * of index id.
 	 */
 	struct index **index;
+	/** Foreign key constraints. */
+	struct fkey *parent_fkey;
+	struct fkey *child_fkey;
 };
 
 /** Initialize a base space instance. */
diff --git a/src/box/sql.c b/src/box/sql.c
index 063743e87..e0c2c924c 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1293,6 +1293,14 @@ void tarantoolSqlite3LoadSchema(InitData *init)
 			       "\"sample\","
 			       "PRIMARY KEY(\"tbl\", \"idx\", \"sample\"))");
 
+	sql_schema_put(init, TARANTOOL_SYS_FK_CONSTRAINT_NAME,
+		       BOX_FK_CONSTRAINT_ID, 0,
+		       "CREATE TABLE \""TARANTOOL_SYS_FK_CONSTRAINT_NAME
+		       "\"(\"name\" TEXT, \"parent_id\" INT, \"child_id\" INT,"
+		       "\"deferred\" INT, \"match\" TEXT, \"on_delete\" TEXT,"
+		       "\"on_update\" TEXT, \"links\","
+		       "PRIMARY KEY(\"name\", \"child_id\"))");
+
 	/* Read _space */
 	if (space_foreach(space_foreach_put_cb, init) != 0) {
 		init->rc = SQL_TARANTOOL_ERROR;
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index be080324f..016ded8d0 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -35,6 +35,7 @@
  */
 #include "coll.h"
 #include "sqliteInt.h"
+#include "box/fkey.h"
 #include "box/schema.h"
 #include "box/session.h"
 #include "tarantoolInt.h"
@@ -726,31 +727,6 @@ sqlite3FkReferences(Table * pTab)
 					pTab->def->name);
 }
 
-/**
- * The second argument is a Trigger structure allocated by the
- * fkActionTrigger() routine. This function deletes the sql_trigger
- * structure and all of its sub-components.
- *
- * The Trigger structure or any of its sub-components may be
- * allocated from the lookaside buffer belonging to database
- * handle dbMem.
- *
- * @param db Database connection.
- * @param trigger AST object.
- */
-static void
-sql_fk_trigger_delete(struct sqlite3 *db, struct sql_trigger *trigger)
-{
-	if (trigger == NULL)
-		return;
-	struct TriggerStep *trigger_step = trigger->step_list;
-	sql_expr_delete(db, trigger_step->pWhere, false);
-	sql_expr_list_delete(db, trigger_step->pExprList);
-	sql_select_delete(db, trigger_step->pSelect);
-	sql_expr_delete(db, trigger->pWhen, false);
-	sqlite3DbFree(db, trigger);
-}
-
 /*
  * The second argument points to an FKey object representing a foreign key
  * for which pTab is the child table. An UPDATE statement against pTab
@@ -1341,7 +1317,7 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		sql_expr_list_delete(db, pList);
 		sql_select_delete(db, pSelect);
 		if (db->mallocFailed == 1) {
-			sql_fk_trigger_delete(db, trigger);
+			fkey_trigger_delete(db, trigger);
 			return 0;
 		}
 		assert(pStep != 0);
@@ -1439,8 +1415,8 @@ sqlite3FkDelete(sqlite3 * db, Table * pTab)
 		assert(pFKey->isDeferred == 0 || pFKey->isDeferred == 1);
 
 		/* Delete any triggers created to implement actions for this FK. */
-		sql_fk_trigger_delete(db, pFKey->apTrigger[0]);
-		sql_fk_trigger_delete(db, pFKey->apTrigger[1]);
+		fkey_trigger_delete(db, pFKey->apTrigger[0]);
+		fkey_trigger_delete(db, pFKey->apTrigger[1]);
 
 		pNext = pFKey->pNextFrom;
 		sqlite3DbFree(db, pFKey);
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index f043a60d1..c31da131d 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -20,6 +20,7 @@
 #define TARANTOOL_SYS_TRUNCATE_NAME "_truncate"
 #define TARANTOOL_SYS_SQL_STAT1_NAME "_sql_stat1"
 #define TARANTOOL_SYS_SQL_STAT4_NAME "_sql_stat4"
+#define TARANTOOL_SYS_FK_CONSTRAINT_NAME "_fk_constraint"
 
 /* Max space id seen so far. */
 #define TARANTOOL_SYS_SCHEMA_MAXID_KEY "max_id"
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 5a2563d55..62c92ca03 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -809,6 +809,11 @@ box.space._space:select()
   - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
         'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
       {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
+  - [350, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
+      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
+      {'name': 'deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
+      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
+      {'name': 'links', 'type': 'array'}]]
 ...
 box.space._func:select()
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index ae042664a..1c4e7b787 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -230,11 +230,11 @@ box.session.su('guest')
 ...
 #box.space._vspace:select{}
 ---
-- 22
+- 23
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 49
 ...
 #box.space._vuser:select{}
 ---
@@ -262,7 +262,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 49
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index c41b52f48..d995261c3 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -107,7 +107,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 350
+- 351
 ...
 space.field_count
 ---
@@ -152,7 +152,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '350' does not exist
+- error: Space '351' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -231,6 +231,7 @@ _index:select{}
   - [348, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string']]]
   - [349, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string'], [
         5, 'scalar']]]
+  - [350, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/misc.result b/test/box/misc.result
index a00d03365..315499f3e 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -487,6 +487,8 @@ t;
   160: box.error.ACTION_MISMATCH
   161: box.error.VIEW_MISSING_SQL
   162: box.error.FOREIGN_KEY_CONSTRAINT
+  163: box.error.CREATE_FK_CONSTRAINT
+  164: box.error.DROP_FK_CONSTRAINT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/engine/iterator.result b/test/engine/iterator.result
index a36761df8..ba9b0545a 100644
--- a/test/engine/iterator.result
+++ b/test/engine/iterator.result
@@ -4211,7 +4211,7 @@ s:replace{35}
 ...
 state, value = gen(param,state)
 ---
-- error: 'builtin/box/schema.lua:1049: usage: next(param, state)'
+- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'
 ...
 value
 ---
diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result
new file mode 100644
index 000000000..77b49d67d
--- /dev/null
+++ b/test/sql/foreign-keys.result
@@ -0,0 +1,316 @@
+env = require('test_run')
+---
+...
+test_run = env.new()
+---
+...
+test_run:cmd('restart server default with cleanup=1')
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+---
+...
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+---
+...
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+---
+...
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+---
+...
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: Space '666' does not exist
+...
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('T2')[1]['id']
+---
+...
+view_id = box.space._space.index.name:select('V1')[1]['id']
+---
+...
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced space can''t
+    be VIEW'
+...
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space can''t
+    be VIEW'
+...
+box.sql.execute("DROP VIEW v1;")
+---
+...
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown MATCH clause'
+...
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON DELETE action'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON UPDATE action'
+...
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space must
+    be empty'
+...
+box.sql.execute("DELETE FROM t2;")
+---
+...
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, child = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': link must be map with
+    2 fields'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1, 2}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': link must be map with
+    2 fields'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{wrong_key = 2, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unexpected key of link
+    0 ''wrong_key'''
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 13, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': foreign key refers to
+    nonexistent field'
+...
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 2}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields don''t
+    compose unique index'
+...
+-- Referencing and referenced fields must feature the same types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 1}, {child = 2, parent = 2}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': field type mismatch'
+...
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+---
+- error: 'Can''t modify space ''T1'': can not drop referenced index'
+...
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+---
+...
+box.space._space:delete(child_id)
+---
+- error: 'Can''t drop space ''T2'': the space has foreign key constraints'
+...
+box.space.T2:drop()
+---
+...
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+---
+- []
+...
+box.space.T1:drop()
+---
+...
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+---
+...
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+---
+...
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {{parent = 0, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 1}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:count()
+---
+- 5
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'fk_1', child_id}
+---
+- ['fk_1', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'self_2', parent_id}
+---
+- ['self_2', 516, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 1}]]
+...
+box.space._fk_constraint:delete{'self_1', child_id}
+---
+- ['self_1', 515, 515, false, 'simple', 'restrict', 'restrict', [{'parent': 0, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'fk_3', parent_id}
+---
+- ['fk_3', 516, 515, false, 'simple', 'restrict', 'restrict', [{'child': 1, 'parent': 0}]]
+...
+box.space._fk_constraint:count()
+---
+- 0
+...
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+---
+- cascade
+...
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred']
+---
+- null
+...
+box.space.CHILD:drop()
+---
+...
+box.space.PARENT:drop()
+---
+...
+-- 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
new file mode 100644
index 000000000..57cc7e118
--- /dev/null
+++ b/test/sql/foreign-keys.test.lua
@@ -0,0 +1,144 @@
+env = require('test_run')
+test_run = env.new()
+test_run:cmd('restart server default with cleanup=1')
+
+
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+child_id = box.space._space.index.name:select('T2')[1]['id']
+view_id = box.space._space.index.name:select('V1')[1]['id']
+
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DROP VIEW v1;")
+
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1}}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DELETE FROM t2;")
+
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, child = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1, 2}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{wrong_key = 2, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 13, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 2}}}
+box.space._fk_constraint:insert(t)
+
+-- Referencing and referenced fields must feature the same types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 1}, {child = 2, parent = 2}}}
+box.space._fk_constraint:insert(t)
+
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+box.space._space:delete(child_id)
+box.space.T2:drop()
+
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+box.space.T1:drop()
+
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {{parent = 0, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 1}}}
+t = box.space._fk_constraint:insert(t)
+
+box.space._fk_constraint:count()
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'fk_1', child_id}
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'self_2', parent_id}
+box.space._fk_constraint:delete{'self_1', child_id}
+box.space._fk_constraint:delete{'fk_3', parent_id}
+box.space._fk_constraint:count()
+
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:replace(t)
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:replace(t)
+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')
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  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-13  2:04 ` [tarantool-patches] [PATCH 2/5] schema: add new system space for FK constraints Nikita Pettik
@ 2018-07-13  2:04 ` Nikita Pettik
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 4/5] sql: display error on FK creation and drop failure Nikita Pettik
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

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
  <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/extra/mkkeywordhash.c b/extra/mkkeywordhash.c
index 990c4199f..6ba872acc 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  },
   { "TABLE",                  "TK_TABLE",       ALWAYS,           true  },
   { "THEN",                   "TK_THEN",        ALWAYS,           true  },
   { "TO",                     "TK_TO",          ALWAYS,           true  },
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;
+}
+
 /**
  * The second argument is a Trigger structure allocated by the
  * fkActionTrigger() routine.This function deletes the Trigger
diff --git a/src/box/sql.c b/src/box/sql.c
index e0c2c924c..21104358b 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;
 
@@ -1531,6 +1532,21 @@ tarantoolSqlite3MakeTableOpts(Table *pTable, const char *zSql, char *buf)
 	return p - buf;
 }
 
+int
+fkey_encode_links(const struct fkey_def *fkey, char *buf)
+{
+	const struct Enc *enc = get_enc(buf);
+	char *p = enc->encode_array(buf, fkey->field_count);
+	for (uint32_t i = 0; i < fkey->field_count; ++i) {
+		p = enc->encode_map(p, 2);
+		p = enc->encode_str(p, "child", strlen("child"));
+		p = enc->encode_uint(p, fkey->links[i].child_field);
+		p = enc->encode_str(p, "parent", strlen("parent"));
+		p = enc->encode_uint(p, fkey->links[i].parent_field);
+	}
+	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 fe54e5531..e81113f58 100644
--- a/src/box/sql/alter.c
+++ b/src/box/sql/alter.c
@@ -150,7 +150,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)
@@ -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;
-	}
 	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
@@ -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"
@@ -373,9 +374,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);
@@ -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)
+{
+	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);
+		return;
+	}
+	size_t real_links_sz = fkey_encode_links(fk, encoded_links);
+	encoded_links[real_links_sz] = '\0';
+	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;
+}
+
 /*
  * 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) {
+					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");
+					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;
+		}
 	}
 
 	/* 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)
+{
+	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,
@@ -2142,6 +2288,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. */
+	for (struct fkey *child_fk = space->child_fkey; child_fk != NULL;
+	     child_fk = child_fk->fkey_child_next) {
+		const char *fk_name_dup = sqlite3DbStrDup(v->db,
+							  child_fk->def->name);
+		if (fk_name_dup == NULL)
+			return;
+		vdbe_fkey_code_drop(parse_context, fk_name_dup, space_id);
+	}
 	/*
 	 * Drop all _space and _index entries that refer to the
 	 * table.
@@ -2256,12 +2411,16 @@ 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 && strcmp(fk->pFrom->def->name, tab->def->name) != 0) {
-		sqlite3ErrorMsg(parse_context, "can't drop parent table %s when "
-				"child table refers to it", space_name);
-		goto exit_drop_table;
+	if (space_fkey_check_references(space)) {
+		for (struct fkey *fk = space->parent_fkey; fk != NULL;
+		     fk = fk->fkey_parent_next) {
+			if (fkey_is_self_referenced(fk->def))
+				continue;
+			sqlite3ErrorMsg(parse_context, "can't drop table %s: "
+					"other objects depend on it",
+					space->def->name);
+			goto exit_drop_table;
+		}
 	}
 	sql_clear_stat_spaces(parse_context, space_name, NULL);
 	sql_code_drop_table(parse_context, space, is_view);
@@ -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;
+	/*
+	 * 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;
+		}
+		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");
+			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++);
+			constraint_name =
+				sqlite3MPrintf(db, "fk_constraint_%d_%s",
+					       fk_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;
+	size_t fk_size = fkey_def_sizeof(child_cols_count,
+					 strlen(constraint_name));
+	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;
+	}
+	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 +
+					   strlen(constraint_name) + 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) != 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) != 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) != 0)
+				goto exit_create_fk;
+	}
+	memcpy(fk->name, constraint_name, strlen(constraint_name));
+	fk->name[strlen(constraint_name)] = '\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)
+		parse_context->new_fkey->fkey = fk;
+	else
+		vdbe_fkey_code_creation(parse_context, fk);
+
+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;
+}
 
- fk_end:
-	sqlite3DbFree(db, pFKey);
-	free(normilized_name);
-#endif				/* !defined(SQLITE_OMIT_FOREIGN_KEY) */
-	sql_expr_list_delete(db, pFromCol);
-	sql_expr_list_delete(db, pToCol);
+void
+fkey_change_defer_mode(struct Parse *parse_context, bool is_deferred)
+{
+	if (parse_context->db->init.busy || parse_context->new_fkey == NULL)
+		return;
+	struct fkey_def *fk = parse_context->new_fkey->fkey;
+	fk->is_deferred = is_deferred;
 }
 
-/*
- * 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)
+sql_drop_foreign_key(struct Parse *parse_context, struct SrcList *table,
+		     struct Token *constraint)
 {
-#ifndef SQLITE_OMIT_FOREIGN_KEY
-	Table *pTab;
-	FKey *pFKey;
-	if ((pTab = pParse->pNewTable) == 0 || (pFKey = pTab->pFKey) == 0)
+	assert(table != NULL && table->nSrc == 1);
+	struct sqlite3 *db = parse_context->db;
+	const char *constraint_name = sqlite3NameFromToken(db, constraint);
+	if (constraint_name == NULL)
 		return;
-	assert(isDeferred == 0 || isDeferred == 1);	/* EV: R-30323-21917 */
-	pFKey->isDeferred = (u8) isDeferred;
-#endif
+	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++;
+		sqlite3DbFree(db, (void *) constraint_name);
+		return;
+	}
+	vdbe_fkey_code_drop(parse_context, constraint_name, child_id);
 }
 
 /*
diff --git a/src/box/sql/callback.c b/src/box/sql/callback.c
index 01e8dd8f1..164ab74a6 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;
 }
@@ -309,7 +308,6 @@ sqlite3SchemaCreate(sqlite3 * db)
 		sqlite3OomFault(db);
 	} else {
 		sqlite3HashInit(&p->tblHash);
-		sqlite3HashInit(&p->fkeyHash);
 	}
 	return p;
 }
diff --git a/src/box/sql/delete.c b/src/box/sql/delete.c
index 5a799714d..9aa4322a3 100644
--- a/src/box/sql/delete.c
+++ b/src/box/sql/delete.c
@@ -126,7 +126,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);
 
@@ -452,14 +452,14 @@ 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);
+		mask |= fkey_old_mask(table->def->id);
 		first_old_reg = parse->nMem + 1;
 		parse->nMem += (1 + (int)table->def->field_count);
 
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)
+		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
 
 /*
  * Deferred and Immediate FKs
@@ -137,8 +138,8 @@
  * 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.
+ *   fkey_old_mask()  - Query for the set of required old.* columns.
  *
  *
  * Externally accessible module functions
@@ -146,10 +147,7 @@
  *
  *   sqlite3FkCheck()    - Check for foreign key violations.
  *   sqlite3FkActions()  - Code triggers for ON UPDATE/ON DELETE actions.
- *   sqlite3FkDelete()   - Delete an FKey structure.
- */
-
-/*
+ *
  * VDBE Calling Convention
  * -----------------------
  *
@@ -166,198 +164,21 @@
  *   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.
- *
- * 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.
- *
- * If the required index cannot be found, either because:
- *
- *   1) The named parent key columns do not exist, or
- *
- *   2) The named parent key columns do exist, but are not subject to a
- *      UNIQUE or PRIMARY KEY constraint, or
- *
- *   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
- *
- *   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.
- *
- * 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.
- */
-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 */
-    )
-{
-	Index *pIdx = 0;	/* Value to return via *ppIdx */
-	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.
-	 *
-	 * 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.
-	 */
-	if (nCol == 1) {
-		/* The FK maps to the IPK if any of the following are true:
-		 *
-		 *   1) There is an INTEGER PRIMARY KEY column and the FK is implicitly
-		 *      mapped to the primary key of table pParent, or
-		 *   2) The FK is explicitly mapped to a column declared as INTEGER
-		 *      PRIMARY KEY.
-		 */
-		if (pParent->iPKey >= 0) {
-			if (!zKey)
-				return 0;
-			if (!strcmp(pParent->def->fields[pParent->iPKey].name,
-				    zKey))
-				return 0;
-		}
-	} else if (paiCol) {
-		assert(nCol > 1);
-		aiCol =
-		    (int *)sqlite3DbMallocRawNN(pParse->db, nCol * sizeof(int));
-		if (!aiCol)
-			return 1;
-		*paiCol = aiCol;
-	}
-
-	for (pIdx = pParent->pIndex; pIdx; pIdx = pIdx->pNext) {
-		int nIdxCol = index_column_count(pIdx);
-		if (nIdxCol == nCol && index_is_unique(pIdx)
-		    && pIdx->pPartIdxWhere == 0) {
-			/* pIdx 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 == 0) {
-				/* 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(pIdx)) {
-					if (aiCol) {
-						int i;
-						for (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;
-				for (i = 0; i < nCol; i++) {
-					i16 iCol = pIdx->aiColumn[i];	/* Index of column in parent tbl */
-					char *zIdxCol;	/* Name of indexed column */
-
-					if (iCol < 0)
-						break;	/* No foreign keys against expression indexes */
-
-					/* 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.
-					 */
-					struct coll *def_coll;
-					uint32_t id;
-					def_coll = sql_column_collation(pParent->def,
-									iCol,
-									&id);
-					struct coll *coll =
-						sql_index_collation(pIdx, i,
-								    &id);
-					if (def_coll != coll)
-						break;
-
-					zIdxCol =
-						pParent->def->fields[iCol].name;
-					for (j = 0; j < nCol; j++) {
-						if (strcmp
-						    (pFKey->aCol[j].zCol,
-						     zIdxCol) == 0) {
-							if (aiCol)
-								aiCol[i] =
-								    pFKey->
-								    aCol[j].
-								    iFrom;
-							break;
-						}
-					}
-					if (j == nCol)
-						break;
-				}
-				if (i == nCol)
-					break;	/* pIdx is usable */
-			}
-		}
-	}
-	if (!pIdx) {
-		sqlite3ErrorMsg(pParse, "foreign key mismatch - "\
-					"\"%w\" referencing \"%w\"",
-				pFKey->pFrom->def->name, pFKey->zTo);
-		sqlite3DbFree(pParse->db, aiCol);
-		return 1;
-	}
-
-	*ppIdx = pIdx;
-	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:
+/**
+ * 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.
+ *
+ * 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".
@@ -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);
+		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) {
+			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) &&
+	    !parse_context->pToplevel && !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);
 }
 
 /*
@@ -604,31 +391,26 @@ 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 */
+	       struct fkey_def *fkey,	/* The foreign key linking pSrc to pTab */
 	       int regData,	/* Parent row data starts here */
 	       int nIncr	/* Amount to increment deferred counter by */
     )
 {
 	sqlite3 *db = pParse->db;	/* Database handle */
-	int i;			/* Iterator variable */
 	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 == 0 || pIdx->pTable == pTab);
-	assert(pIdx == 0 || (int)index_column_count(pIdx) == pFKey->nCol);
-	assert(pIdx != 0);
-
 	if (nIncr < 0) {
 		iFkIfZero =
-		    sqlite3VdbeAddOp2(v, OP_FkIfZero, pFKey->isDeferred, 0);
+		    sqlite3VdbeAddOp2(v, OP_FkIfZero, fkey->is_deferred, 0);
 		VdbeCoverage(v);
 	}
 
+	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> ...
@@ -637,18 +419,18 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 	 * 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 (i = 0; i < pFKey->nCol; i++) {
+	for (uint32_t i = 0; i < fkey->field_count; 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 *zCol;	/* Name of column in child table */
 
-		iCol = pIdx ? pIdx->aiColumn[i] : -1;
+		iCol = fkey->links[i].parent_field;
 		pLeft = exprTableRegister(pParse, pTab, regData, iCol);
-		iCol = aiCol ? aiCol[i] : pFKey->aCol[0].iFrom;
+		iCol = fkey->links[i].child_field;
 		assert(iCol >= 0);
-		zCol = pFKey->pFrom->def->fields[iCol].name;
+		zCol = child_space->def->fields[iCol].name;
 		pRight = sqlite3Expr(db, TK_ID, zCol);
 		pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight);
 		pWhere = sqlite3ExprAnd(db, pWhere, pEq);
@@ -661,22 +443,18 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 	 *     NOT( $current_a==a AND $current_b==b AND ... )
 	 *     The primary key is (a,b,...)
 	 */
-	if (pTab == pFKey->pFrom && nIncr > 0) {
+	if (pTab->def->id == fkey->child_id && 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 != 0);
-		int col_count = index_column_count(pPk);
-		for (i = 0; i < col_count; i++) {
-			i16 iCol = pIdx->aiColumn[i];
+		for (uint32_t i = 0; i < fkey->field_count; i++) {
+			i16 iCol = fkey->links[i].parent_field;
 			assert(iCol >= 0);
 			pLeft = exprTableRegister(pParse, pTab, regData, iCol);
-			pRight =
-				exprTableColumn(db, pTab->def,
-						pSrc->a[0].iCursor, iCol);
+			pRight = exprTableColumn(db, pTab->def,
+						 pSrc->a[0].iCursor, iCol);
 			pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight);
 			pAll = sqlite3ExprAnd(db, pAll, pEq);
 		}
@@ -695,7 +473,7 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 	 * foreign key constraint counter.
 	 */
 	pWInfo = sqlite3WhereBegin(pParse, pSrc, pWhere, 0, 0, 0, 0);
-	sqlite3VdbeAddOp2(v, OP_FkCounter, pFKey->isDeferred, nIncr);
+	sqlite3VdbeAddOp2(v, OP_FkCounter, fkey->is_deferred, nIncr);
 	if (pWInfo) {
 		sqlite3WhereEnd(pWInfo);
 	}
@@ -706,103 +484,68 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 		sqlite3VdbeJumpHere(v, iFkIfZero);
 }
 
-/*
- * 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.
+/**
+ * The second argument points to an fkey object representing
+ * a foreign key. An UPDATE statement against child table is
+ * currently being processed. For each column of the table that
+ * is actually updated, the corresponding element in the 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, 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).
+/**
+ * Works the same as fkey_child_is_modified(), but checks are
+ * provided on parent table.
  *
- * This function returns true if any of the columns that are part of the
- * parent key for FK constraint *p are modified.
+ * @param fkey FK constraint definition.
+ * @param changes Array indicating modified columns.
+ * @retval true, if any of the columns that are part of the parent
+ *         key for FK constraint are modified.
  */
-static int
-fkParentIsModified(Table * pTab, FKey * p, int *aChange)
+static bool
+fkey_parent_is_modified(const struct fkey_def *fkey, 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) {
+		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;
 }
 
 /*
@@ -834,7 +577,6 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
     )
 {
 	sqlite3 *db = pParse->db;	/* Database handle */
-	FKey *pFKey;		/* Used to iterate through FKs */
 	struct session *user_session = current_session();
 
 	/* Exactly one of regOld and regNew should be non-zero. */
@@ -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 &&
+		    !fkey_child_is_modified(fk_def, aChange))
 			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 || sqlite3FkLocateIndex(pParse, pTo, pFKey, &pIdx,
-					    &aiFree))
-				return;
-		assert(pFKey->nCol == 1 || (aiFree && pIdx));
-
-		if (aiFree) {
-			aiCol = aiFree;
-		} else {
-			iCol = pFKey->aCol[0].iFrom;
-			aiCol = &iCol;
-		}
-		for (i = 0; i < pFKey->nCol; i++) {
-			if (aiCol[i] == pTab->iPKey) {
-				aiCol[i] = -1;
-			}
-			assert(pIdx == 0 || pIdx->aiColumn[i] >= 0);
-		}
-
 		pParse->nTab++;
-
+		struct space *parent = space_by_id(fk_def->parent_id);
+		assert(parent != NULL);
 		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.
 			 */
-			fkLookupParent(pParse, pTo, pIdx, pFKey, aiCol,
-				       regOld, -1, bIgnore);
+			fkey_lookup_parent(pParse, parent, fk_def, fk->index_id,
+					   regOld, -1, bIgnore);
 		}
-		if (regNew != 0 && !isSetNullAction(pParse, pFKey)) {
+		if (regNew != 0 && !fkey_action_is_set_null(pParse, 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.
 			 *
@@ -906,29 +620,23 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 			 * 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(pParse, parent, fk_def, fk->index_id,
+					   regNew, +1, bIgnore);
 		}
-
-		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) {
+	for (struct fkey *fk = space->parent_fkey; fk != NULL;
+	     fk = fk->fkey_parent_next) {
+		struct fkey_def *fk_def = fk->def;
+		if (aChange != NULL &&
+		    !fkey_parent_is_modified(fk_def, aChange))
 			continue;
-		}
-
-		if (!pFKey->isDeferred
-		    && !(user_session->sql_flags & SQLITE_DeferFKs)
-		    && !pParse->pToplevel && !pParse->isMultiWrite) {
+		if (!fk_def->is_deferred &&
+		    !(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.
@@ -936,29 +644,30 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 			continue;
 		}
 
-		if (sqlite3FkLocateIndex(pParse, pTab, pFKey, &pIdx, &aiCol))
-			return;
-		assert(aiCol || pFKey->nCol == 1);
-
 		/* 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 *pSrc = sqlite3SrcListAppend(db, 0, 0);
+		if (pSrc != NULL) {
 			struct SrcList_item *pItem = pSrc->a;
-			pItem->pTab = pFKey->pFrom;
-			pItem->zName = pFKey->pFrom->def->name;
+			struct space *child = space_by_id(fk->def->child_id);
+			assert(child != NULL);
+			struct Table *tab =
+				sqlite3HashFind(&db->pSchema->tblHash,
+						child->def->name);
+			pItem->pTab = tab;
+			pItem->zName = sqlite3DbStrDup(db, child->def->name);
 			pItem->pTab->nTabRef++;
 			pItem->iCursor = pParse->nTab++;
 
 			if (regNew != 0) {
-				fkScanChildren(pParse, pSrc, pTab, pIdx, pFKey,
-					       aiCol, regNew, -1);
+				fkScanChildren(pParse, pSrc, pTab, fk->def,
+					       regNew, -1);
 			}
 			if (regOld != 0) {
-				int eAction = pFKey->aAction[aChange != 0];
-				fkScanChildren(pParse, pSrc, pTab, pIdx, pFKey,
-					       aiCol, regOld, 1);
+				enum fkey_action action = fk_def->on_update;
+				fkScanChildren(pParse, pSrc, pTab, fk->def,
+					       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.
@@ -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)))
 
-/*
- * 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)
 {
-	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;
 }
 
-/*
- * 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, 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.
+		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 (sqlite3FkReferences(pTab) || pTab->pFKey);
+			return space->parent_fkey != NULL ||
+			       space->child_fkey != NULL;
 		} else {
-			/* This is an UPDATE. Foreign key processing is only required if the
-			 * operation modifies one or more child or parent key columns.
+			/*
+			 * This is an UPDATE. FK 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;
+			for (struct fkey *p = space->child_fkey; p != NULL;
+			     p = p->fkey_child_next) {
+				if (fkey_child_is_modified(p->def, changes))
+					return true;
 			}
-
-			/* Check if any parent key columns are being modified. */
-			for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) {
-				if (fkParentIsModified(pTab, p, aChange))
-					return 1;
+			for (struct fkey *p = space->parent_fkey; p != NULL;
+			     p = p->fkey_parent_next) {
+				if (fkey_parent_is_modified(p->def, changes))
+					return true;
 			}
 		}
 	}
-	return 0;
+	return false;
 }
 
 /**
@@ -1114,40 +797,29 @@ sqlite3FkRequired(Table * pTab,	/* Table being modified */
  * @retval NULL on failure.
  */
 static struct sql_trigger *
-fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
+fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct fkey *fkey,
 		struct ExprList *pChanges)
 {
 	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 */
+	bool is_update = pChanges != NULL;
+	struct fkey_def *fk_def = fkey->def;
+	enum fkey_action action = is_update ? fk_def->on_update :
+					      fk_def->on_delete;
+	if (action == FKEY_ACTION_RESTRICT &&
+	    (user_session->sql_flags & SQLITE_DeferFKs))
+		return NULL;
+	struct sql_trigger *trigger = is_update ? fkey->on_update_trigger :
+						  fkey->on_delete_trigger;
+	if (action != FKEY_NO_ACTION && trigger == NULL) {
 		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++) {
+		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) {
 			Token tOld = { "old", 3, false };	/* Literal "old" token */
 			Token tNew = { "new", 3, false };	/* Literal "new" token */
 			Token tFromCol;	/* Name of column in child table */
@@ -1155,20 +827,12 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 			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 != 0
-			       || (pTab->iPKey >= 0
-				   && pTab->iPKey <
-				      (int)pTab->def->field_count));
-			assert(pIdx == 0 || pIdx->aiColumn[i] >= 0);
+			iFromCol = fk_def->links[i].child_field;
 			sqlite3TokenInit(&tToCol,
-					 pTab->def->fields[pIdx ? pIdx->
-						    aiColumn[i] : pTab->iPKey].
-					 name);
+					 pTab->def->fields[fk_def->links[i].parent_field].name);
+
 			sqlite3TokenInit(&tFromCol,
-					 pFKey->pFrom->def->fields[
-						iFromCol].name);
+					 child_space->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
@@ -1215,10 +879,10 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 				pWhen = sqlite3ExprAnd(db, pWhen, pEq);
 			}
 
-			if (action != OE_Restrict
-			    && (action != OE_Cascade || pChanges)) {
+			if (action != FKEY_ACTION_RESTRICT
+			    && (action != FKEY_ACTION_CASCADE || pChanges)) {
 				Expr *pNew;
-				if (action == OE_Cascade) {
+				if (action == FKEY_ACTION_CASCADE) {
 					pNew = sqlite3PExpr(pParse, TK_DOT,
 							    sqlite3ExprAlloc(db,
 									     TK_ID,
@@ -1228,10 +892,8 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 									     TK_ID,
 									     &tToCol,
 									     0));
-				} else if (action == OE_SetDflt) {
-					uint32_t space_id =
-						SQLITE_PAGENO_TO_SPACEID(
-							pFKey->pFrom->tnum);
+				} else if (action == FKEY_ACTION_SET_DEFAULT) {
+					uint32_t space_id = fk_def->child_id;
 					Expr *pDflt =
 						space_column_default_expr(
 							space_id, (uint32_t)iFromCol);
@@ -1256,12 +918,11 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 						       0);
 			}
 		}
-		sqlite3DbFree(db, aiCol);
 
-		zFrom = pFKey->pFrom->def->name;
-		nFrom = sqlite3Strlen30(zFrom);
+		const char *zFrom = child_space->def->name;
+		uint32_t nFrom = sqlite3Strlen30(zFrom);
 
-		if (action == OE_Restrict) {
+		if (action == FKEY_ACTION_RESTRICT) {
 			Token tFrom;
 			Expr *pRaise;
 
@@ -1285,7 +946,6 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 
 		/* Disable lookaside memory allocation */
 		db->lookaside.bDisable++;
-
 		size_t trigger_size = sizeof(struct sql_trigger) +
 				      sizeof(TriggerStep) + nFrom + 1;
 		trigger =
@@ -1323,11 +983,11 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		assert(pStep != 0);
 
 		switch (action) {
-		case OE_Restrict:
+		case FKEY_ACTION_RESTRICT:
 			pStep->op = TK_SELECT;
 			break;
-		case OE_Cascade:
-			if (!pChanges) {
+		case FKEY_ACTION_CASCADE:
+			if (pChanges == NULL) {
 				pStep->op = TK_DELETE;
 				break;
 			}
@@ -1335,9 +995,13 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		default:
 			pStep->op = TK_UPDATE;
 		}
+
 		pStep->trigger = trigger;
-		pFKey->apTrigger[iAction] = trigger;
-		trigger->op = pChanges ? TK_UPDATE : TK_DELETE;
+		if (is_update)
+			fkey->on_update_trigger = trigger;
+		else
+			fkey->on_delete_trigger = trigger;
+		trigger->op = (pChanges ? TK_UPDATE : TK_DELETE);
 	}
 
 	return trigger;
@@ -1362,13 +1026,14 @@ sqlite3FkActions(Parse * pParse,	/* Parse context */
 	 * 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 space *space = space_by_id(pTab->def->id);
+		assert(space != NULL);
+		for (struct fkey *fkey = space->parent_fkey; fkey != NULL;
+		     fkey = fkey->fkey_parent_next) {
+			if (aChange == 0 ||
+			    fkey_parent_is_modified(fkey->def, aChange)) {
 				struct sql_trigger *pAct =
-					fkActionTrigger(pParse, pTab, pFKey,
+					fkActionTrigger(pParse, pTab, fkey,
 							pChanges);
 				if (pAct == NULL)
 					continue;
@@ -1381,45 +1046,4 @@ sqlite3FkActions(Parse * pParse,	/* Parse context */
 	}
 }
 
-/*
- * 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. */
-		fkey_trigger_delete(db, pFKey->apTrigger[0]);
-		fkey_trigger_delete(db, pFKey->apTrigger[1]);
-
-		pNext = pFKey->pNextFrom;
-		sqlite3DbFree(db, pFKey);
-	}
-}
-#endif				/* ifndef SQLITE_OMIT_FOREIGN_KEY */
+#endif				/* ifndef SQLITE_OMIT_TRIGGER */
diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c
index c12043bde..f7bee1a66 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -1360,13 +1360,13 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		bool no_delete_triggers =
 			(0 == (user_session->sql_flags &
 			       SQLITE_RecTriggers) ||
-			 sql_triggers_exist(pTab, TK_DELETE, NULL, NULL) ==
+			 sql_triggers_exist(pTab, TK_DELETE,NULL, NULL) ==
 			 NULL);
+		struct space *space = space_by_id(space_id);
+		assert(space != NULL);
 		bool no_foreign_keys =
-			(0 == (user_session->sql_flags &
-			       SQLITE_ForeignKeys) ||
-			 (0 == pTab->pFKey &&
-			  0 == sqlite3FkReferences(pTab)));
+			(0 == (user_session->sql_flags & SQLITE_ForeignKeys) ||
+			 (space->child_fkey == NULL && space->parent_fkey));
 
 		if (no_secondary_indexes && no_foreign_keys &&
 		    proper_error_action && no_delete_triggers) {
@@ -1606,7 +1606,7 @@ sqlite3OpenTableAndIndices(Parse * pParse,	/* Parsing context */
 
 		if (isUpdate || 			/* Condition 1 */
 		    IsPrimaryKeyIndex(pIdx) ||		/* Condition 2 */
-		    sqlite3FkReferences(pTab) ||	/* Condition 3 */
+		    space->parent_fkey != NULL ||	/* Condition 3 */
 		    /* Condition 4 */
 		    (index_is_unique(pIdx) && pIdx->onError !=
 		     ON_CONFLICT_ACTION_DEFAULT &&
@@ -1883,10 +1883,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 &&
+	    dest->child_fkey != NULL)
 		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 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);
 
 	/* 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
@@ -51,6 +51,7 @@
 //
 %include {
 #include "sqliteInt.h"
+#include "box/fkey.h"
 
 /*
 ** Disable all error recovery processing in the parser push-down
@@ -285,8 +286,8 @@ ccons ::= UNIQUE onconf(R).      {sql_create_index(pParse,0,0,0,R,0,0,
 						   SQLITE_IDXTYPE_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
@@ -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; }
 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;}
@@ -338,8 +343,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;}
@@ -1441,6 +1445,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 31581b17f..8a736859a 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -35,6 +35,7 @@
 #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.
  */
@@ -594,210 +565,41 @@ 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)
+		uint32_t space_id = box_space_id_by_name(zRight,
+							 strlen(zRight));
+		if (space_id == BOX_ID_NIL)
 			break;
-		FKey *fkey = table->pFKey;
+		struct space *space = space_by_id(space_id);
+		struct fkey *fkey = space->child_fkey;
 		if (fkey == NULL)
 			break;
 		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;
+			for (uint32_t j = 0; j < fkey->def->field_count; j++) {
+				struct space *parent =
+					space_by_id(fkey->def->parent_id);
+				assert(parent != NULL);
+				uint32_t ch_fl = fkey->def->links[j].child_field;
+				const char *child_col =
+					space->def->fields[ch_fl].name;
+				uint32_t pr_fl = fkey->def->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[fkey->def->on_delete],
+						     fkey_action_strs[fkey->def->on_update],
 						     "NONE");
 				sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 8);
 			}
 			++i;
-			fkey = fkey->pNextFrom;
+			fkey = fkey->fkey_child_next;
 		}
 		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) {
-						if (pIdx == 0) {
-							sqlite3OpenTable(pParse,
-									 i,
-									 pParent,
-									 OP_OpenRead);
-						} else {
-							sqlite3VdbeAddOp3(v,
-									  OP_OpenRead,
-									  i,
-									  pIdx->
-									  tnum,
-									  0);
-							sql_vdbe_set_p4_key_def(pParse,
-										pIdx);
-						}
-					} else {
-						k = 0;
-						break;
-					}
-				}
-				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);
-						if (iKey != pTab->iPKey) {
-							sqlite3VdbeAddOp3(v,
-									  OP_Column,
-									  0,
-									  iKey,
-									  regRow);
-							sqlite3ColumnDefault(v,
-									     pTab->def,
-									     iKey,
-									     regRow);
-							sqlite3VdbeAddOp2(v,
-									  OP_IsNull,
-									  regRow,
-									  addrOk);
-							VdbeCoverage(v);
-						}
-						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..b1a169ed1 100644
--- a/src/box/sql/pragma.h
+++ b/src/box/sql/pragma.h
@@ -135,13 +135,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 629f68e4f..3d0f4fb8a 100644
--- a/src/box/sql/prepare.c
+++ b/src/box/sql/prepare.c
@@ -447,6 +447,11 @@ sql_parser_destroy(Parse *parser)
 	sqlite3 *db = parser->db;
 	sqlite3DbFree(db, parser->aLabel);
 	sql_expr_list_delete(db, parser->pConstExpr);
+	struct fkey_parse *fk = parser->new_fkey;
+	while (fk != NULL) {
+		sql_expr_list_delete(db, fk->selfref_cols);
+		fk = fk->next;
+	}
 	if (db != NULL) {
 		assert(db->lookaside.bDisable >=
 		       parser->disableLookaside);
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
@@ -1473,7 +1473,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;
@@ -1526,7 +1525,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 */
@@ -1978,42 +1975,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
@@ -2863,6 +2825,34 @@ enum ast_type {
 	ast_type_MAX
 };
 
+/**
+ * Structure representing foreign keys constraints appeared
+ * within CREATE TABLE statement. Used only during parsing.
+ */
+struct fkey_parse {
+	/**
+	 * List of foreign keys constraints 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 fkey_parse *next;
+};
+
 /*
  * 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
@@ -2957,7 +2947,8 @@ 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 */
-
+	/** Foreign key constraint appeared in CREATE TABLE stmt. */
+	struct fkey_parse *new_fkey;
 	bool initiateTTrans;	/* Initiate Tarantool transaction */
 	/** If set - do not emit byte code at all, just parse.  */
 	bool parse_only;
@@ -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.
+ *
+ * @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 *);
@@ -4665,27 +4705,36 @@ void sqlite3WithPush(Parse *, With *, u8);
  * 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 sqlite3FkDropTable(Parse *, SrcList *, Table *);
 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 sqlite3FkDropTable(a,b,c)
-#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 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, 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);
 
 /*
  * 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..ac50b46bd 100644
--- a/src/box/sql/status.c
+++ b/src/box/sql/status.c
@@ -247,10 +247,7 @@ sqlite3_db_status(sqlite3 * db,	/* The database connection whose status is desir
 
 				nByte +=
 				    ROUND8(sizeof(HashElem)) *
-				    (pSchema->tblHash.count +
-				     pSchema->fkeyHash.count);
-				nByte += sqlite3_msize(pSchema->tblHash.ht);
-				nByte += sqlite3_msize(pSchema->fkeyHash.ht);
+				    (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 c31da131d..6944a19ef 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -155,6 +155,18 @@ int tarantoolSqlite3MakeTableFormat(Table * pTable, void *buf);
  */
 int tarantoolSqlite3MakeTableOpts(Table * pTable, const char *zSql, char *buf);
 
+/**
+ * Encode links of given foreign key constraint into MsgPack.
+ *
+ * @param fkey Encode links of this foreign key contraint.
+ * @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(const struct fkey_def *fkey, 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 212adbcb3..3e65ab771 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
@@ -431,7 +431,7 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 	 * information is needed
 	 */
 	if (chngPk != 0 || hasFK != 0 || trigger != NULL) {
-		u32 oldmask = (hasFK ? sqlite3FkOldmask(pParse, pTab) : 0);
+		u32 oldmask = (hasFK ? fkey_old_mask(pTab->def->id) : 0);
 		oldmask |= sql_trigger_colmask(pParse, trigger, pChanges, 0,
 					       TRIGGER_BEFORE | TRIGGER_AFTER,
 					       pTab, on_error);
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 2c6bd2ba8..b9723e2e7 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"
@@ -4699,7 +4700,6 @@ case OP_RenameTable: {
 	const char *zOldTableName;
 	const char *zNewTableName;
 	Table *pTab;
-	FKey *pFKey;
 	int iRootPage;
 	InitData initData;
 	char *argv[4] = {NULL, NULL, NULL, NULL};
@@ -4722,20 +4722,6 @@ case OP_RenameTable: {
 					 &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);
-		assert(pFKey->pFrom);
-		rc = tarantoolSqlite3RenameParentTable(pFKey->pFrom->tnum,
-						       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);
 
 	initData.db = db;
diff --git a/test/sql-tap/alter.test.lua b/test/sql-tap/alter.test.lua
index 3e5c6102b..075e61ce3 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..e4470ecbb
--- /dev/null
+++ b/test/sql-tap/alter2.test.lua
@@ -0,0 +1,196 @@
+#!/usr/bin/env tarantool
+test = require("sqltester")
+test:plan(15)
+
+-- 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_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;
+    ]], {
+        -- <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: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 89a9279da..ddcf3116e 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>
@@ -300,7 +300,7 @@ test:do_catchsql_test(
     [[
         CREATE TABLE t9(a PRIMARY KEY REFERENCES nosuchtable, b);
     ]], {
-        1, "foreign key constraint references nonexistent table: NOSUCHTABLE"
+        1, "Space 'NOSUCHTABLE' does not exist"
     })
 
 test:do_catchsql_test(
@@ -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, "table \"P\" doesn't feature column 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, "foreign key mismatch - \"C\" referencing \"V\""
+        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, "foreign key mismatch - \"C\" referencing \"P\""
+        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, "number of columns in foreign key does not match the number of columns in the 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>
     })
 
@@ -1076,14 +1070,14 @@ test:do_catchsql_test(
         DROP TABLE t1;
     ]], {
         -- <fkey2-10.8>
-        1, "FOREIGN KEY constraint failed"
+        1, "can't drop table T1: other objects depend on it"
         -- </fkey2-10.8>
     })
 
 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, "table \"PP\" doesn't feature column 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);
@@ -1189,28 +1132,30 @@ test:do_execsql_test(
         DROP TABLE b1;
     ]], {
         -- <fkey2-10.18>
+        1, "can't drop table B1: other objects depend on it"
         -- </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);
         DROP TABLE b2;
     ]], {
         -- <fkey2-10.19>
+        1, "can't drop table B2: other objects depend on it"
         -- </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>
     })
 
@@ -1222,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;
     ]], {
@@ -1292,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;
     ]], {
@@ -1364,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);
 
@@ -1428,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));
@@ -1464,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 2532ec6a0..84385d10c 100755
--- a/test/sql-tap/fkey3.test.lua
+++ b/test/sql-tap/fkey3.test.lua
@@ -36,7 +36,7 @@ test:do_catchsql_test(
         DROP TABLE t1;
     ]], {
         -- <fkey3-1.3.1>
-        1, "can't drop parent table T1 when child table refers to it"
+        1, "can't drop table T1: other objects depend on it"
         -- </fkey3-1.3.1>
     })
 
@@ -46,7 +46,7 @@ test:do_catchsql_test(
         DROP TABLE t1;
     ]], {
         -- <fkey3-1.3.2>
-        1, "can't drop parent table T1 when child table refers to it"
+        1, "can't drop table T1: other objects depend on it"
         -- </fkey3-1.3.2>
     })
 
@@ -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/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);
         INSERT INTO p1 VALUES (1, 'one');
         INSERT INTO p1 VALUES (2, 'two');
         INSERT INTO c1 VALUES (1);
diff --git a/test/sql-tap/orderby1.test.lua b/test/sql-tap/orderby1.test.lua
index e216df2ca..f4c426397 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 6aa290742..24f494852 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, "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;
+        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
@@ -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, "number of columns in foreign key does not match the number of columns in the 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, [[no such column X]]
         -- </table-10.12>
     })
 
@@ -876,7 +878,7 @@ test:do_test(
         ]]
     end, {
         -- <table-10.13>
-        1, [[unknown column "X" in foreign key definition]]
+        1, [[no such column X]]
         -- </table-10.13>
     })
 
diff --git a/test/sql-tap/tkt-b1d3a2e531.test.lua b/test/sql-tap/tkt-b1d3a2e531.test.lua
index 951299dbd..3b70be36a 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
         );
@@ -124,7 +124,7 @@ test:do_catchsql_test(
           DROP TABLE cc1;
     ]], {
         -- <3.2>
-        1, "can't drop parent table PP1 when child table refers to it"
+        1, "can't drop table PP1: other objects depend on it"
         -- </3.2>
     })
 
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, '');
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] [PATCH 4/5] sql: display error on FK creation and drop failure
  2018-07-13  2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik
                   ` (2 preceding siblings ...)
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 3/5] sql: introduce ADD CONSTRAINT statement Nikita Pettik
@ 2018-07-13  2:04 ` Nikita Pettik
  2018-07-17 21:04   ` [tarantool-patches] " Vladislav Shpilevoy
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 5/5] sql: remove SQLITE_OMIT_FOREIGN_KEY define guard Nikita Pettik
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

Before insertion to _fk_constraint we must be sure that there in no
entry with given <name, child id>. Otherwise, insertion will fail and
'duplicate key' will be shown. Such error message doesn't seem to be
informative enough, so lets manually iterate through whole space looking
for appropriate record.
The same is for dropping constraint, but here vice versa: we test
that _fk_contraint contains entry with given name and child id.

It is worth mentioning that during CREATE TABLE processing schema id
changes and check in OP_OpenRead opcode fails (which in turn shows that
pointer to space may expire). On the other hand, _fk_constraint space
itself remains immutable, so as a temporary workaround lets use flag
indicating pointer to system space passed to OP_OpenRead. It makes
possible to use pointer to space, even if schema has changed.

Closes #3271
---
 src/box/errcode.h            |  2 ++
 src/box/sql/build.c          | 43 +++++++++++++++++++++++++++++++------------
 src/box/sql/sqliteInt.h      | 10 +++++++---
 src/box/sql/trigger.c        | 24 ++++++++++++++++--------
 src/box/sql/vdbe.c           |  3 ++-
 test/box/misc.result         |  2 ++
 test/sql-tap/alter2.test.lua | 25 ++++++++++++++++++++++++-
 7 files changed, 84 insertions(+), 25 deletions(-)

diff --git a/src/box/errcode.h b/src/box/errcode.h
index 1558cfae8..a3c27d6b8 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -217,6 +217,8 @@ struct errcode_record {
 	/*162 */_(ER_FOREIGN_KEY_CONSTRAINT,	"Can not commit transaction: deferred foreign keys violations are not resolved") \
 	/*163 */_(ER_CREATE_FK_CONSTRAINT,	"Failed to create foreign key constraint '%s': %s") \
 	/*164 */_(ER_DROP_FK_CONSTRAINT,	"Failed to drop foreign key constraint '%s': %s") \
+	/*165 */_(ER_NO_SUCH_CONSTRAINT,	"Constraint %s does not exist") \
+	/*165 */_(ER_CONSTRAINT_EXISTS,		"Constraint %s already exists") \
 
 
 
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index c2d3cd035..20ace09e4 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -1784,6 +1784,20 @@ vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
 		sqlite3VdbeAddOp2(vdbe, OP_Integer, fk->parent_id,
 				  constr_tuple_reg + 2);
 	}
+	/*
+	 * Lets check that constraint with this name hasn't
+	 * been created before.
+	 */
+	const char *error_msg =
+		tt_sprintf(tnt_errcode_desc(ER_CONSTRAINT_EXISTS), name_copy);
+	if (vdbe_emit_halt_with_presence_test(parse_context,
+					      BOX_FK_CONSTRAINT_ID, 0,
+					      constr_tuple_reg, 2,
+					      ER_CONSTRAINT_EXISTS, error_msg,
+					      false, OP_NoConflict) != 0) {
+		free((void *) name_copy);
+		return;
+	}
 	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,
@@ -2224,6 +2238,17 @@ vdbe_fkey_code_drop(struct Parse *parse_context, const char *constraint_name,
 	sqlite3VdbeAddOp4(vdbe, OP_String8, 0, key_reg, 0, constraint_name,
 			  P4_DYNAMIC);
 	sqlite3VdbeAddOp2(vdbe, OP_Integer, child_id,  key_reg + 1);
+	const char *error_msg =
+		tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT),
+			   constraint_name);
+	if (vdbe_emit_halt_with_presence_test(parse_context,
+					      BOX_FK_CONSTRAINT_ID, 0,
+					      key_reg, 2, ER_NO_SUCH_CONSTRAINT,
+					      error_msg, false,
+					      OP_Found) != 0) {
+		free((void *) constraint_name);
+		return;
+	}
 	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);
@@ -4247,7 +4272,7 @@ sqlite3WithDelete(sqlite3 * db, With * pWith)
 
 int
 vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
-				  int index_id, const char *name_src,
+				  int index_id, int key_reg, uint32_t key_len,
 				  int tarantool_error_code,
 				  const char *error_src, bool no_error,
 				  int cond_opcode)
@@ -4257,22 +4282,16 @@ vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
 	assert(v != NULL);
 
 	struct sqlite3 *db = parser->db;
-	char *name = sqlite3DbStrDup(db, name_src);
-	if (name == NULL)
-		return -1;
 	char *error = sqlite3DbStrDup(db, error_src);
-	if (error == NULL) {
-		sqlite3DbFree(db, name);
+	if (error == NULL)
 		return -1;
-	}
 
 	int cursor = parser->nTab++;
 	vdbe_emit_open_cursor(parser, cursor, index_id, space_by_id(space_id));
-
-	int name_reg = parser->nMem++;
-	int label = sqlite3VdbeAddOp4(v, OP_String8, 0, name_reg, 0, name,
-				      P4_DYNAMIC);
-	sqlite3VdbeAddOp4Int(v, cond_opcode, cursor, label + 3, name_reg, 1);
+	sqlite3VdbeChangeP5(v, OPFLAG_SYSTEMSP);
+	int label = sqlite3VdbeCurrentAddr(v);
+	sqlite3VdbeAddOp4Int(v, cond_opcode, cursor, label + 3, key_reg,
+			     key_len);
 	if (no_error) {
 		sqlite3VdbeAddOp0(v, OP_Halt);
 	} else {
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 2489b31b2..f882d747d 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -3012,6 +3012,9 @@ struct Parse {
 					 * is fresh, even in case schema
 					 * changes previously.
 					 */
+#define OPFLAG_SYSTEMSP      0x40	/* OP_Open**: set if space pointer
+					 * points to system space.
+					 */
 
 /*
  * Each trigger present in the database schema is stored as an
@@ -4870,7 +4873,7 @@ table_column_nullable_action(struct Table *tab, uint32_t column);
 
 /**
  * Generate VDBE code to halt execution with correct error if
- * the object with specified name is already present (or doesn't
+ * the object with specified key is already present (or doesn't
  * present - configure with cond_opcodeq) in specified space.
  * The function allocates error and name resources for VDBE
  * itself.
@@ -4878,7 +4881,8 @@ table_column_nullable_action(struct Table *tab, uint32_t column);
  * @param parser Parsing context.
  * @param space_id Space to lookup identifier.
  * @param index_id Index identifier of key.
- * @param name_src Name of object to test on existence.
+ * @param key_reg Register where key to be found is held.
+ * @param key_len Lenght of key (number of resiters).
  * @param tarantool_error_code to set on halt.
  * @param error_src Error message to display on VDBE halt.
  * @param no_error Do not raise error flag.
@@ -4890,7 +4894,7 @@ table_column_nullable_action(struct Table *tab, uint32_t column);
  */
 int
 vdbe_emit_halt_with_presence_test(struct Parse *parser, int space_id,
-				  int index_id, const char *name_src,
+				  int index_id, int key_reg, uint32_t key_len,
 				  int tarantool_error_code,
 				  const char *error_src, bool no_error,
 				  int cond_opcode);
diff --git a/src/box/sql/trigger.c b/src/box/sql/trigger.c
index 801013b5a..c24235128 100644
--- a/src/box/sql/trigger.c
+++ b/src/box/sql/trigger.c
@@ -125,8 +125,14 @@ sql_trigger_begin(struct Parse *parse, struct Token *name, int tr_tm,
 		const char *error_msg =
 			tt_sprintf(tnt_errcode_desc(ER_TRIGGER_EXISTS),
 				   trigger_name);
+		char *name_copy = sqlite3DbStrDup(db, trigger_name);
+		if (name_copy == NULL)
+			goto trigger_cleanup;
+		int name_reg = ++parse->nMem;
+		sqlite3VdbeAddOp4(parse->pVdbe, OP_String8, 0, name_reg, 0,
+				  name_copy, P4_DYNAMIC);
 		if (vdbe_emit_halt_with_presence_test(parse, BOX_TRIGGER_ID, 0,
-						      trigger_name,
+						      name_reg, 1,
 						      ER_TRIGGER_EXISTS,
 						      error_msg, (no_err != 0),
 						      OP_NoConflict) != 0)
@@ -472,26 +478,28 @@ sql_drop_trigger(struct Parse *parser, struct SrcList *name, bool no_err)
 	if (db->mallocFailed)
 		goto drop_trigger_cleanup;
 	assert(db->pSchema != NULL);
-
+	struct Vdbe *v = sqlite3GetVdbe(parser);
 	/* Do not account nested operations: the count of such
 	 * operations depends on Tarantool data dictionary internals,
 	 * such as data layout in system spaces. Activate the counter
 	 * here to account DROP TRIGGER IF EXISTS case if the trigger
 	 * actually does not exist.
 	 */
-	if (!parser->nested) {
-		Vdbe *v = sqlite3GetVdbe(parser);
-		if (v != NULL)
-			sqlite3VdbeCountChanges(v);
-	}
+	if (!parser->nested && v != NULL)
+		sqlite3VdbeCountChanges(v);
 
 	assert(name->nSrc == 1);
 	const char *trigger_name = name->a[0].zName;
 	const char *error_msg =
 		tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_TRIGGER),
 			   trigger_name);
+	char *name_copy = sqlite3DbStrDup(db, trigger_name);
+	if (name_copy == NULL)
+		goto drop_trigger_cleanup;
+	int name_reg = ++parser->nMem;
+	sqlite3VdbeAddOp4(v, OP_String8, 0, name_reg, 0, name_copy, P4_DYNAMIC);
 	if (vdbe_emit_halt_with_presence_test(parser, BOX_TRIGGER_ID, 0,
-					      trigger_name, ER_NO_SUCH_TRIGGER,
+					      name_reg, 1, ER_NO_SUCH_TRIGGER,
 					      error_msg, no_err, OP_Found) != 0)
 		goto drop_trigger_cleanup;
 
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index b9723e2e7..0f227e637 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -3172,7 +3172,8 @@ case OP_OpenWrite:
 	 * during runtime.
 	 */
 	if (box_schema_version() != p->schema_ver &&
-	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0) {
+	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0 &&
+	    (pOp->p5 & OPFLAG_SYSTEMSP) == 0) {
 		p->expired = 1;
 		rc = SQLITE_ERROR;
 		sqlite3VdbeError(p, "schema version has changed: " \
diff --git a/test/box/misc.result b/test/box/misc.result
index 315499f3e..fb7c5311c 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -489,6 +489,8 @@ t;
   162: box.error.FOREIGN_KEY_CONSTRAINT
   163: box.error.CREATE_FK_CONSTRAINT
   164: box.error.DROP_FK_CONSTRAINT
+  165: box.error.NO_SUCH_CONSTRAINT
+  166: box.error.CONSTRAINT_EXISTS
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/sql-tap/alter2.test.lua b/test/sql-tap/alter2.test.lua
index e4470ecbb..be83c225f 100755
--- a/test/sql-tap/alter2.test.lua
+++ b/test/sql-tap/alter2.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(15)
+test:plan(17)
 
 -- This suite is aimed to test ALTER TABLE ADD CONSTRAINT statement.
 --
@@ -193,4 +193,27 @@ test:do_execsql_test(
         -- </alter2-3.2>
     })
 
+test:do_catchsql_test(
+    "alter2-4.1",
+    [[
+        DROP TABLE child;
+        CREATE TABLE child (id PRIMARY KEY, a UNIQUE);
+        ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES child;
+        ALTER TABLE child ADD CONSTRAINT fk FOREIGN KEY (a) REFERENCES child;
+    ]], {
+        -- <alter2-4.1>
+        1, "Constraint FK already exists"
+        -- </alter2-4.1>
+    })
+
+test:do_catchsql_test(
+    "alter2-4.2",
+    [[
+        ALTER TABLE child DROP CONSTRAINT fake;
+    ]], {
+        -- <alter2-4.2>
+        1, "Constraint FAKE does not exist"
+        -- </alter2-4.2>
+    })
+
 test:finish_test()
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] [PATCH 5/5] sql: remove SQLITE_OMIT_FOREIGN_KEY define guard
  2018-07-13  2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik
                   ` (3 preceding siblings ...)
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 4/5] sql: display error on FK creation and drop failure Nikita Pettik
@ 2018-07-13  2:04 ` 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
  6 siblings, 0 replies; 32+ messages in thread
From: Nikita Pettik @ 2018-07-13  2:04 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

We always compile with enabled foreign keys constraints. They still can
be turned off by <pragma foreign_keys = false> in runtime.

Follow-up #3271
---
 extra/mkkeywordhash.c   | 6 +-----
 src/box/sql/insert.c    | 2 --
 src/box/sql/pragma.c    | 2 --
 src/box/sql/pragma.h    | 6 ------
 src/box/sql/sqliteInt.h | 7 -------
 src/box/sql/vdbe.c      | 2 --
 src/box/sql/vdbeInt.h   | 4 ----
 src/box/sql/vdbeaux.c   | 4 ----
 8 files changed, 1 insertion(+), 32 deletions(-)

diff --git a/extra/mkkeywordhash.c b/extra/mkkeywordhash.c
index 6ba872acc..f8e6636f6 100644
--- a/extra/mkkeywordhash.c
+++ b/extra/mkkeywordhash.c
@@ -80,11 +80,7 @@ struct Keyword {
 #  define CONFLICT   0x00000080
 #endif
 #define EXPLAIN      0x00000100
-#ifdef SQLITE_OMIT_FOREIGN_KEY
-#  define FKEY       0
-#else
-#  define FKEY       0x00000200
-#endif
+#define FKEY         0x00000200
 #ifdef SQLITE_OMIT_PRAGMA
 #  define PRAGMA     0
 #else
diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c
index f7bee1a66..890a251b5 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -1877,7 +1877,6 @@ xferOptimization(Parse * pParse,	/* Parser context */
 		/* Tables have different CHECK constraints.  Ticket #2252 */
 		return 0;
 	}
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 	/* Disallow the transfer optimization if the destination table constains
 	 * any foreign key constraints.  This is more restrictive than necessary.
 	 * So the extra complication to make this rule less restrictive is probably
@@ -1888,7 +1887,6 @@ xferOptimization(Parse * pParse,	/* Parser context */
 	if ((user_session->sql_flags & SQLITE_ForeignKeys) != 0 &&
 	    dest->child_fkey != NULL)
 		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/pragma.c b/src/box/sql/pragma.c
index 8a736859a..50f1ff894 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -561,7 +561,6 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 	}
 #endif				/* SQLITE_OMIT_SCHEMA_PRAGMAS */
 
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 	case PragTyp_FOREIGN_KEY_LIST:{
 		if (zRight == NULL)
 			break;
@@ -599,7 +598,6 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 		}
 		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 b1a169ed1..68bc0d9c3 100644
--- a/src/box/sql/pragma.h
+++ b/src/box/sql/pragma.h
@@ -127,31 +127,25 @@ static const PragmaName aPragmaName[] = {
 	 /* iArg:      */ SQLITE_CountRows},
 #endif
 #if !defined(SQLITE_OMIT_FLAG_PRAGMAS)
-#if !defined(SQLITE_OMIT_FOREIGN_KEY)
 	{ /* zName:     */ "defer_foreign_keys",
 	 /* ePragTyp:  */ PragTyp_FLAG,
 	 /* ePragFlg:  */ PragFlg_Result0 | PragFlg_NoColumns1,
 	 /* ColNames:  */ 0, 0,
 	 /* iArg:      */ SQLITE_DeferFKs},
 #endif
-#endif
-#if !defined(SQLITE_OMIT_FOREIGN_KEY)
 	{ /* zName:     */ "foreign_key_list",
 	 /* ePragTyp:  */ PragTyp_FOREIGN_KEY_LIST,
 	 /* ePragFlg:  */
 	 PragFlg_NeedSchema | PragFlg_Result1 | PragFlg_SchemaOpt,
 	 /* ColNames:  */ 29, 8,
 	 /* iArg:      */ 0},
-#endif
 #if !defined(SQLITE_OMIT_FLAG_PRAGMAS)
-#if !defined(SQLITE_OMIT_FOREIGN_KEY)
 	{ /* zName:     */ "foreign_keys",
 	 /* ePragTyp:  */ PragTyp_FLAG,
 	 /* ePragFlg:  */ PragFlg_Result0 | PragFlg_NoColumns1,
 	 /* ColNames:  */ 0, 0,
 	 /* iArg:      */ SQLITE_ForeignKeys},
 #endif
-#endif
 #if !defined(SQLITE_OMIT_FLAG_PRAGMAS)
 	{ /* zName:     */ "full_column_names",
 	 /* ePragTyp:  */ PragTyp_FLAG,
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index f882d747d..36a2feaa5 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -4701,13 +4701,6 @@ 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).
- */
 void sqlite3FkCheck(Parse *, Table *, int, int, int *);
 void sqlite3FkDropTable(Parse *, SrcList *, Table *);
 void sqlite3FkActions(Parse *, Table *, ExprList *, int, int *);
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 0f227e637..c2dd5f093 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -4967,7 +4967,6 @@ case OP_Param: {           /* out2 */
 	break;
 }
 
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 /* Opcode: FkCounter P1 P2 * * *
  * Synopsis: fkctr[P1]+=P2
  *
@@ -5011,7 +5010,6 @@ case OP_FkIfZero: {         /* jump */
 	}
 	break;
 }
-#endif /* #ifndef SQLITE_OMIT_FOREIGN_KEY */
 
 /* Opcode: IfPos P1 P2 P3 * *
  * Synopsis: if r[P1]>0 then r[P1]-=P3, goto P2
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index e5ed94cea..8d4fa9bb7 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -511,11 +511,7 @@ void sqlite3VdbeMemAboutToChange(Vdbe *, Mem *);
 int sqlite3VdbeCheckMemInvariants(Mem *);
 #endif
 
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 int sqlite3VdbeCheckFk(Vdbe *, int);
-#else
-#define sqlite3VdbeCheckFk(p,i) 0
-#endif
 
 int sqlite3VdbeMemTranslate(Mem *, u8);
 #ifdef SQLITE_DEBUG
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index cec0ed647..2a551e09b 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -631,11 +631,9 @@ sqlite3VdbeAssertMayAbort(Vdbe * v, int mayAbort)
 			hasAbort = 1;
 			break;
 		}
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 		if (opcode == OP_FkCounter && pOp->p1 == 0 && pOp->p2 == 1) {
 			hasFkCounter = 1;
 		}
-#endif
 	}
 	sqlite3DbFree(v->db, sIter.apSub);
 
@@ -2333,7 +2331,6 @@ sqlite3VdbeCloseStatement(Vdbe * p, int eOp)
  * SQLITE_ERROR, set the result of the VM to SQLITE_CONSTRAINT_FOREIGNKEY
  * and write an error message to it. Then return SQLITE_ERROR.
  */
-#ifndef SQLITE_OMIT_FOREIGN_KEY
 int
 sqlite3VdbeCheckFk(Vdbe * p, int deferred)
 {
@@ -2347,7 +2344,6 @@ sqlite3VdbeCheckFk(Vdbe * p, int deferred)
 	}
 	return SQLITE_OK;
 }
-#endif
 
 int
 sql_txn_begin(Vdbe *p)
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 0/5] Move FK constraints to server
  2018-07-13  2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik
                   ` (4 preceding siblings ...)
  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 ` Vladislav Shpilevoy
  2018-08-07 14:57 ` Kirill Yukhin
  6 siblings, 0 replies; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-17 21:04 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik

Hello. Thanks for the patchset! See 5 possible
typos below.

On 13/07/2018 05:04, Nikita Pettik wrote:
> Branch: https://github.com/tarantool/tarantool/commits/np/move-fk-to-server
> Issue: https://github.com/tarantool/tarantool/issues/3271
> 
> The aim of this patch-set is to move foreign key constraint to server
> and them be closer to ANSI specification.

1. Them -> then?

> 
> First patch is preliminary and enables additional restrictions
> for FK constraints (ban opportunity to drop space referenced space,

2. 'Space referenced space'? Can not parse.

> create FK referencing VIEW etc).
> 
> In original SQLite FK constraints may appear only during CREATE TABLE
> statement. Thus, it was enough to hold string of CREATE TABLE statement
> and reparse it once on instance loading. This approach defers all
> resolutions until FK usage. For instance:
> 
> CREATE TABLE t1(id PRIMARY KEY REFERENCES t2);
> CREATE TABLE t2(id PRIMARY KEY);
> 
> We decided to use another approach - where FK constraints are always consistent
> and all DD links are kept up. For instance, if we attempted to satisfy all
> restrictions using SQLite schema - we wouldn't be able to create circular
> dependencies. To support circular dependecies, we must allow to create them

3. dependecies -> dependencies.

> after space itself. In turn, to create FK constraints outside CREATE STATEMENT,
> we must persist them.  To implement these steps, firstly _fk_constraint system
> space is added - it contains tuples describing FK. Then, separate SQL statement
> <ALTER TABLE name ADD CONSTRAINT ...> is introduced which processes insertion
> and deletion from this space. Finally, FK processing has been refactored to
> rely on new DD in server (struct fkey and struct fkey_def). It seems that
> perfomance of FK handling has become a little bit better: now we don't need

4. perfomance -> performance.

> find suitable index on each FK invocation - its id is held into FK struct
> itself.
> 
> The last patch is simple follow-up which removes obsolete define guard
> for FK constraints.
> 
> Nikita Pettik (5):
>    sql: prohibit creation of FK on unexisting tables

5. unexisting -> non-existing? But I am not so good in English as you,
so maybe I am wrong.

>    schema: add new system space for FK constraints
>    sql: introduce ADD CONSTRAINT statement
>    sql: display error on FK creation and drop failure
>    sql: remove SQLITE_OMIT_FOREIGN_KEY define guard
> 
>   extra/mkkeywordhash.c                |    9 +-
>   src/box/CMakeLists.txt               |    1 +
>   src/box/alter.cc                     |  432 +++++++++++++-
>   src/box/alter.h                      |    1 +
>   src/box/bootstrap.snap               |  Bin 1704 -> 1798 bytes
>   src/box/errcode.h                    |    6 +
>   src/box/fkey.c                       |   69 +++
>   src/box/fkey.h                       |  169 ++++++
>   src/box/lua/schema.lua               |    6 +
>   src/box/lua/space.cc                 |    2 +
>   src/box/lua/upgrade.lua              |   16 +
>   src/box/schema.cc                    |   16 +
>   src/box/schema_def.h                 |   14 +
>   src/box/space.c                      |    2 +
>   src/box/space.h                      |    3 +
>   src/box/sql.c                        |   24 +
>   src/box/sql/alter.c                  |    7 -
>   src/box/sql/build.c                  |  601 ++++++++++++++-----
>   src/box/sql/callback.c               |    2 -
>   src/box/sql/delete.c                 |    6 +-
>   src/box/sql/expr.c                   |   10 +-
>   src/box/sql/fkey.c                   | 1077 ++++++++++------------------------
>   src/box/sql/insert.c                 |   21 +-
>   src/box/sql/main.c                   |    5 -
>   src/box/sql/parse.y                  |   37 +-
>   src/box/sql/pragma.c                 |  242 +-------
>   src/box/sql/pragma.h                 |   13 -
>   src/box/sql/prepare.c                |    5 +
>   src/box/sql/sqliteInt.h              |  185 +++---
>   src/box/sql/status.c                 |    5 +-
>   src/box/sql/tarantoolInt.h           |   13 +
>   src/box/sql/trigger.c                |   24 +-
>   src/box/sql/update.c                 |    4 +-
>   src/box/sql/vdbe.c                   |   51 +-
>   src/box/sql/vdbeInt.h                |    4 -
>   src/box/sql/vdbeaux.c                |    4 -
>   test/box/access_misc.result          |    5 +
>   test/box/access_sysview.result       |    6 +-
>   test/box/alter.result                |    5 +-
>   test/box/misc.result                 |    4 +
>   test/engine/iterator.result          |    2 +-
>   test/sql-tap/alter.test.lua          |    2 +-
>   test/sql-tap/alter2.test.lua         |  219 +++++++
>   test/sql-tap/fkey1.test.lua          |   65 +-
>   test/sql-tap/fkey2.test.lua          |  148 ++---
>   test/sql-tap/fkey3.test.lua          |   19 +-
>   test/sql-tap/fkey4.test.lua          |    2 +-
>   test/sql-tap/orderby1.test.lua       |    6 +-
>   test/sql-tap/suite.ini               |    1 +
>   test/sql-tap/table.test.lua          |   19 +-
>   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 +-
>   test/sql/foreign-keys.result         |  316 ++++++++++
>   test/sql/foreign-keys.test.lua       |  144 +++++
>   56 files changed, 2534 insertions(+), 1529 deletions(-)
>   create mode 100644 src/box/fkey.c
>   create mode 100644 src/box/fkey.h
>   create mode 100755 test/sql-tap/alter2.test.lua
>   create mode 100644 test/sql/foreign-keys.result
>   create mode 100644 test/sql/foreign-keys.test.lua
> 

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 4/5] sql: display error on FK creation and drop failure
  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   ` Vladislav Shpilevoy
  2018-07-25 10:03     ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-17 21:04 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik

Thanks for the patch! See 4 comments below.

On 13/07/2018 05:04, Nikita Pettik wrote:
> Before insertion to _fk_constraint we must be sure that there in no
> entry with given <name, child id>. Otherwise, insertion will fail and
> 'duplicate key' will be shown. Such error message doesn't seem to be
> informative enough, so lets manually iterate through whole space looking
> for appropriate record.

1. As I know, vdbe_emit_halt_with_presence_test do not iterate through
the whole space. It uses an index to search for the record fast.

> The same is for dropping constraint, but here vice versa: we test
> that _fk_contraint contains entry with given name and child id.

2. Typo: _fk_contraint -> _fk_constraint.

> 
> It is worth mentioning that during CREATE TABLE processing schema id
> changes and check in OP_OpenRead opcode fails (which in turn shows that
> pointer to space may expire). On the other hand, _fk_constraint space
> itself remains immutable, so as a temporary workaround lets use flag
> indicating pointer to system space passed to OP_OpenRead. It makes
> possible to use pointer to space, even if schema has changed.
> 
> Closes #3271
> ---
>   src/box/errcode.h            |  2 ++
>   src/box/sql/build.c          | 43 +++++++++++++++++++++++++++++++------------
>   src/box/sql/sqliteInt.h      | 10 +++++++---
>   src/box/sql/trigger.c        | 24 ++++++++++++++++--------
>   src/box/sql/vdbe.c           |  3 ++-
>   test/box/misc.result         |  2 ++
>   test/sql-tap/alter2.test.lua | 25 ++++++++++++++++++++++++-
>   7 files changed, 84 insertions(+), 25 deletions(-)
> 
> diff --git a/src/box/sql/build.c b/src/box/sql/build.c
> index c2d3cd035..20ace09e4 100644
> --- a/src/box/sql/build.c
> +++ b/src/box/sql/build.c
> @@ -1784,6 +1784,20 @@ vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
>   		sqlite3VdbeAddOp2(vdbe, OP_Integer, fk->parent_id,
>   				  constr_tuple_reg + 2);
>   	}
> +	/*
> +	 * Lets check that constraint with this name hasn't
> +	 * been created before.
> +	 */
> +	const char *error_msg =
> +		tt_sprintf(tnt_errcode_desc(ER_CONSTRAINT_EXISTS), name_copy);
> +	if (vdbe_emit_halt_with_presence_test(parse_context,
> +					      BOX_FK_CONSTRAINT_ID, 0,
> +					      constr_tuple_reg, 2,
> +					      ER_CONSTRAINT_EXISTS, error_msg,
> +					      false, OP_NoConflict) != 0) {
> +		free((void *) name_copy);

3. Name_copy is allocated on db.

> +		return;
> +	}
> diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
> index b9723e2e7..0f227e637 100644
> --- a/src/box/sql/vdbe.c
> +++ b/src/box/sql/vdbe.c
> @@ -3172,7 +3172,8 @@ case OP_OpenWrite:
>   	 * during runtime.
>   	 */
>   	if (box_schema_version() != p->schema_ver &&
> -	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0) {
> +	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0 &&
> +	    (pOp->p5 & OPFLAG_SYSTEMSP) == 0) {

4. Why not p5 & (FRESH_PTR | SYSTEMSP) ?

>   		p->expired = 1;
>   		rc = SQLITE_ERROR;
>   		sqlite3VdbeError(p, "schema version has changed: " \

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-07-13  2:04 ` [tarantool-patches] [PATCH 3/5] sql: introduce ADD CONSTRAINT statement Nikita Pettik
@ 2018-07-17 21:05   ` Vladislav Shpilevoy
  2018-07-25 10:03     ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-17 21:05 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik

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>
>       })
>   

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  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   ` Vladislav Shpilevoy
  2018-07-25 10:03     ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-17 21:05 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik

Thanks for the patch! See 18 comments below and a patch on
the branch.

On 13/07/2018 05:04, Nikita Pettik wrote:
> This patch introduces new system space to persist foreign keys

1. Typos: contraints, chils.

> contraints. Format of the space:
> 
> _fk_constraint (space id = 350)
> 
> [<contraint name> STR, <parent id> UINT, <child id> UINT,>   <is deferred> BOOL, <match> STR, <on delete action> STR,
>   <on update action> STR, <field links> ARRAY<MAP< UINT : UINT >>]
> 
> FK constraint is local to space, so every pair <FK name, child id>
> is unique (and it is PK in _fk_constraint space).
> 
> After insertion into this space, new instance describing FK constraint
> is created. FK constraints are held in data-dictionary as two lists
> (for child and parent constraints) in struct space.
> 
> There is a list of FK restrictions:
>   - At the time of FK creation parent and chils spaces must exist;
>   - VIEWs can't be involved into FK processing;
>   - Child space must be empty;
>   - Types of referencing and referenced fields must match;

2. How about not match, but fit? See field_type1_contains_type2() and
its usages.

>   - Collations of referencing and referenced fields must match;
>   - Referenced fields must compose unique index;
> 
> Until space (child) features FK constraints it isn't allowed to be
> dropped. Implicitly referenced index also can't be dropped
> (and that is why parent space can't be dropped). But :drop() method
> of child space firstly deletes all FK constraint (the same as SQL
> triggers, indexes etc) and then removes entry from _space.
> 
> Part of #3271
> ---
>   src/box/CMakeLists.txt         |   1 +
>   src/box/alter.cc               | 432 ++++++++++++++++++++++++++++++++++++++++-
>   src/box/alter.h                |   1 +
>   src/box/bootstrap.snap         | Bin 1704 -> 1798 bytes
>   src/box/errcode.h              |   4 +
>   src/box/fkey.c                 |  69 +++++++
>   src/box/fkey.h                 | 163 ++++++++++++++++
>   src/box/lua/schema.lua         |   6 +
>   src/box/lua/space.cc           |   2 +
>   src/box/lua/upgrade.lua        |  16 ++
>   src/box/schema.cc              |  16 ++
>   src/box/schema_def.h           |  14 ++
>   src/box/space.c                |   2 +
>   src/box/space.h                |   3 +
>   src/box/sql.c                  |   8 +
>   src/box/sql/fkey.c             |  32 +--
>   src/box/sql/tarantoolInt.h     |   1 +
>   test/box/access_misc.result    |   5 +
>   test/box/access_sysview.result |   6 +-
>   test/box/alter.result          |   5 +-
>   test/box/misc.result           |   2 +
>   test/engine/iterator.result    |   2 +-
>   test/sql/foreign-keys.result   | 316 ++++++++++++++++++++++++++++++
>   test/sql/foreign-keys.test.lua | 144 ++++++++++++++
>   24 files changed, 1214 insertions(+), 36 deletions(-)
>   create mode 100644 src/box/fkey.c
>   create mode 100644 src/box/fkey.h
>   create mode 100644 test/sql/foreign-keys.result
>   create mode 100644 test/sql/foreign-keys.test.lua
> 
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 89b11dcd3..aaa56bd21 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -1889,6 +1916,22 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
>   			  "can not add a secondary key before primary");
>   	}
>   
> +	/*
> +	 * Can't drop index if foreign key constraints references
> +	 * this index.
> +	 */
> +	if (new_tuple == NULL) {

3. But it can be non-drop, but alter, for example. Drop case is processed
below:

     /* Case 1: drop the index, if it is dropped. */
     if (old_index != NULL && new_tuple == NULL) {

What if I want to rename an index? Also see index_def_change_requires_rebuild.

> +		struct fkey *fk = old_space->parent_fkey;
> +		while (fk != NULL) {
> +			if (old_space->parent_fkey->index_id == iid) {
> +				tnt_raise(ClientError, ER_ALTER_SPACE,
> +					  space_name(old_space),
> +					  "can not drop referenced index");
> +			}
> +			fk = fk->fkey_parent_next;
> +		}
> +	}
> +
> @@ -3404,6 +3447,387 @@ on_replace_dd_trigger
>   	txn_on_commit(txn, on_commit);
>   }
>   
> +/**
> + * Decode MsgPack array of links. It consists from maps:
> + * {parent_id (UINT) : child_id (UINT)}.
> + *
> + * @param data MsgPack array of links.
> + * @param[out] out_count Count of links.
> + * @param constraint_name Constraint name to use in error
> + *			  messages.
> + * @param constraint_len Length of constraint name.
> + * @param errcode Errcode for client errors.
> + * @retval Array of links.
> + */
> +static struct field_link *
> +fkey_links_decode(const char *data, uint32_t *out_count,
> +		  const char *constraint_name, uint32_t constraint_len,
> +		  uint32_t errcode)
> +{
> +	assert(mp_typeof(*data) == MP_ARRAY);
> +	uint32_t count = mp_decode_array(&data);
> +	if (count == 0) {
> +		tnt_raise(ClientError, errcode,
> +			  tt_cstr(constraint_name, constraint_len),
> +			  "at least one link must be specified");
> +	}
> +	*out_count = count;
> +	size_t size = count * sizeof(struct field_link);
> +	struct field_link *region_links =
> +		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
> +	memset(region_links, 0, size);
> +	const char **map = &data;
> +	for (uint32_t i = 0; i < count; ++i) {
> +		uint32_t map_sz = mp_decode_map(map);
> +		if (map_sz != 2) {
> +			tnt_raise(ClientError, errcode,
> +				  tt_cstr(constraint_name, constraint_len),
> +				  tt_sprintf("link must be map with 2 fields"));
> +		}
> +		if (mp_typeof(**map) != MP_STR) {
> +			tnt_raise(ClientError, errcode,
> +				  tt_cstr(constraint_name, constraint_len),
> +				  tt_sprintf("link %d is not map "\
> +					     "with string keys", i));
> +		}

4. What about a second key? mp_typeof(**map) right after decoding its
header returns type of the first key. But second can be of another type.

And why do we need this {child = , parent = }, ... sequence? Lets just
use {<uint>, <uint>}, {<uint>, <uint>}, {<uint>, <uint>} ... It is more
compact and simpler to parse. Or even just two arrays: first child and
second parent field numbers. It is the most canonical way similar to
SQL, it is not? When you specify two column ranges.

But in the internal format I vote to keep field_link. In the next
patches I see, that common usage case is to iterate over the pairs,
so for cache it is better to keep them.

> +		for (uint8_t j = 0; j < map_sz; ++j) {
> +			uint32_t key_len;
> +			const char *key = mp_decode_str(map, &key_len);
> +			if (key_len == 6 &&
> +			    memcmp(key, "parent", key_len) == 0) {
> +				region_links[i].parent_field =
> +					mp_decode_uint(map);
> +			} else if (key_len == 5 &&
> +				   memcmp(key, "child", key_len) == 0) {
> +				region_links[i].child_field =
> +					mp_decode_uint(map);
> +			} else {
> +				char *errmsg = tt_static_buf();
> +				snprintf(errmsg, TT_STATIC_BUF_LEN,
> +					 "unexpected key of link %d '%.*s'", i,
> +					 key_len, key);
> +				tnt_raise(ClientError, errcode,
> +					  tt_cstr(constraint_name,
> +						  constraint_len), errmsg);
> +			}
> +		}
> +	}
> +	return region_links;
> +}
> +
> +/**
> + * Replace entry in child's and parent's lists of
> + * FK constraints.
> + *
> + * @param child Child space of FK constraint.
> + * @param parent Parent space of FK constraint.
> + * @param new_fkey Constraint to be added to child and parent.
> + * @param[out] old_fkey Constraint to be found and replaced.
> + */
> +static void
> +fkey_list_replace(struct space *child, struct space *parent, const char *name,
> +		  struct fkey *new_fkey, struct fkey **old_fkey)
> +{

5. I see that for fkey you actually have invented rlist. Please, see rlist.h
and use it for fkeys. It already has rlist_foreach_entry, rlist_add,
rlist_add_tail, rlist_del etc. The struct foreign key will have two
rlist links: in_child and in_parent.

> +	*old_fkey = NULL;
> +	struct fkey **fk = &parent->parent_fkey;
> +	while (*fk != NULL && !(strcmp((*fk)->def->name, name) == 0 &&
> +			        (*fk)->def->child_id == child->def->id))
> +		fk = &((*fk)->fkey_parent_next);
> +	if (*fk != NULL) {
> +		*old_fkey = *fk;
> +		*fk = (*fk)->fkey_parent_next;
> +	}
> +	if (new_fkey != NULL) {
> +		new_fkey->fkey_parent_next = parent->parent_fkey;
> +		parent->parent_fkey = new_fkey;
> +	}
> +	fk = &child->child_fkey;
> +	/* In child's list all constraints are unique by name. */
> +	while (*fk != NULL && strcmp((*fk)->def->name, name) != 0)
> +		fk = &((*fk)->fkey_child_next);
> +	if (*fk != NULL) {
> +		assert(*old_fkey == *fk);
> +		*fk = (*fk)->fkey_child_next;
> +	}
> +	if (new_fkey != NULL) {
> +		new_fkey->fkey_child_next = child->child_fkey;
> +		child->child_fkey = new_fkey;
> +	}
> +}
> +
> +/**
> + * On rollback of creation we remove FK constraint from DD, i.e.
> + * from parent's and child's lists of constraints and
> + * release memory.
> + */
> +static void
> +on_create_fkey_rollback(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	struct space *parent = space_by_id(fk->def->parent_id);
> +	struct space *child = space_by_id(fk->def->child_id);
> +	struct fkey *fkey = NULL;
> +	fkey_list_replace(child, parent, fk->def->name, NULL, &fkey);
> +	fkey_delete(fkey);

6. After you fixed the previous remark, this function would
collapse into two rlist_del, it is not? The same about the
next function. Btw, these functions are identical. Can you
keep only one?

Same about on_drop_fkey_commit and on_replace_fkey_commit.

> +}
> +
> +/** Return old FK and release memory for the new one. */
> +static void
> +on_replace_fkey_rollback(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	struct space *parent = space_by_id(fk->def->parent_id);
> +	struct space *child = space_by_id(fk->def->child_id);
> +	struct fkey *old_fkey = NULL;
> +	fkey_list_replace(child, parent, fk->def->name, fk, &old_fkey);
> +	fkey_delete(old_fkey);
> +}
> +
> +/** Release memory for old foreign key. */
> +static void
> +on_replace_fkey_commit(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	fkey_delete(fk);
> +}
> +
> +/** On rollback of drop simply return back FK to DD. */
> +static void
> +on_drop_fkey_rollback(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk_to_restore = (struct fkey *)trigger->data;
> +	struct space *parent = space_by_id(fk_to_restore->def->parent_id);
> +	struct space *child = space_by_id(fk_to_restore->def->child_id);
> +	struct fkey *old_fk;
> +	fkey_list_replace(child, parent, fk_to_restore->def->name, fk_to_restore,
> +			  &old_fk);
> +	assert(old_fk == NULL);
> +}
> +
> +/**
> + * On commit of drop we have already deleted foreign key from
> + * both (parent's and child's) lists, so just release memory.
> + */
> +static void
> +on_drop_fkey_commit(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	fkey_delete(fk);
> +}
> +
> +/** A trigger invoked on replace in the _fk_constraint space. */
> +static void
> +on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
> +{
> +	struct txn *txn = (struct txn *) event;
> +	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
> +	struct txn_stmt *stmt = txn_current_stmt(txn);
> +	struct tuple *old_tuple = stmt->old_tuple;
> +	struct tuple *new_tuple = stmt->new_tuple;
> +	if (new_tuple != NULL) {
> +		/* Create or replace foreign key. */
> +		struct fkey_def *fk_def =
> +			fkey_def_new_from_tuple(new_tuple,
> +						ER_CREATE_FK_CONSTRAINT);
> +		auto fkey_def_guard = make_scoped_guard([=] { free(fk_def); });
> +		struct space *child_space =
> +			space_cache_find_xc(fk_def->child_id);
> +		if (child_space->def->opts.is_view) {
> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +				  fk_def->name,
> +				  "referencing space can't be VIEW");
> +		}
> +		struct space *parent_space =
> +			space_cache_find_xc(fk_def->parent_id);
> +		if (parent_space->def->opts.is_view) {
> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +				  fk_def->name,
> +				  "referenced space can't be VIEW");
> +		}
> +		/*
> +		 * FIXME: until SQL triggers are completely
> +		 * integrated into server (i.e. we are able to
> +		 * invoke triggers even if DML occurred via Lua
> +		 * interface), it makes no sense to provide any
> +		 * checks on existing data in space.
> +		 */
> +		struct index *pk = space_index(child_space, 0);
> +		if (index_count(pk, ITER_ALL, NULL, 0) > 0) {

7. Lets better use index_size. Index_count on Vinyl takes O(N) time
scanning disk. I think, it is ok to forbid new fk on a Vinyl space,
that logically is empty, but actually still contains some non-compacted
garbage. Anyway user now is able to force the compaction and try to
create fk again.

> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +				  fk_def->name,
> +				  "referencing space must be empty");
> +		}
> +		/* Check types of referenced fields. */
> +		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
> +			uint32_t child_fieldno = fk_def->links[i].child_field;
> +			uint32_t parent_fieldno = fk_def->links[i].parent_field;
> +			if (child_fieldno >= child_space->def->field_count ||
> +			    parent_fieldno >= parent_space->def->field_count) {
> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +					  fk_def->name, "foreign key refers to "
> +						        "nonexistent field");
> +			}
> +			if (child_space->def->fields[child_fieldno].type !=
> +			    parent_space->def->fields[parent_fieldno].type) {
> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +					  fk_def->name, "field type mismatch");
> +			}
> +			if (child_space->def->fields[child_fieldno].coll_id !=
> +			    parent_space->def->fields[parent_fieldno].coll_id) {
> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +					  fk_def->name,
> +					  "field collation mismatch");
> +			}
> +		}
> +		/*
> +		 * Search for suitable index in parent space:
> +		 * it must be unique and consist exactly from
> +		 * referenced columns (but order may be different).

8. Should this index has the same collations? For a column we can define
multiple collations in different indexes. And in one index the value may
be unique, but in another may be not.

> +		 */
> +		struct index *fk_index = NULL;
> +		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
> +			struct index *idx = space_index(parent_space, i);
> +			if (!idx->def->opts.is_unique)
> +				continue;
> +			if (idx->def->key_def->part_count !=
> +			    fk_def->field_count)
> +				continue;
> +			uint32_t j;
> +			for (j = 0; j < fk_def->field_count; ++j) {
> +				if (idx->def->key_def->parts[j].fieldno !=
> +				    fk_def->links[j].parent_field)
> +					break;
> +			}
> +			if (j != fk_def->field_count)
> +				continue;

9. See key_def_find.

10. Above you have said that order may be different, but here I see
that once you have found a unique index, you merely check that it
has sequentially the same field numbers as the fk.

> +			fk_index = idx;
> +			break;
> +		}
> +		if (fk_index == NULL) {
> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +				  fk_def->name, "referenced fields don't "
> +					      "compose unique index");
> +		}
> +		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));

11. I know how you like memory mapping. How about merge fkey_def into
fkey memory? I see, that it is relatively simple and linear thing.

> +		if (fkey == NULL)
> +			tnt_raise(OutOfMemory, sizeof(*fkey), "malloc", "fkey");
> +		auto fkey_guard = make_scoped_guard([=] { free(fkey); });
> +		memset(fkey, 0, sizeof(*fkey));
> +		fkey->def = fk_def;
> +		fkey->index_id = fk_index->def->iid;
> +		struct fkey *old_fk;
> +		fkey_list_replace(child_space, parent_space, fk_def->name,
> +				  fkey, &old_fk);
> +		if (old_tuple == NULL) {
> +			struct trigger *on_rollback =
> +				txn_alter_trigger_new(on_create_fkey_rollback,
> +						      fkey);
> +			txn_on_rollback(txn, on_rollback);
> +			assert(old_fk == NULL);
> +		} else {
> +			struct trigger *on_rollback =
> +				txn_alter_trigger_new(on_replace_fkey_rollback,
> +						      fkey);
> +			txn_on_rollback(txn, on_rollback);
> +			struct trigger *on_commit =
> +				txn_alter_trigger_new(on_replace_fkey_commit,
> +						      old_fk);
> +			txn_on_commit(txn, on_commit);
> +		}
> +		fkey_def_guard.is_active = false;
> +		fkey_guard.is_active = false;
> +	} else if (new_tuple == NULL && old_tuple != NULL) {
> +		/* Drop foreign key. */
> +		struct fkey_def *fk_def =
> +			fkey_def_new_from_tuple(old_tuple,
> +						ER_DROP_FK_CONSTRAINT);
> +		auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
> +		struct space *parent_space =
> +			space_cache_find_xc(fk_def->parent_id);
> +		struct space *child_space =
> +			space_cache_find_xc(fk_def->child_id);
> +		struct fkey *old_fkey = NULL;
> +		fkey_list_replace(child_space, parent_space, fk_def->name, NULL,
> +				  &old_fkey);
> +		struct trigger *on_commit =
> +			txn_alter_trigger_new(on_drop_fkey_commit, old_fkey);
> +		txn_on_commit(txn, on_commit);
> +		struct trigger *on_rollback =
> +			txn_alter_trigger_new(on_drop_fkey_rollback, old_fkey);
> +		txn_on_rollback(txn, on_rollback);
> +	}
> +}
> +
> diff --git a/src/box/fkey.c b/src/box/fkey.c
> new file mode 100644
> index 000000000..e45889a0d
> --- /dev/null
> +++ b/src/box/fkey.c
> +void
> +fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
> +{

12. Why do you need this function when sql_trigger_delete exists?

> +	if (p != NULL) {
> +		struct TriggerStep *step = p->step_list;
> +		sql_expr_delete(db, step->pWhere, false);
> +		sql_expr_list_delete(db, step->pExprList);
> +		sql_select_delete(db, step->pSelect);
> +		sql_expr_delete(db, p->pWhen, false);
> +		sqlite3DbFree(db, p);
> +	}
> +}
> +
> diff --git a/src/box/fkey.h b/src/box/fkey.h
> new file mode 100644
> index 000000000..1b6ea71d9
> --- /dev/null
> +++ b/src/box/fkey.h
> @@ -0,0 +1,163 @@
> +#ifndef TARANTOOL_BOX_FKEY_H_INCLUDED
> +#define TARANTOOL_BOX_FKEY_H_INCLUDED
> +/*
> + * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
> + *
> + * Redistribution and use in source and binary forms, with or
> + * without modification, are permitted provided that the following
> + * conditions are met:
> + *
> + * 1. Redistributions of source code must retain the above
> + *    copyright notice, this list of conditions and the
> + *    following disclaimer.
> + *
> + * 2. Redistributions in binary form must reproduce the above
> + *    copyright notice, this list of conditions and the following
> + *    disclaimer in the documentation and/or other materials
> + *    provided with the distribution.
> + *
> + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
> + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
> + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
> + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
> + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
> + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
> + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
> + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
> + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
> + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
> + * SUCH DAMAGE.
> + */
> +#include <stdbool.h>
> +#include <stdint.h>
> +
> +#include "space.h"
> +
> +#if defined(__cplusplus)
> +extern "C" {
> +#endif /* defined(__cplusplus) */
> +
> +struct sqlite3;

13. Is it possible to drop this announcement after
you replaced fkey_trigger_delete with sql_trigger_delete?

> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index 87c79bdde..30d8b0081 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -506,6 +506,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
>       local _vindex = box.space[box.schema.VINDEX_ID]
>       local _truncate = box.space[box.schema.TRUNCATE_ID]
>       local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
> +    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
>       local sequence_tuple = _space_sequence:delete{space_id}
>       if sequence_tuple ~= nil and sequence_tuple[3] == true then
>           -- Delete automatically generated sequence.
> @@ -519,6 +520,11 @@ box.schema.space.drop = function(space_id, space_name, opts)
>           local v = keys[i]
>           _index:delete{v[1], v[2]}
>       end
> +    for _, t in _fk_constraint.index.primary:pairs() do
> +        if t.child_id == space_id then

14. Fullscan here looks bad. Can we create a secondary index on
_fk_constraint by child_id? Or create a Lua C function, that takes
space and returns names of its child fks? If we use the index, then
we are able to optimize VDBE deletion of the FKs in the main patch
(sql: introduce ADD CONSTRAINT statement), where you delete by
the composite key {name, child_id}, but actually child_id is enough.

> +            _fk_constraint:delete{t.name, t.child_id}
> +        end
> +    end
> diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
> index f112a93ae..772f55cb2 100644
> --- a/src/box/lua/upgrade.lua
> +++ b/src/box/lua/upgrade.lua
> @@ -509,6 +509,22 @@ local function upgrade_to_2_1_0()
>                     {unique = true}, {{0, 'string'}, {1, 'string'},
>                                       {5, 'scalar'}}}
>   
> +    local fk_constr_ft = {{name='name', type='string'},
> +                          {name='child_id', type='unsigned'},
> +                          {name='parent_id', type='unsigned'},
> +                          {name='deferred', type='boolean'},

15. Flag, so 'is_deferred', it is not?

> +                          {name='match', type='string'},
> +                          {name='on_delete', type='string'},
> +                          {name='on_update', type='string'},
> +                          {name='links', type='array'}}
> +    log.info("create space _fk_constraint")
> +    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
> +                  0, setmap({}), fk_constr_ft}
> +
> +    log.info("create index primary on _fk_constraint")
> +    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
> +                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
> +
>       -- Nullability wasn't skipable. This was fixed in 1-7.
>       -- Now, abscent field means NULL, so we can safely set second
>       -- field in format, marking it nullable.
> diff --git a/src/box/space.h b/src/box/space.h
> index 7da2ee51f..fc5e8046f 100644
> --- a/src/box/space.h
> +++ b/src/box/space.h
> @@ -183,6 +183,9 @@ struct space {
>   	 * of index id.
>   	 */
>   	struct index **index;
> +	/** Foreign key constraints. */

16. It would be good to have here an explanation about who is
child, who is parent.

> +	struct fkey *parent_fkey;
> +	struct fkey *child_fkey;
>   };
>   
> diff --git a/test/box/access_misc.result b/test/box/access_misc.result
> index 5a2563d55..62c92ca03 100644
> --- a/test/box/access_misc.result
> +++ b/test/box/access_misc.result
> @@ -809,6 +809,11 @@ box.space._space:select()
>     - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
>           'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
>         {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
> +  - [350, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
> +      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
> +      {'name': 'deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
> +      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
> +      {'name': 'links', 'type': 'array'}]]

17. Please, keep spare room after sql_stat4 in 8 identifiers for future needs. So as
_fk_constraint was 356 (sql_stat1 348 + 8).

>   ...
>   box.space._func:select()
>   ---
> diff --git a/test/engine/iterator.result b/test/engine/iterator.result
> index a36761df8..ba9b0545a 100644
> --- a/test/engine/iterator.result
> +++ b/test/engine/iterator.result
> @@ -4211,7 +4211,7 @@ s:replace{35}
>   ...
>   state, value = gen(param,state)
>   ---
> -- error: 'builtin/box/schema.lua:1049: usage: next(param, state)'
> +- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'

18. This test fails on each schema change. Lets remove this file:line
from the error message alongside the patch.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  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   ` Vladislav Shpilevoy
  2018-07-25 10:03     ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-17 21:05 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik

Thanks for the patch! See 12 comments below and a
separate commit on the branch.

On 13/07/2018 05:04, Nikita Pettik wrote:
> Originally, SQLite allows to create table with foreign keys contraint

1. contraint -> constraint. And two the same typos below.

> which refers to yet not created parent table. For instance:
> 
> CREATE TABLE child(id INT PRIMARY KEY REFERENCES parent);
> CREATE TABLE parent(id INT PRIMARY KEY);
> 
> This patch bans such ability since it contradicts SQL ANSI.
> Moreover, SQLite allows to drop parent table if deletion of all rows
> wouldn't result in FK contraint violations. This feature has been
> removed since in such situation child table would become inconsistent.
> 
> Finally, within current patch ability to create FK contraints on VIEWs
> is banned as well.
> 
> Part of #3271
> ---
>   src/box/sql/build.c                  | 41 ++++++++++++----
>   src/box/sql/fkey.c                   | 95 +++---------------------------------
>   src/box/sql/sqliteInt.h              |  1 -
>   src/box/sql/vdbe.c                   | 30 ------------
>   test/sql-tap/alter.test.lua          |  6 +--
>   test/sql-tap/fkey1.test.lua          | 18 +++----
>   test/sql-tap/fkey2.test.lua          | 25 ++++------
>   test/sql-tap/fkey3.test.lua          |  4 +-
>   test/sql-tap/suite.ini               |  1 +
>   test/sql-tap/table.test.lua          |  1 +
>   test/sql-tap/tkt-b1d3a2e531.test.lua |  2 +-
>   11 files changed, 67 insertions(+), 157 deletions(-)
> 
> diff --git a/src/box/sql/build.c b/src/box/sql/build.c
> index 0072f842e..0c762fac9 100644
> --- a/src/box/sql/build.c
> +++ b/src/box/sql/build.c
> @@ -2256,10 +2256,14 @@ 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.
>   	 */
> -
> -	sql_clear_stat_spaces(parse_context, space_name, NULL);
>   	struct Table *tab = sqlite3HashFind(&db->pSchema->tblHash, space_name);
> -	sqlite3FkDropTable(parse_context, table_name_list, tab);
> +	struct FKey *fk = sqlite3FkReferences(tab);
> +	if (fk != NULL && strcmp(fk->pFrom->def->name, tab->def->name) != 0) {

2. Is it sufficient to compare space ids in lieu of names?

> +		sqlite3ErrorMsg(parse_context, "can't drop parent table %s when "
> +				"child table refers to it", space_name);

3. How about ER_DROP_SPACE? Now we have merely < 140 sqlite3ErrorMsg calls,
and if we did not use the latter in new code, we would gradually get rid
of it, nErr, redundant errmsg in Parse.

> +		goto exit_drop_table;
> +	}
> +	sql_clear_stat_spaces(parse_context, space_name, NULL);
>   	sql_code_drop_table(parse_context, space, is_view);
>   
>    exit_drop_table:
> @@ -2301,6 +2305,26 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>   	char *z;
>   
>   	assert(pTo != 0);
> +	char *normilized_name = strndup(pTo->z, pTo->n);

4. normilized -> normalized?

5. Can we involve Parser.region here?

> +	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));

6. It is possible to use pTo->n here in lieu of strlen?

> +	if (parent_id == BOX_ID_NIL &&
> +	    strcmp(normilized_name, p->def->name) != 0) {
> +		sqlite3ErrorMsg(pParse, "foreign key constraint references "\
> +				"nonexistent table: %s", normilized_name);

7. Lets move ER_CREATE_FK_CONSTRAINT into this patch from the next ones
and use it. Also I dream we can move into this patch all the refactoring
about FKey -> fkey, fkey_def, fkey_parse, and other non-functional changes,
but it is almost impossible, as I understand((

> +		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;
> +	}
>   	if (p == 0)
>   		goto fk_end;
>   	if (pFromCol == 0) {
> @@ -2322,8 +2346,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>   	} else {
>   		nCol = pFromCol->nExpr;
>   	}
> -	nByte =
> -	    sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) + pTo->n + 1;
> +	nByte = sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) +
> +		strlen(normilized_name) + 1;

8. Why strlen()? I thought length of the normalized name is equal to pTo->n,
it is not? You had created the normalized name as strndup of pTo->n bytes.
Same about the next hunk.

>   	if (pToCol) {
>   		for (i = 0; i < pToCol->nExpr; i++) {
>   			nByte += sqlite3Strlen30(pToCol->a[i].zName) + 1;
> @@ -2337,10 +2361,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>   	pFKey->pNextFrom = p->pFKey;
>   	z = (char *)&pFKey->aCol[nCol];
>   	pFKey->zTo = z;
> -	memcpy(z, pTo->z, pTo->n);
> -	z[pTo->n] = 0;
> -	sqlite3NormalizeName(z);
> -	z += pTo->n + 1;
> +	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;> diff --git a/test/sql-tap/alter.test.lua b/test/sql-tap/alter.test.lua
> index cfe280121..3e5c6102b 100755
> --- a/test/sql-tap/alter.test.lua
> +++ b/test/sql-tap/alter.test.lua
> @@ -313,9 +313,9 @@ test:do_execsql_test(
>           DROP TABLE IF EXISTS t1;
>           DROP TABLE IF EXISTS t2;
>           DROP TABLE IF EXISTS t3;
> -        CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));
> -        CREATE TABLE t2(id PRIMARY KEY);
> -        CREATE TABLE t3(id PRIMARY KEY);
> +        CREATE TABLE t2(id INT PRIMARY KEY);
> +	CREAte TABLE t3(id INT PRIMARY KEY);
> +	CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));

9. Some problems with indentation and capitalization.

>           INSERT INTO t2 VALUES(1);
>           INSERT INTO t3 VALUES(2);
>           INSERT INTO t1 VALUES(1, 1, 2);
> diff --git a/test/sql-tap/suite.ini b/test/sql-tap/suite.ini
> index 0637cffc1..e9c3d65ed 100644
> --- a/test/sql-tap/suite.ini
> +++ b/test/sql-tap/suite.ini
> @@ -3,6 +3,7 @@ core = app
>   description = Database tests with #! using TAP
>   disabled =
>   	reindex.test.lua ; This test is banned in scope of #2174
> +	gh-2953-drop-table-with-FK.test.lua

10. Leading white space.

11. Why did you disable it? If it can not be fixed, then just
delete.

>   lua_libs = lua/sqltester.lua ../sql/lua/sql_tokenizer.lua ../box/lua/identifier.lua
>   is_parallel = True
>   release_disabled = debug_mode_only.test.lua
> diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua
> index 31330a5a0..6aa290742 100755
> --- a/test/sql-tap/table.test.lua
> +++ b/test/sql-tap/table.test.lua
> @@ -730,6 +730,7 @@ test:do_catchsql_test(
>       "table-10.2",
>       [[
>           DROP TABLE t6;
> +	CREATE TABLE t4(a INT PRIMARY KEY);

12. Indentation. Something is wrong.

>           CREATE TABLE t6(a REFERENCES t4(a) MATCH PARTIAL primary key);
>       ]], {
>           -- <table-10.2>

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
@ 2018-07-25 10:03     ` n.pettik
  2018-07-26 20:12       ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-07-25 10:03 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


>> Originally, SQLite allows to create table with foreign keys contraint
> 
> 1. contraint -> constraint. And two the same typos below.

Fixed.

>> --- a/src/box/sql/build.c
>> +++ b/src/box/sql/build.c
>> @@ -2256,10 +2256,14 @@ 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.
>>  	 */
>> -
>> -	sql_clear_stat_spaces(parse_context, space_name, NULL);
>>  	struct Table *tab = sqlite3HashFind(&db->pSchema->tblHash, space_name);
>> -	sqlite3FkDropTable(parse_context, table_name_list, tab);
>> +	struct FKey *fk = sqlite3FkReferences(tab);
>> +	if (fk != NULL && strcmp(fk->pFrom->def->name, tab->def->name) != 0) {
> 
> 2. Is it sufficient to compare space ids in lieu of names?

Seems to be enough:

+++ b/src/box/sql/build.c
@@ -2258,9 +2258,11 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list,
         */
        struct Table *tab = sqlite3HashFind(&db->pSchema->tblHash, space_name);
        struct FKey *fk = sqlite3FkReferences(tab);
-       if (fk != NULL && strcmp(fk->pFrom->def->name, tab->def->name) != 0) {
-               sqlite3ErrorMsg(parse_context, "can't drop parent table %s when "
-                               "child table refers to it", space_name);
+       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++;

> 
>> +		sqlite3ErrorMsg(parse_context, "can't drop parent table %s when "
>> +				"child table refers to it", space_name);
> 
> 3. How about ER_DROP_SPACE? Now we have merely < 140 sqlite3ErrorMsg calls,
> and if we did not use the latter in new code, we would gradually get rid
> of it, nErr, redundant errmsg in Parse.

Ok, see diff above.

> 
>> +		goto exit_drop_table;
>> +	}
>> +	sql_clear_stat_spaces(parse_context, space_name, NULL);
>>  	sql_code_drop_table(parse_context, space, is_view);
>>     exit_drop_table:
>> @@ -2301,6 +2305,26 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>>  	char *z;
>>    	assert(pTo != 0);
>> +	char *normilized_name = strndup(pTo->z, pTo->n);
> 
> 4. normilized -> normalized?

Surely, it is a typo.

> 
> 5. Can we involve Parser.region here?

It doesn’t really matter: in further patches I reworked this function significantly
and region is used there.

> 
>> +	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));
> 
> 6. It is possible to use pTo->n here in lieu of strlen?

After ’normalisation’ name can become shorter:
“tab” -> tab
So, I guess strlen is right choice.

> 
>> +	if (parent_id == BOX_ID_NIL &&
>> +	    strcmp(normilized_name, p->def->name) != 0) {
>> +		sqlite3ErrorMsg(pParse, "foreign key constraint references "\
>> +				"nonexistent table: %s", normilized_name);
> 
> 7. Lets move ER_CREATE_FK_CONSTRAINT into this patch from the next ones
> and use it. Also I dream we can move into this patch all the refactoring
> about FKey -> fkey, fkey_def, fkey_parse, and other non-functional changes,
> but it is almost impossible, as I understand((

Particularly this moment doesn’t require ER_CREATE_FK_CONSTRAINT:

+               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;

It seems to be quite complicated to use in current patch ER_CREATE_FK_CONSTRAINT
since it uses constraint name and before next patch FK constraints never feature one.

>> +		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;
>> +	}
>>  	if (p == 0)
>>  		goto fk_end;
>>  	if (pFromCol == 0) {
>> @@ -2322,8 +2346,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>>  	} else {
>>  		nCol = pFromCol->nExpr;
>>  	}
>> -	nByte =
>> -	    sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) + pTo->n + 1;
>> +	nByte = sizeof(*pFKey) + (nCol - 1) * sizeof(pFKey->aCol[0]) +
>> +		strlen(normilized_name) + 1;
> 
> 8. Why strlen()? I thought length of the normalized name is equal to pTo->n,
> it is not? You had created the normalized name as strndup of pTo->n bytes.
> Same about the next hunk.

Due to the same reason I replied above.

> 
>>  	if (pToCol) {
>>  		for (i = 0; i < pToCol->nExpr; i++) {
>>  			nByte += sqlite3Strlen30(pToCol->a[i].zName) + 1;
>> @@ -2337,10 +2361,8 @@ sqlite3CreateForeignKey(Parse * pParse,	/* Parsing context */
>>  	pFKey->pNextFrom = p->pFKey;
>>  	z = (char *)&pFKey->aCol[nCol];
>>  	pFKey->zTo = z;
>> -	memcpy(z, pTo->z, pTo->n);
>> -	z[pTo->n] = 0;
>> -	sqlite3NormalizeName(z);
>> -	z += pTo->n + 1;
>> +	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;> diff --git a/test/sql-tap/alter.test.lua b/test/sql-tap/alter.test.lua
>> index cfe280121..3e5c6102b 100755
>> --- a/test/sql-tap/alter.test.lua
>> +++ b/test/sql-tap/alter.test.lua
>> @@ -313,9 +313,9 @@ test:do_execsql_test(
>>          DROP TABLE IF EXISTS t1;
>>          DROP TABLE IF EXISTS t2;
>>          DROP TABLE IF EXISTS t3;
>> -        CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));
>> -        CREATE TABLE t2(id PRIMARY KEY);
>> -        CREATE TABLE t3(id PRIMARY KEY);
>> +        CREATE TABLE t2(id INT PRIMARY KEY);
>> +	CREAte TABLE t3(id INT PRIMARY KEY);
>> +	CREATE TABLE t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));
> 
> 9. Some problems with indentation and capitalization.

+++ b/test/sql-tap/alter.test.lua
@@ -314,8 +314,8 @@ test:do_execsql_test(
         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 t1(a PRIMARY KEY, b, c, FOREIGN KEY(b) REFERENCES t2(id), FOREIGN KEY(c) REFERENCES t3(id));
+        CREATE TABLE t3(id INT 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);
>>          INSERT INTO t1 VALUES(1, 1, 2);
>> diff --git a/test/sql-tap/suite.ini b/test/sql-tap/suite.ini
>> index 0637cffc1..e9c3d65ed 100644
>> --- a/test/sql-tap/suite.ini
>> +++ b/test/sql-tap/suite.ini
>> @@ -3,6 +3,7 @@ core = app
>>  description = Database tests with #! using TAP
>>  disabled =
>>  	reindex.test.lua ; This test is banned in scope of #2174
>> +	gh-2953-drop-table-with-FK.test.lua
> 
> 10. Leading white space.
> 
> 11. Why did you disable it? If it can not be fixed, then just
> delete.

Ok, just removed whole file.

diff --git a/test/sql-tap/gh-2953-drop-table-with-FK.test.lua b/test/sql-tap/gh-2953-drop-table-with-FK.test.lua
deleted file mode 100755

> 
>>  lua_libs = lua/sqltester.lua ../sql/lua/sql_tokenizer.lua ../box/lua/identifier.lua
>>  is_parallel = True
>>  release_disabled = debug_mode_only.test.lua
>> diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua
>> index 31330a5a0..6aa290742 100755
>> --- a/test/sql-tap/table.test.lua
>> +++ b/test/sql-tap/table.test.lua
>> @@ -730,6 +730,7 @@ test:do_catchsql_test(
>>      "table-10.2",
>>      [[
>>          DROP TABLE t6;
>> +	CREATE TABLE t4(a INT PRIMARY KEY);
> 
> 12. Indentation. Something is wrong.

+++ b/test/sql-tap/table.test.lua
@@ -730,7 +730,7 @@ test:do_catchsql_test(
     "table-10.2",
     [[
         DROP TABLE t6;
-       CREATE TABLE t4(a INT PRIMARY KEY);
+        CREATE TABLE t4(a INT PRIMARY KEY);

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
@ 2018-07-25 10:03     ` n.pettik
  2018-07-26 20:12       ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-07-25 10:03 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


> On 18 Jul 2018, at 00:05, Vladislav Shpilevoy <v.shpilevoy@tarantool.org> wrote:
> 
> Thanks for the patch! See 18 comments below and a patch on
> the branch.
> 
> On 13/07/2018 05:04, Nikita Pettik wrote:
>> This patch introduces new system space to persist foreign keys
> 
> 1. Typos: contraints, chils.

Fixed.

> 
>> contraints. Format of the space:
>> _fk_constraint (space id = 350)
>> [<contraint name> STR, <parent id> UINT, <child id> UINT,>   <is deferred> BOOL, <match> STR, <on delete action> STR,
>>  <on update action> STR, <field links> ARRAY<MAP< UINT : UINT >>]
>> FK constraint is local to space, so every pair <FK name, child id>
>> is unique (and it is PK in _fk_constraint space).
>> After insertion into this space, new instance describing FK constraint
>> is created. FK constraints are held in data-dictionary as two lists
>> (for child and parent constraints) in struct space.
>> There is a list of FK restrictions:
>>  - At the time of FK creation parent and chils spaces must exist;
>>  - VIEWs can't be involved into FK processing;
>>  - Child space must be empty;
>>  - Types of referencing and referenced fields must match;
> 
> 2. How about not match, but fit? See field_type1_contains_type2() and
> its usages.

Seems that it doesn’t contradicts ANSI:

• The declared type of each referencing column shall be comparable to the declared type of the corresponding referenced column. 
There shall not be corresponding constituents of the declared type of a referencing column and the declared type of the corresponding
referenced column such that one constituent is datetime with time zone and the other is datetime without time zone. 

Hence, they must be simply ‘comparable’ which in turn means
that following situation is OK:

CREATE TABLE parent (id INT PRIMARY KEY, a UNQUE);
CREATE TABLE child (id INT PRIMARY KEY REFERENCES parent (a));

In other words, INT can be mapped into scalar (i.e. is subset of scalar type),
but not vice versa:

@@ -3737,13 +3784,16 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                                          fk_def->name, "foreign key refers to "
                                                        "nonexistent field");
                        }
-                       if (child_space->def->fields[child_fieldno].type !=
-                           parent_space->def->fields[parent_fieldno].type) {
+                       struct field_def child_field =
+                               child_space->def->fields[child_fieldno];
+                       struct field_def parent_field =
+                               parent_space->def->fields[parent_fieldno];
+                       if (! field_type1_contains_type2(parent_field.type,
+                                                        child_field.type)) {
                                tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
                                          fk_def->name, "field type mismatch");
                        }
-                       if (child_space->def->fields[child_fieldno].coll_id !=
-                           parent_space->def->fields[parent_fieldno].coll_id) {
+                       if (child_field.coll_id != parent_field.coll_id) {

>> diff --git a/src/box/alter.cc b/src/box/alter.cc
>> index 89b11dcd3..aaa56bd21 100644
>> --- a/src/box/alter.cc
>> +++ b/src/box/alter.cc
>> @@ -1889,6 +1916,22 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
>>  			  "can not add a secondary key before primary");
>>  	}
>>  +	/*
>> +	 * Can't drop index if foreign key constraints references
>> +	 * this index.
>> +	 */
>> +	if (new_tuple == NULL) {
> 
> 3. But it can be non-drop, but alter, for example. Drop case is processed
> below:
> 
>    /* Case 1: drop the index, if it is dropped. */
>    if (old_index != NULL && new_tuple == NULL) {
> 
> What if I want to rename an index? Also see index_def_change_requires_rebuild.

Fixed:

+++ b/src/box/alter.cc
@@ -1920,7 +1920,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
         * Can't drop index if foreign key constraints references
         * this index.
         */
-       if (new_tuple == NULL) {
+       if (old_index != NULL && new_tuple == NULL) {

Also, I didn’t get what you mean by mentioning index_def_change_requires_rebuild.
Can index id change on its renaming?

> 
>> +		struct fkey *fk = old_space->parent_fkey;
>> +		while (fk != NULL) {
>> +			if (old_space->parent_fkey->index_id == iid) {
>> +				tnt_raise(ClientError, ER_ALTER_SPACE,
>> +					  space_name(old_space),
>> +					  "can not drop referenced index");
>> +			}
>> +			fk = fk->fkey_parent_next;
>> +		}
>> +	}
>> +
>> @@ -3404,6 +3447,387 @@ on_replace_dd_trigger
>>  	txn_on_commit(txn, on_commit);
>>  }
>>  +/**
>> + * Decode MsgPack array of links. It consists from maps:
>> + * {parent_id (UINT) : child_id (UINT)}.
>> + *
>> + * @param data MsgPack array of links.
>> + * @param[out] out_count Count of links.
>> + * @param constraint_name Constraint name to use in error
>> + *			  messages.
>> + * @param constraint_len Length of constraint name.
>> + * @param errcode Errcode for client errors.
>> + * @retval Array of links.
>> + */
>> +static struct field_link *
>> +fkey_links_decode(const char *data, uint32_t *out_count,
>> +		  const char *constraint_name, uint32_t constraint_len,
>> +		  uint32_t errcode)
>> +{
>> +	assert(mp_typeof(*data) == MP_ARRAY);
>> +	uint32_t count = mp_decode_array(&data);
>> +	if (count == 0) {
>> +		tnt_raise(ClientError, errcode,
>> +			  tt_cstr(constraint_name, constraint_len),
>> +			  "at least one link must be specified");
>> +	}
>> +	*out_count = count;
>> +	size_t size = count * sizeof(struct field_link);
>> +	struct field_link *region_links =
>> +		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
>> +	memset(region_links, 0, size);
>> +	const char **map = &data;
>> +	for (uint32_t i = 0; i < count; ++i) {
>> +		uint32_t map_sz = mp_decode_map(map);
>> +		if (map_sz != 2) {
>> +			tnt_raise(ClientError, errcode,
>> +				  tt_cstr(constraint_name, constraint_len),
>> +				  tt_sprintf("link must be map with 2 fields"));
>> +		}
>> +		if (mp_typeof(**map) != MP_STR) {
>> +			tnt_raise(ClientError, errcode,
>> +				  tt_cstr(constraint_name, constraint_len),
>> +				  tt_sprintf("link %d is not map "\
>> +					     "with string keys", i));
>> +		}
> 
> 4. What about a second key? mp_typeof(**map) right after decoding its
> header returns type of the first key. But second can be of another type.

Fixed:

-               if (mp_typeof(**map) != MP_STR) {
-                       tnt_raise(ClientError, errcode,
-                                 tt_cstr(constraint_name, constraint_len),
-                                 tt_sprintf("link %d is not map "\
-                                            "with string keys", i));
-               }
                for (uint8_t j = 0; j < map_sz; ++j) {
+                       if (mp_typeof(**map) != MP_STR) {
+                               tnt_raise(ClientError, errcode,
+                                         tt_cstr(constraint_name,
+                                                 constraint_len),
+                                         tt_sprintf("link %d is not map "\
+                                                    "with string keys", i));
+                       }

> 
> And why do we need this {child = , parent = }, ... sequence? Lets just
> use {<uint>, <uint>}, {<uint>, <uint>}, {<uint>, <uint>} ... It is more
> compact and simpler to parse. Or even just two arrays: first child and
> second parent field numbers. It is the most canonical way similar to
> SQL, it is not? When you specify two column ranges.

I use this format since it is easier to avoid confusing parent and child ids
(i.e. what comes first and what comes second). Even if we add separate
convenient Lua API for that purpose. 
If you insist on doing that, I will change format.

> But in the internal format I vote to keep field_link. In the next
> patches I see, that common usage case is to iterate over the pairs,
> so for cache it is better to keep them.
> 
>> +/**
>> + * Replace entry in child's and parent's lists of
>> + * FK constraints.
>> + *
>> + * @param child Child space of FK constraint.
>> + * @param parent Parent space of FK constraint.
>> + * @param new_fkey Constraint to be added to child and parent.
>> + * @param[out] old_fkey Constraint to be found and replaced.
>> + */
>> +static void
>> +fkey_list_replace(struct space *child, struct space *parent, const char *name,
>> +		  struct fkey *new_fkey, struct fkey **old_fkey)
>> +{
> 
> 5. I see that for fkey you actually have invented rlist. Please, see rlist.h
> and use it for fkeys. It already has rlist_foreach_entry, rlist_add,
> rlist_add_tail, rlist_del etc. The struct foreign key will have two
> rlist links: in_child and in_parent.


diff --git a/src/box/alter.cc b/src/box/alter.cc
index 37a7063f0..5fd8771e3 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -576,12 +576,8 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
 static void
 space_swap_fkeys(struct space *new_space, struct space *old_space)
 {
-       struct fkey *child_fkey = new_space->child_fkey;
-       struct fkey *parent_fkey = new_space->parent_fkey;
-       new_space->child_fkey = old_space->child_fkey;
-       new_space->parent_fkey = old_space->parent_fkey;
-       old_space->child_fkey = child_fkey;
-       old_space->parent_fkey = parent_fkey;
+       rlist_swap(&new_space->child_fkey, &old_space->child_fkey);
+       rlist_swap(&new_space->parent_fkey, &old_space->parent_fkey);
 }
 
 /**
@@ -1764,7 +1760,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
                 * one referenced index which can't be dropped
                 * before constraint itself.
                 */
-               if (old_space->child_fkey != NULL) {
+               if (! rlist_empty(&old_space->child_fkey)) {
                        tnt_raise(ClientError, ER_DROP_SPACE,
                                  space_name(old_space),
                                  "the space has foreign key constraints");
@@ -1972,14 +1968,13 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
         * this index.
         */
        if (old_index != NULL && new_tuple == NULL) {
-               struct fkey *fk = old_space->parent_fkey;
-               while (fk != NULL) {
-                       if (old_space->parent_fkey->index_id == iid) {
+               struct fkey *fk;
+               rlist_foreach_entry(fk, &old_space->parent_fkey, parent_link) {
+                       if (fk->index_id == iid) {
                                tnt_raise(ClientError, ER_ALTER_SPACE,
                                          space_name(old_space),
                                          "can not drop referenced index");
                        }
-                       fk = fk->fkey_parent_next;
                }
        }
 
@@ -3637,43 +3632,26 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
 }
 
 /**
- * Replace entry in child's and parent's lists of
- * FK constraints.
+ * Remove FK constraint from child's list.
+ * Entries in child list are supposed to be unique
+ * by their name.
  *
- * @param child Child space of FK constraint.
- * @param parent Parent space of FK constraint.
- * @param new_fkey Constraint to be added to child and parent.
- * @param[out] old_fkey Constraint to be found and replaced.
+ * @param list List of child FK constraints.
+ * @param fkey_name Name of constraint to be removed.
+ * @retval FK being removed.
  */
-static void
-fkey_list_replace(struct space *child, struct space *parent, const char *name,
-                 struct fkey *new_fkey, struct fkey **old_fkey)
-{
-       *old_fkey = NULL;
-       struct fkey **fk = &parent->parent_fkey;
-       while (*fk != NULL && !(strcmp((*fk)->def->name, name) == 0 &&
-                               (*fk)->def->child_id == child->def->id))
-               fk = &((*fk)->fkey_parent_next);
-       if (*fk != NULL) {
-               *old_fkey = *fk;
-               *fk = (*fk)->fkey_parent_next;
-       }
-       if (new_fkey != NULL) {
-               new_fkey->fkey_parent_next = parent->parent_fkey;
-               parent->parent_fkey = new_fkey;
-       }
-       fk = &child->child_fkey;
-       /* In child's list all constraints are unique by name. */
-       while (*fk != NULL && strcmp((*fk)->def->name, name) != 0)
-               fk = &((*fk)->fkey_child_next);
-       if (*fk != NULL) {
-               assert(*old_fkey == *fk);
-               *fk = (*fk)->fkey_child_next;
-       }
-       if (new_fkey != NULL) {
-               new_fkey->fkey_child_next = child->child_fkey;
-               child->child_fkey = new_fkey;
+static struct fkey *
+fkey_remove_child(struct rlist *list, const char *fkey_name)
+{
+       struct fkey *fk;
+       rlist_foreach_entry(fk, list, child_link) {
+               if (strcmp(fkey_name, fk->def->name) == 0) {
+                       rlist_del_entry(fk, child_link);
+                       return fk;
+               }
        }
+       unreachable();
+       return NULL;
 }
 
 /**
@@ -3686,11 +3664,9 @@ on_create_fkey_rollback(struct trigger *trigger, void *event)
 {
        (void) event;
        struct fkey *fk = (struct fkey *)trigger->data;
-       struct space *parent = space_by_id(fk->def->parent_id);
-       struct space *child = space_by_id(fk->def->child_id);
-       struct fkey *fkey = NULL;
-       fkey_list_replace(child, parent, fk->def->name, NULL, &fkey);
-       fkey_delete(fkey);
+       rlist_del_entry(fk, parent_link);
+       rlist_del_entry(fk, child_link);
+       fkey_delete(fk);
 }
 
 /** Return old FK and release memory for the new one. */
@@ -3701,9 +3677,12 @@ on_replace_fkey_rollback(struct trigger *trigger, void *event)
        struct fkey *fk = (struct fkey *)trigger->data;
        struct space *parent = space_by_id(fk->def->parent_id);
        struct space *child = space_by_id(fk->def->child_id);
-       struct fkey *old_fkey = NULL;
-       fkey_list_replace(child, parent, fk->def->name, fk, &old_fkey);
+       struct fkey *old_fkey = fkey_remove_child(&child->child_fkey,
+                                                 fk->def->name);
+       rlist_del_entry(old_fkey, parent_link);
        fkey_delete(old_fkey);
+       rlist_add_entry(&child->child_fkey, fk, child_link);
+       rlist_add_entry(&parent->parent_fkey, fk, parent_link);
 }
 
 /** On rollback of drop simply return back FK to DD. */
@@ -3714,10 +3693,8 @@ on_drop_fkey_rollback(struct trigger *trigger, void *event)
        struct fkey *fk_to_restore = (struct fkey *)trigger->data;
        struct space *parent = space_by_id(fk_to_restore->def->parent_id);
        struct space *child = space_by_id(fk_to_restore->def->child_id);
-       struct fkey *old_fk;
-       fkey_list_replace(child, parent, fk_to_restore->def->name, fk_to_restore,
-                         &old_fk);
-       assert(old_fk == NULL);
+       rlist_add_entry(&child->child_fkey, fk_to_restore, child_link);
+       rlist_add_entry(&parent->parent_fkey, fk_to_restore, parent_link);
 }
 
 /**
@@ -3845,16 +3822,24 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                memset(fkey, 0, sizeof(*fkey));
                fkey->def = fk_def;
                fkey->index_id = fk_index->def->iid;
-               struct fkey *old_fk;
-               fkey_list_replace(child_space, parent_space, fk_def->name,
-                                 fkey, &old_fk);
                if (old_tuple == NULL) {
+                       rlist_add_entry(&child_space->child_fkey, fkey,
+                                       child_link);
+                       rlist_add_entry(&parent_space->parent_fkey, fkey,
+                                       parent_link);
                        struct trigger *on_rollback =
                                txn_alter_trigger_new(on_create_fkey_rollback,
                                                      fkey);
                        txn_on_rollback(txn, on_rollback);
-                       assert(old_fk == NULL);
                } else {
+                       struct fkey *old_fk =
+                               fkey_remove_child(&child_space->child_fkey,
+                                                 fk_def->name);
+                       rlist_del_entry(old_fk, parent_link);
+                       rlist_add_entry(&child_space->child_fkey, fkey,
+                                       child_link);
+                       rlist_add_entry(&parent_space->parent_fkey, fkey,
+                                       parent_link);
                        struct trigger *on_rollback =
                                txn_alter_trigger_new(on_replace_fkey_rollback,
                                                      fkey);
@@ -3872,13 +3857,12 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                        fkey_def_new_from_tuple(old_tuple,
                                                ER_DROP_FK_CONSTRAINT);
                auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
-               struct space *parent_space =
-                       space_cache_find_xc(fk_def->parent_id);
                struct space *child_space =
                        space_cache_find_xc(fk_def->child_id);
-               struct fkey *old_fkey = NULL;
-               fkey_list_replace(child_space, parent_space, fk_def->name, NULL,
-                                 &old_fkey);
+               struct fkey *old_fkey =
+                       fkey_remove_child(&child_space->child_fkey,
+                                         fk_def->name);
+               rlist_del_entry(old_fkey, parent_link);
                struct trigger *on_commit =
                        txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
                                              old_fkey);
diff --git a/src/box/fkey.h b/src/box/fkey.h
index b30136a1d..0d537b1a7 100644
--- a/src/box/fkey.h
+++ b/src/box/fkey.h
@@ -97,9 +97,9 @@ struct fkey {
        /** Triggers for actions. */
        struct sql_trigger *on_delete_trigger;
        struct sql_trigger *on_update_trigger;
-       /** Linked lists */
-       struct fkey *fkey_parent_next;
-       struct fkey *fkey_child_next;
+       /** Links for parent and child lists. */
+       struct rlist parent_link;
+       struct rlist child_link;
 };
 
 /**
diff --git a/src/box/space.c b/src/box/space.c
index 1a7851636..90a80ed7b 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -163,6 +163,8 @@ space_create(struct space *space, struct engine *engine,
                space->index_map[index_def->iid] = index;
        }
        space_fill_index_map(space);
+       rlist_create(&space->parent_fkey);
+       rlist_create(&space->child_fkey);
        return 0;
 
 fail_free_indexes:
@@ -220,8 +222,8 @@ space_delete(struct space *space)
         * on_replace_dd_trigger on deletion from _trigger.
         */
        assert(space->sql_triggers == NULL);
-       assert(space->child_fkey == NULL);
-       assert(space->parent_fkey == NULL);
+       assert(rlist_empty(&space->parent_fkey));
+       assert(rlist_empty(&space->child_fkey));
        space->vtab->destroy(space);
 }
 
diff --git a/src/box/space.h b/src/box/space.h
index 2cf76a032..97650cffe 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -190,8 +190,8 @@ struct space {
         * other words the table that is named in the REFERENCES
         * clause.
         */
-       struct fkey *parent_fkey;
-       struct fkey *child_fkey;
+       struct rlist parent_fkey;
+       struct rlist child_fkey;
 };

>> +/**
>> + * On rollback of creation we remove FK constraint from DD, i.e.
>> + * from parent's and child's lists of constraints and
>> + * release memory.
>> + */
>> +static void
>> +on_create_fkey_rollback(struct trigger *trigger, void *event)
>> +{
>> +	(void) event;
>> +	struct fkey *fk = (struct fkey *)trigger->data;
>> +	struct space *parent = space_by_id(fk->def->parent_id);
>> +	struct space *child = space_by_id(fk->def->child_id);
>> +	struct fkey *fkey = NULL;
>> +	fkey_list_replace(child, parent, fk->def->name, NULL, &fkey);
>> +	fkey_delete(fkey);
> 
> 6. After you fixed the previous remark, this function would
> collapse into two rlist_del, it is not. The same about the
> next function. Btw, these functions are identical. Can you
> keep only one?

They don’t seem to de identical: on_create rollback just deletes new entry,
but on_replace - deletes new entry and inserts old one.

> Same about on_drop_fkey_commit and on_replace_fkey_commit.

Ok:

-/** Release memory for old foreign key. */
-static void
-on_replace_fkey_commit(struct trigger *trigger, void *event)
-{
-       (void) event;
-       struct fkey *fk = (struct fkey *)trigger->data;
-       fkey_delete(fk);
-}
-
 /** On rollback of drop simply return back FK to DD. */
 static void
 on_drop_fkey_rollback(struct trigger *trigger, void *event)
@@ -3674,11 +3665,12 @@ on_drop_fkey_rollback(struct trigger *trigger, void *event)
 }
 
 /**
- * On commit of drop we have already deleted foreign key from
- * both (parent's and child's) lists, so just release memory.
+ * On commit of drop or replace we have already deleted old
+ * foreign key entry from both (parent's and child's) lists,
+ * so just release memory.
  */
 static void
-on_drop_fkey_commit(struct trigger *trigger, void *event)
+on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)

@@ -3801,7 +3793,7 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                                                      fkey);
                        txn_on_rollback(txn, on_rollback);
                        struct trigger *on_commit =
-                               txn_alter_trigger_new(on_replace_fkey_commit,
+                               txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
                                                      old_fk);
                        txn_on_commit(txn, on_commit);
                }
@@ -3821,7 +3813,8 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                fkey_list_replace(child_space, parent_space, fk_def->name, NULL,
                                  &old_fkey);
                struct trigger *on_commit =
-                       txn_alter_trigger_new(on_drop_fkey_commit, old_fkey);
+                       txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+                                             old_fkey);


>> +		struct index *pk = space_index(child_space, 0);
>> +		if (index_count(pk, ITER_ALL, NULL, 0) > 0) {
> 
> 7. Lets better use index_size. Index_count on Vinyl takes O(N) time
> scanning disk. I think, it is ok to forbid new fk on a Vinyl space,
> that logically is empty, but actually still contains some non-compacted
> garbage. Anyway user now is able to force the compaction and try to
> create fk again.

Ok, fair enough:

@@ -3722,7 +3714,7 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                 * checks on existing data in space.
                 */
                struct index *pk = space_index(child_space, 0);
-               if (index_count(pk, ITER_ALL, NULL, 0) > 0) {
+               if (index_size(pk) > 0) {

> 
>> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +				  fk_def->name,
>> +				  "referencing space must be empty");
>> +		}
>> +		/* Check types of referenced fields. */
>> +		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
>> +			uint32_t child_fieldno = fk_def->links[i].child_field;
>> +			uint32_t parent_fieldno = fk_def->links[i].parent_field;
>> +			if (child_fieldno >= child_space->def->field_count ||
>> +			    parent_fieldno >= parent_space->def->field_count) {
>> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +					  fk_def->name, "foreign key refers to "
>> +						        "nonexistent field");
>> +			}
>> +			if (child_space->def->fields[child_fieldno].type !=
>> +			    parent_space->def->fields[parent_fieldno].type) {
>> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +					  fk_def->name, "field type mismatch");
>> +			}
>> +			if (child_space->def->fields[child_fieldno].coll_id !=
>> +			    parent_space->def->fields[parent_fieldno].coll_id) {
>> +				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +					  fk_def->name,
>> +					  "field collation mismatch");
>> +			}
>> +		}
>> +		/*
>> +		 * Search for suitable index in parent space:
>> +		 * it must be unique and consist exactly from
>> +		 * referenced columns (but order may be different).
> 
> 8. Should this index has the same collations? For a column we can define
> multiple collations in different indexes. And in one index the value may
> be unique, but in another may be not.

No, they (indexes) may have different collations. The only requirement
for index is uniqueness. I am not completely sure about the fact that
field collations must match: what ANSI says I linked at the beginning of
this letter. I guess it seems to be dubious question.

> 
>> +		 */
>> +		struct index *fk_index = NULL;
>> +		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
>> +			struct index *idx = space_index(parent_space, i);
>> +			if (!idx->def->opts.is_unique)
>> +				continue;
>> +			if (idx->def->key_def->part_count !=
>> +			    fk_def->field_count)
>> +				continue;
>> +			uint32_t j;
>> +			for (j = 0; j < fk_def->field_count; ++j) {
>> +				if (idx->def->key_def->parts[j].fieldno !=
>> +				    fk_def->links[j].parent_field)
>> +					break;
>> +			}
>> +			if (j != fk_def->field_count)
>> +				continue;
> 
> 9. See key_def_find.

@@ -3760,12 +3829,13 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                        if (!idx->def->opts.is_unique)
                                continue;
                        if (idx->def->key_def->part_count !=
                            fk_def->field_count)
                                continue;
                        uint32_t j;
                        for (j = 0; j < fk_def->field_count; ++j) {
-                               if (idx->def->key_def->parts[j].fieldno !=
-                                   fk_def->links[j].parent_field)
+                               if (key_def_find(idx->def->key_def,
+                                                fk_def->links[j].parent_field)
+                                   == NULL)
                                        break;

> 
> 10. Above you have said that order may be different, but here I see
> that once you have found a unique index, you merely check that it
> has sequentially the same field numbers as the fk.

For some reason I forgot to fix this when was preparing patch.
Fixed by now. Also, according to ANSI (11.8 4.a):
• Each referenced column shall identify a column of the referenced table and the same column shall not be identi ed more than once.

@@ -3710,6 +3710,41 @@ on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
        fkey_delete(fk);
 }
 
+static int
+cmp_uint32(const void *_a, const void *_b)
+{
+       const uint32_t *a = (const uint32_t *) _a, *b = (const uint32_t *) _b;
+       if (*a == *b)
+               return 0;
+       return (*a > *b) ? 1 : -1;
+}
+
+/**
+ * ANSI SQL doesn't allow list of referenced fields to contain
+ * duplicates.
+ */
+static int
+fkey_links_check_duplicates(struct fkey_def *fk_def)
+{
+       uint32_t *parent_fields =
+               (uint32_t *) region_alloc(&fiber()->gc, fk_def->field_count);
+       if (parent_fields == NULL) {
+               tnt_raise(OutOfMemory, fk_def->field_count, "region",
+                         "parent_fields");
+       }
+       for (uint32_t i = 0; i < fk_def->field_count; ++i)
+               parent_fields[i] = fk_def->links[i].parent_field;
+       qsort(parent_fields, fk_def->field_count, sizeof(*parent_fields),
+             cmp_uint32);
+       uint32_t prev_val = parent_fields[0];
+       for (uint32_t i = 1; i < fk_def->field_count; ++i) {
+               if (prev_val == parent_fields[i])
+                       return -1;
+               prev_val = parent_fields[i];
+       }
+       return 0;
+}
+

 on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
@@ -3777,6 +3812,11 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                                          "field collation mismatch");
                        }
                }
+               if (fkey_links_check_duplicates(fk_def)) {
+                       tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+                                 fk_def->name, "referenced fields can not "
+                                               "contain duplicates");
+               }


Probably this is not best implementation. As an improvement I can add kind of optimisation:

*pseudo code below*

uint_64 mask;
for parent_field in fk_def:
	if (pk_mask & (((uint64_t) 1) << parent_field ==1)
		return -1;
	column_mask_set_field(&mask, parent_field);
end
if (pk_mask & (((uint64_t) 1) << 63)) != 0
	fkey_links_check_duplicates(…) // Full version of checking.
else
	return 1;

Is it worth the effort? I mean here we waste O(field_count) in case the largest
filedno in parent table > 63. On the other hand, I don’t think that many users
have tables with field count > 63, so it is likely to be reasonable.

Moreover, it would be cool if column_mask_set_fieldno() worked the same
as bit_set() from lib/bit, i.e. returned previous bit value.

And very simple test:

@@ -71,6 +71,11 @@ box.space._fk_constraint:insert(t)
 
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 1}}}
+box.space._fk_constraint:insert(t)
+

> 
>> +			fk_index = idx;
>> +			break;
>> +		}
>> +		if (fk_index == NULL) {
>> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +				  fk_def->name, "referenced fields don't "
>> +					      "compose unique index");
>> +		}
>> +		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
> 
> 11. I know how you like memory mapping. How about merge fkey_def into
> fkey memory? I see, that it is relatively simple and linear thing.

What is the point of doing this? Do you suggest to allocate enough memory for fkey
right in fkey_def_new_from_tuple() ? Like this:

@@ -3536,7 +3536,8 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
        struct field_link *links = fkey_links_decode(links_raw, &link_count,
                                                     name, name_len, errcode);
        size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
-       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
+       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz +
+                                                            sizeof(struct fkey);

If so, it would be quite confusing I think (since function returns only
fkey_def, but memory would be allocated for fkey_def + fkey).
If you mean smth else or insist on this change, I will fix it.

>> diff --git a/src/box/fkey.c b/src/box/fkey.c
>> new file mode 100644
>> index 000000000..e45889a0d
>> --- /dev/null
>> +++ b/src/box/fkey.c
>> +void
>> +fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
>> +{
> 
> 12. Why do you need this function when sql_trigger_delete exists?

Memory layout of FK trigger differs from ordinary one (from sql/fkey.c):

size_t trigger_size = sizeof(struct sql_trigger) +
                    sizeof(TriggerStep) + nFrom + 1;
trigger =
       (struct sql_trigger *)sqlite3DbMallocZero(db,
                                            trigger_size);

One can see, memory for TriggerStep, sql_trigger and name of
target table is allocated in one chunk. Thus, fkey_trigger_delete()
doesn’t release memory for TriggerStep. Overall, if compare
these functions fkey_trigger_delete() looks much simpler.

> 
>> +	if (p != NULL) {
>> +		struct TriggerStep *step = p->step_list;
>> +		sql_expr_delete(db, step->pWhere, false);
>> +		sql_expr_list_delete(db, step->pExprList);
>> +		sql_select_delete(db, step->pSelect);
>> +		sql_expr_delete(db, p->pWhen, false);
>> +		sqlite3DbFree(db, p);
>> +	}
>> +}
>> +
>> +#include <stdbool.h>
>> +#include <stdint.h>
>> +
>> +#include "space.h"
>> +
>> +#if defined(__cplusplus)
>> +extern "C" {
>> +#endif /* defined(__cplusplus) */
>> +
>> +struct sqlite3;
> 
> 13. Is it possible to drop this announcement after
> you replaced fkey_trigger_delete with sql_trigger_delete?

See reply above. I can struggle with adopting sql_trigger_delete
for FK triggers, but is it worth the effort?

> 
>> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
>> index 87c79bdde..30d8b0081 100644
>> --- a/src/box/lua/schema.lua
>> +++ b/src/box/lua/schema.lua
>> @@ -506,6 +506,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
>>      local _vindex = box.space[box.schema.VINDEX_ID]
>>      local _truncate = box.space[box.schema.TRUNCATE_ID]
>>      local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
>> +    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
>>      local sequence_tuple = _space_sequence:delete{space_id}
>>      if sequence_tuple ~= nil and sequence_tuple[3] == true then
>>          -- Delete automatically generated sequence.
>> @@ -519,6 +520,11 @@ box.schema.space.drop = function(space_id, space_name, opts)
>>          local v = keys[i]
>>          _index:delete{v[1], v[2]}
>>      end
>> +    for _, t in _fk_constraint.index.primary:pairs() do
>> +        if t.child_id == space_id then
> 
> 14. Fullscan here looks bad. Can we create a secondary index on
> _fk_constraint by child_id?

Well, lets add secondary index on child_id field:

@@ -525,6 +525,10 @@ local function upgrade_to_2_1_0()
     _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
                   {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
 
+    log.info("create secondary index child_id on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 1, 'child_id', 'tree',
+                  {unique = false}, {{1, 'unsigned'}}}
+

+++ b/src/box/lua/schema.lua
@@ -520,10 +520,8 @@ box.schema.space.drop = function(space_id, space_name, opts)
         local v = keys[i]
         _index:delete{v[1], v[2]}
     end
-    for _, t in _fk_constraint:pairs() do
-        if t.child_id == space_id then
-            _fk_constraint:delete{t.name, t.child_id}
-        end
+    for _, t in _fk_constraint.index.child_id:pairs({space_id}) do
+        _fk_constraint:delete({t.name, space_id})
     end

> Or create a Lua C function, that takes
> space and returns names of its child fks? If we use the index, then
> we are able to optimize VDBE deletion of the FKs in the main patch
> (sql: introduce ADD CONSTRAINT statement), where you delete by
> the composite key {name, child_id}, but actually child_id is enough.
> 
>> +            _fk_constraint:delete{t.name, t.child_id}
>> +        end
>> +    end
>> diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
>> index f112a93ae..772f55cb2 100644
>> --- a/src/box/lua/upgrade.lua
>> +++ b/src/box/lua/upgrade.lua
>> @@ -509,6 +509,22 @@ local function upgrade_to_2_1_0()
>>                    {unique = true}, {{0, 'string'}, {1, 'string'},
>>                                      {5, 'scalar'}}}
>>  +    local fk_constr_ft = {{name='name', type='string'},
>> +                          {name='child_id', type='unsigned'},
>> +                          {name='parent_id', type='unsigned'},
>> +                          {name='deferred', type='boolean'},
> 
> 15. Flag, so 'is_deferred', it is not?

Surely:

-                          {name='deferred', type='boolean'},
+                          {name='is_deferred', type='boolean'},

> 
>> +                          {name='match', type='string'},
>> +                          {name='on_delete', type='string'},
>> +                          {name='on_update', type='string'},
>> +                          {name='links', type='array'}}
>> +    log.info("create space _fk_constraint")
>> +    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
>> +                  0, setmap({}), fk_constr_ft}
>> +
>> +    log.info("create index primary on _fk_constraint")
>> +    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
>> +                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
>> +
>>      -- Nullability wasn't skipable. This was fixed in 1-7.
>>      -- Now, abscent field means NULL, so we can safely set second
>>      -- field in format, marking it nullable.
>> diff --git a/src/box/space.h b/src/box/space.h
>> index 7da2ee51f..fc5e8046f 100644
>> --- a/src/box/space.h
>> +++ b/src/box/space.h
>> @@ -183,6 +183,9 @@ struct space {
>>  	 * of index id.
>>  	 */
>>  	struct index **index;
>> +	/** Foreign key constraints. */
> 
> 16. It would be good to have here an explanation about who is
> child, who is parent.

Ok:

+++ b/src/box/space.h
@@ -183,7 +183,13 @@ struct space {
         * of index id.
         */
        struct index **index;
-       /** Foreign key constraints. */
+       /**
+        * Lists of foreign key constraints. In SQL terms parent
+        * space is the "from" table i.e. the table that contains
+        * the REFERENCES clause. Child space is "to" table, in
+        * other words the table that is named in the REFERENCES
+        * clause.
+        */

> 
>> +	struct fkey *parent_fkey;
>> +	struct fkey *child_fkey;
>>  };
>>  diff --git a/test/box/access_misc.result b/test/box/access_misc.result
>> index 5a2563d55..62c92ca03 100644
>> --- a/test/box/access_misc.result
>> +++ b/test/box/access_misc.result
>> @@ -809,6 +809,11 @@ box.space._space:select()
>>    - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
>>          'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
>>        {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
>> +  - [350, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
>> +      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
>> +      {'name': 'deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
>> +      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
>> +      {'name': 'links', 'type': 'array'}]]
> 
> 17. Please, keep spare room after sql_stat4 in 8 identifiers for future needs. So as
> _fk_constraint was 356 (sql_stat1 348 + 8).

Ok:

diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 22621fc11..6022ea072 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -108,7 +108,7 @@ enum {
        BOX_SQL_STAT1_ID = 348,
        BOX_SQL_STAT4_ID = 349,
        /** Space id of _fk_constraint. */
-       BOX_FK_CONSTRAINT_ID = 350,
+       BOX_FK_CONSTRAINT_ID = 356,

Tests are fixed as well.

> 
>>  ...
>>  box.space._func:select()
>>  ---
>> diff --git a/test/engine/iterator.result b/test/engine/iterator.result
>> index a36761df8..ba9b0545a 100644
>> --- a/test/engine/iterator.result
>> +++ b/test/engine/iterator.result
>> @@ -4211,7 +4211,7 @@ s:replace{35}
>>  ...
>>  state, value = gen(param,state)
>>  ---
>> -- error: 'builtin/box/schema.lua:1049: usage: next(param, state)'
>> +- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'
> 
> 18. This test fails on each schema change. Lets remove this file:line
> from the error message alongside the patch.

Ok:

diff --git a/test/engine/iterator.result b/test/engine/iterator.result
index 8cd067ebc..2204ff647 100644
--- a/test/engine/iterator.result
+++ b/test/engine/iterator.result
@@ -4207,18 +4207,6 @@ value
 ---
 - null
 ...
-s:replace{35}
----
-- [35]
-...
-state, value = gen(param,state)
----
-- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'
-...
-value
----
-- null
-...
 s:drop()
 ---
 ...
diff --git a/test/engine/iterator.test.lua b/test/engine/iterator.test.lua
index fcf753f46..c48dbf1b8 100644
--- a/test/engine/iterator.test.lua
+++ b/test/engine/iterator.test.lua
@@ -399,9 +399,6 @@ value
 gen,param,state = i:pairs({35})
 state, value = gen(param,state)
 value
-s:replace{35}
-state, value = gen(param,state)
-value

Full patch is below:

===============================================================================

Subject: [PATCH 2/5] schema: add new system space for FK constraints

This patch introduces new system space to persist foreign keys
constraints. Format of the space:

_fk_constraint (space id = 358)

[<contraint name> STR, <parent id> UINT, <child id> UINT,
 <is deferred> BOOL, <match> STR, <on delete action> STR,
 <on update action> STR, <field links> ARRAY<MAP< UINT : UINT >>]

FK constraint is local to space, so every pair <FK name, child id>
is unique (and it is PK in _fk_constraint space).

After insertion into this space, new instance describing FK constraint
is created. FK constraints are held in data-dictionary as two lists
(for child and parent constraints) in struct space.

There is a list of FK restrictions:
 - At the time of FK creation parent and child spaces must exist;
 - VIEWs can't be involved into FK processing;
 - Child space must be empty;
 - Types of referencing and referenced fields must be comparable;
 - Collations of referencing and referenced fields must match;
 - Referenced fields must compose unique index;
 - Referenced fields can not contain duplicates.

Until space (child) features FK constraints it isn't allowed to be
dropped. Implicitly referenced index also can't be dropped
(and that is why parent space can't be dropped). But :drop() method
of child space firstly deletes all FK constraint (the same as SQL
triggers, indexes etc) and then removes entry from _space.

Part of #3271
---
 src/box/CMakeLists.txt         |   1 +
 src/box/alter.cc               | 455 ++++++++++++++++++++++++++++++++++++++++-
 src/box/alter.h                |   1 +
 src/box/bootstrap.snap         | Bin 1704 -> 1806 bytes
 src/box/errcode.h              |   2 +
 src/box/fkey.c                 |  69 +++++++
 src/box/fkey.h                 | 149 ++++++++++++++
 src/box/lua/schema.lua         |   4 +
 src/box/lua/space.cc           |   2 +
 src/box/lua/upgrade.lua        |  20 ++
 src/box/schema.cc              |  16 ++
 src/box/schema_def.h           |  14 ++
 src/box/space.c                |   4 +
 src/box/space.h                |   9 +
 src/box/sql.c                  |   8 +
 src/box/sql/fkey.c             |  32 +--
 src/box/sql/tarantoolInt.h     |   1 +
 test/box/access_misc.result    |   5 +
 test/box/access_sysview.result |   6 +-
 test/box/alter.result          |   6 +-
 test/box/misc.result           |   2 +
 test/engine/iterator.result    |  12 --
 test/engine/iterator.test.lua  |   3 -
 test/sql/foreign-keys.result   | 326 +++++++++++++++++++++++++++++
 test/sql/foreign-keys.test.lua | 149 ++++++++++++++
 25 files changed, 1246 insertions(+), 50 deletions(-)
 create mode 100644 src/box/fkey.c
 create mode 100644 src/box/fkey.h
 create mode 100644 test/sql/foreign-keys.result
 create mode 100644 test/sql/foreign-keys.test.lua

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index a467d3517..6dd2c75b9 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -92,6 +92,7 @@ add_library(box STATIC
     space.c
     space_def.c
     sequence.c
+    fkey.c
     func.c
     func_def.c
     alter.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 7b6bd1a5a..c5d1f75df 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -33,6 +33,7 @@
 #include "user.h"
 #include "space.h"
 #include "index.h"
+#include "fkey.h"
 #include "func.h"
 #include "coll_id_cache.h"
 #include "coll_id_def.h"
@@ -571,6 +572,14 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
 	old_space->sql_triggers = new_value;
 }
 
+/** The same as for triggers - swap lists of FK constraints. */
+static void
+space_swap_fkeys(struct space *new_space, struct space *old_space)
+{
+	rlist_swap(&new_space->child_fkey, &old_space->child_fkey);
+	rlist_swap(&new_space->parent_fkey, &old_space->parent_fkey);
+}
+
 /**
  * True if the space has records identified by key 'uid'.
  * Uses 'iid' index.
@@ -781,9 +790,10 @@ alter_space_rollback(struct trigger *trigger, void * /* event */)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	struct space *new_space = space_cache_replace(alter->old_space);
 	assert(new_space == alter->new_space);
 	(void) new_space;
@@ -879,9 +889,10 @@ alter_space_do(struct txn *txn, struct alter_space *alter)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	/*
 	 * The new space is ready. Time to update the space
 	 * cache with it.
@@ -1742,6 +1753,18 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 				  space_name(old_space),
 				  "other views depend on this space");
 		}
+		/*
+		 * No need to check existence of parent keys,
+		 * since if we went so far, space would'n have
+		 * any indexes. But referenced space has at least
+		 * one referenced index which can't be dropped
+		 * before constraint itself.
+		 */
+		if (! rlist_empty(&old_space->child_fkey)) {
+			tnt_raise(ClientError, ER_DROP_SPACE,
+				  space_name(old_space),
+				  "the space has foreign key constraints");
+		}
 		/**
 		 * The space must be deleted from the space
 		 * cache right away to achieve linearisable
@@ -1940,6 +1963,21 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 			  "can not add a secondary key before primary");
 	}
 
+	/*
+	 * Can't drop index if foreign key constraints references
+	 * this index.
+	 */
+	if (old_index != NULL && new_tuple == NULL) {
+		struct fkey *fk;
+		rlist_foreach_entry(fk, &old_space->parent_fkey, parent_link) {
+			if (fk->index_id == iid) {
+				tnt_raise(ClientError, ER_ALTER_SPACE,
+					  space_name(old_space),
+					  "can not drop referenced index");
+			}
+		}
+	}
+
 	struct alter_space *alter = alter_space_new(old_space);
 	auto scoped_guard =
 		make_scoped_guard([=] { alter_space_delete(alter); });
@@ -3459,6 +3497,415 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 	txn_on_commit(txn, on_commit);
 }
 
+/**
+ * Decode MsgPack array of links. It consists from maps:
+ * {parent_id (UINT) : child_id (UINT)}.
+ *
+ * @param data MsgPack array of links.
+ * @param[out] out_count Count of links.
+ * @param constraint_name Constraint name to use in error
+ *			  messages.
+ * @param constraint_len Length of constraint name.
+ * @param errcode Errcode for client errors.
+ * @retval Array of links.
+ */
+static struct field_link *
+fkey_links_decode(const char *data, uint32_t *out_count,
+		  const char *constraint_name, uint32_t constraint_len,
+		  uint32_t errcode)
+{
+	assert(mp_typeof(*data) == MP_ARRAY);
+	uint32_t count = mp_decode_array(&data);
+	if (count == 0) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "at least one link must be specified");
+	}
+	*out_count = count;
+	size_t size = count * sizeof(struct field_link);
+	struct field_link *region_links =
+		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
+	memset(region_links, 0, size);
+	const char **map = &data;
+	for (uint32_t i = 0; i < count; ++i) {
+		uint32_t map_sz = mp_decode_map(map);
+		if (map_sz != 2) {
+			tnt_raise(ClientError, errcode,
+				  tt_cstr(constraint_name, constraint_len),
+				  tt_sprintf("link must be map with 2 fields"));
+		}
+		for (uint8_t j = 0; j < map_sz; ++j) {
+			if (mp_typeof(**map) != MP_STR) {
+				tnt_raise(ClientError, errcode,
+					  tt_cstr(constraint_name,
+						  constraint_len),
+					  tt_sprintf("link %d is not map "\
+						     "with string keys", i));
+			}
+			uint32_t key_len;
+			const char *key = mp_decode_str(map, &key_len);
+			if (key_len == 6 &&
+			    memcmp(key, "parent", key_len) == 0) {
+				region_links[i].parent_field =
+					mp_decode_uint(map);
+			} else if (key_len == 5 &&
+				   memcmp(key, "child", key_len) == 0) {
+				region_links[i].child_field =
+					mp_decode_uint(map);
+			} else {
+				char *errmsg = tt_static_buf();
+				snprintf(errmsg, TT_STATIC_BUF_LEN,
+					 "unexpected key of link %d '%.*s'", i,
+					 key_len, key);
+				tnt_raise(ClientError, errcode,
+					  tt_cstr(constraint_name,
+						  constraint_len), errmsg);
+			}
+		}
+	}
+	return region_links;
+}
+
+/** Create an instance of foreign key def constraint from tuple. */
+static struct fkey_def *
+fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
+{
+	uint32_t name_len;
+	const char *name =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_NAME,
+				   &name_len);
+	if (name_len > BOX_NAME_MAX) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(name, BOX_INVALID_NAME_MAX),
+			  "constraint name is too long");
+	}
+	identifier_check_xc(name, name_len);
+	const char *links_raw =
+		tuple_field_with_type_xc(tuple, BOX_FK_CONSTRAINT_FIELD_LINKS,
+					 MP_ARRAY);
+	uint32_t link_count;
+	struct field_link *links = fkey_links_decode(links_raw, &link_count,
+						     name, name_len, errcode);
+	size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
+	struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
+	if (fk_def == NULL)
+		tnt_raise(OutOfMemory, fkey_sz, "malloc", "fk_def");
+	auto def_guard = make_scoped_guard([=] { free(fk_def); });
+	memcpy(fk_def->name, name, name_len);
+	fk_def->name[name_len] = '\0';
+	fk_def->links = (struct field_link *)((char *)&fk_def->name +
+					      name_len + 1);
+	memcpy(fk_def->links, links, link_count * sizeof(struct field_link));
+	fk_def->field_count = link_count;
+	fk_def->child_id = tuple_field_u32_xc(tuple,
+					      BOX_FK_CONSTRAINT_FIELD_CHILD_ID);
+	fk_def->parent_id =
+		tuple_field_u32_xc(tuple, BOX_FK_CONSTRAINT_FIELD_PARENT_ID);
+	fk_def->is_deferred =
+		tuple_field_bool_xc(tuple, BOX_FK_CONSTRAINT_FIELD_DEFERRED);
+	const char *match = tuple_field_str_xc(tuple,
+					       BOX_FK_CONSTRAINT_FIELD_MATCH,
+					       &name_len);
+	fk_def->match = STRN2ENUM(fkey_match, match, name_len);
+	if (fk_def->match == fkey_match_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown MATCH clause");
+	}
+	const char *on_delete_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_DELETE,
+				   &name_len);
+	fk_def->on_delete = STRN2ENUM(fkey_action, on_delete_action, name_len);
+	if (fk_def->on_delete == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON DELETE action");
+	}
+	const char *on_update_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_UPDATE,
+				   &name_len);
+	fk_def->on_update = STRN2ENUM(fkey_action, on_update_action, name_len);
+	if (fk_def->on_update == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON UPDATE action");
+	}
+	def_guard.is_active = false;
+	return fk_def;
+}
+
+/**
+ * Remove FK constraint from child's list.
+ * Entries in child list are supposed to be unique
+ * by their name.
+ *
+ * @param list List of child FK constraints.
+ * @param fkey_name Name of constraint to be removed.
+ * @retval FK being removed.
+ */
+static struct fkey *
+fkey_remove_child(struct rlist *list, const char *fkey_name)
+{
+	struct fkey *fk;
+	rlist_foreach_entry(fk, list, child_link) {
+		if (strcmp(fkey_name, fk->def->name) == 0) {
+			rlist_del_entry(fk, child_link);
+			return fk;
+		}
+	}
+	unreachable();
+	return NULL;
+}
+
+/**
+ * On rollback of creation we remove FK constraint from DD, i.e.
+ * from parent's and child's lists of constraints and
+ * release memory.
+ */
+static void
+on_create_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	rlist_del_entry(fk, parent_link);
+	rlist_del_entry(fk, child_link);
+	fkey_delete(fk);
+}
+
+/** Return old FK and release memory for the new one. */
+static void
+on_replace_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk->def->parent_id);
+	struct space *child = space_by_id(fk->def->child_id);
+	struct fkey *old_fkey = fkey_remove_child(&child->child_fkey,
+						  fk->def->name);
+	rlist_del_entry(old_fkey, parent_link);
+	fkey_delete(old_fkey);
+	rlist_add_entry(&child->child_fkey, fk, child_link);
+	rlist_add_entry(&parent->parent_fkey, fk, parent_link);
+}
+
+/** On rollback of drop simply return back FK to DD. */
+static void
+on_drop_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk_to_restore = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk_to_restore->def->parent_id);
+	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);
+}
+
+/**
+ * On commit of drop or replace we have already deleted old
+ * foreign key entry from both (parent's and child's) lists,
+ * so just release memory.
+ */
+static void
+on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	fkey_delete(fk);
+}
+
+static int
+cmp_uint32(const void *_a, const void *_b)
+{
+	const uint32_t *a = (const uint32_t *) _a, *b = (const uint32_t *) _b;
+	if (*a == *b)
+		return 0;
+	return (*a > *b) ? 1 : -1;
+}
+
+/**
+ * ANSI SQL doesn't allow list of referenced fields to contain
+ * duplicates.
+ */
+static int
+fkey_links_check_duplicates(struct fkey_def *fk_def)
+{
+	uint32_t *parent_fields =
+		(uint32_t *) region_alloc(&fiber()->gc, fk_def->field_count);
+	if (parent_fields == NULL) {
+		tnt_raise(OutOfMemory, fk_def->field_count, "region",
+			  "parent_fields");
+	}
+	for (uint32_t i = 0; i < fk_def->field_count; ++i)
+		parent_fields[i] = fk_def->links[i].parent_field;
+	qsort(parent_fields, fk_def->field_count, sizeof(*parent_fields),
+	      cmp_uint32);
+	uint32_t prev_val = parent_fields[0];
+	for (uint32_t i = 1; i < fk_def->field_count; ++i) {
+		if (prev_val == parent_fields[i])
+			return -1;
+		prev_val = parent_fields[i];
+	}
+	return 0;
+}
+
+/** A trigger invoked on replace in the _fk_constraint space. */
+static void
+on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
+{
+	struct txn *txn = (struct txn *) event;
+	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct tuple *old_tuple = stmt->old_tuple;
+	struct tuple *new_tuple = stmt->new_tuple;
+	if (new_tuple != NULL) {
+		/* Create or replace foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(new_tuple,
+						ER_CREATE_FK_CONSTRAINT);
+		auto fkey_def_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		if (child_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space can't be VIEW");
+		}
+		struct space *parent_space =
+			space_cache_find_xc(fk_def->parent_id);
+		if (parent_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referenced space can't be VIEW");
+		}
+		/*
+		 * FIXME: until SQL triggers are completely
+		 * integrated into server (i.e. we are able to
+		 * invoke triggers even if DML occurred via Lua
+		 * interface), it makes no sense to provide any
+		 * checks on existing data in space.
+		 */
+		struct index *pk = space_index(child_space, 0);
+		if (index_size(pk) > 0) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space must be empty");
+		}
+		/* Check types of referenced fields. */
+		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+			uint32_t child_fieldno = fk_def->links[i].child_field;
+			uint32_t parent_fieldno = fk_def->links[i].parent_field;
+			if (child_fieldno >= child_space->def->field_count ||
+			    parent_fieldno >= parent_space->def->field_count) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "foreign key refers to "
+						        "nonexistent field");
+			}
+			struct field_def child_field =
+				child_space->def->fields[child_fieldno];
+			struct field_def parent_field =
+				parent_space->def->fields[parent_fieldno];
+			if (! field_type1_contains_type2(parent_field.type,
+							 child_field.type)) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "field type mismatch");
+			}
+			if (child_field.coll_id != parent_field.coll_id) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name,
+					  "field collation mismatch");
+			}
+		}
+		if (fkey_links_check_duplicates(fk_def)) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name, "referenced fields can not "
+						"contain duplicates");
+		}
+		/*
+		 * Search for suitable index in parent space:
+		 * it must be unique and consist exactly from
+		 * referenced columns (but order may be
+		 * different).
+		 */
+		struct index *fk_index = NULL;
+		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
+			struct index *idx = space_index(parent_space, i);
+			if (!idx->def->opts.is_unique)
+				continue;
+			if (idx->def->key_def->part_count !=
+			    fk_def->field_count)
+				continue;
+			uint32_t j;
+			for (j = 0; j < fk_def->field_count; ++j) {
+				if (key_def_find(idx->def->key_def,
+						 fk_def->links[j].parent_field)
+				    == NULL)
+					break;
+			}
+			if (j != fk_def->field_count)
+				continue;
+			fk_index = idx;
+			break;
+		}
+		if (fk_index == NULL) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name, "referenced fields don't "
+						"compose unique index");
+		}
+		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
+		if (fkey == NULL)
+			tnt_raise(OutOfMemory, sizeof(*fkey), "malloc", "fkey");
+		auto fkey_guard = make_scoped_guard([=] { free(fkey); });
+		memset(fkey, 0, sizeof(*fkey));
+		fkey->def = fk_def;
+		fkey->index_id = fk_index->def->iid;
+		if (old_tuple == NULL) {
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_create_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+		} else {
+			struct fkey *old_fk =
+				fkey_remove_child(&child_space->child_fkey,
+						  fk_def->name);
+			rlist_del_entry(old_fk, parent_link);
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_replace_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+			struct trigger *on_commit =
+				txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+						      old_fk);
+			txn_on_commit(txn, on_commit);
+		}
+		fkey_def_guard.is_active = false;
+		fkey_guard.is_active = false;
+	} else if (new_tuple == NULL && old_tuple != NULL) {
+		/* Drop foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(old_tuple,
+						ER_DROP_FK_CONSTRAINT);
+		auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		struct fkey *old_fkey =
+			fkey_remove_child(&child_space->child_fkey,
+					  fk_def->name);
+		rlist_del_entry(old_fkey, parent_link);
+		struct trigger *on_commit =
+			txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+					      old_fkey);
+		txn_on_commit(txn, on_commit);
+		struct trigger *on_rollback =
+			txn_alter_trigger_new(on_drop_fkey_rollback, old_fkey);
+		txn_on_rollback(txn, on_rollback);
+	}
+}
+
 struct trigger alter_space_on_replace_space = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL
 };
@@ -3523,4 +3970,8 @@ struct trigger on_replace_trigger = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_trigger, NULL, NULL
 };
 
+struct trigger on_replace_fk_constraint = {
+	RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL
+};
+
 /* vim: set foldmethod=marker */
diff --git a/src/box/alter.h b/src/box/alter.h
index 8ea29c77b..4108fa47c 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -45,6 +45,7 @@ extern struct trigger on_replace_sequence;
 extern struct trigger on_replace_sequence_data;
 extern struct trigger on_replace_space_sequence;
 extern struct trigger on_replace_trigger;
+extern struct trigger on_replace_fk_constraint;
 extern struct trigger on_stmt_begin_space;
 extern struct trigger on_stmt_begin_index;
 extern struct trigger on_stmt_begin_truncate;
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index a8a00ec29e7106afbd06294cf6ffe291f90a2e10..84e9bbdba82bc059acc4ec9c9c177bb83971285c 100644
GIT binary patch
delta 1802
zcmV+l2le=<4UP_w8Gko6FfC^@Gc#p0I5RY13Q2BrbYX5|WjY{ZG-76CHeoO=GdDLl
zEi^D>Gc93dF*GeXIXE^jFgQ6dFgP{}RzqxWV{1AfdwmKD)w&D1%?6VI&KhYGX{7)F
z0000ewJ-euP*pSlsz-PaNx&F!0w{{2D2k#e&ZBt<!h$K72!91&#*NtprjlM!-1=ZP
zpp_}9a+7swE5BRnBB9tA#bUCBx3Y$Euf!aWYT%boR}01SGNlyY0N4QQ00THnO%k_e
zyptnM#NQ~^$XDO5eZ~`fy~<pdH!wQ^lIbybEQ;fIhw6&}&Qi0=iz+ONrM^59eJNts
z`O<i1zP`xpTYp&7Ryyy_Yfrv94G3n0{`A(jPTYFd$FHrr)FLI_C;(@v`P#UZH)&(_
zmHyrN?MVLakOjb5Y8q*hBZZ4ueQ^A;E^SrTy>OhRCaqSFneTVml5m!qTWS%Q?+<R(
zDrcz);wC(uJP@Z8PKXleUzH-E{!Jv4D#S+$aViW;_<zHoWq;?T_}O5cOQrOR3lI@A
z$^#jeEIL*ISPB%ME_c5|l?MW6sd+=PTIJpEt?oSFEHz8mz15r-SKegG0cWZCs~58?
zZ&K*??}?~JqW`gLyjM0?-sC+iszGc&FG~zKOHEcUacn;??^!*UB%dxr*R9o>1)Qa3
z*`Fu*bblG%v(hM#bqO{m&pea;JgD=n)fog#Tz9C-9OxLlP$~rjoTaA5FOH*1{9e!c
z@OSo&J3oK0&aK)0)pwn3<abVEqQC5UeuZjG0iVw<{^B;*wN)4bcyD~-)*i#!3X9>{
zbvb?)6hDVVI<NHCl>CQDert6UJ1TZk)EpE!I)AC0)QOY{Sk0nJP!tKyQj>!rL5(Ph
zD5Xj$563%sA!<xDH$gQZXi795XQ>%P^P#hWb)*>&u$c}r9As)_XjmMxC~Sz)5R)MW
z1Iz^&3oxCvqnQc|qEbf{Ml!Wh;4C#?Vkj)A%9lM^m$f7RyVIykEs}LJ6N>08o){#{
ztA9tK{^EmIk3y`k<JP(NhYwz!iS%zw3V~u~eM`E|cz3SH?-mI!lnP4VEHyV4@kh2W
zuYsbT^}L?ROhI5er~QC=CaU_Z)N$um?BRE)0vUnd@qYcQ%kvYWC>2Kp&QcTBaVM1L
zrBI%CLZwn9Q|U432J}oM6b{Y}$-^fwRDYR13|cCz%fS_f)SJTzu{=RiKuSMqd~`iz
zcvP4joTa9HsYR#NQT3Snr_1nWYpg@%*s^8Im5PJ2)Ev?0`(0U=w;P<LCICxotV7w@
zPnP_w6>5B0B!3fCj>e1QrhQpdm1M2jSm({qxdr00RvDb7W{FkEB41q8dBNiAP=CY6
z`@Cdyk9^_tE|zOb39~qf#H>%~$bMeZI{$KYRvPQP7@Vc1P7WBeI)*yi?B}J8&CjTk
zQfsWEfl<L1AWr}ok{~W%Kz44m1r8QGD{6sVsNqy(5E_^P0003{0PzI}Dd&m?5`eHc
zilZ=&ff$HG5R3v7XMg|{gajI(T7NLK&*(tis#(}3cM84Ef9L2qdX63$r)TG_nJt(&
z9+cQ3<d~L!BP$RHkex6j^vBX^NixnpTZqAaiy9S%Cjxh2QPPH(XH%fy{?x|+<EYM9
z*|-6`k}L=(0H+uP4{MUHNHeoI=eNlmMrEEaxh3`H{tM_$^;DCA1)2y=`F|j=*29+K
zxTMM=5u#bDQmT7jTbo50>6WtT>)63nS1901qUYC685VoX<7Agp^XRamYRfK-atG>$
zdDCt*ck7UBtstvXD(1F8m(r8XHxTGJ7o@5KP0+c3CT-qq++dYLF?Wj=y+#j`s3B#;
zANx@w-GRDc-n1LdZDwqUwtr0aYdk<7tTN+LKhYWpLD);Ye#;sR<%|6o2gu>E7DjDF
z&~sifvC<hTYXdAPVEP>og^PO0QiAg+^W(bKJsqn_MU~P|vhbX6q*sSH(r&xFfxS00
zD~UeKTppJuq{v`%cJSHQk?!c`xg?l4;GY?(qW%~ue||xJFp1Ff(0}FEgino4lDM6s
z*v%KzM|f6U$3~3MNC)>2<F-E87;c%V)v&4ja+I8-4=Y;4f3o|_Z6Vx#A<mDH&|TWF
zHkD~*4IL$1J|<Em@F<QQ;OJ9plR}L{zFF!MYZK6v>0&M2;`*p07C_6!*>hVDc4mZg
zdTb16b~fIzr_PYGb${V}5=n5PCIJ9rh`%m^&nNXa%EE&bwsR|C6=;&k*D5ZD)~WN|
z=89gDEnNm~lUJ{*|G%67kGx}%*h%l=Br2fVA0+<Tzw7l!sOGwd6}k`#0$M2yhhMFs
ss@gF<!|tr?9h5;e73isp{y|vRl6wscxQ+<GS7AL#a-tm75Cg643Sr(;MF0Q*

delta 1699
zcmV;U23+}$4yX-~8Gki5FfC^@G-fw8H#rJPZgX^DZewLSAYx=OVKg#1V=Xo^Ff=VR
zH#adYIA$<6Eiz^~VK^`|F)=qdI0{xnY;R+0Iv{&}3JTS_3%bn)FaXY~@A2EE00000
z04TLD{QyusF#sw<untMk7&id`Up&AM2ap_8K2ct%)hPuc5r2u%1Dtdthk@FZnGtQL
zq{+<`lO*YB%N4E_fREI4wd=GkVz`4fdcG}`x2rRP>+&h36w?690OJ7SjC}R|`e!^<
zufxoBSySaEK}34Y9gE@!en$00z_rw*@?r{$VyQo#kG>SK>wIY(Gha_6_ARVsq0YPW
z*^{p>3xXA)K!3gUtrNGN1@ezpT`H224JN>~)O>B+%A2IIdZ>SQUNe%x&&UF}mYPMF
z<VfKnR$n!OS(mg*w7uMGsY$EVW9Iu^v?SM3GpQn}@&|-lvwAHxzsossplA9N4p%a9
zo>%g6a!e(V!@(sGEAUSy(+NYC{hgO0NP}%ImC`FNL4Q!RY*}%n3YHx!04xm(FRr{v
zl!I%j`QeM%l{Z=Rb?}Fz#-snSYrI36D{t~UtJh#?KQB85*HV+!OFXonm)}`Amn5Jr
zSFUeXZ5CWh&9XmF0_t-0owY^*(UxFi^2{+JpjYYq%_<FoC9cn?&K&4i^*yN+3|vc1
zjb9wclz#}mo&{p?>>GD}0%4t7vHjt9ooVEEE?c6%^m+ajRha@mpI!XLX|9h}V+i2A
z@rhe|T+Ko(uFkH@5&Tr*=hczUq5heY2QkUNS%n#JEj3Bpit$d4G7*DgL3m(5TGT2F
z94mTMRGt((D5}nmjt=Ss$^@zeY-T+Y)EjXvHGeM*i6^2%bzXPkLU15BlTD5dj*v}P
z!_|DamYPB}H(Lp|d7$Y4qv0@PGgC9OfyFV4f(DrkG8kko#8`;gVrZ;|sZe1;Q!301
z424=>WUi&=%LED&qVlCr*5%B||L!vCQjv%@8VT1@b7K*MWDE0|O4Ko**D)efkeJS8
zK!2(n6H`6bb=>(U`tZ-F0)bI|$NTk%AJ0yXpi~?YuB9fdV^Ej7e?_$i!nM?_Aqm60
z`&X+x53Z$V3HxeQC!Tlex*X2Ks1;7>#I3wicRHC$CJrY=2}I$TaHmRP$WmdOj*tpL
zDiEFf!}j<<_2_um?AUb7<ft(?uBE1ZseeePy;1d;JE+T5&DNpKvyqgPbfwm~mYOj1
z`F>Z{WsSzQ)C6E@9ooDZ`^l1@wSo*Vi{$U4dSkpeZW)-xR7uXN9oje;I=58#to6mU
z)GQIMSmcX~J1<y(9cuXapO=izkuQGU#d1mMFpHN+%mRyy?B}Jd^Do1*(xHv(xqp_L
zJ2_y)>bTPRW<M`)Zh*#Alv;<j8W#azNCJUKsNq;-5DClx0Du4}0PzI}Dd&m?5`eHc
zio-CDff$BE0E|KtM?fGb2njSmwO}-3>|otvVxbn$nEy|s!{{(Nj1CZ|XWOmW^1O_Q
z#C9R1B6~4P0)Yit2t#sze0O3}a)0*OYw7YYtf)91`JUH`Pa9%0YaYejkXxhcI3*z3
zdE&uuxjDLwcwchr;ms`(kQeo&CcC+u2o3ol@WqTwal~b%ln5QO6oL_ZE4HeMEh(My
z_v_fe^><ak7er6BFLY%562{3Rr^e{WMg>zAXfXbi)iZOl46U*mTj66G#((TQBk0n5
z@)<oY4g-ONQth~(3rms{8<`ta`P_f=wD_@mkOU1mHT1wuY%u<m)iZOl3{AjWf3|$~
zt8m|QXkeD0hZ@Msd-hKiZ0KL;Q@khOrOoi`UOgQ>poLS)q+!uN;jpegcC;P#`N7`U
znw9D$>kp61-&FJnXm;?~*ng4k=;HZ8FhRgSHd00X7<B%WiF{!a-SZ&o*D9aNO_EGI
zB{7vRtd9oyxDE<2a=|>f+87shpvG{^Ol^j3&MJS%IeJ)8Cti{*Ub%&E>I-oWh=lIa
zrQD!Ql+Ehs<+5HPA%914?C?i#t&Krytl`a4o=ls7UrZNkEfZHnq<_5tDm2be+j_v6
zk)6|HU_kR{(H-|;3^`j1=aVc3C;Vs%FuC|kE%^FTe<LpZkFbK<maIKZW_(1&;Gu~+
zlWeroE4jI4;5K>1vid)s6X20|EK)(~T|9{xS1W;}zgF#f{gJ7;9%W`;4+U&31%|V|
tLZAw@V?4uhT=^Z8O&m3K=&6$aL0H$8dkzbvjtIq9$$FA7L^;(Et?f_{Enxrv

diff --git a/src/box/errcode.h b/src/box/errcode.h
index b61b387f2..213a1864b 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -219,6 +219,8 @@ struct errcode_record {
 	/*164 */_(ER_NO_SUCH_GROUP,		"Replication group '%s' does not exist") \
 	/*165 */_(ER_NO_SUCH_MODULE,		"Module '%s' does not exist") \
 	/*166 */_(ER_NO_SUCH_COLLATION,		"Collation '%s' does not exist") \
+	/*167 */_(ER_CREATE_FK_CONSTRAINT,	"Failed to create foreign key constraint '%s': %s") \
+	/*168 */_(ER_DROP_FK_CONSTRAINT,	"Failed to drop foreign key constraint '%s': %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/fkey.c b/src/box/fkey.c
new file mode 100644
index 000000000..b3980c874
--- /dev/null
+++ b/src/box/fkey.c
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "fkey.h"
+#include "sql.h"
+#include "sql/sqliteInt.h"
+
+const char *fkey_action_strs[] = {
+	/* [FKEY_ACTION_RESTRICT]    = */ "no_action",
+	/* [FKEY_ACTION_SET_NULL]    = */ "set_null",
+	/* [FKEY_ACTION_SET_DEFAULT] = */ "set_default",
+	/* [FKEY_ACTION_CASCADE]     = */ "cascade",
+	/* [FKEY_ACTION_NO_ACTION]   = */ "restrict"
+};
+
+const char *fkey_match_strs[] = {
+	/* [FKEY_MATCH_SIMPLE]  = */ "simple",
+	/* [FKEY_MATCH_PARTIAL] = */ "partial",
+	/* [FKEY_MATCH_FULL]    = */ "full"
+};
+
+void
+fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
+{
+	if (p != NULL) {
+		struct TriggerStep *step = p->step_list;
+		sql_expr_delete(db, step->pWhere, false);
+		sql_expr_list_delete(db, step->pExprList);
+		sql_select_delete(db, step->pSelect);
+		sql_expr_delete(db, p->pWhen, false);
+		sqlite3DbFree(db, p);
+	}
+}
+
+void
+fkey_delete(struct fkey *fkey)
+{
+	fkey_trigger_delete(sql_get(), fkey->on_delete_trigger);
+	fkey_trigger_delete(sql_get(), fkey->on_update_trigger);
+	free(fkey->def);
+	free(fkey);
+}
diff --git a/src/box/fkey.h b/src/box/fkey.h
new file mode 100644
index 000000000..0d537b1a7
--- /dev/null
+++ b/src/box/fkey.h
@@ -0,0 +1,149 @@
+#ifndef TARANTOOL_BOX_FKEY_H_INCLUDED
+#define TARANTOOL_BOX_FKEY_H_INCLUDED
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "space.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct sqlite3;
+
+enum fkey_action {
+	FKEY_NO_ACTION = 0,
+	FKEY_ACTION_SET_NULL,
+	FKEY_ACTION_SET_DEFAULT,
+	FKEY_ACTION_CASCADE,
+	FKEY_ACTION_RESTRICT,
+	fkey_action_MAX
+};
+
+enum fkey_match {
+	FKEY_MATCH_SIMPLE = 0,
+	FKEY_MATCH_PARTIAL,
+	FKEY_MATCH_FULL,
+	fkey_match_MAX
+};
+
+extern const char *fkey_action_strs[];
+
+extern const char *fkey_match_strs[];
+
+/** Structure describing field dependencies for foreign keys. */
+struct field_link {
+	uint32_t parent_field;
+	uint32_t child_field;
+};
+
+/** Definition of foreign key constraint. */
+struct fkey_def {
+	/** Id of space containing the REFERENCES clause (child). */
+	uint32_t child_id;
+	/** Id of space that the key points to (parent). */
+	uint32_t parent_id;
+	/** Number of fields in this key. */
+	uint32_t field_count;
+	/** True if constraint checking is deferred till COMMIT. */
+	bool is_deferred;
+	/** Match condition for foreign key. SIMPLE by default. */
+	enum fkey_match match;
+	/** ON DELETE action. NO ACTION by default. */
+	enum fkey_action on_delete;
+	/** ON UPDATE action. NO ACTION by default. */
+	enum fkey_action on_update;
+	/** Mapping of fields in child to fields in parent. */
+	struct field_link *links;
+	/** Name of the constraint. */
+	char name[0];
+};
+
+/** Structure representing foreign key relationship. */
+struct fkey {
+	struct fkey_def *def;
+	/** Index id of referenced index in parent space. */
+	uint32_t index_id;
+	/** Triggers for actions. */
+	struct sql_trigger *on_delete_trigger;
+	struct sql_trigger *on_update_trigger;
+	/** Links for parent and child lists. */
+	struct rlist parent_link;
+	struct rlist child_link;
+};
+
+/**
+ * Alongside with struct fkey_def itself, we reserve memory for
+ * string containing its name and for array of links.
+ * Memory layout:
+ * +-------------------------+ <- Allocated memory starts here
+ * |     struct fkey_def     |
+ * |-------------------------|
+ * |        name + \0        |
+ * |-------------------------|
+ * |          links          |
+ * +-------------------------+
+ */
+static inline size_t
+fkey_def_sizeof(uint32_t links_count, uint32_t name_len)
+{
+	return sizeof(struct fkey) + links_count * sizeof(struct field_link) +
+	       name_len + 1;
+}
+
+static inline bool
+fkey_is_self_referenced(const struct fkey_def *fkey)
+{
+	return fkey->child_id == fkey->parent_id;
+}
+
+/**
+ * The second argument is a Trigger structure allocated by the
+ * fkActionTrigger() routine.This function deletes the Trigger
+ * structure and all of its sub-components.
+ *
+ * @param db Database handler.
+ * @param p Trigger to be freed.
+ */
+void
+fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p);
+
+/** Release memory for foreign key and its triggers, if any. */
+void
+fkey_delete(struct fkey *fkey);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* TARANTOOL_BOX_FKEY_H_INCLUDED */
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index d14dd748b..b73d9ab78 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -508,6 +508,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _vindex = box.space[box.schema.VINDEX_ID]
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
+    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
     if sequence_tuple ~= nil and sequence_tuple[3] == true then
         -- Delete automatically generated sequence.
@@ -521,6 +522,9 @@ box.schema.space.drop = function(space_id, space_name, opts)
         local v = keys[i]
         _index:delete{v[1], v[2]}
     end
+    for _, t in _fk_constraint.index.child_id:pairs({space_id}) do
+        _fk_constraint:delete({t.name, space_id})
+    end
     revoke_object_privs('space', space_id)
     _truncate:delete{space_id}
     if _space:delete{space_id} == nil then
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index ca3fefc0d..d07560d6c 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -551,6 +551,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "SQL_STAT1_ID");
 	lua_pushnumber(L, BOX_SQL_STAT4_ID);
 	lua_setfield(L, -2, "SQL_STAT4_ID");
+	lua_pushnumber(L, BOX_FK_CONSTRAINT_ID);
+	lua_setfield(L, -2, "FK_CONSTRAINT_ID");
 	lua_pushnumber(L, BOX_TRUNCATE_ID);
 	lua_setfield(L, -2, "TRUNCATE_ID");
 	lua_pushnumber(L, BOX_SEQUENCE_ID);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index f112a93ae..a6d6980c6 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -509,6 +509,26 @@ local function upgrade_to_2_1_0()
                   {unique = true}, {{0, 'string'}, {1, 'string'},
                                     {5, 'scalar'}}}
 
+    local fk_constr_ft = {{name='name', type='string'},
+                          {name='child_id', type='unsigned'},
+                          {name='parent_id', type='unsigned'},
+                          {name='is_deferred', type='boolean'},
+                          {name='match', type='string'},
+                          {name='on_delete', type='string'},
+                          {name='on_update', type='string'},
+                          {name='links', type='array'}}
+    log.info("create space _fk_constraint")
+    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
+                  0, setmap({}), fk_constr_ft}
+
+    log.info("create index primary on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
+                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
+
+    log.info("create secondary index child_id on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 1, 'child_id', 'tree',
+                  {unique = false}, {{1, 'unsigned'}}}
+
     -- Nullability wasn't skipable. This was fixed in 1-7.
     -- Now, abscent field means NULL, so we can safely set second
     -- field in format, marking it nullable.
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 86c56ee2e..faad53700 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -412,6 +412,22 @@ schema_init()
 			 COLL_NONE, SORT_ORDER_ASC);
 	/* _sql_stat4 - extensive statistics on space, seen in SQL. */
 	sc_space_new(BOX_SQL_STAT4_ID, "_sql_stat4", key_def, NULL, NULL);
+
+	key_def_delete(key_def);
+	key_def = key_def_new(2);
+	if (key_def == NULL)
+		diag_raise();
+	/* Constraint name. */
+	key_def_set_part(key_def, 0, BOX_FK_CONSTRAINT_FIELD_NAME,
+			 FIELD_TYPE_STRING, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* Child space. */
+	key_def_set_part(key_def, 1, BOX_FK_CONSTRAINT_FIELD_CHILD_ID,
+			 FIELD_TYPE_UNSIGNED, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* _fk_сonstraint - foreign keys constraints. */
+	sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_def,
+		     &on_replace_fk_constraint, NULL);
 }
 
 void
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 5ab4bb002..6022ea072 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -107,6 +107,8 @@ enum {
 	/** Space ids for SQL statictics. */
 	BOX_SQL_STAT1_ID = 348,
 	BOX_SQL_STAT4_ID = 349,
+	/** Space id of _fk_constraint. */
+	BOX_FK_CONSTRAINT_ID = 356,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -224,6 +226,18 @@ enum {
 	BOX_TRIGGER_FIELD_OPTS = 2,
 };
 
+/** _fk_constraint fields. */
+enum {
+	BOX_FK_CONSTRAINT_FIELD_NAME = 0,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_ID = 1,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_ID = 2,
+	BOX_FK_CONSTRAINT_FIELD_DEFERRED = 3,
+	BOX_FK_CONSTRAINT_FIELD_MATCH = 4,
+	BOX_FK_CONSTRAINT_FIELD_ON_DELETE = 5,
+	BOX_FK_CONSTRAINT_FIELD_ON_UPDATE = 6,
+	BOX_FK_CONSTRAINT_FIELD_LINKS = 7,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/space.c b/src/box/space.c
index e53f1598c..90a80ed7b 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -163,6 +163,8 @@ space_create(struct space *space, struct engine *engine,
 		space->index_map[index_def->iid] = index;
 	}
 	space_fill_index_map(space);
+	rlist_create(&space->parent_fkey);
+	rlist_create(&space->child_fkey);
 	return 0;
 
 fail_free_indexes:
@@ -220,6 +222,8 @@ space_delete(struct space *space)
 	 * on_replace_dd_trigger on deletion from _trigger.
 	 */
 	assert(space->sql_triggers == NULL);
+	assert(rlist_empty(&space->parent_fkey));
+	assert(rlist_empty(&space->child_fkey));
 	space->vtab->destroy(space);
 }
 
diff --git a/src/box/space.h b/src/box/space.h
index 01a4af726..97650cffe 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -183,6 +183,15 @@ struct space {
 	 * of index id.
 	 */
 	struct index **index;
+	/**
+	 * Lists of foreign key constraints. In SQL terms parent
+	 * space is the "from" table i.e. the table that contains
+	 * the REFERENCES clause. Child space is "to" table, in
+	 * other words the table that is named in the REFERENCES
+	 * clause.
+	 */
+	struct rlist parent_fkey;
+	struct rlist child_fkey;
 };
 
 /** Initialize a base space instance. */
diff --git a/src/box/sql.c b/src/box/sql.c
index d48c3cfe5..d4b0d7fcc 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1251,6 +1251,14 @@ void tarantoolSqlite3LoadSchema(struct init_data *init)
 			       "\"sample\","
 			       "PRIMARY KEY(\"tbl\", \"idx\", \"sample\"))");
 
+	sql_init_callback(init, TARANTOOL_SYS_FK_CONSTRAINT_NAME,
+			  BOX_FK_CONSTRAINT_ID, 0,
+			  "CREATE TABLE \""TARANTOOL_SYS_FK_CONSTRAINT_NAME
+			  "\"(\"name\" TEXT, \"parent_id\" INT, \"child_id\" INT,"
+			  "\"deferred\" INT, \"match\" TEXT, \"on_delete\" TEXT,"
+			  "\"on_update\" TEXT, \"links\","
+			  "PRIMARY KEY(\"name\", \"child_id\"))");
+
 	/* Read _space */
 	if (space_foreach(space_foreach_put_cb, init) != 0) {
 		init->rc = SQL_TARANTOOL_ERROR;
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index e4bf5a6be..f87f610dc 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -35,6 +35,7 @@
  */
 #include "coll.h"
 #include "sqliteInt.h"
+#include "box/fkey.h"
 #include "box/schema.h"
 #include "box/session.h"
 #include "tarantoolInt.h"
@@ -707,31 +708,6 @@ sqlite3FkReferences(Table * pTab)
 					pTab->def->name);
 }
 
-/**
- * The second argument is a Trigger structure allocated by the
- * fkActionTrigger() routine. This function deletes the sql_trigger
- * structure and all of its sub-components.
- *
- * The Trigger structure or any of its sub-components may be
- * allocated from the lookaside buffer belonging to database
- * handle dbMem.
- *
- * @param db Database connection.
- * @param trigger AST object.
- */
-static void
-sql_fk_trigger_delete(struct sqlite3 *db, struct sql_trigger *trigger)
-{
-	if (trigger == NULL)
-		return;
-	struct TriggerStep *trigger_step = trigger->step_list;
-	sql_expr_delete(db, trigger_step->pWhere, false);
-	sql_expr_list_delete(db, trigger_step->pExprList);
-	sql_select_delete(db, trigger_step->pSelect);
-	sql_expr_delete(db, trigger->pWhen, false);
-	sqlite3DbFree(db, trigger);
-}
-
 /*
  * The second argument points to an FKey object representing a foreign key
  * for which pTab is the child table. An UPDATE statement against pTab
@@ -1309,7 +1285,7 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		sql_expr_list_delete(db, pList);
 		sql_select_delete(db, pSelect);
 		if (db->mallocFailed == 1) {
-			sql_fk_trigger_delete(db, trigger);
+			fkey_trigger_delete(db, trigger);
 			return 0;
 		}
 		assert(pStep != 0);
@@ -1407,8 +1383,8 @@ sqlite3FkDelete(sqlite3 * db, Table * pTab)
 		assert(pFKey->isDeferred == 0 || pFKey->isDeferred == 1);
 
 		/* Delete any triggers created to implement actions for this FK. */
-		sql_fk_trigger_delete(db, pFKey->apTrigger[0]);
-		sql_fk_trigger_delete(db, pFKey->apTrigger[1]);
+		fkey_trigger_delete(db, pFKey->apTrigger[0]);
+		fkey_trigger_delete(db, pFKey->apTrigger[1]);
 
 		pNext = pFKey->pNextFrom;
 		sqlite3DbFree(db, pFKey);
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index e1430a398..bc61e8426 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -20,6 +20,7 @@
 #define TARANTOOL_SYS_TRUNCATE_NAME "_truncate"
 #define TARANTOOL_SYS_SQL_STAT1_NAME "_sql_stat1"
 #define TARANTOOL_SYS_SQL_STAT4_NAME "_sql_stat4"
+#define TARANTOOL_SYS_FK_CONSTRAINT_NAME "_fk_constraint"
 
 /* Max space id seen so far. */
 #define TARANTOOL_SYS_SCHEMA_MAXID_KEY "max_id"
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 53af3e8bd..370bbfa01 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -815,6 +815,11 @@ box.space._space:select()
   - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
         'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
       {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
+  - [356, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
+      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
+      {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
+      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
+      {'name': 'links', 'type': 'array'}]]
 ...
 box.space._func:select()
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index ae042664a..77a24b425 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -230,11 +230,11 @@ box.session.su('guest')
 ...
 #box.space._vspace:select{}
 ---
-- 22
+- 23
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
@@ -262,7 +262,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index c41b52f48..0d50855d2 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -107,7 +107,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 350
+- 357
 ...
 space.field_count
 ---
@@ -152,7 +152,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '350' does not exist
+- error: Space '357' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -231,6 +231,8 @@ _index:select{}
   - [348, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string']]]
   - [349, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string'], [
         5, 'scalar']]]
+  - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
+  - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/misc.result b/test/box/misc.result
index 892851823..a680f752e 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -491,6 +491,8 @@ t;
   164: box.error.NO_SUCH_GROUP
   165: box.error.NO_SUCH_MODULE
   166: box.error.NO_SUCH_COLLATION
+  167: box.error.CREATE_FK_CONSTRAINT
+  168: box.error.DROP_FK_CONSTRAINT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/engine/iterator.result b/test/engine/iterator.result
index 98b0b3e7d..2204ff647 100644
--- a/test/engine/iterator.result
+++ b/test/engine/iterator.result
@@ -4207,18 +4207,6 @@ value
 ---
 - null
 ...
-s:replace{35}
----
-- [35]
-...
-state, value = gen(param,state)
----
-- error: 'builtin/box/schema.lua:1051: usage: next(param, state)'
-...
-value
----
-- null
-...
 s:drop()
 ---
 ...
diff --git a/test/engine/iterator.test.lua b/test/engine/iterator.test.lua
index fcf753f46..c48dbf1b8 100644
--- a/test/engine/iterator.test.lua
+++ b/test/engine/iterator.test.lua
@@ -399,9 +399,6 @@ value
 gen,param,state = i:pairs({35})
 state, value = gen(param,state)
 value
-s:replace{35}
-state, value = gen(param,state)
-value
 
 s:drop()
 
diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result
new file mode 100644
index 000000000..8c53daa83
--- /dev/null
+++ b/test/sql/foreign-keys.result
@@ -0,0 +1,326 @@
+env = require('test_run')
+---
+...
+test_run = env.new()
+---
+...
+test_run:cmd('restart server default with cleanup=1')
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+---
+...
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+---
+...
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+---
+...
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+---
+...
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: Space '666' does not exist
+...
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('T2')[1]['id']
+---
+...
+view_id = box.space._space.index.name:select('V1')[1]['id']
+---
+...
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced space can''t
+    be VIEW'
+...
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space can''t
+    be VIEW'
+...
+box.sql.execute("DROP VIEW v1;")
+---
+...
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown MATCH clause'
+...
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON DELETE action'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {{child = 0, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON UPDATE action'
+...
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space must
+    be empty'
+...
+box.sql.execute("DELETE FROM t2;")
+---
+...
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, child = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': link must be map with
+    2 fields'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1, 2}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': link must be map with
+    2 fields'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{wrong_key = 2, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unexpected key of link
+    0 ''wrong_key'''
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 13, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': foreign key refers to
+    nonexistent field'
+...
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 2}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields don''t
+    compose unique index'
+...
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': field type mismatch'
+...
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 1}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields can
+    not contain duplicates'
+...
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+---
+- error: 'Can''t modify space ''T1'': can not drop referenced index'
+...
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+---
+...
+box.space._space:delete(child_id)
+---
+- error: 'Can''t drop space ''T2'': the space has foreign key constraints'
+...
+box.space.T2:drop()
+---
+...
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+---
+- []
+...
+box.space.T1:drop()
+---
+...
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+---
+...
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+---
+...
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {{parent = 0, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 1}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:count()
+---
+- 5
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'fk_1', child_id}
+---
+- ['fk_1', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'self_2', parent_id}
+---
+- ['self_2', 516, 516, false, 'simple', 'restrict', 'restrict', [{'parent': 1, 'child': 1}]]
+...
+box.space._fk_constraint:delete{'self_1', child_id}
+---
+- ['self_1', 515, 515, false, 'simple', 'restrict', 'restrict', [{'parent': 0, 'child': 0}]]
+...
+box.space._fk_constraint:delete{'fk_3', parent_id}
+---
+- ['fk_3', 516, 515, false, 'simple', 'restrict', 'restrict', [{'child': 1, 'parent': 0}]]
+...
+box.space._fk_constraint:count()
+---
+- 0
+...
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+---
+- cascade
+...
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred']
+---
+- true
+...
+box.space.CHILD:drop()
+---
+...
+box.space.PARENT:drop()
+---
+...
+-- 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
new file mode 100644
index 000000000..9475a7df9
--- /dev/null
+++ b/test/sql/foreign-keys.test.lua
@@ -0,0 +1,149 @@
+env = require('test_run')
+test_run = env.new()
+test_run:cmd('restart server default with cleanup=1')
+
+
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+child_id = box.space._space.index.name:select('T2')[1]['id']
+view_id = box.space._space.index.name:select('V1')[1]['id']
+
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DROP VIEW v1;")
+
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {{child = 0, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1}}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DELETE FROM t2;")
+
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, child = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 2, parent = 1, 2}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{wrong_key = 2, parent = 1}}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 13, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 2}}}
+box.space._fk_constraint:insert(t)
+
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+box.space._fk_constraint:insert(t)
+
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 0, parent = 1}, {child = 1, parent = 1}}}
+box.space._fk_constraint:insert(t)
+
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+box.space._space:delete(child_id)
+box.space.T2:drop()
+
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+box.space.T1:drop()
+
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {{child = 1, parent = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {{parent = 0, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 1}}}
+t = box.space._fk_constraint:insert(t)
+
+box.space._fk_constraint:count()
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'fk_1', child_id}
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'self_2', parent_id}
+box.space._fk_constraint:delete{'self_1', child_id}
+box.space._fk_constraint:delete{'fk_3', parent_id}
+box.space._fk_constraint:count()
+
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:replace(t)
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {{parent = 1, child = 0}}}
+t = box.space._fk_constraint:replace(t)
+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')
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-07-17 21:05   ` [tarantool-patches] " Vladislav Shpilevoy
@ 2018-07-25 10:03     ` n.pettik
  2018-07-26 20:12       ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-07-25 10:03 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


> Also see other fixes on the branch in a separate commit.

Thx for fixes. I have squashed them all.

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.)

> 1. Typos below: paren, refrencing.

Fixed.

> 
> 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?

Typo: I mean paren (i.e. bracket) - exactly this word is used for ANSI syntax.

>>  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).

Yep, sort of extra check and it can be removed.

>> +
>>  /**
>>   * 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?

Well, firstly this is dead code: we don't support ALTER TABLE ADD COLUMN
by no means. So, this function can be completely removed. Idk why it still exists.
Secondly, I am not even sure that ANSI allows to do 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.

Fixed:

+++ b/src/box/sql/fkey.c
@@ -807,7 +807,7 @@ fkey_is_required(uint32_t space_id, int *changes)
  *
  * 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().

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

I called it this way since we already have sql_code_drop_table…
But I think you are right:

@@ -2057,7 +2057,7 @@ sql_clear_stat_spaces(struct Parse *parse, const char *table_name,
  * @param child_id Id of table which constraint belongs to.
  */
 static void
-vdbe_fkey_code_drop(struct Parse *parse_context, const char *constraint_name,
+vdbe_emit_fkey_drop(struct Parse *parse_context, const char *constraint_name,

@@ -2135,7 +2135,7 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space,
                                                          child_fk->def->name);
                if (fk_name_dup == NULL)
                        return;
-               vdbe_fkey_code_drop(parse_context, fk_name_dup, space_id);
+               vdbe_emit_fkey_drop(parse_context, fk_name_dup, space_id);

@@ -2531,7 +2531,7 @@ sql_drop_foreign_key(struct Parse *parse_context, struct SrcList *table,
                sqlite3DbFree(db, (void *) constraint_name);
                return;
        }
-       vdbe_fkey_code_drop(parse_context, constraint_name, child_id);
+       vdbe_emit_fkey_drop(parse_context, constraint_name, child_id);

@@ -1557,7 +1557,7 @@ emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id
  * @param fk Foreign key to be created.
  */
 static void
-vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
+vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk)

@@ -1837,7 +1837,7 @@ sqlite3EndTable(Parse * pParse,   /* Parse context */
                                fk->parent_id = iSpaceId;
                        }
                        fk->child_id = iSpaceId;
-                       vdbe_fkey_code_creation(pParse, fk);
+                       vdbe_emit_fkey_create(pParse, fk);

@@ -2488,7 +2488,7 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child,
        if (!is_alter)
                parse_context->new_fkey->fkey = fk;
        else
-               vdbe_fkey_code_creation(parse_context, fk);
+               vdbe_emit_fkey_create(parse_context, 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, 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.

Oops, fixed:

@@ -1796,7 +1796,7 @@ vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
        char *encoded_links = sqlite3DbMallocRaw(parse_context->db,
                                                 encoded_links_sz);
        if (encoded_links == NULL) {
-               free((void *) name_copy);
+               sqlite3DbFree(parse_context->db, (void *) name_copy);
                return;

>> +		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.

Just in case. But if it annoys you, I will remove it:

@@ -1792,15 +1792,14 @@ vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
                          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;
+       size_t encoded_links_sz = fkey_encode_links(fk, NULL);
        char *encoded_links = sqlite3DbMallocRaw(parse_context->db,
                                                 encoded_links_sz);
	...
        size_t real_links_sz = fkey_encode_links(fk, encoded_links);
-       encoded_links[real_links_sz] = '\0';

> 
>> +	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?

Wouldn’t it be over-engineering (as Kostya says)? I mean it would be used
only for this (creating FK constraints during CREATE TABLE execution) and
anyway resurrected from scratch after real space creation. 

>> +
>>  /*
>>   * 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?

Yep, it is guaranteed that cols->nExpr must’n exceed fk->field_count.
The case this code handles is:

CREATE TABLE child (id INT PRIMARY KEY, a REFERENCES parent (a));

OR

CREATE TABLE child (id INT PRIMARY KEY, a, b, FOREIGN KEY (a, b) REFERENCES parent (a, b));

In other words, when FK constraints are create within CREATE TABLE statement and are
not self-referenced. In such case we verify that number of columns in both (parent and child)
list are equal (sql_create_foreign_key()):

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");
              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;
       }
}

> 
>> +					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.

Ok, I’ve replaced sqlite3ErrorMgs() with diag_set():

@@ -2013,10 +2012,14 @@ sqlite3EndTable(Parse * pParse, /* Parse context */
                        } 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");
+                                       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");
+                                       pParse->rc = SQL_TARANTOOL_ERROR;
+                                       pParse->nErr++;
                                        return;

> 
>> +					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.

Ok, got rid off hand-made list:

@@ -1804,8 +1804,8 @@ sqlite3EndTable(Parse * pParse,   /* Parse context */
                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_parse *fk_parse;
+               rlist_foreach_entry(fk_parse, &pParse->new_fkey, link) {

@@ -2351,9 +2350,7 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child,
                        goto tnt_error;
                }
                memset(fk, 0, sizeof(*fk));
-               struct fkey_parse *last_fk = parse_context->new_fkey;
-               parse_context->new_fkey = fk;
-               fk->next = last_fk;
+               rlist_add_entry(&parse_context->new_fkey, fk, link);

@@ -2372,8 +2369,11 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child,
        if (parent_id == BOX_ID_NIL) {
                parent_space = NULL;
                if (is_self_referenced) {
-                       parse_context->new_fkey->selfref_cols = parent_cols;
-                       parse_context->new_fkey->is_self_referenced = true;
+                       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;

@@ -2485,10 +2485,14 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child,
         * lets delay it until sqlite3EndTable() call and simply
         * maintain list of all FK constraints inside parser.
         */
-       if (!is_alter)
-               parse_context->new_fkey->fkey = fk;
-       else
-               vdbe_fkey_code_creation(parse_context, fk);
+       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);
+       }

@@ -2506,10 +2510,13 @@ tnt_error:
 void
 fkey_change_defer_mode(struct Parse *parse_context, bool is_deferred)
 {
-       if (parse_context->db->init.busy || parse_context->new_fkey == NULL)
+       if (parse_context->db->init.busy ||
+           rlist_empty(&parse_context->new_fkey))
                return;
-       struct fkey_def *fk = parse_context->new_fkey->fkey;
-       fk->is_deferred = is_deferred;
+       struct fkey_parse *fk_parse =
+               rlist_first_entry(&parse_context->new_fkey, struct fkey_parse,
+                                 link);
+       fk_parse->fkey->is_deferred = is_deferred;
 }

+++ 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,11 +429,10 @@ sql_parser_destroy(Parse *parser)
        sqlite3 *db = parser->db;
        sqlite3DbFree(db, parser->aLabel);
        sql_expr_list_delete(db, parser->pConstExpr);
-       struct fkey_parse *fk = parser->new_fkey;
-       while (fk != NULL) {
+       struct fkey_parse *fk;
+       rlist_foreach_entry(fk, &parser->new_fkey, link)
                sql_expr_list_delete(db, fk->selfref_cols);
-               fk = fk->next;
-       }

@@ -2831,7 +2830,7 @@ struct fkey_parse {
         */
        bool is_self_referenced;
        /** Organize these structs into linked list. */
-       struct fkey_parse *next;
+       struct rlist link;

@@ -2934,7 +2933,7 @@ struct Parse {
        /**
         * Foreign key constraint appeared in CREATE TABLE stmt.
         */
-       struct fkey_parse *new_fkey;
+       struct rlist new_fkey;

>> +		}
>>  	}
>>    	/* 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?

Fixed alongside with creation procedure. See above.

>> +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.

During VDBE execution of code emitted by CREATE TABLE statement
parser is implicitly called again by sqlite3InitCallback() in OP_ParseSchema.
Thus, this function is invoked second time.

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

Well, in fact this check is redundant. Lets remove it:

@@ -2320,11 +2319,7 @@ sql_create_foreign_key(struct Parse *parse_context, struct SrcList *child,
        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;
-               }
+               assert(!is_alter);

Anyway added tests:

--- a/test/sql-tap/alter2.test.lua
+++ b/test/sql-tap/alter2.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(15)
+test:plan(17)
 
 -- This suite is aimed to test ALTER TABLE ADD CONSTRAINT statement.
 --
@@ -193,4 +193,24 @@ test:do_execsql_test(
         -- </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>
+    })

>> +	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.

Actually, it appears twice, but anyway done:

+       const char *error_msg = "number of columns in foreign key does not"
+                               "match the number of columns in the "
+                               "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;
+               }
+       }

>> +			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?

Ok, but I suggest to store it in parse context.
fkey_parse are orginized in linked list so each member of this list
would contain count of previous entry + 1. Hence, it doesn’t seem
to be reasonable to store in each parse_fkey count of FK constraints.

        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++);
                        constraint_name =
                                sqlite3MPrintf(db, "fk_constraint_%d_%s",
-                                              fk_count, new_tab->def->name);
+                                              ++parse_context->fkey_count,
+                                              new_tab->def->name);
                } else {

+++ b/src/box/sql/sqliteInt.h
@@ -2947,6 +2947,11 @@ 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;

> 
>> 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?

This is not even my code. I was lazy to cherry-pick Nikita’s T. commit
and simply copied his code. Now, his patch has hit the trunk, so
it is disappeared from my patch after rebase on fresh 2.0.

Also, I have to cherry-pick Gosha’s fixes for bash script which
produces opcodes.h.

>> +		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?

Idk.. Removed with unused header:

diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 3989ea61f..278cf3769 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_TRIGGER

> 
>> @@ -366,150 +187,116 @@ sqlite3FkLocateIndex(Parse * pParse,	/* Parse context to store any error in */
>> +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)
>>  {
>> +	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.

Fixed during rebasing.

>> +		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?

Yep, fixed:

+++ b/src/box/sql/fkey.c
@@ -249,7 +249,7 @@ fkey_lookup_parent(struct Parse *parse_context, struct space *parent,
         * 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) {
+       if (fkey_is_self_referenced(fk_def) && incr_count == 1) {

> 
>> +			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.

I just didn’t notice it. Lets remove these checks from parser:

@@ -266,10 +263,8 @@ 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);
-       struct session *user_session = current_session();
-       if (!fk_def->is_deferred &&
-           (user_session->sql_flags & SQLITE_DeferFKs) == 0 &&
-           parse_context->pToplevel == NULL && !parse_context->isMultiWrite) {
+       if (!fk_def->is_deferred && parse_context->pToplevel == NULL &&
+           !parse_context->isMultiWrite) {

@@ -642,9 +637,8 @@ sqlite3FkCheck(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 &&
-                   (user_session->sql_flags & SQLITE_DeferFKs) == 0 &&
-                   parser->pToplevel == NULL && !parser->isMultiWrite) {
+               if (!fk_def->is_deferred && parser->pToplevel == NULL &&
+                   !parser->isMultiWrite) {

@@ -819,12 +813,10 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct fkey *fkey,
                bool is_update)
 {
        sqlite3 *db = pParse->db;       /* Database handle */
-       struct session *user_session = current_session();
        struct fkey_def *fk_def = fkey->def;
        enum fkey_action action = is_update ? fk_def->on_update :
                                              fk_def->on_delete;
-       if (action == FKEY_ACTION_RESTRICT &&
-           (user_session->sql_flags & SQLITE_DeferFKs))

> 
>> @@ -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?

Yep.

@@ -598,7 +598,7 @@ sqlite3FkCheck(struct Parse *parser, struct Table *tab, int reg_old,
             fk = fk->fkey_child_next) {
                struct fkey_def *fk_def = fk->def;
                if (changed_cols != NULL &&
-                   space->def->id != fk_def->parent_id &&
+                   !fkey_is_self_referenced(fk_def) &&

>> +		    !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.

It is not so easy to do: the same mask is used for triggers as well.
So if we want to change format of mast for FK, we should do it
almost everywhere in SQL source code. I may do it as separate
follow-up patch, if you wish.

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

>>  {
>> -	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?

This assert seems to be broken or to be very ancient thing.
In fact, busy flag has nothing in common with DDL.
For this reason also fails drop of triggers:
https://github.com/tarantool/tarantool/issues/3529

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

Idk, just because :) Yep, it will work with 0:

-refarg(A) ::= MATCH matcharg(X).     { A.value = X<<16; A.mask = 0xff0000; }
+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; }
>> 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?

Not exactly, it is quite tricky place. When we are processing ALTER
we can unambiguously point out DEFER clause:

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);
}

So, we pass it directly to sql_create_foreign_key()

When we are handling CREATE TABLE, grammar features ambiguous
statement. It is a long story, but in a nutshell I didn’t manage to
put defer_subclause(D) (even modified) right after REFERENCES clause.

So, I just keep things as they were in original SQLite.

>> + *
>> + * @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).

Because types of referenced and referencing fields must match.
And the only way to get INT type is to declare field as INT PRIMARY KEY.
Hence, if referencing column is INT but not PK, it will be stored with SCALAR.
In its turn SCALAR can’t be mapped to INT.

I know that it kind of contradicts our plans on static typing, but I had nothing
left to do...

> 
>>              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?

Seems like I accidentally changed it. Returned back:

+++ 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);
+        CREATE TABLE c1(x PRIMARY KEY REFERENCES p1 DEFERRABLE INITIALLY DEFERRED);

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

Fixed.

> 
>> +        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?

Sure:

@@ -1629,7 +1629,8 @@ resolve_link(struct Parse *parse_context, const struct space_def *def,
                        return 0;
                }
        }
-       sqlite3ErrorMsg(parse_context, "no such column %s", field_name);
+       sqlite3ErrorMsg(parse_context, "unknown column %s in foreign key "
+                       "definition", field_name);


Full patch after changes is below:

===============================================================================

Subject: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement

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 paren> <referencing columns> <right paren> REFERENCES
  <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 referencing 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 +
 extra/mkopcodeh.sh                   |   33 +-
 src/box/fkey.c                       |    1 +
 src/box/sql.c                        |  113 +--
 src/box/sql/alter.c                  |   82 ---
 src/box/sql/build.c                  |  579 +++++++++++----
 src/box/sql/callback.c               |   10 +-
 src/box/sql/delete.c                 |    6 +-
 src/box/sql/fkey.c                   | 1333 +++++++++++++---------------------
 src/box/sql/insert.c                 |   24 +-
 src/box/sql/main.c                   |    5 -
 src/box/sql/parse.y                  |   37 +-
 src/box/sql/pragma.c                 |  239 +-----
 src/box/sql/pragma.h                 |   11 +-
 src/box/sql/prepare.c                |    4 +
 src/box/sql/sqliteInt.h              |  176 +++--
 src/box/sql/status.c                 |    9 +-
 src/box/sql/tarantoolInt.h           |   17 +-
 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         |  216 ++++++
 test/sql-tap/engine.cfg              |    3 +
 test/sql-tap/fkey1.test.lua          |   51 +-
 test/sql-tap/fkey2.test.lua          |  125 +---
 test/sql-tap/fkey3.test.lua          |   15 +-
 test/sql-tap/orderby1.test.lua       |    6 +-
 test/sql-tap/table.test.lua          |   22 +-
 test/sql-tap/tkt-b1d3a2e531.test.lua |    4 +-
 test/sql-tap/triggerC.test.lua       |    2 +-
 test/sql-tap/whereG.test.lua         |    4 +-
 test/sql-tap/with1.test.lua          |    2 +-
 32 files changed, 1471 insertions(+), 1685 deletions(-)
 create mode 100755 test/sql-tap/alter2.test.lua

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..9e97a50f0 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" ;;
@@ -220,8 +224,12 @@ while [ "$i" -lt "$nOp" ]; do
     i=$((i + 1))
 done
 max="$cnt"
+echo "//*************** $max $nOp $mxTk"
+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 +259,21 @@ done
 # Generate the bitvectors:
 ARRAY_bv_0=0
 i=0
-while [ "$i" -le "$max" ]; do
+while [ "$i" -le "$mxTk" ]; do
+    eval "is_exists=\${ARRAY_def_$i:-}"
+    if [ ! -n "$is_exists" ] ; then
+    echo "//SKIP $i"
+        i=$((i + 1))
+	continue
+    fi
     eval "name=\$ARRAY_def_$i"
     x=0
+    eval "is_exists=\${ARRAY_jump_$name:-}"
+    if [ ! -n "$is_exists" ] ; then
+    echo "//SKIP2 $i"
+        i=$((i + 1))
+	continue
+    fi
     eval "jump=\$ARRAY_jump_$name"
     eval "in1=\$ARRAY_in1_$name"
     eval "in2=\$ARRAY_in2_$name"
@@ -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:-}"
+    if [ ! -n "$is_exists" ] ; 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/fkey.c b/src/box/fkey.c
index b3980c874..8b7f5130c 100644
--- a/src/box/fkey.c
+++ b/src/box/fkey.c
@@ -28,6 +28,7 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
+
 #include "fkey.h"
 #include "sql.h"
 #include "sql/sqliteInt.h"
diff --git a/src/box/sql.c b/src/box/sql.c
index d4b0d7fcc..c51617ce4 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,21 @@ tarantoolSqlite3MakeTableOpts(Table *pTable, const char *zSql, char *buf)
 	return p - buf;
 }
 
+int
+fkey_encode_links(const struct fkey_def *fkey, char *buf)
+{
+	const struct Enc *enc = get_enc(buf);
+	char *p = enc->encode_array(buf, fkey->field_count);
+	for (uint32_t i = 0; i < fkey->field_count; ++i) {
+		p = enc->encode_map(p, 2);
+		p = enc->encode_str(p, "child", strlen("child"));
+		p = enc->encode_uint(p, fkey->links[i].child_field);
+		p = enc->encode_str(p, "parent", strlen("parent"));
+		p = enc->encode_uint(p, fkey->links[i].parent_field);
+	}
+	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 789a628d6..fc097a319 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,91 @@ 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_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, 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);
+	char *encoded_links = sqlite3DbMallocRaw(parse_context->db,
+						 encoded_links_sz);
+	if (encoded_links == NULL) {
+		sqlite3DbFree(parse_context->db, (void *) name_copy);
+		return;
+	}
+	size_t real_links_sz = fkey_encode_links(fk, encoded_links);
+	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);
+	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);
+	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");
+					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 +2047,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 +2128,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 +2245,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 +2262,280 @@ 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.
  */
+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);
+		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)
 {
-	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;
-	}
-	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;
+	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 "
+				"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;
+	size_t fk_size = fkey_def_sizeof(child_cols_count,
+					 strlen(constraint_name));
+	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 +
+					   strlen(constraint_name) + 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) != 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) != 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) != 0) {
+			goto exit_create_fk;
+		}
 	}
-
-	/* Link the foreign key to the table as the last step.
+	memcpy(fk->name, constraint_name, strlen(constraint_name));
+	fk->name[strlen(constraint_name)] = '\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
+	struct fkey_parse *fk_parse =
+		rlist_first_entry(&parse_context->new_fkey, struct fkey_parse,
+				  link);
+	fk_parse->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);
+	struct sqlite3 *db = parse_context->db;
+	const char *constraint_name = sqlite3NameFromToken(db, constraint);
+	if (constraint_name == NULL)
+		return;
+	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++;
+		sqlite3DbFree(db, (void *) constraint_name);
+		return;
+	}
+	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..cca09f1ad 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,14 @@ 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);
+		mask |= fkey_old_mask(table->def->id);
 		first_old_reg = parse->nMem + 1;
 		parse->nMem += (1 + (int)table->def->field_count);
 
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index f87f610dc..76c456f6e 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,8 +134,8 @@
  * 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.
+ *   fkey_old_mask()  - Query for the set of required old.* columns.
  *
  *
  * Externally accessible module functions
@@ -146,10 +143,7 @@
  *
  *   sqlite3FkCheck()    - Check for foreign key violations.
  *   sqlite3FkActions()  - Code triggers for ON UPDATE/ON DELETE actions.
- *   sqlite3FkDelete()   - Delete an FKey structure.
- */
-
-/*
+ *
  * VDBE Calling Convention
  * -----------------------
  *
@@ -166,332 +160,132 @@
  *   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);
+	if (!fk_def->is_deferred && 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,519 +345,446 @@ 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 */
-    )
+fkScanChildren(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);
-}
-
-/*
- * 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);
+	sql_expr_delete(db, where, false);
+	if (fkifzero_label != 0)
+		sqlite3VdbeJumpHere(v, fkifzero_label);
 }
 
-/*
- * 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, 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, 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.
+ * 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
-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) */
-    )
+sqlite3FkCheck(struct Parse *parser, struct Table *tab, int reg_old,
+	       int reg_new, 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 || sqlite3FkLocateIndex(pParse, pTo, pFKey, &pIdx,
-					    &aiFree))
-				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 && 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))
-			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);
+		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)
+			fkScanChildren(parser, src, tab, fk->def, reg_new, -1);
+		if (reg_old != 0) {
+			enum fkey_action action = fk_def->on_update;
+			fkScanChildren(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);
 		}
-		sqlite3DbFree(db, aiCol);
+		sqlite3SrcListDelete(db, src);
 	}
 }
 
 #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 */
-    )
+uint32_t
+fkey_old_mask(uint32_t space_id)
 {
-	u32 mask = 0;
+	uint32_t 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 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);
 		}
-		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);
-				}
-			}
+		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;
 }
 
-/*
- * 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, 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.
+	if ((user_session->sql_flags & SQLITE_ForeignKeys) != 0) {
+		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 (sqlite3FkReferences(pTab) || pTab->pFKey);
+			return ! rlist_empty(&space->parent_fkey) ||
+			       ! rlist_empty(&space->child_fkey);
 		} else {
-			/* This is an UPDATE. Foreign key processing is only required if the
-			 * operation modifies one or more child or parent key columns.
+			/*
+			 * This is an UPDATE. FK 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;
+			struct fkey *fk;
+			rlist_foreach_entry (fk, &space->child_fkey,
+					     child_link) {
+				if (fkey_child_is_modified(fk->def, changes))
+					return true;
 			}
-
-			/* Check if any parent key columns are being modified. */
-			for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) {
-				if (fkParentIsModified(pTab, p, aChange))
-					return 1;
+			rlist_foreach_entry (fk, &space->parent_fkey,
+					     parent_link) {
+			if (fkey_parent_is_modified(fk->def, changes))
+					return true;
 			}
 		}
 	}
-	return 0;
+	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()).
  *
- * 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);
@@ -1077,68 +798,47 @@ 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)
+fkActionTrigger(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 */
+	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) {
 		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++) {
+		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) {
 			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);
+			uint32_t pcol = fk_def->links[i].parent_field;
+			sqlite3TokenInit(&tToCol, pTab->def->fields[pcol].name);
 
-			uint32_t fieldno = pIdx->def->key_def->parts[i].fieldno;
-			sqlite3TokenInit(&tToCol,
-					 pTab->def->fields[fieldno].name);
+			uint32_t chcol = fk_def->links[i].child_field;
 			sqlite3TokenInit(&tFromCol,
-					 pFKey->pFrom->def->fields[
-						iFromCol].name);
+					 child_space->def->fields[chcol].name);
 
 			/* Create the expression "OLD.zToCol = zFromCol". It is important
 			 * that the "OLD.zToCol" term is on the LHS of the = operator, so
@@ -1165,7 +865,7 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 			 *
 			 *    WHEN NOT(old.col1 = new.col1 AND ... AND old.colN = new.colN)
 			 */
-			if (pChanges) {
+			if (is_update) {
 				pEq = sqlite3PExpr(pParse, TK_EQ,
 						   sqlite3PExpr(pParse, TK_DOT,
 								sqlite3ExprAlloc
@@ -1185,10 +885,10 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 				pWhen = sqlite3ExprAnd(db, pWhen, pEq);
 			}
 
-			if (action != OE_Restrict
-			    && (action != OE_Cascade || pChanges)) {
+			if (action != FKEY_ACTION_RESTRICT &&
+			    (action != FKEY_ACTION_CASCADE || is_update)) {
 				Expr *pNew;
-				if (action == OE_Cascade) {
+				if (action == FKEY_ACTION_CASCADE) {
 					pNew = sqlite3PExpr(pParse, TK_DOT,
 							    sqlite3ExprAlloc(db,
 									     TK_ID,
@@ -1198,11 +898,11 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 									     TK_ID,
 									     &tToCol,
 									     0));
-				} else if (action == OE_SetDflt) {
+				} else if (action == FKEY_ACTION_SET_DEFAULT) {
+					uint32_t space_id = fk_def->child_id;
 					Expr *pDflt =
 						space_column_default_expr(
-							pFKey->pFrom->def->id,
-							(uint32_t)iFromCol);
+							space_id, chcol);
 					if (pDflt) {
 						pNew =
 						    sqlite3ExprDup(db, pDflt,
@@ -1224,12 +924,11 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 						       0);
 			}
 		}
-		sqlite3DbFree(db, aiCol);
 
-		zFrom = pFKey->pFrom->def->name;
-		nFrom = sqlite3Strlen30(zFrom);
+		const char *zFrom = child_space->def->name;
+		uint32_t nFrom = strlen(zFrom);
 
-		if (action == OE_Restrict) {
+		if (action == FKEY_ACTION_RESTRICT) {
 			Token tFrom;
 			Expr *pRaise;
 
@@ -1253,7 +952,6 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 
 		/* Disable lookaside memory allocation */
 		db->lookaside.bDisable++;
-
 		size_t trigger_size = sizeof(struct sql_trigger) +
 				      sizeof(TriggerStep) + nFrom + 1;
 		trigger =
@@ -1291,11 +989,11 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		assert(pStep != 0);
 
 		switch (action) {
-		case OE_Restrict:
+		case FKEY_ACTION_RESTRICT:
 			pStep->op = TK_SELECT;
 			break;
-		case OE_Cascade:
-			if (!pChanges) {
+		case FKEY_ACTION_CASCADE:
+			if (! is_update) {
 				pStep->op = TK_DELETE;
 				break;
 			}
@@ -1303,9 +1001,15 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 		default:
 			pStep->op = TK_UPDATE;
 		}
+
 		pStep->trigger = trigger;
-		pFKey->apTrigger[iAction] = trigger;
-		trigger->op = pChanges ? TK_UPDATE : TK_DELETE;
+		if (is_update) {
+			fkey->on_update_trigger = trigger;
+			trigger->op = TK_UPDATE;
+		} else {
+			fkey->on_delete_trigger = trigger;
+			trigger->op = TK_DELETE;
+		}
 	}
 
 	return trigger;
@@ -1329,65 +1033,20 @@ sqlite3FkActions(Parse * pParse,	/* Parse context */
 	 * 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. */
-		fkey_trigger_delete(db, pFKey->apTrigger[0]);
-		fkey_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(pTab->def->id);
+	assert(space != NULL);
+	struct fkey *fk;
+	rlist_foreach_entry (fk, &space->parent_fkey, parent_link)  {
+		if (aChange != NULL &&
+		    !fkey_parent_is_modified(fk->def, aChange))
+			continue;
+		struct sql_trigger *pAct =
+			fkActionTrigger(pParse, pTab, fk, pChanges != NULL);
+		if (pAct == NULL)
+			continue;
+		vdbe_code_row_trigger_direct(pParse, pAct, pTab, regOld,
+					     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..94f697f91 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -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..6869feb9c 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; 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..94ff7168a 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -35,6 +35,7 @@
 #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,38 @@ 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) {
+			for (uint32_t j = 0; j < fkey->def->field_count; j++) {
+				struct space *parent =
+					space_by_id(fkey->def->parent_id);
+				assert(parent != NULL);
+				uint32_t ch_fl = fkey->def->links[j].child_field;
+				const char *child_col =
+					space->def->fields[ch_fl].name;
+				uint32_t pr_fl = fkey->def->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[fkey->def->on_delete],
+						     fkey_action_strs[fkey->def->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..b340056d4 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.
@@ -4668,25 +4712,35 @@ void sqlite3WithPush(Parse *, With *, u8);
  * 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 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, 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);
 
 /*
  * 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..69c2b9bc6 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,18 @@ int tarantoolSqlite3MakeTableFormat(Table * pTable, void *buf);
  */
 int tarantoolSqlite3MakeTableOpts(Table * pTable, const char *zSql, char *buf);
 
+/**
+ * Encode links of given foreign key constraint into MsgPack.
+ *
+ * @param fkey Encode links of this foreign key contraint.
+ * @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(const struct fkey_def *fkey, 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..8eb5f8f13 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,7 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 	 * information is needed
 	 */
 	if (chngPk != 0 || hasFK != 0 || trigger != NULL) {
-		u32 oldmask = (hasFK ? sqlite3FkOldmask(pParse, pTab) : 0);
+		u32 oldmask = hasFK ? fkey_old_mask(pTab->def->id) : 0;
 		oldmask |= sql_trigger_colmask(pParse, trigger, pChanges, 0,
 					       TRIGGER_BEFORE | TRIGGER_AFTER,
 					       pTab, on_error);
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 8e6e14f5d..159a4ad78 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..f990a4c07
--- /dev/null
+++ b/test/sql-tap/alter2.test.lua
@@ -0,0 +1,216 @@
+#!/usr/bin/env tarantool
+test = require("sqltester")
+test:plan(17)
+
+-- 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_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;
+    ]], {
+        -- <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/engine.cfg b/test/sql-tap/engine.cfg
index ce9dd68d8..006e31c37 100644
--- a/test/sql-tap/engine.cfg
+++ b/test/sql-tap/engine.cfg
@@ -2,6 +2,9 @@
     "analyze9.test.lua": {
         "memtx": {"engine": "memtx"}
     },
+    "alter2.test.lua" : {
+        "memtx": {"engine": "memtx"}
+    },
     "*": {
         "memtx": {"engine": "memtx"},
         "vinyl": {"engine": "vinyl"}
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..310e245de 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, "table \"P\" doesn't feature column 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 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, "table \"PP\" doesn't feature column 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..90cf65161 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, "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;
+        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 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 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 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, [[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, [[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, '');
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 4/5] sql: display error on FK creation and drop failure
  2018-07-17 21:04   ` [tarantool-patches] " Vladislav Shpilevoy
@ 2018-07-25 10:03     ` n.pettik
  2018-07-26 20:11       ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-07-25 10:03 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


>> Before insertion to _fk_constraint we must be sure that there in no
>> entry with given <name, child id>. Otherwise, insertion will fail and
>> 'duplicate key' will be shown. Such error message doesn't seem to be
>> informative enough, so lets manually iterate through whole space looking
>> for appropriate record.
> 
> 1. As I know, vdbe_emit_halt_with_presence_test do not iterate through
> the whole space. It uses an index to search for the record fast.

You are right, I’ve fixed commit message.

> 
>> The same is for dropping constraint, but here vice versa: we test
>> that _fk_contraint contains entry with given name and child id.
> 
> 2. Typo: _fk_contraint -> _fk_constraint.

Fixed.

> 
>> It is worth mentioning that during CREATE TABLE processing schema id
>> changes and check in OP_OpenRead opcode fails (which in turn shows that
>> pointer to space may expire). On the other hand, _fk_constraint space
>> itself remains immutable, so as a temporary workaround lets use flag
>> indicating pointer to system space passed to OP_OpenRead. It makes
>> possible to use pointer to space, even if schema has changed.
>> Closes #3271
>> ---
>>  src/box/errcode.h            |  2 ++
>>  src/box/sql/build.c          | 43 +++++++++++++++++++++++++++++++------------
>>  src/box/sql/sqliteInt.h      | 10 +++++++---
>>  src/box/sql/trigger.c        | 24 ++++++++++++++++--------
>>  src/box/sql/vdbe.c           |  3 ++-
>>  test/box/misc.result         |  2 ++
>>  test/sql-tap/alter2.test.lua | 25 ++++++++++++++++++++++++-
>>  7 files changed, 84 insertions(+), 25 deletions(-)
>> diff --git a/src/box/sql/build.c b/src/box/sql/build.c
>> index c2d3cd035..20ace09e4 100644
>> --- a/src/box/sql/build.c
>> +++ b/src/box/sql/build.c
>> @@ -1784,6 +1784,20 @@ vdbe_fkey_code_creation(struct Parse *parse_context, const struct fkey_def *fk)
>>  		sqlite3VdbeAddOp2(vdbe, OP_Integer, fk->parent_id,
>>  				  constr_tuple_reg + 2);
>>  	}
>> +	/*
>> +	 * Lets check that constraint with this name hasn't
>> +	 * been created before.
>> +	 */
>> +	const char *error_msg =
>> +		tt_sprintf(tnt_errcode_desc(ER_CONSTRAINT_EXISTS), name_copy);
>> +	if (vdbe_emit_halt_with_presence_test(parse_context,
>> +					      BOX_FK_CONSTRAINT_ID, 0,
>> +					      constr_tuple_reg, 2,
>> +					      ER_CONSTRAINT_EXISTS, error_msg,
>> +					      false, OP_NoConflict) != 0) {
>> +		free((void *) name_copy);
> 
> 3. Name_copy is allocated on db.

Fixed:

+++ b/src/box/sql/build.c
@@ -1603,7 +1603,7 @@ vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk)
                                              constr_tuple_reg, 2,
                                              ER_CONSTRAINT_EXISTS, error_msg,
                                              false, OP_NoConflict) != 0) {
-               free((void *) name_copy);
+               sqlite3DbFree(parse_context->db, (void *) name_copy);

>> +		return;
>> +	}
>> diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
>> index b9723e2e7..0f227e637 100644
>> --- a/src/box/sql/vdbe.c
>> +++ b/src/box/sql/vdbe.c
>> @@ -3172,7 +3172,8 @@ case OP_OpenWrite:
>>  	 * during runtime.
>>  	 */
>>  	if (box_schema_version() != p->schema_ver &&
>> -	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0) {
>> +	    (pOp->p5 & OPFLAG_FRESH_PTR) == 0 &&
>> +	    (pOp->p5 & OPFLAG_SYSTEMSP) == 0) {
> 
> 4. Why not p5 & (FRESH_PTR | SYSTEMSP) ?

Because I am not used to work with bit flags..
This is definitely proper way to do it. Anyway, FRESH_PTR flag has gone,
so now it is again simple (pOp->p5 & OPFLAG_SYSTEMSP) == 0 check.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 4/5] sql: display error on FK creation and drop failure
  2018-07-25 10:03     ` n.pettik
@ 2018-07-26 20:11       ` Vladislav Shpilevoy
  0 siblings, 0 replies; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-26 20:11 UTC (permalink / raw)
  To: tarantool-patches, n.pettik

Thanks for the fixes! I have pushed my fixes on the branch.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-07-25 10:03     ` n.pettik
@ 2018-07-26 20:12       ` Vladislav Shpilevoy
  2018-08-01 20:54         ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-26 20:12 UTC (permalink / raw)
  To: tarantool-patches, n.pettik

Thanks for the fixes! See my answers, 8 comments and a
commit on the branch.

On 25/07/2018 13:03, n.pettik wrote:
> 
>> Also see other fixes on the branch in a separate commit.
> 
> Thx for fixes. I have squashed them all.
> 
> 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.

>>> +		    !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.
> 
> It is not so easy to do: the same mask is used for triggers as well.
> So if we want to change format of mast for FK, we should do it
> almost everywhere in SQL source code. I may do it as separate
> follow-up patch, if you wish.

No, I opened an issue:
https://github.com/tarantool/tarantool/issues/3571
Lets do it later.

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

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

> +    if [ ! -n "$is_exists" ] ; 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/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.

> -	    && 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 ?

> +	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.

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.

> +					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.
> @@ -2106,177 +2262,280 @@ 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.
>    */
> +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?

> +		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.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-07-25 10:03     ` n.pettik
@ 2018-07-26 20:12       ` Vladislav Shpilevoy
  2018-08-01 20:54         ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-26 20:12 UTC (permalink / raw)
  To: tarantool-patches, n.pettik

Thanks for the patch! See my answers, 4 comments and a
patch on the branch.

> 
> +++ b/src/box/alter.cc
> @@ -1920,7 +1920,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
>           * Can't drop index if foreign key constraints references
>           * this index.
>           */
> -       if (new_tuple == NULL) {
> +       if (old_index != NULL && new_tuple == NULL) {
> 
> Also, I didn’t get what you mean by mentioning index_def_change_requires_rebuild.
> Can index id change on its renaming?

Index_id can not change. But a one can remove an index part, or add a new
one. Or change nullability that breaks uniqueness. I do not see how do you
deal with that. To detect such dramatic changes
index_def_change_requires_rebuild() is used in on_replace_dd_index().

>>
>> 4. What about a second key? mp_typeof(**map) right after decoding its
>> header returns type of the first key. But second can be of another type.
> 
> Fixed:
> 
> -               if (mp_typeof(**map) != MP_STR) {
> -                       tnt_raise(ClientError, errcode,
> -                                 tt_cstr(constraint_name, constraint_len),
> -                                 tt_sprintf("link %d is not map "\
> -                                            "with string keys", i));
> -               }
>                  for (uint8_t j = 0; j < map_sz; ++j) {
> +                       if (mp_typeof(**map) != MP_STR) {
> +                               tnt_raise(ClientError, errcode,
> +                                         tt_cstr(constraint_name,
> +                                                 constraint_len),
> +                                         tt_sprintf("link %d is not map "\
> +                                                    "with string keys", i));
> +                       }
> 
>>
>> And why do we need this {child = , parent = }, ... sequence? Lets just
>> use {<uint>, <uint>}, {<uint>, <uint>}, {<uint>, <uint>} ... It is more
>> compact and simpler to parse. Or even just two arrays: first child and
>> second parent field numbers. It is the most canonical way similar to
>> SQL, it is not? When you specify two column ranges.
> 
> I use this format since it is easier to avoid confusing parent and child ids
> (i.e. what comes first and what comes second). Even if we add separate
> convenient Lua API for that purpose.
> If you insist on doing that, I will change format.

Lua API is not about _fk_constraint space. Lua API will be like
space:create_fk() with fancy arguments etc.

I think, _fk_constraint should be compact and easy to store and parse,
not to manually read or write. My opinion is that 3 the best options
exist:

{{..., ..., ...}, {..., ..., ...}} - array of 2 arrays. First is child,
second is parent. Or:

, {..., ..., ...}, {..., ..., ...},  - create separate fields in
_fk_constraint for child and parent arrays. Or:

{child = {..., ..., ...}, parent = {..., ..., ...}} - map with two
keys and two array values.

I do not insist on my format but think we should consult
Kostja and Kirill on the final one.

>>
>> 10. Above you have said that order may be different, but here I see
>> that once you have found a unique index, you merely check that it
>> has sequentially the same field numbers as the fk.
> 
> For some reason I forgot to fix this when was preparing patch.
> Fixed by now. Also, according to ANSI (11.8 4.a):
> • Each referenced column shall identify a column of the referenced table and the same column shall not be identi ed more than once.
> 
> @@ -3710,6 +3710,41 @@ on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
>          fkey_delete(fk);
>   }
>   
> +static int
> +cmp_uint32(const void *_a, const void *_b)
> +{
> +       const uint32_t *a = (const uint32_t *) _a, *b = (const uint32_t *) _b;
> +       if (*a == *b)
> +               return 0;
> +       return (*a > *b) ? 1 : -1;
> +}

Looks like in Tarantool source we have plenty of various cmp functions
for qsort. I greped uint32_compare, cmp_uint32 (your), cmp_i64, size_compator,
nums_comparator, int64_cmp. How about to merge them into trivia/util.h as
cmp_uint32, cmp_int64, cmp_size?

> +
> +/**
> + * ANSI SQL doesn't allow list of referenced fields to contain
> + * duplicates.
> + */
> +static int
> +fkey_links_check_duplicates(struct fkey_def *fk_def)
> +{
> +       uint32_t *parent_fields =
> +               (uint32_t *) region_alloc(&fiber()->gc, fk_def->field_count);
> +       if (parent_fields == NULL) {
> +               tnt_raise(OutOfMemory, fk_def->field_count, "region",
> +                         "parent_fields");
> +       }
> +       for (uint32_t i = 0; i < fk_def->field_count; ++i)
> +               parent_fields[i] = fk_def->links[i].parent_field;
> +       qsort(parent_fields, fk_def->field_count, sizeof(*parent_fields),
> +             cmp_uint32);
> +       uint32_t prev_val = parent_fields[0];
> +       for (uint32_t i = 1; i < fk_def->field_count; ++i) {
> +               if (prev_val == parent_fields[i])
> +                       return -1;
> +               prev_val = parent_fields[i];

Why not

     for (uint32_t i = 1; i < fk_def->field_count; ++i) {
         if (parent_fields[i - 1] == parent_fields[i])
             return -1;
     }

So you do not need prev_val actually. Also lets do tnt_raise here
instead of return -1.

> +       }
> +       return 0;
> +}
> +
> 
>   on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
> @@ -3777,6 +3812,11 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>                                            "field collation mismatch");
>                          }
>                  }
> +               if (fkey_links_check_duplicates(fk_def)) {
> +                       tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
> +                                 fk_def->name, "referenced fields can not "
> +                                               "contain duplicates");
> +               }
> 
> 
> Probably this is not best implementation. As an improvement I can add kind of optimisation:
> 
> *pseudo code below*
> 
> uint_64 mask;
> for parent_field in fk_def:
> 	if (pk_mask & (((uint64_t) 1) << parent_field ==1)
> 		return -1;
> 	column_mask_set_field(&mask, parent_field);
> end
> if (pk_mask & (((uint64_t) 1) << 63)) != 0
> 	fkey_links_check_duplicates(…) // Full version of checking.
> else
> 	return 1;
> 
> Is it worth the effort? I mean here we waste O(field_count) in case the largest
> filedno in parent table > 63. On the other hand, I don’t think that many users
> have tables with field count > 63, so it is likely to be reasonable.

I like this optimization. What is more, I think, we can remove the sorting
things above then. For users who have field numbers > 63 the smallest problem
is slow alter. So for them we can just use O(field_count ^ 2) with no region
and sorting.

> 
> Moreover, it would be cool if column_mask_set_fieldno() worked the same
> as bit_set() from lib/bit, i.e. returned previous bit value.

You are free to extend the API.

>>
>>> +			fk_index = idx;
>>> +			break;
>>> +		}
>>> +		if (fk_index == NULL) {
>>> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>>> +				  fk_def->name, "referenced fields don't "
>>> +					      "compose unique index");
>>> +		}
>>> +		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
>>
>> 11. I know how you like memory mapping. How about merge fkey_def into
>> fkey memory? I see, that it is relatively simple and linear thing.
> 
> What is the point of doing this? Do you suggest to allocate enough memory for fkey
> right in fkey_def_new_from_tuple() ? Like this:
> 
> @@ -3536,7 +3536,8 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
>          struct field_link *links = fkey_links_decode(links_raw, &link_count,
>                                                       name, name_len, errcode);
>          size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
> -       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
> +       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz +
> +                                                            sizeof(struct fkey);
> 
> If so, it would be quite confusing I think (since function returns only
> fkey_def, but memory would be allocated for fkey_def + fkey).
> If you mean smth else or insist on this change, I will fix it.

I just like when structure is self-sufficient. When it can be freed with
one free, and can fit in one cache line for better performance on access fields,
maybe partially. If you do not want to merge them, I do not insist though.

> 
>>> diff --git a/src/box/fkey.c b/src/box/fkey.c
>>> new file mode 100644
>>> index 000000000..e45889a0d
>>> --- /dev/null
>>> +++ b/src/box/fkey.c
>>> +void
>>> +fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
>>> +{
>>
>> 12. Why do you need this function when sql_trigger_delete exists?
> 
> Memory layout of FK trigger differs from ordinary one (from sql/fkey.c):
> 
> size_t trigger_size = sizeof(struct sql_trigger) +
>                      sizeof(TriggerStep) + nFrom + 1;
> trigger =
>         (struct sql_trigger *)sqlite3DbMallocZero(db,
>                                              trigger_size);
> 
> One can see, memory for TriggerStep, sql_trigger and name of
> target table is allocated in one chunk. Thus, fkey_trigger_delete()
> doesn’t release memory for TriggerStep. Overall, if compare
> these functions fkey_trigger_delete() looks much simpler.

I very do not like that the same structure is allocated on different
layouts, it confuses. It is ok only during alter when we temporary
allocate some things on region. Lets allocate this trigger like
ordinary ones and remove fkey_trigger_delete. I like when a
structure is compact, as I say in the previous comment, but not when
it is sometimes compact and other times not.

>>> diff --git a/test/engine/iterator.result b/test/engine/iterator.result
>>> index a36761df8..ba9b0545a 100644
>>> --- a/test/engine/iterator.result
>>> +++ b/test/engine/iterator.result
>>> @@ -4211,7 +4211,7 @@ s:replace{35}
>>>   ...
>>>   state, value = gen(param,state)
>>>   ---
>>> -- error: 'builtin/box/schema.lua:1049: usage: next(param, state)'
>>> +- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'
>>
>> 18. This test fails on each schema change. Lets remove this file:line
>> from the error message alongside the patch.
> 
> Ok:

I meant remove 'file:line' from the error message. Not the test itself.
For example, you can catch the error and match the message using
string.match("usage: next(param, state)").

> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 7b6bd1a5a..c5d1f75df 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> +/**
> + * Decode MsgPack array of links. It consists from maps:
> + * {parent_id (UINT) : child_id (UINT)}.
> + *
> + * @param data MsgPack array of links.
> + * @param[out] out_count Count of links.
> + * @param constraint_name Constraint name to use in error
> + *			  messages.
> + * @param constraint_len Length of constraint name.
> + * @param errcode Errcode for client errors.
> + * @retval Array of links.
> + */
> +static struct field_link *
> +fkey_links_decode(const char *data, uint32_t *out_count,
> +		  const char *constraint_name, uint32_t constraint_len,
> +		  uint32_t errcode)
> +{
> +	assert(mp_typeof(*data) == MP_ARRAY);
> +	uint32_t count = mp_decode_array(&data);
> +	if (count == 0) {
> +		tnt_raise(ClientError, errcode,
> +			  tt_cstr(constraint_name, constraint_len),
> +			  "at least one link must be specified");
> +	}
> +	*out_count = count;
> +	size_t size = count * sizeof(struct field_link);
> +	struct field_link *region_links =
> +		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
> +	memset(region_links, 0, size);
> +	const char **map = &data;
> +	for (uint32_t i = 0; i < count; ++i) {
> +		uint32_t map_sz = mp_decode_map(map);
> +		if (map_sz != 2) {
> +			tnt_raise(ClientError, errcode,
> +				  tt_cstr(constraint_name, constraint_len),
> +				  tt_sprintf("link must be map with 2 fields"));
> +		}
> +		for (uint8_t j = 0; j < map_sz; ++j) {
> +			if (mp_typeof(**map) != MP_STR) {
> +				tnt_raise(ClientError, errcode,
> +					  tt_cstr(constraint_name,
> +						  constraint_len),
> +					  tt_sprintf("link %d is not map "\
> +						     "with string keys", i));
> +			}
> +			uint32_t key_len;
> +			const char *key = mp_decode_str(map, &key_len);

1. Until we have this https://github.com/tarantool/tarantool/issues/1253
we should check tuple field internals. Here we need to check for MP_UINT
or the crash is possible:

	box.cfg{}
	t = {'fk_1', 1, 1, false, 'simple', 'restrict', 'restrict', {{child = 'crash', parent = 'crash'}}}
	box.space._fk_constraint:insert(t)

	Assertion failed: (0), function mp_decode_uint, file /Users/v.shpilevoy/Work/Repositories/tarantool/src/lib/msgpuck/msgpuck.h, line 1434.
	Process 76355 stopped
	* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
	    frame #0: 0x00007fff56cf5b66 libsystem_kernel.dylib`__pthread_kill + 10
	libsystem_kernel.dylib`__pthread_kill:
	->  0x7fff56cf5b66 <+10>: jae    0x7fff56cf5b70            ; <+20>
	    0x7fff56cf5b68 <+12>: movq   %rax, %rdi
	    0x7fff56cf5b6b <+15>: jmp    0x7fff56cecae9            ; cerror_nocancel
	    0x7fff56cf5b70 <+20>: retq
	Target 0: (tarantool) stopped.

> +			if (key_len == 6 &&
> +			    memcmp(key, "parent", key_len) == 0) {
> +				region_links[i].parent_field =
> +					mp_decode_uint(map);
> +			} else if (key_len == 5 &&
> +				   memcmp(key, "child", key_len) == 0) {
> +				region_links[i].child_field =
> +					mp_decode_uint(map);
> +			} else {
> +				char *errmsg = tt_static_buf();
> +				snprintf(errmsg, TT_STATIC_BUF_LEN,
> +					 "unexpected key of link %d '%.*s'", i,
> +					 key_len, key);
> +				tnt_raise(ClientError, errcode,
> +					  tt_cstr(constraint_name,
> +						  constraint_len), errmsg);
> +			}
> +		}
> +	}
> +	return region_links;
> +}
> +
> +/**
> + * Remove FK constraint from child's list.
> + * Entries in child list are supposed to be unique
> + * by their name.
> + *
> + * @param list List of child FK constraints.
> + * @param fkey_name Name of constraint to be removed.
> + * @retval FK being removed.
> + */
> +static struct fkey *
> +fkey_remove_child(struct rlist *list, const char *fkey_name)
> +{
> +	struct fkey *fk;
> +	rlist_foreach_entry(fk, list, child_link) {
> +		if (strcmp(fkey_name, fk->def->name) == 0) {
> +			rlist_del_entry(fk, child_link);
> +			return fk;
> +		}
> +	}

2. In all 3 places of usage you remove the fd from parent list too.
Lets move the deletion there and rename this function to something
like fkey_snatch_by_name or something.

> diff --git a/src/box/fkey.h b/src/box/fkey.h
> new file mode 100644
> index 000000000..0d537b1a7
> --- /dev/null
> +++ b/src/box/fkey.h
> +/** Definition of foreign key constraint. */
> +struct fkey_def {
> +	/** Id of space containing the REFERENCES clause (child). */
> +	uint32_t child_id;
> +	/** Id of space that the key points to (parent). */
> +	uint32_t parent_id;
> +	/** Number of fields in this key. */
> +	uint32_t field_count;
> +	/** True if constraint checking is deferred till COMMIT. */
> +	bool is_deferred;

3. What is the difference between this and pragma defer_foreign_keys?
Can we remove this flag or the pragma?

> +	/** Match condition for foreign key. SIMPLE by default. */
> +	enum fkey_match match;
> +	/** ON DELETE action. NO ACTION by default. */
> +	enum fkey_action on_delete;
> +	/** ON UPDATE action. NO ACTION by default. */
> +	enum fkey_action on_update;
> +	/** Mapping of fields in child to fields in parent. */
> +	struct field_link *links;
> +	/** Name of the constraint. */
> +	char name[0];
> +};
> diff --git a/src/box/space.h b/src/box/space.h
> index 01a4af726..97650cffe 100644
> --- a/src/box/space.h
> +++ b/src/box/space.h
> @@ -183,6 +183,15 @@ struct space {
>   	 * of index id.
>   	 */
>   	struct index **index;
> +	/**
> +	 * Lists of foreign key constraints. In SQL terms parent
> +	 * space is the "from" table i.e. the table that contains
> +	 * the REFERENCES clause. Child space is "to" table, in
> +	 * other words the table that is named in the REFERENCES
> +	 * clause.

4. Are you sure?

https://en.wikipedia.org/wiki/Foreign_key

"The table containing the foreign key is called the child table,
and the table containing the candidate key is called the
referenced or parent table."

> +	 */
> +	struct rlist parent_fkey;
> +	struct rlist child_fkey;
>   };
>   
>   /** Initialize a base space instance. */

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-07-25 10:03     ` n.pettik
@ 2018-07-26 20:12       ` Vladislav Shpilevoy
  2018-08-01 20:54         ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-07-26 20:12 UTC (permalink / raw)
  To: tarantool-patches, n.pettik

Hi! Thanks for the fixes!

1. sql-tap/fkey2.test.lua fails on this commit. Travis fails
as well.

2. I have pushed more review fixes in a separate commit (the
test fails before it too). Please, squash if you are ok.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-07-26 20:12       ` Vladislav Shpilevoy
@ 2018-08-01 20:54         ` n.pettik
  2018-08-02 22:15           ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-08-01 20:54 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


> Thanks for the patch! See my answers, 4 comments and a
> patch on the branch.

I’ve squashed your fixes.

>> +++ b/src/box/alter.cc
>> @@ -1920,7 +1920,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
>>          * Can't drop index if foreign key constraints references
>>          * this index.
>>          */
>> -       if (new_tuple == NULL) {
>> +       if (old_index != NULL && new_tuple == NULL) {
>> Also, I didn’t get what you mean by mentioning index_def_change_requires_rebuild.
>> Can index id change on its renaming?
> 
> Index_id can not change. But a one can remove an index part, or add a new
> one. Or change nullability that breaks uniqueness. I do not see how do you
> deal with that. To detect such dramatic changes
> index_def_change_requires_rebuild() is used in on_replace_dd_index().

Ok:

+/**
+ * Check whether given index is referenced by some foreign key
+ * constraint or not.
+ *
+ * @fkey_head List of FK constraints belonging to parent space.
+ * @iid Index id which belongs to parent space and to be tested.
+ * @retval True if at least one FK constraint references this
+ *         index; false otherwise.
+ */
+bool
+index_is_fkey_referenced(struct rlist *fkey_head, uint32_t iid)
+{
+       struct fkey *fk;
+       rlist_foreach_entry(fk, fkey_head, parent_link) {
+               if (fk->index_id == iid)
+                       return true;
+       }
+       return false;
+}
+

@@ -1980,13 +2001,11 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
                 * Can't drop index if foreign key constraints
                 * references this index.
                 */
-               struct fkey *fk;
-               rlist_foreach_entry(fk, &old_space->parent_fkey, parent_link) {
-                       if (fk->index_id == iid) {
-                               tnt_raise(ClientError, ER_ALTER_SPACE,
-                                         space_name(old_space),
-                                         "can not drop referenced index");
-                       }
+               if (index_is_fkey_referenced(&old_space->parent_fkey,
+                                            iid)) {
+                       tnt_raise(ClientError, ER_ALTER_SPACE,
+                                 space_name(old_space),
+                                 "can not drop referenced index”);

@@ -2048,6 +2067,12 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
                        (void) new MoveIndex(alter, old_index->def->iid);
                } else if (index_def_change_requires_rebuild(old_index,
                                                             index_def)) {
+                       if (index_is_fkey_referenced(&old_space->parent_fkey,
+                                                    iid)) {
+                               tnt_raise(ClientError, ER_ALTER_SPACE,
+                                         space_name(old_space),
+                                         "can not alter referenced index");
+                       }

++ b/test/sql/foreign-keys.test.lua
@@ -86,6 +86,11 @@ t = box.space._fk_constraint:insert(t)
 --
 box.sql.execute("DROP INDEX i1 on t1;")
 
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}

+++ b/test/sql/foreign-keys.result
@@ -181,6 +181,16 @@ box.sql.execute("DROP INDEX i1 on t1;")
 ---
 - error: 'Can''t modify space ''T1'': can not drop referenced index'
 ...
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+---
+- error: 'Can''t modify space ''T1'': can not alter referenced index'
+...
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}
+---
+- [512, 1, 'I2', 'tree', {'unique': true}, [{'field': 1, 'type': 'unsigned', 'is_nullable': true}]]
+...

>> I use this format since it is easier to avoid confusing parent and child ids
>> (i.e. what comes first and what comes second). Even if we add separate
>> convenient Lua API for that purpose.
>> If you insist on doing that, I will change format.
> 
> Lua API is not about _fk_constraint space. Lua API will be like
> space:create_fk() with fancy arguments etc.
> 
> I think, _fk_constraint should be compact and easy to store and parse,
> not to manually read or write. My opinion is that 3 the best options
> exist:
> 
> {{..., ..., ...}, {..., ..., ...}} - array of 2 arrays. First is child,
> second is parent. Or:
> 
> , {..., ..., ...}, {..., ..., ...},  - create separate fields in
> _fk_constraint for child and parent arrays. Or:
> 
> {child = {..., ..., ...}, parent = {..., ..., ...}} - map with two
> keys and two array values.
> 
> I do not insist on my format but think we should consult
> Kostja and Kirill on the final one.

Ok, finally we decided to implement second variant:

+++ b/src/box/alter.cc
@@ -3516,10 +3516,10 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 }
 
 /**
- * Decode MsgPack array of links. It consists from maps:
- * {parent_id (UINT) : child_id (UINT)}.
+ * Decode MsgPack arrays of links. They are stored as two
+ * separate arrays filled with unsigned fields numbers.
  *
- * @param data MsgPack array of links.
+ * @param tuple Tuple to be inserted into _fk_constraints.
  * @param[out] out_count Count of links.
  * @param constraint_name Constraint name to use in error
  *                       messages.
@@ -3528,65 +3528,48 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
  * @retval Array of links.
  */
 static struct field_link *
-fkey_links_decode(const char *data, uint32_t *out_count,
+fkey_links_decode(const struct tuple *tuple, uint32_t *out_count,
                  const char *constraint_name, uint32_t constraint_len,
                  uint32_t errcode)
 {
-       assert(mp_typeof(*data) == MP_ARRAY);
-       uint32_t count = mp_decode_array(&data);
+       const char *parent_cols =
+               tuple_field_with_type_xc(tuple,
+                                        BOX_FK_CONSTRAINT_FIELD_PARENT_COLS,
+                                        MP_ARRAY);
+       assert(mp_typeof(*parent_cols) == MP_ARRAY);
+       uint32_t count = mp_decode_array(&parent_cols);
        if (count == 0) {
                tnt_raise(ClientError, errcode,
                          tt_cstr(constraint_name, constraint_len),
                          "at least one link must be specified");
        }
+       const char *child_cols =
+               tuple_field_with_type_xc(tuple,
+                                        BOX_FK_CONSTRAINT_FIELD_CHILD_COLS,
+                                        MP_ARRAY);
+       assert(mp_typeof(*child_cols) == MP_ARRAY);
+       if (mp_decode_array(&child_cols) != count) {
+               tnt_raise(ClientError, errcode,
+                         tt_cstr(constraint_name, constraint_len),
+                         "number of referenced and referencing fields "
+                         "must be the same");
+       }
        *out_count = count;
        size_t size = count * sizeof(struct field_link);
        struct field_link *region_links =
                (struct field_link *) region_alloc_xc(&fiber()->gc, size);
        memset(region_links, 0, size);
-       const char **map = &data;
        for (uint32_t i = 0; i < count; ++i) {
-               uint32_t map_sz = mp_decode_map(map);
-               if (map_sz != 2) {
+               if (mp_typeof(*parent_cols) != MP_UINT ||
+                   mp_typeof(*child_cols) != MP_UINT) {
                        tnt_raise(ClientError, errcode,
-                                 tt_cstr(constraint_name, constraint_len),
-                                 tt_sprintf("link must be map with 2 fields"));
-               }
-               for (uint8_t j = 0; j < map_sz; ++j) {
-                       if (mp_typeof(**map) != MP_STR) {
-                               tnt_raise(ClientError, errcode,
-                                         tt_cstr(constraint_name,
-                                                 constraint_len),
-                                         tt_sprintf("link %d is not map "\
-                                                    "with string keys", i));
-                       }
-                       uint32_t key_len;
-                       const char *key = mp_decode_str(map, &key_len);
-                       if (mp_typeof(**map) != MP_UINT) {
-                               tnt_raise(ClientError, errcode,
-                                         tt_cstr(constraint_name,
-                                                 constraint_len),
-                                         tt_sprintf("value of %d link is not "\
-                                                    "unsigned", i));
-                       }
-                       if (key_len == 6 &&
-                           memcmp(key, "parent", key_len) == 0) {
-                               region_links[i].parent_field =
-                                       mp_decode_uint(map);
-                       } else if (key_len == 5 &&
-                                  memcmp(key, "child", key_len) == 0) {
-                               region_links[i].child_field =
-                                       mp_decode_uint(map);
-                       } else {
-                               char *errmsg = tt_static_buf();
-                               snprintf(errmsg, TT_STATIC_BUF_LEN,
-                                        "unexpected key of link %d '%.*s'", i,
-                                        key_len, key);
-                               tnt_raise(ClientError, errcode,
-                                         tt_cstr(constraint_name,
-                                                 constraint_len), errmsg);
-                       }
+                                 tt_cstr(constraint_name,
+                                         constraint_len),
+                                 tt_sprintf("value of %d link is not "\
+                                            "unsigned", i));
                }
+               region_links[i].parent_field = mp_decode_uint(&parent_cols);
+               region_links[i].child_field = mp_decode_uint(&child_cols);
        }
        return region_links;
 }
@@ -3605,12 +3588,9 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
                          "constraint name is too long");
        }
        identifier_check_xc(name, name_len);
-       const char *links_raw =
-               tuple_field_with_type_xc(tuple, BOX_FK_CONSTRAINT_FIELD_LINKS,
-                                        MP_ARRAY);
        uint32_t link_count;
-       struct field_link *links = fkey_links_decode(links_raw, &link_count,
-                                                    name, name_len, errcode);
+       struct field_link *links = fkey_links_decode(tuple, &link_count, name,
+                                                    name_len, errcode);

+++ b/src/box/schema_def.h
@@ -235,7 +235,8 @@ enum {
        BOX_FK_CONSTRAINT_FIELD_MATCH = 4,
        BOX_FK_CONSTRAINT_FIELD_ON_DELETE = 5,
        BOX_FK_CONSTRAINT_FIELD_ON_UPDATE = 6,
-       BOX_FK_CONSTRAINT_FIELD_LINKS = 7,
+       BOX_FK_CONSTRAINT_FIELD_CHILD_COLS = 7,
+       BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8,

+++ b/src/box/lua/upgrade.lua
@@ -516,7 +516,8 @@ local function upgrade_to_2_1_0()
                           {name='match', type='string'},
                           {name='on_delete', type='string'},
                           {name='on_update', type='string'},
-                          {name='links', type='array'}}
+                          {name='child_cols', type='array'},
+                          {name='parent_cols', type='array'}}

+++ b/src/box/sql.c
@@ -1256,7 +1256,7 @@ void tarantoolSqlite3LoadSchema(struct init_data *init)
                          "CREATE TABLE \""TARANTOOL_SYS_FK_CONSTRAINT_NAME
                          "\"(\"name\" TEXT, \"parent_id\" INT, \"child_id\" INT,"
                          "\"deferred\" INT, \"match\" TEXT, \"on_delete\" TEXT,"
-                         "\"on_update\" TEXT, \"links\","
+                         "\"on_update\" TEXT, \"child_cols\", \"parent_cols\”,"

Tests are fixed as well, but it is boring diff so if you want - see whole patch
at the end of letter.

Also, diff from the next patch:

diff --git a/src/box/sql.c b/src/box/sql.c
index bbd057a41..46a0c3472 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1394,16 +1394,18 @@ tarantoolSqlite3MakeTableOpts(Table *pTable, const char *zSql, char *buf)
 }
 
 int
-fkey_encode_links(const struct fkey_def *fkey, char *buf)
+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, fkey->field_count);
-       for (uint32_t i = 0; i < fkey->field_count; ++i) {
-               p = enc->encode_map(p, 2);
-               p = enc->encode_str(p, "child", strlen("child"));
-               p = enc->encode_uint(p, fkey->links[i].child_field);
-               p = enc->encode_str(p, "parent", strlen("parent"));
-               p = enc->encode_uint(p, fkey->links[i].parent_field);
+       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;
 }
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 80216524d..d2aa7f47c 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -1567,7 +1567,7 @@ vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk)
         * Occupy registers for 8 fields: each member in
         * _constraint space plus one for final msgpack tuple.
         */
-       int constr_tuple_reg = sqlite3GetTempRange(parse_context, 9);
+       int constr_tuple_reg = sqlite3GetTempRange(parse_context, 10);
        char *name_copy = sqlite3DbStrDup(parse_context->db, fk->name);
        if (name_copy == NULL)
                return;
@@ -1600,22 +1600,54 @@ vdbe_emit_fkey_create(struct Parse *parse_context, const struct fkey_def *fk)
                          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);
+       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_links_sz);
+                                                encoded_child_sz +
+                                                encoded_parent_sz);
        if (encoded_links == NULL) {
                sqlite3DbFree(parse_context->db, name_copy);
                return;
        }
-       size_t real_links_sz = fkey_encode_links(fk, encoded_links);
-       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);
+       /*
+        * 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 + 8);
+                         constr_tuple_reg + 9);
        sqlite3VdbeChangeP5(vdbe, OPFLAG_NCHANGE);
-       sqlite3ReleaseTempRange(parse_context, constr_tuple_reg, 9);
+       sqlite3ReleaseTempRange(parse_context, constr_tuple_reg, 10);
 }
 
 static int
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index 69c2b9bc6..cbf8e5acb 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -151,15 +151,19 @@ 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 fkey Encode links of this foreign key contraint.
+ * @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(const struct fkey_def *fkey, char *buf);
+fkey_encode_links(uint32_t *links, uint32_t link_count, char *buf);

> 
>>> 
>>> 10. Above you have said that order may be different, but here I see
>>> that once you have found a unique index, you merely check that it
>>> has sequentially the same field numbers as the fk.
>> For some reason I forgot to fix this when was preparing patch.
>> Fixed by now. Also, according to ANSI (11.8 4.a):
>> • Each referenced column shall identify a column of the referenced table and the same column shall not be identi ed more than once.
>> @@ -3710,6 +3710,41 @@ on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
>>         fkey_delete(fk);
>>  }
>>  +static int
>> +cmp_uint32(const void *_a, const void *_b)
>> +{
>> +       const uint32_t *a = (const uint32_t *) _a, *b = (const uint32_t *) _b;
>> +       if (*a == *b)
>> +               return 0;
>> +       return (*a > *b) ? 1 : -1;
>> +}
> 
> Looks like in Tarantool source we have plenty of various cmp functions
> for qsort. I greped uint32_compare, cmp_uint32 (your), cmp_i64, size_compator,
> nums_comparator, int64_cmp. How about to merge them into trivia/util.h as
> cmp_uint32, cmp_int64, cmp_size?

Since I don’t need ‘em anymore in current patch, lets simply open issue:
https://github.com/tarantool/tarantool/issues/3598

> 
>> +
>> +/**
>> + * ANSI SQL doesn't allow list of referenced fields to contain
>> + * duplicates.
>> + */
>> +static int
>> +fkey_links_check_duplicates(struct fkey_def *fk_def)
>> +{
>> +       uint32_t *parent_fields =
>> +               (uint32_t *) region_alloc(&fiber()->gc, fk_def->field_count);
>> +       if (parent_fields == NULL) {
>> +               tnt_raise(OutOfMemory, fk_def->field_count, "region",
>> +                         "parent_fields");
>> +       }
>> +       for (uint32_t i = 0; i < fk_def->field_count; ++i)
>> +               parent_fields[i] = fk_def->links[i].parent_field;
>> +       qsort(parent_fields, fk_def->field_count, sizeof(*parent_fields),
>> +             cmp_uint32);
>> +       uint32_t prev_val = parent_fields[0];
>> +       for (uint32_t i = 1; i < fk_def->field_count; ++i) {
>> +               if (prev_val == parent_fields[i])
>> +                       return -1;
>> +               prev_val = parent_fields[i];
> 
> Why not
> 
>    for (uint32_t i = 1; i < fk_def->field_count; ++i) {
>        if (parent_fields[i - 1] == parent_fields[i])
>            return -1;
>    }
> 
> So you do not need prev_val actually. Also lets do tnt_raise here
> instead of return -1.

Nevermind. I reworked this function taking into consideration
suggested optimization. Look below.

> 
>> +       }
>> +       return 0;
>> +}
>> +
>>  on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>> @@ -3777,6 +3812,11 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>>                                           "field collation mismatch");
>>                         }
>>                 }
>> +               if (fkey_links_check_duplicates(fk_def)) {
>> +                       tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>> +                                 fk_def->name, "referenced fields can not "
>> +                                               "contain duplicates");
>> +               }
>> Probably this is not best implementation. As an improvement I can add kind of optimisation:
>> *pseudo code below*
>> uint_64 mask;
>> for parent_field in fk_def:
>> 	if (pk_mask & (((uint64_t) 1) << parent_field ==1)
>> 		return -1;
>> 	column_mask_set_field(&mask, parent_field);
>> end
>> if (pk_mask & (((uint64_t) 1) << 63)) != 0
>> 	fkey_links_check_duplicates(…) // Full version of checking.
>> else
>> 	return 1;
>> Is it worth the effort? I mean here we waste O(field_count) in case the largest
>> filedno in parent table > 63. On the other hand, I don’t think that many users
>> have tables with field count > 63, so it is likely to be reasonable.
> 
> I like this optimization. What is more, I think, we can remove the sorting
> things above then. For users who have field numbers > 63 the smallest problem
> is slow alter. So for them we can just use O(field_count ^ 2) with no region
> and sorting.

Ok, done:

 /**
  * ANSI SQL doesn't allow list of referenced fields to contain
- * duplicates.
 /**
  * ANSI SQL doesn't allow list of referenced fields to contain
- * duplicates.
+ * duplicates. Firstly, we try to follow the easiest way:
+ * if all referenced fields numbers are less than 63, we can
+ * use bit mask. Otherwise, fall through slow check where we
+ * use O(field_cont^2) simple nested cycle iterations.
  */
-static int
+static void
 fkey_links_check_duplicates(struct fkey_def *fk_def)
 {
-       uint32_t *parent_fields =
-               (uint32_t *) region_alloc(&fiber()->gc, fk_def->field_count);
-       if (parent_fields == NULL) {
-               tnt_raise(OutOfMemory, fk_def->field_count, "region",
-                         "parent_fields");
-       }
-       for (uint32_t i = 0; i < fk_def->field_count; ++i)
-               parent_fields[i] = fk_def->links[i].parent_field;
-       qsort(parent_fields, fk_def->field_count, sizeof(*parent_fields),
-             cmp_uint32);
-       uint32_t prev_val = parent_fields[0];
-       for (uint32_t i = 1; i < fk_def->field_count; ++i) {
-               if (prev_val == parent_fields[i])
-                       return -1;
-               prev_val = parent_fields[i];
+       uint64_t field_mask = 0;
+       for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+               uint32_t parent_field = fk_def->links[i].parent_field;
+               if (parent_field > 63)
+                       goto slow_check;
+               if (field_mask & (((uint64_t) 1) << parent_field))
+                       goto error;
+               column_mask_set_fieldno(&field_mask, parent_field);
        }
-       return 0;
+       return;
+slow_check:
+       for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+               uint32_t parent_field = fk_def->links[i].parent_field;
+               for (uint32_t j = i + 1; j < fk_def->field_count; ++j) {
+                       if (parent_field == fk_def->links[j].parent_field)
+                               goto error;
+               }
+       }
+       return;
+error:
+       tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT, fk_def->name,
+                 "referenced fields can not contain duplicates");

> 
>> Moreover, it would be cool if column_mask_set_fieldno() worked the same
>> as bit_set() from lib/bit, i.e. returned previous bit value.
> 
> You are free to extend the API.

Well, I see that in other places return value is not used, so lets keep things
as they are.

> 
>>> 
>>>> +			fk_index = idx;
>>>> +			break;
>>>> +		}
>>>> +		if (fk_index == NULL) {
>>>> +			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
>>>> +				  fk_def->name, "referenced fields don't "
>>>> +					      "compose unique index");
>>>> +		}
>>>> +		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
>>> 
>>> 11. I know how you like memory mapping. How about merge fkey_def into
>>> fkey memory? I see, that it is relatively simple and linear thing.
>> What is the point of doing this? Do you suggest to allocate enough memory for fkey
>> right in fkey_def_new_from_tuple() ? Like this:
>> @@ -3536,7 +3536,8 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
>>         struct field_link *links = fkey_links_decode(links_raw, &link_count,
>>                                                      name, name_len, errcode);
>>         size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
>> -       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
>> +       struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz +
>> +                                                            sizeof(struct fkey);
>> If so, it would be quite confusing I think (since function returns only
>> fkey_def, but memory would be allocated for fkey_def + fkey).
>> If you mean smth else or insist on this change, I will fix it.
> 
> I just like when structure is self-sufficient. When it can be freed with
> one free, and can fit in one cache line for better performance on access fields,
> maybe partially. If you do not want to merge them, I do not insist though.

I don’t feel that it really matters: creation of FK constraints doesn’t seem
to be frequently executed thing. On the other hand, such strange allocation
may be misleading for those who investigate source code.

> 
>>>> diff --git a/src/box/fkey.c b/src/box/fkey.c
>>>> new file mode 100644
>>>> index 000000000..e45889a0d
>>>> --- /dev/null
>>>> +++ b/src/box/fkey.c
>>>> +void
>>>> +fkey_trigger_delete(struct sqlite3 *db, struct sql_trigger *p)
>>>> +{
>>> 
>>> 12. Why do you need this function when sql_trigger_delete exists?
>> Memory layout of FK trigger differs from ordinary one (from sql/fkey.c):
>> size_t trigger_size = sizeof(struct sql_trigger) +
>>                     sizeof(TriggerStep) + nFrom + 1;
>> trigger =
>>        (struct sql_trigger *)sqlite3DbMallocZero(db,
>>                                             trigger_size);
>> One can see, memory for TriggerStep, sql_trigger and name of
>> target table is allocated in one chunk. Thus, fkey_trigger_delete()
>> doesn’t release memory for TriggerStep. Overall, if compare
>> these functions fkey_trigger_delete() looks much simpler.
> 
> I very do not like that the same structure is allocated on different
> layouts, it confuses. It is ok only during alter when we temporary
> allocate some things on region. Lets allocate this trigger like
> ordinary ones and remove fkey_trigger_delete. I like when a
> structure is compact, as I say in the previous comment, but not when
> it is sometimes compact and other times not.

Ok, I slightly reworked allocation for FK triggers and it allows to use
everywhere sql_trigger_delete():

+++ b/src/box/sql/fkey.c
@@ -1251,20 +1251,14 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
                                                   pWhere, 0, 0, 0, 0, 0, 0);
                        pWhere = 0;
                }
-
-               /* Disable lookaside memory allocation */
-               db->lookaside.bDisable++;
-
-               size_t trigger_size = sizeof(struct sql_trigger) +
-                                     sizeof(TriggerStep) + nFrom + 1;
-               trigger =
-                       (struct sql_trigger *)sqlite3DbMallocZero(db,
-                                                                 trigger_size);
+               trigger = (struct sql_trigger *)sqlite3DbMallocZero(db,
+                                                                   sizeof(*trigger));
                if (trigger != NULL) {
-                       pStep = trigger->step_list = (TriggerStep *)&trigger[1];
+                       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((char *)pStep->zTarget, zFrom, nFrom);
-
+                       memcpy(pStep->zTarget, zFrom, nFrom);
                        pStep->pWhere =
                            sqlite3ExprDup(db, pWhere, EXPRDUP_REDUCE);
                        pStep->pExprList =
@@ -1278,15 +1272,12 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
                        }
                }
 
-               /* Re-enable the lookaside buffer, if it was disabled earlier. */
-               db->lookaside.bDisable—;

>>>> diff --git a/test/engine/iterator.result b/test/engine/iterator.result
>>>> index a36761df8..ba9b0545a 100644
>>>> --- a/test/engine/iterator.result
>>>> +++ b/test/engine/iterator.result
>>>> @@ -4211,7 +4211,7 @@ s:replace{35}
>>>>  ...
>>>>  state, value = gen(param,state)
>>>>  ---
>>>> -- error: 'builtin/box/schema.lua:1049: usage: next(param, state)'
>>>> +- error: 'builtin/box/schema.lua:1055: usage: next(param, state)'
>>> 
>>> 18. This test fails on each schema change. Lets remove this file:line
>>> from the error message alongside the patch.
>> Ok:
> 
> I meant remove 'file:line' from the error message. Not the test itself.
> For example, you can catch the error and match the message using
> string.match("usage: next(param, state)”).

Ok, returned back (sorry, I just used Occam's razor):

+++ b/test/engine/iterator.test.lua
@@ -399,6 +399,11 @@ value
 gen,param,state = i:pairs({35})
 state, value = gen(param,state)
 value
+s:replace{35}
+f = function() return gen(param, state) end
+_, errmsg = pcall(f)
+errmsg:match('usage: next%(param, state%)')
+value

+++ b/test/engine/iterator.result
@@ -4207,6 +4207,24 @@ value
 ---
 - null
 ...
+s:replace{35}
+---
+- [35]
+...
+f = function() return gen(param, state) end
+---
+...
+_, errmsg = pcall(f)
+---
+...
+errmsg:match('usage: next%(param, state%)')
+---
+- 'usage: next(param, state)'
+...
+value
+---
+- null
+...

> 
>> diff --git a/src/box/alter.cc b/src/box/alter.cc
>> index 7b6bd1a5a..c5d1f75df 100644
>> --- a/src/box/alter.cc
>> +++ b/src/box/alter.cc
>> +/**
>> + * Decode MsgPack array of links. It consists from maps:
>> + * {parent_id (UINT) : child_id (UINT)}.
>> + *
>> + * @param data MsgPack array of links.
>> + * @param[out] out_count Count of links.
>> + * @param constraint_name Constraint name to use in error
>> + *			  messages.
>> + * @param constraint_len Length of constraint name.
>> + * @param errcode Errcode for client errors.
>> + * @retval Array of links.
>> + */
>> +static struct field_link *
>> +fkey_links_decode(const char *data, uint32_t *out_count,
>> +		  const char *constraint_name, uint32_t constraint_len,
>> +		  uint32_t errcode)
>> +{
>> +	assert(mp_typeof(*data) == MP_ARRAY);
>> +	uint32_t count = mp_decode_array(&data);
>> +	if (count == 0) {
>> +		tnt_raise(ClientError, errcode,
>> +			  tt_cstr(constraint_name, constraint_len),
>> +			  "at least one link must be specified");
>> +	}
>> +	*out_count = count;
>> +	size_t size = count * sizeof(struct field_link);
>> +	struct field_link *region_links =
>> +		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
>> +	memset(region_links, 0, size);
>> +	const char **map = &data;
>> +	for (uint32_t i = 0; i < count; ++i) {
>> +		uint32_t map_sz = mp_decode_map(map);
>> +		if (map_sz != 2) {
>> +			tnt_raise(ClientError, errcode,
>> +				  tt_cstr(constraint_name, constraint_len),
>> +				  tt_sprintf("link must be map with 2 fields"));
>> +		}
>> +		for (uint8_t j = 0; j < map_sz; ++j) {
>> +			if (mp_typeof(**map) != MP_STR) {
>> +				tnt_raise(ClientError, errcode,
>> +					  tt_cstr(constraint_name,
>> +						  constraint_len),
>> +					  tt_sprintf("link %d is not map "\
>> +						     "with string keys", i));
>> +			}
>> +			uint32_t key_len;
>> +			const char *key = mp_decode_str(map, &key_len);
> 
> 1. Until we have this https://github.com/tarantool/tarantool/issues/1253
> we should check tuple field internals. Here we need to check for MP_UINT
> or the crash is possible:
> 
> 	box.cfg{}
> 	t = {'fk_1', 1, 1, false, 'simple', 'restrict', 'restrict', {{child = 'crash', parent = 'crash'}}}
> 	box.space._fk_constraint:insert(t)
> 
> 	Assertion failed: (0), function mp_decode_uint, file /Users/v.shpilevoy/Work/Repositories/tarantool/src/lib/msgpuck/msgpuck.h, line 1434.
> 	Process 76355 stopped
> 	* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
> 	    frame #0: 0x00007fff56cf5b66 libsystem_kernel.dylib`__pthread_kill + 10
> 	libsystem_kernel.dylib`__pthread_kill:
> 	->  0x7fff56cf5b66 <+10>: jae    0x7fff56cf5b70            ; <+20>
> 	    0x7fff56cf5b68 <+12>: movq   %rax, %rdi
> 	    0x7fff56cf5b6b <+15>: jmp    0x7fff56cecae9            ; cerror_nocancel
> 	    0x7fff56cf5b70 <+20>: retq
> 	Target 0: (tarantool) stopped.

Added appropriate checks (see fixes to format of _fk_constraint space).
And tests:

@@ -58,6 +58,8 @@ t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{wro
 box.space._fk_constraint:insert(t)
 t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 13, parent = 1}}}
 box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 'crash', parent = 'crash'}}}
+box.space._fk_constraint:insert(t)

+++ b/test/sql/foreign-keys.result
@@ -135,6 +135,14 @@ box.space._fk_constraint:insert(t)
 - error: 'Failed to create foreign key constraint ''fk_1'': foreign key refers to
     nonexistent field'
 ...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {{child = 'crash', parent = 'crash'}}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': value of 0 link is not
+    unsigned'
+...

> 
>> +			if (key_len == 6 &&
>> +			    memcmp(key, "parent", key_len) == 0) {
>> +				region_links[i].parent_field =
>> +					mp_decode_uint(map);
>> +			} else if (key_len == 5 &&
>> +				   memcmp(key, "child", key_len) == 0) {
>> +				region_links[i].child_field =
>> +					mp_decode_uint(map);
>> +			} else {
>> +				char *errmsg = tt_static_buf();
>> +				snprintf(errmsg, TT_STATIC_BUF_LEN,
>> +					 "unexpected key of link %d '%.*s'", i,
>> +					 key_len, key);
>> +				tnt_raise(ClientError, errcode,
>> +					  tt_cstr(constraint_name,
>> +						  constraint_len), errmsg);
>> +			}
>> +		}
>> +	}
>> +	return region_links;
>> +}
>> +
>> +/**
>> + * Remove FK constraint from child's list.
>> + * Entries in child list are supposed to be unique
>> + * by their name.
>> + *
>> + * @param list List of child FK constraints.
>> + * @param fkey_name Name of constraint to be removed.
>> + * @retval FK being removed.
>> + */
>> +static struct fkey *
>> +fkey_remove_child(struct rlist *list, const char *fkey_name)
>> +{
>> +	struct fkey *fk;
>> +	rlist_foreach_entry(fk, list, child_link) {
>> +		if (strcmp(fkey_name, fk->def->name) == 0) {
>> +			rlist_del_entry(fk, child_link);
>> +			return fk;
>> +		}
>> +	}
> 
> 2. In all 3 places of usage you remove the fd from parent list too.
> Lets move the deletion there and rename this function to something
> like fkey_snatch_by_name or something.

Ok:

@@ -3625,21 +3626,22 @@ fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
 }
 
 /**
- * Remove FK constraint from child's list.
- * Entries in child list are supposed to be unique
- * by their name.
+ * Remove FK constraint from child's and parent's lists and
+ * return it. Entries in child list are supposed to be
+ * unique by their name.
  *
  * @param list List of child FK constraints.
  * @param fkey_name Name of constraint to be removed.
  * @retval FK being removed.
  */
 static struct fkey *
-fkey_remove_child(struct rlist *list, const char *fkey_name)
+fkey_grab_by_name(struct rlist *list, const char *fkey_name)
 {
        struct fkey *fk;
        rlist_foreach_entry(fk, list, child_link) {
                if (strcmp(fkey_name, fk->def->name) == 0) {
                        rlist_del_entry(fk, child_link);
+                       rlist_del_entry(fk, parent_link);

@@ -3670,9 +3672,8 @@ on_replace_fkey_rollback(struct trigger *trigger, void *event)
        struct fkey *fk = (struct fkey *)trigger->data;
        struct space *parent = space_by_id(fk->def->parent_id);
        struct space *child = space_by_id(fk->def->child_id);
-       struct fkey *old_fkey = fkey_remove_child(&child->child_fkey,
+       struct fkey *old_fkey = fkey_grab_by_name(&child->child_fkey,
                                                  fk->def->name);
-       rlist_del_entry(old_fkey, parent_link);

@@ -3859,9 +3852,8 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                        txn_on_rollback(txn, on_rollback);
                } else {
                        struct fkey *old_fk =
-                               fkey_remove_child(&child_space->child_fkey,
+                               fkey_grab_by_name(&child_space->child_fkey,
                                                  fk_def->name);
-                       rlist_del_entry(old_fk, parent_link);
                        rlist_add_entry(&child_space->child_fkey, fkey,
                                        child_link);
                        rlist_add_entry(&parent_space->parent_fkey, fkey,
@@ -3886,9 +3878,8 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                struct space *child_space =
                        space_cache_find_xc(fk_def->child_id);
                struct fkey *old_fkey =
-                       fkey_remove_child(&child_space->child_fkey,
+                       fkey_grab_by_name(&child_space->child_fkey,
                                          fk_def->name);
-               rlist_del_entry(old_fkey, parent_link);

>> diff --git a/src/box/fkey.h b/src/box/fkey.h
>> new file mode 100644
>> index 000000000..0d537b1a7
>> --- /dev/null
>> +++ b/src/box/fkey.h
>> +/** Definition of foreign key constraint. */
>> +struct fkey_def {
>> +	/** Id of space containing the REFERENCES clause (child). */
>> +	uint32_t child_id;
>> +	/** Id of space that the key points to (parent). */
>> +	uint32_t parent_id;
>> +	/** Number of fields in this key. */
>> +	uint32_t field_count;
>> +	/** True if constraint checking is deferred till COMMIT. */
>> +	bool is_deferred;
> 
> 3. What is the difference between this and pragma defer_foreign_keys?
> Can we remove this flag or the pragma?

Using pragma we can force defer FK checking (for all FK constraints despite
their original is_deferred statuses) or vice versa — completely disable it.
IDK whether smb need this feature or not, it is SQLite legacy, but still it seems to work.

>> diff --git a/src/box/space.h b/src/box/space.h
>> index 01a4af726..97650cffe 100644
>> --- a/src/box/space.h
>> +++ b/src/box/space.h
>> @@ -183,6 +183,15 @@ struct space {
>>  	 * of index id.
>>  	 */
>>  	struct index **index;
>> +	/**
>> +	 * Lists of foreign key constraints. In SQL terms parent
>> +	 * space is the "from" table i.e. the table that contains
>> +	 * the REFERENCES clause. Child space is "to" table, in
>> +	 * other words the table that is named in the REFERENCES
>> +	 * clause.
> 
> 4. Are you sure?
> 
> https://en.wikipedia.org/wiki/Foreign_key
> 
> "The table containing the foreign key is called the child table,
> and the table containing the candidate key is called the
> referenced or parent table.”

It is definitely mistake. In other places I use correct terminology.
Fixed.

Updated patch:

=======================================================================

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index a467d3517..6dd2c75b9 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -92,6 +92,7 @@ add_library(box STATIC
     space.c
     space_def.c
     sequence.c
+    fkey.c
     func.c
     func_def.c
     alter.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 7b6bd1a5a..5b55bfd7a 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -33,9 +33,11 @@
 #include "user.h"
 #include "space.h"
 #include "index.h"
+#include "fkey.h"
 #include "func.h"
 #include "coll_id_cache.h"
 #include "coll_id_def.h"
+#include "column_mask.h"
 #include "txn.h"
 #include "tuple.h"
 #include "fiber.h" /* for gc_pool */
@@ -571,6 +573,14 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
 	old_space->sql_triggers = new_value;
 }
 
+/** The same as for triggers - swap lists of FK constraints. */
+static void
+space_swap_fkeys(struct space *new_space, struct space *old_space)
+{
+	rlist_swap(&new_space->child_fkey, &old_space->child_fkey);
+	rlist_swap(&new_space->parent_fkey, &old_space->parent_fkey);
+}
+
 /**
  * True if the space has records identified by key 'uid'.
  * Uses 'iid' index.
@@ -781,9 +791,10 @@ alter_space_rollback(struct trigger *trigger, void * /* event */)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	struct space *new_space = space_cache_replace(alter->old_space);
 	assert(new_space == alter->new_space);
 	(void) new_space;
@@ -879,9 +890,10 @@ alter_space_do(struct txn *txn, struct alter_space *alter)
 	space_fill_index_map(alter->old_space);
 	space_fill_index_map(alter->new_space);
 	/*
-	 * Don't forget about space triggers.
+	 * Don't forget about space triggers and foreign keys.
 	 */
 	space_swap_triggers(alter->new_space, alter->old_space);
+	space_swap_fkeys(alter->new_space, alter->old_space);
 	/*
 	 * The new space is ready. Time to update the space
 	 * cache with it.
@@ -1742,6 +1754,18 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 				  space_name(old_space),
 				  "other views depend on this space");
 		}
+		/*
+		 * No need to check existence of parent keys,
+		 * since if we went so far, space would'n have
+		 * any indexes. But referenced space has at least
+		 * one referenced index which can't be dropped
+		 * before constraint itself.
+		 */
+		if (! rlist_empty(&old_space->child_fkey)) {
+			tnt_raise(ClientError, ER_DROP_SPACE,
+				  space_name(old_space),
+				  "the space has foreign key constraints");
+		}
 		/**
 		 * The space must be deleted from the space
 		 * cache right away to achieve linearisable
@@ -1842,6 +1866,26 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 	}
 }
 
+/**
+ * Check whether given index is referenced by some foreign key
+ * constraint or not.
+ *
+ * @fkey_head List of FK constraints belonging to parent space.
+ * @iid Index id which belongs to parent space and to be tested.
+ * @retval True if at least one FK constraint references this
+ *         index; false otherwise.
+ */
+bool
+index_is_fkey_referenced(struct rlist *fkey_head, uint32_t iid)
+{
+	struct fkey *fk;
+	rlist_foreach_entry(fk, fkey_head, parent_link) {
+		if (fk->index_id == iid)
+			return true;
+	}
+	return false;
+}
+
 /**
  * Just like with _space, 3 major cases:
  *
@@ -1951,12 +1995,18 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	 * 3. Change of an index which does not require a rebuild.
 	 * 4. Change of an index which does require a rebuild.
 	 */
-	/*
-	 * First, move all unchanged indexes from the old space
-	 * to the new one.
-	 */
 	/* Case 1: drop the index, if it is dropped. */
 	if (old_index != NULL && new_tuple == NULL) {
+		/*
+		 * Can't drop index if foreign key constraints
+		 * references this index.
+		 */
+		if (index_is_fkey_referenced(&old_space->parent_fkey,
+					     iid)) {
+			tnt_raise(ClientError, ER_ALTER_SPACE,
+				  space_name(old_space),
+				  "can not drop referenced index");
+		}
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
 	}
@@ -2017,6 +2067,12 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 			(void) new MoveIndex(alter, old_index->def->iid);
 		} else if (index_def_change_requires_rebuild(old_index,
 							     index_def)) {
+			if (index_is_fkey_referenced(&old_space->parent_fkey,
+						     iid)) {
+				tnt_raise(ClientError, ER_ALTER_SPACE,
+					  space_name(old_space),
+					  "can not alter referenced index");
+			}
 			/*
 			 * Operation demands an index rebuild.
 			 */
@@ -3459,6 +3515,395 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 	txn_on_commit(txn, on_commit);
 }
 
+/**
+ * Decode MsgPack arrays of links. They are stored as two
+ * separate arrays filled with unsigned fields numbers.
+ *
+ * @param tuple Tuple to be inserted into _fk_constraints.
+ * @param[out] out_count Count of links.
+ * @param constraint_name Constraint name to use in error
+ *			  messages.
+ * @param constraint_len Length of constraint name.
+ * @param errcode Errcode for client errors.
+ * @retval Array of links.
+ */
+static struct field_link *
+fkey_links_decode(const struct tuple *tuple, uint32_t *out_count,
+		  const char *constraint_name, uint32_t constraint_len,
+		  uint32_t errcode)
+{
+	const char *parent_cols =
+		tuple_field_with_type_xc(tuple,
+					 BOX_FK_CONSTRAINT_FIELD_PARENT_COLS,
+					 MP_ARRAY);
+	assert(mp_typeof(*parent_cols) == MP_ARRAY);
+	uint32_t count = mp_decode_array(&parent_cols);
+	if (count == 0) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "at least one link must be specified");
+	}
+	const char *child_cols =
+		tuple_field_with_type_xc(tuple,
+					 BOX_FK_CONSTRAINT_FIELD_CHILD_COLS,
+					 MP_ARRAY);
+	assert(mp_typeof(*child_cols) == MP_ARRAY);
+	if (mp_decode_array(&child_cols) != count) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(constraint_name, constraint_len),
+			  "number of referenced and referencing fields "
+			  "must be the same");
+	}
+	*out_count = count;
+	size_t size = count * sizeof(struct field_link);
+	struct field_link *region_links =
+		(struct field_link *) region_alloc_xc(&fiber()->gc, size);
+	memset(region_links, 0, size);
+	for (uint32_t i = 0; i < count; ++i) {
+		if (mp_typeof(*parent_cols) != MP_UINT ||
+		    mp_typeof(*child_cols) != MP_UINT) {
+			tnt_raise(ClientError, errcode,
+				  tt_cstr(constraint_name,
+					  constraint_len),
+				  tt_sprintf("value of %d link is not "\
+					     "unsigned", i));
+		}
+		region_links[i].parent_field = mp_decode_uint(&parent_cols);
+		region_links[i].child_field = mp_decode_uint(&child_cols);
+	}
+	return region_links;
+}
+
+/** Create an instance of foreign key def constraint from tuple. */
+static struct fkey_def *
+fkey_def_new_from_tuple(const struct tuple *tuple, uint32_t errcode)
+{
+	uint32_t name_len;
+	const char *name =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_NAME,
+				   &name_len);
+	if (name_len > BOX_NAME_MAX) {
+		tnt_raise(ClientError, errcode,
+			  tt_cstr(name, BOX_INVALID_NAME_MAX),
+			  "constraint name is too long");
+	}
+	identifier_check_xc(name, name_len);
+	uint32_t link_count;
+	struct field_link *links = fkey_links_decode(tuple, &link_count, name,
+						     name_len, errcode);
+	size_t fkey_sz = fkey_def_sizeof(link_count, name_len);
+	struct fkey_def *fk_def = (struct fkey_def *) malloc(fkey_sz);
+	if (fk_def == NULL)
+		tnt_raise(OutOfMemory, fkey_sz, "malloc", "fk_def");
+	auto def_guard = make_scoped_guard([=] { free(fk_def); });
+	memcpy(fk_def->name, name, name_len);
+	fk_def->name[name_len] = '\0';
+	fk_def->links = (struct field_link *)((char *)&fk_def->name +
+					      name_len + 1);
+	memcpy(fk_def->links, links, link_count * sizeof(struct field_link));
+	fk_def->field_count = link_count;
+	fk_def->child_id = tuple_field_u32_xc(tuple,
+					      BOX_FK_CONSTRAINT_FIELD_CHILD_ID);
+	fk_def->parent_id =
+		tuple_field_u32_xc(tuple, BOX_FK_CONSTRAINT_FIELD_PARENT_ID);
+	fk_def->is_deferred =
+		tuple_field_bool_xc(tuple, BOX_FK_CONSTRAINT_FIELD_DEFERRED);
+	const char *match = tuple_field_str_xc(tuple,
+					       BOX_FK_CONSTRAINT_FIELD_MATCH,
+					       &name_len);
+	fk_def->match = STRN2ENUM(fkey_match, match, name_len);
+	if (fk_def->match == fkey_match_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown MATCH clause");
+	}
+	const char *on_delete_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_DELETE,
+				   &name_len);
+	fk_def->on_delete = STRN2ENUM(fkey_action, on_delete_action, name_len);
+	if (fk_def->on_delete == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON DELETE action");
+	}
+	const char *on_update_action =
+		tuple_field_str_xc(tuple, BOX_FK_CONSTRAINT_FIELD_ON_UPDATE,
+				   &name_len);
+	fk_def->on_update = STRN2ENUM(fkey_action, on_update_action, name_len);
+	if (fk_def->on_update == fkey_action_MAX) {
+		tnt_raise(ClientError, errcode, fk_def->name,
+			  "unknown ON UPDATE action");
+	}
+	def_guard.is_active = false;
+	return fk_def;
+}
+
+/**
+ * Remove FK constraint from child's and parent's lists and
+ * return it. Entries in child list are supposed to be
+ * unique by their name.
+ *
+ * @param list List of child FK constraints.
+ * @param fkey_name Name of constraint to be removed.
+ * @retval FK being removed.
+ */
+static struct fkey *
+fkey_grab_by_name(struct rlist *list, const char *fkey_name)
+{
+	struct fkey *fk;
+	rlist_foreach_entry(fk, list, child_link) {
+		if (strcmp(fkey_name, fk->def->name) == 0) {
+			rlist_del_entry(fk, child_link);
+			rlist_del_entry(fk, parent_link);
+			return fk;
+		}
+	}
+	unreachable();
+	return NULL;
+}
+
+/**
+ * On rollback of creation we remove FK constraint from DD, i.e.
+ * from parent's and child's lists of constraints and
+ * release memory.
+ */
+static void
+on_create_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	rlist_del_entry(fk, parent_link);
+	rlist_del_entry(fk, child_link);
+	fkey_delete(fk);
+}
+
+/** Return old FK and release memory for the new one. */
+static void
+on_replace_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk->def->parent_id);
+	struct space *child = space_by_id(fk->def->child_id);
+	struct fkey *old_fkey = fkey_grab_by_name(&child->child_fkey,
+						  fk->def->name);
+	fkey_delete(old_fkey);
+	rlist_add_entry(&child->child_fkey, fk, child_link);
+	rlist_add_entry(&parent->parent_fkey, fk, parent_link);
+}
+
+/** On rollback of drop simply return back FK to DD. */
+static void
+on_drop_fkey_rollback(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk_to_restore = (struct fkey *)trigger->data;
+	struct space *parent = space_by_id(fk_to_restore->def->parent_id);
+	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);
+}
+
+/**
+ * On commit of drop or replace we have already deleted old
+ * foreign key entry from both (parent's and child's) lists,
+ * so just release memory.
+ */
+static void
+on_drop_or_replace_fkey_commit(struct trigger *trigger, void *event)
+{
+	(void) event;
+	struct fkey *fk = (struct fkey *)trigger->data;
+	fkey_delete(fk);
+}
+
+/**
+ * ANSI SQL doesn't allow list of referenced fields to contain
+ * duplicates. Firstly, we try to follow the easiest way:
+ * if all referenced fields numbers are less than 63, we can
+ * use bit mask. Otherwise, fall through slow check where we
+ * use O(field_cont^2) simple nested cycle iterations.
+ */
+static void
+fkey_links_check_duplicates(struct fkey_def *fk_def)
+{
+	uint64_t field_mask = 0;
+	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+		uint32_t parent_field = fk_def->links[i].parent_field;
+		if (parent_field > 63)
+			goto slow_check;
+		if (field_mask & (((uint64_t) 1) << parent_field))
+			goto error;
+		column_mask_set_fieldno(&field_mask, parent_field);
+	}
+	return;
+slow_check:
+	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+		uint32_t parent_field = fk_def->links[i].parent_field;
+		for (uint32_t j = i + 1; j < fk_def->field_count; ++j) {
+			if (parent_field == fk_def->links[j].parent_field)
+				goto error;
+		}
+	}
+	return;
+error:
+	tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT, fk_def->name,
+		  "referenced fields can not contain duplicates");
+}
+
+/** A trigger invoked on replace in the _fk_constraint space. */
+static void
+on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
+{
+	struct txn *txn = (struct txn *) event;
+	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct tuple *old_tuple = stmt->old_tuple;
+	struct tuple *new_tuple = stmt->new_tuple;
+	if (new_tuple != NULL) {
+		/* Create or replace foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(new_tuple,
+						ER_CREATE_FK_CONSTRAINT);
+		auto fkey_def_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		if (child_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space can't be VIEW");
+		}
+		struct space *parent_space =
+			space_cache_find_xc(fk_def->parent_id);
+		if (parent_space->def->opts.is_view) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referenced space can't be VIEW");
+		}
+		/*
+		 * FIXME: until SQL triggers are completely
+		 * integrated into server (i.e. we are able to
+		 * invoke triggers even if DML occurred via Lua
+		 * interface), it makes no sense to provide any
+		 * checks on existing data in space.
+		 */
+		struct index *pk = space_index(child_space, 0);
+		if (index_size(pk) > 0) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name,
+				  "referencing space must be empty");
+		}
+		/* Check types of referenced fields. */
+		for (uint32_t i = 0; i < fk_def->field_count; ++i) {
+			uint32_t child_fieldno = fk_def->links[i].child_field;
+			uint32_t parent_fieldno = fk_def->links[i].parent_field;
+			if (child_fieldno >= child_space->def->field_count ||
+			    parent_fieldno >= parent_space->def->field_count) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "foreign key refers to "
+						        "nonexistent field");
+			}
+			struct field_def *child_field =
+				&child_space->def->fields[child_fieldno];
+			struct field_def *parent_field =
+				&parent_space->def->fields[parent_fieldno];
+			if (! field_type1_contains_type2(parent_field->type,
+							 child_field->type)) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name, "field type mismatch");
+			}
+			if (child_field->coll_id != parent_field->coll_id) {
+				tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+					  fk_def->name,
+					  "field collation mismatch");
+			}
+		}
+		fkey_links_check_duplicates(fk_def);
+		/*
+		 * Search for suitable index in parent space:
+		 * it must be unique and consist exactly from
+		 * referenced columns (but order may be
+		 * different).
+		 */
+		struct index *fk_index = NULL;
+		for (uint32_t i = 0; i < parent_space->index_count; ++i) {
+			struct index *idx = space_index(parent_space, i);
+			if (!idx->def->opts.is_unique)
+				continue;
+			if (idx->def->key_def->part_count !=
+			    fk_def->field_count)
+				continue;
+			uint32_t j;
+			for (j = 0; j < fk_def->field_count; ++j) {
+				if (key_def_find(idx->def->key_def,
+						 fk_def->links[j].parent_field)
+				    == NULL)
+					break;
+			}
+			if (j != fk_def->field_count)
+				continue;
+			fk_index = idx;
+			break;
+		}
+		if (fk_index == NULL) {
+			tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT,
+				  fk_def->name, "referenced fields don't "
+						"compose unique index");
+		}
+		struct fkey *fkey = (struct fkey *) malloc(sizeof(*fkey));
+		if (fkey == NULL)
+			tnt_raise(OutOfMemory, sizeof(*fkey), "malloc", "fkey");
+		auto fkey_guard = make_scoped_guard([=] { free(fkey); });
+		memset(fkey, 0, sizeof(*fkey));
+		fkey->def = fk_def;
+		fkey->index_id = fk_index->def->iid;
+		if (old_tuple == NULL) {
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_create_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+		} else {
+			struct fkey *old_fk =
+				fkey_grab_by_name(&child_space->child_fkey,
+						  fk_def->name);
+			rlist_add_entry(&child_space->child_fkey, fkey,
+					child_link);
+			rlist_add_entry(&parent_space->parent_fkey, fkey,
+					parent_link);
+			struct trigger *on_rollback =
+				txn_alter_trigger_new(on_replace_fkey_rollback,
+						      fkey);
+			txn_on_rollback(txn, on_rollback);
+			struct trigger *on_commit =
+				txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+						      old_fk);
+			txn_on_commit(txn, on_commit);
+		}
+		fkey_def_guard.is_active = false;
+		fkey_guard.is_active = false;
+	} else if (new_tuple == NULL && old_tuple != NULL) {
+		/* Drop foreign key. */
+		struct fkey_def *fk_def =
+			fkey_def_new_from_tuple(old_tuple,
+						ER_DROP_FK_CONSTRAINT);
+		auto fkey_guard = make_scoped_guard([=] { free(fk_def); });
+		struct space *child_space =
+			space_cache_find_xc(fk_def->child_id);
+		struct fkey *old_fkey =
+			fkey_grab_by_name(&child_space->child_fkey,
+					  fk_def->name);
+		struct trigger *on_commit =
+			txn_alter_trigger_new(on_drop_or_replace_fkey_commit,
+					      old_fkey);
+		txn_on_commit(txn, on_commit);
+		struct trigger *on_rollback =
+			txn_alter_trigger_new(on_drop_fkey_rollback, old_fkey);
+		txn_on_rollback(txn, on_rollback);
+	}
+}
+
 struct trigger alter_space_on_replace_space = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL
 };
@@ -3523,4 +3968,8 @@ struct trigger on_replace_trigger = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_trigger, NULL, NULL
 };
 
+struct trigger on_replace_fk_constraint = {
+	RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL
+};
+
 /* vim: set foldmethod=marker */
diff --git a/src/box/alter.h b/src/box/alter.h
index 8ea29c77b..4108fa47c 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -45,6 +45,7 @@ extern struct trigger on_replace_sequence;
 extern struct trigger on_replace_sequence_data;
 extern struct trigger on_replace_space_sequence;
 extern struct trigger on_replace_trigger;
+extern struct trigger on_replace_fk_constraint;
 extern struct trigger on_stmt_begin_space;
 extern struct trigger on_stmt_begin_index;
 extern struct trigger on_stmt_begin_truncate;
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index a8a00ec29e7106afbd06294cf6ffe291f90a2e10..10f77f641b6308209ba01e6449bb22cc35963a9f 100644
GIT binary patch
delta 1814
zcmV+x2kH2z4Vn&+8Gko6FfC^}WiT^1V=`hg3Q2BrbYX5|WjY{XGdMLeGBIK;GG%39
zEi_?cGc95_Ff=VNHDzUCVPj%qVPP~1RzqxWV{1AfdwmKD)w&D1%?6(U&U2<Mm8Adx
z0000ewJ-euP_;Aw`bUTlNzfRn003Wngcn>16PR8tG{9G2A%B*nvW-`ySL`6KHf6VV
z%amlLB-W*Se@Rl?3cz(5$a~6rR6)dCAE{j~8m?KWsS!9-rj!EW0N4QR0HXn2ouqhO
zG~UIq23{P3HZj)s>z}avUb8aSBks#hfP{3I`vt)e+adZQpsSOl@}TO1;HWLnL_dP}
zRmSwCk+CmAdw<u}HI~l1bK8rtUIUUDnXvA9S9x9E!rb1d3oTOOjRJIalCgbVd5gDJ
zXX)Ra<Bnu(hbn-sPSOaI94TDT=zk&3dc08~@1^VNBqyyp%zXdrmZYnb+(L`KY+=`B
zqg|aO4uNx8;goPzs?;T^N;fEy3H6;wClL6ORH~d*DSwq?!jNTu=Oc)3GS7w1X~hL-
z1UI4uhULmm6##Vt1^r?@?rw9m2SQgTi9^C-<=rig@;vD3BuAOWQJt4p-eSr@S10+b
z2lFd$F=sn|BC3$+KkWK$*6o$IXiJJ}Fx$^Z6N9c!vem~s+s{W^(vnLOtjE>M+Gx#!
zu1<37&wrC(J+5p?X%q-~3HBAsOcR3rr?a)u83apSc8JOx==ZZvC<Oyuous}GhM@|?
zv2Wpv%f4^tCG6_#qU~pW*ZC%X=d~sJN1x|5M`H^7e17p5zrE~@!VtK7-}1V4xU#V>
zxGK9ILu^0r^NL7kmVTR($5-UmMgu%4s16F9oqv>0>O{(f%vMn)CW=H?C)vr6phlQP
zm{KNGDMSfGDW~RCgA-J9Lt~;Tb#;<KG#@-0nMa!OAe-qR!$GD-hNi`Di^7H&4KW#F
zFu+`Zu>j-MI+~%fpeb}zVI)K83x%#u@+D@<f~frHll5pj^1pj+y3isaZ)QSQCrJc`
zcz@`A;%If4>ks~Cb(m}6eqH6>zxvPeOr(EbG53MFg_m@l@9tcO*cMsYC-jrh)k*FP
zUM#vUw|$_d`MjnHor1u0UW0v^2C6#E)Nkh&^x<}h0vUbZ?|%I(%d-<AC=^G8u1-=G
zr+ZZ@eD8lwiF7K3P?2!(J(2GJ4M}CXxqrz);am(^sLa#36^2l&(+R0PK~g|WKWcn<
zJ!E)Lm>pf6WPPDUWz|7-m^;?v3RmgOL*>}AWs4PxqpOn~&FA}HS&z6IU7aL=I_b<q
z+1M|RyrdOs;0%htiD*aT!Eo1b234bIqjlz$X6WqtZb_+(u1>N9Yt<5C9Nc-q0)Ka?
z;p2Z|GI~jT@$;|NON!~XI7z%MoIA3gkG0N!WR-Nzyi$y=PI4y)3>pnrI@|2$qm2zL
zR7Ih5=Fz~Q-;1v&fDB0x7cw9`INAaSi=GvfM@4~PsPRx_5E_^P0003{0PzI}Dd&m?
z5`eHkilZ=&ffxuw5R3v7XMg|{gntAYpjt4r-^d`k=SQL>dmg;ae~0m7<UBfHoSn_K
zW}>9xd#qxM5Nd0_7-fOLf-Hg|p+A-mn55wKvv;8EU;I(IcxrT)6(wzmc{T;w?qGdP
zTaN0>nGc)5E7^iUAvGRX<z<nP8QJ3izfE%3nmK&QVXZ0lU%+mxCy?xH=zm0L;DbzK
zBdUr6k|?W07-Xp$x~_fNssjNe_k>Nnjt#DUWdUCxdJ1$^m;2T_PG&hZj}9ZMcJ0s+
z$6(bU-x!;Xoq7mcLzq=5m17&AOY6yY7zyl{10)9wC&0O|Bsrn2+@PwWly^gmxMmNM
zs3B#;9|#+_><z<yiEs}n&40miuWb44r&<cqrb!DT`c-HIcpuL`H;nohiGoY;;nU9W
zV;%@VnAP%2cs#=GmV4zDDJz{(vo>RCT+@$wC=lv6OKH}l%!TV(@N}#u6<JC@$HH^M
zp;+DKXuIt12KGYIthD(ob9r2vkYYpJ*}-RH$GW4N=W;MX!aws;MSmw_eEgXk`N1SY
z&qJ4AlRq^uNg{TN@P#i@A5&R(9n>*Gp$x8Oj4Lg+G29w6yJ1tg<_I|l6c&ky|77<U
zY$1I9Lds&0&|UhJwv}mR4IL#~J}put_$baD;OJFrvqO!6zFFFnZZlw<>EbPAaK))4
z7C?-~nY69poEiC?9)EiVG<UY#L2u5Gv%PRU$zpI8LxBPFia(fw&lmMK%EE&bwsSjS
zrD$U0)2b+krqr2iqm5q4X)gn}$*Wh_|6fkXM-Hz@{-l?15*4WSCy9Tp-j(_zU32|m
zA031OL(7uk=2!kyUv+%KuxwUl2W4GN9eS#ye{j~d<WR!`t`8&9@KvOqBn43p)ex=i
E3amY33;+NC

delta 1699
zcmV;U23+}?4yX-~8Gki5FfC^@G-fw8H#rJPZgX^DZewLSAYx=OVKg#1V=Xo^Ff=VR
zH#adYIA$<6Eiz^~VK^`|F)=qdI0{xnY;R+0Iv{&}3JTS_3%bn)FaXY~@A2EE00000
z04TLD{QyusF#sw<untMk7&id`Up&AM2ap_8K2ct%)hPuc5r2u%1Dtdthk@FZnGtQL
zq{+<`lO*YB%N4E_fREI4wd=GkVz`4fdcG}`x2rRP>+&h36w?690OJ7SjC}R|`e!^<
zufxoBSySaEK}34Y9gE@!en$00z_rw*@?r{$VyQo#kG>SK>wIY(Gha_6_ARVsq0YPW
z*^{p>3xXA)K!3gUtrNGN1@ezpT`H224JN>~)O>B+%A2IIdZ>SQUNe%x&&UF}mYPMF
z<VfKnR$n!OS(mg*w7uMGsY$EVW9Iu^v?SM3GpQn}@&|-lvwAHxzsossplA9N4p%a9
zo>%g6a!e(V!@(sGEAUSy(+NYC{hgO0NP}%ImC`FNL4Q!RY*}%n3YHx!04xm(FRr{v
zl!I%j`QeM%l{Z=Rb?}Fz#-snSYrI36D{t~UtJh#?KQB85*HV+!OFXonm)}`Amn5Jr
zSFUeXZ5CWh&9XmF0_t-0owY^*(UxFi^2{+JpjYYq%_<FoC9cn?&K&4i^*yN+3|vc1
zjb9wclz#}mo&{p?>>GD}0%4t7vHjt9ooVEEE?c6%^m+ajRha@mpI!XLX|9h}V+i2A
z@rhe|T+Ko(uFkH@5&Tr*=hczUq5heY2QkUNS%n#JEj3Bpit$d4G7*DgL3m(5TGT2F
z94mTMRGt((D5}nmjt=Ss$^@zeY-T+Y)EjXvHGeM*i6^2%bzXPkLU15BlTD5dj*v}P
z!_|DamYPB}H(Lp|d7$Y4qv0@PGgC9OfyFV4f(DrkG8kko#8`;gVrZ;|sZe1;Q!301
z424=>WUi&=%LED&qVlCr*5%B||L!vCQjv%@8VT1@b7K*MWDE0|O4Ko**D)efkeJS8
zK!2(n6H`6bb=>(U`tZ-F0)bI|$NTk%AJ0yXpi~?YuB9fdV^Ej7e?_$i!nM?_Aqm60
z`&X+x53Z$V3HxeQC!Tlex*X2Ks1;7>#I3wicRHC$CJrY=2}I$TaHmRP$WmdOj*tpL
zDiEFf!}j<<_2_um?AUb7<ft(?uBE1ZseeePy;1d;JE+T5&DNpKvyqgPbfwm~mYOj1
z`F>Z{WsSzQ)C6E@9ooDZ`^l1@wSo*Vi{$U4dSkpeZW)-xR7uXN9oje;I=58#to6mU
z)GQIMSmcX~J1<y(9cuXapO=izkuQGU#d1mMFpHN+%mRyy?B}Jd^Do1*(xHv(xqp_L
zJ2_y)>bTPRW<M`)Zh*#Alv;<j8W#azNCJUKsNq;-5DClx0Du4}0PzI}Dd&m?5`eHc
zio-CDff$BE0E|KtM?fGb2njSmwO}-3>|otvVxbn$nEy|s!{{(Nj1CZ|XWOmW^1O_Q
z#C9R1B6~4P0)Yit2t#sze0O3}a)0*OYw7YYtf)91`JUH`Pa9%0YaYejkXxhcI3*z3
zdE&uuxjDLwcwchr;ms`(kQeo&CcC+u2o3ol@WqTwal~b%ln5QO6oL_ZE4HeMEh(My
z_v_fe^><ak7er6BFLY%562{3Rr^e{WMg>zAXfXbi)iZOl46U*mTj66G#((TQBk0n5
z@)<oY4g-ONQth~(3rms{8<`ta`P_f=wD_@mkOU1mHT1wuY%u<m)iZOl3{AjWf3|$~
zt8m|QXkeD0hZ@Msd-hKiZ0KL;Q@khOrOoi`UOgQ>poLS)q+!uN;jpegcC;P#`N7`U
znw9D$>kp61-&FJnXm;?~*ng4k=;HZ8FhRgSHd00X7<B%WiF{!a-SZ&o*D9aNO_EGI
zB{7vRtd9oyxDE<2a=|>f+87shpvG{^Ol^j3&MJS%IeJ)8Cti{*Ub%&E>I-oWh=lIa
zrQD!Ql+Ehs<+5HPA%914?C?i#t&Krytl`a4o=ls7UrZNkEfZHnq<_5tDm2be+j_v6
zk)6|HU_kR{(H-|;3^`j1=aVc3C;Vs%FuC|kE%^FTe<LpZkFbK<maIKZW_(1&;Gu~+
zlWeroE4jI4;5K>1vid)s6X20|EK)(~T|9{xS1W;}zgF#f{gJ7;9%W`;4+U&31%|V|
tLZAw@V?4uhT=^Z8O&m3K=&6$aL0H$8dkzbvjtIq9$$FA7L^;(Et?i$*Eo}e*

diff --git a/src/box/errcode.h b/src/box/errcode.h
index b61b387f2..213a1864b 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -219,6 +219,8 @@ struct errcode_record {
 	/*164 */_(ER_NO_SUCH_GROUP,		"Replication group '%s' does not exist") \
 	/*165 */_(ER_NO_SUCH_MODULE,		"Module '%s' does not exist") \
 	/*166 */_(ER_NO_SUCH_COLLATION,		"Collation '%s' does not exist") \
+	/*167 */_(ER_CREATE_FK_CONSTRAINT,	"Failed to create foreign key constraint '%s': %s") \
+	/*168 */_(ER_DROP_FK_CONSTRAINT,	"Failed to drop foreign key constraint '%s': %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/fkey.c b/src/box/fkey.c
new file mode 100644
index 000000000..0bdccb521
--- /dev/null
+++ b/src/box/fkey.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "fkey.h"
+#include "sql.h"
+#include "sql/sqliteInt.h"
+
+const char *fkey_action_strs[] = {
+	/* [FKEY_ACTION_RESTRICT]    = */ "no_action",
+	/* [FKEY_ACTION_SET_NULL]    = */ "set_null",
+	/* [FKEY_ACTION_SET_DEFAULT] = */ "set_default",
+	/* [FKEY_ACTION_CASCADE]     = */ "cascade",
+	/* [FKEY_ACTION_NO_ACTION]   = */ "restrict"
+};
+
+const char *fkey_match_strs[] = {
+	/* [FKEY_MATCH_SIMPLE]  = */ "simple",
+	/* [FKEY_MATCH_PARTIAL] = */ "partial",
+	/* [FKEY_MATCH_FULL]    = */ "full"
+};
+
+void
+fkey_delete(struct fkey *fkey)
+{
+	sql_trigger_delete(sql_get(), fkey->on_delete_trigger);
+	sql_trigger_delete(sql_get(), fkey->on_update_trigger);
+	free(fkey->def);
+	free(fkey);
+}
diff --git a/src/box/fkey.h b/src/box/fkey.h
new file mode 100644
index 000000000..ed99617ca
--- /dev/null
+++ b/src/box/fkey.h
@@ -0,0 +1,138 @@
+#ifndef TARANTOOL_BOX_FKEY_H_INCLUDED
+#define TARANTOOL_BOX_FKEY_H_INCLUDED
+/*
+ * Copyright 2010-2018, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "space.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct sqlite3;
+
+enum fkey_action {
+	FKEY_NO_ACTION = 0,
+	FKEY_ACTION_SET_NULL,
+	FKEY_ACTION_SET_DEFAULT,
+	FKEY_ACTION_CASCADE,
+	FKEY_ACTION_RESTRICT,
+	fkey_action_MAX
+};
+
+enum fkey_match {
+	FKEY_MATCH_SIMPLE = 0,
+	FKEY_MATCH_PARTIAL,
+	FKEY_MATCH_FULL,
+	fkey_match_MAX
+};
+
+extern const char *fkey_action_strs[];
+
+extern const char *fkey_match_strs[];
+
+/** Structure describing field dependencies for foreign keys. */
+struct field_link {
+	uint32_t parent_field;
+	uint32_t child_field;
+};
+
+/** Definition of foreign key constraint. */
+struct fkey_def {
+	/** Id of space containing the REFERENCES clause (child). */
+	uint32_t child_id;
+	/** Id of space that the key points to (parent). */
+	uint32_t parent_id;
+	/** Number of fields in this key. */
+	uint32_t field_count;
+	/** True if constraint checking is deferred till COMMIT. */
+	bool is_deferred;
+	/** Match condition for foreign key. SIMPLE by default. */
+	enum fkey_match match;
+	/** ON DELETE action. NO ACTION by default. */
+	enum fkey_action on_delete;
+	/** ON UPDATE action. NO ACTION by default. */
+	enum fkey_action on_update;
+	/** Mapping of fields in child to fields in parent. */
+	struct field_link *links;
+	/** Name of the constraint. */
+	char name[0];
+};
+
+/** Structure representing foreign key relationship. */
+struct fkey {
+	struct fkey_def *def;
+	/** Index id of referenced index in parent space. */
+	uint32_t index_id;
+	/** Triggers for actions. */
+	struct sql_trigger *on_delete_trigger;
+	struct sql_trigger *on_update_trigger;
+	/** Links for parent and child lists. */
+	struct rlist parent_link;
+	struct rlist child_link;
+};
+
+/**
+ * Alongside with struct fkey_def itself, we reserve memory for
+ * string containing its name and for array of links.
+ * Memory layout:
+ * +-------------------------+ <- Allocated memory starts here
+ * |     struct fkey_def     |
+ * |-------------------------|
+ * |        name + \0        |
+ * |-------------------------|
+ * |          links          |
+ * +-------------------------+
+ */
+static inline size_t
+fkey_def_sizeof(uint32_t links_count, uint32_t name_len)
+{
+	return sizeof(struct fkey) + links_count * sizeof(struct field_link) +
+	       name_len + 1;
+}
+
+static inline bool
+fkey_is_self_referenced(const struct fkey_def *fkey)
+{
+	return fkey->child_id == fkey->parent_id;
+}
+
+/** Release memory for foreign key and its triggers, if any. */
+void
+fkey_delete(struct fkey *fkey);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* TARANTOOL_BOX_FKEY_H_INCLUDED */
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index d14dd748b..b73d9ab78 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -508,6 +508,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _vindex = box.space[box.schema.VINDEX_ID]
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
+    local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
     if sequence_tuple ~= nil and sequence_tuple[3] == true then
         -- Delete automatically generated sequence.
@@ -521,6 +522,9 @@ box.schema.space.drop = function(space_id, space_name, opts)
         local v = keys[i]
         _index:delete{v[1], v[2]}
     end
+    for _, t in _fk_constraint.index.child_id:pairs({space_id}) do
+        _fk_constraint:delete({t.name, space_id})
+    end
     revoke_object_privs('space', space_id)
     _truncate:delete{space_id}
     if _space:delete{space_id} == nil then
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index ca3fefc0d..d07560d6c 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -551,6 +551,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "SQL_STAT1_ID");
 	lua_pushnumber(L, BOX_SQL_STAT4_ID);
 	lua_setfield(L, -2, "SQL_STAT4_ID");
+	lua_pushnumber(L, BOX_FK_CONSTRAINT_ID);
+	lua_setfield(L, -2, "FK_CONSTRAINT_ID");
 	lua_pushnumber(L, BOX_TRUNCATE_ID);
 	lua_setfield(L, -2, "TRUNCATE_ID");
 	lua_pushnumber(L, BOX_SEQUENCE_ID);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index f112a93ae..8a30e9f7d 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -509,6 +509,27 @@ local function upgrade_to_2_1_0()
                   {unique = true}, {{0, 'string'}, {1, 'string'},
                                     {5, 'scalar'}}}
 
+    local fk_constr_ft = {{name='name', type='string'},
+                          {name='child_id', type='unsigned'},
+                          {name='parent_id', type='unsigned'},
+                          {name='is_deferred', type='boolean'},
+                          {name='match', type='string'},
+                          {name='on_delete', type='string'},
+                          {name='on_update', type='string'},
+                          {name='child_cols', type='array'},
+                          {name='parent_cols', type='array'}}
+    log.info("create space _fk_constraint")
+    _space:insert{box.schema.FK_CONSTRAINT_ID, ADMIN, '_fk_constraint', 'memtx',
+                  0, setmap({}), fk_constr_ft}
+
+    log.info("create index primary on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 0, 'primary', 'tree',
+                  {unique = true}, {{0, 'string'}, {1, 'unsigned'}}}
+
+    log.info("create secondary index child_id on _fk_constraint")
+    _index:insert{box.schema.FK_CONSTRAINT_ID, 1, 'child_id', 'tree',
+                  {unique = false}, {{1, 'unsigned'}}}
+
     -- Nullability wasn't skipable. This was fixed in 1-7.
     -- Now, abscent field means NULL, so we can safely set second
     -- field in format, marking it nullable.
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 86c56ee2e..faad53700 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -412,6 +412,22 @@ schema_init()
 			 COLL_NONE, SORT_ORDER_ASC);
 	/* _sql_stat4 - extensive statistics on space, seen in SQL. */
 	sc_space_new(BOX_SQL_STAT4_ID, "_sql_stat4", key_def, NULL, NULL);
+
+	key_def_delete(key_def);
+	key_def = key_def_new(2);
+	if (key_def == NULL)
+		diag_raise();
+	/* Constraint name. */
+	key_def_set_part(key_def, 0, BOX_FK_CONSTRAINT_FIELD_NAME,
+			 FIELD_TYPE_STRING, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* Child space. */
+	key_def_set_part(key_def, 1, BOX_FK_CONSTRAINT_FIELD_CHILD_ID,
+			 FIELD_TYPE_UNSIGNED, ON_CONFLICT_ACTION_ABORT, NULL,
+			 COLL_NONE, SORT_ORDER_ASC);
+	/* _fk_сonstraint - foreign keys constraints. */
+	sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_def,
+		     &on_replace_fk_constraint, NULL);
 }
 
 void
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 5ab4bb002..fd57d22b9 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -107,6 +107,8 @@ enum {
 	/** Space ids for SQL statictics. */
 	BOX_SQL_STAT1_ID = 348,
 	BOX_SQL_STAT4_ID = 349,
+	/** Space id of _fk_constraint. */
+	BOX_FK_CONSTRAINT_ID = 356,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -224,6 +226,19 @@ enum {
 	BOX_TRIGGER_FIELD_OPTS = 2,
 };
 
+/** _fk_constraint fields. */
+enum {
+	BOX_FK_CONSTRAINT_FIELD_NAME = 0,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_ID = 1,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_ID = 2,
+	BOX_FK_CONSTRAINT_FIELD_DEFERRED = 3,
+	BOX_FK_CONSTRAINT_FIELD_MATCH = 4,
+	BOX_FK_CONSTRAINT_FIELD_ON_DELETE = 5,
+	BOX_FK_CONSTRAINT_FIELD_ON_UPDATE = 6,
+	BOX_FK_CONSTRAINT_FIELD_CHILD_COLS = 7,
+	BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/space.c b/src/box/space.c
index e53f1598c..90a80ed7b 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -163,6 +163,8 @@ space_create(struct space *space, struct engine *engine,
 		space->index_map[index_def->iid] = index;
 	}
 	space_fill_index_map(space);
+	rlist_create(&space->parent_fkey);
+	rlist_create(&space->child_fkey);
 	return 0;
 
 fail_free_indexes:
@@ -220,6 +222,8 @@ space_delete(struct space *space)
 	 * on_replace_dd_trigger on deletion from _trigger.
 	 */
 	assert(space->sql_triggers == NULL);
+	assert(rlist_empty(&space->parent_fkey));
+	assert(rlist_empty(&space->child_fkey));
 	space->vtab->destroy(space);
 }
 
diff --git a/src/box/space.h b/src/box/space.h
index 01a4af726..d60ba6c56 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -183,6 +183,15 @@ struct space {
 	 * of index id.
 	 */
 	struct index **index;
+	/**
+	 * Lists of foreign key constraints. In SQL terms child
+	 * space is the "from" table i.e. the table that contains
+	 * the REFERENCES clause. Parent space is "to" table, in
+	 * other words the table that is named in the REFERENCES
+	 * clause.
+	 */
+	struct rlist parent_fkey;
+	struct rlist child_fkey;
 };
 
 /** Initialize a base space instance. */
diff --git a/src/box/sql.c b/src/box/sql.c
index d48c3cfe5..9795ad2ac 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1251,6 +1251,14 @@ void tarantoolSqlite3LoadSchema(struct init_data *init)
 			       "\"sample\","
 			       "PRIMARY KEY(\"tbl\", \"idx\", \"sample\"))");
 
+	sql_init_callback(init, TARANTOOL_SYS_FK_CONSTRAINT_NAME,
+			  BOX_FK_CONSTRAINT_ID, 0,
+			  "CREATE TABLE \""TARANTOOL_SYS_FK_CONSTRAINT_NAME
+			  "\"(\"name\" TEXT, \"parent_id\" INT, \"child_id\" INT,"
+			  "\"deferred\" INT, \"match\" TEXT, \"on_delete\" TEXT,"
+			  "\"on_update\" TEXT, \"child_cols\", \"parent_cols\","
+			  "PRIMARY KEY(\"name\", \"child_id\"))");
+
 	/* Read _space */
 	if (space_foreach(space_foreach_put_cb, init) != 0) {
 		init->rc = SQL_TARANTOOL_ERROR;
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 222405a1f..c941b6e58 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -35,6 +35,7 @@
  */
 #include "coll.h"
 #include "sqliteInt.h"
+#include "box/fkey.h"
 #include "box/schema.h"
 #include "box/session.h"
 #include "tarantoolInt.h"
@@ -707,31 +708,6 @@ sqlite3FkReferences(Table * pTab)
 					pTab->def->name);
 }
 
-/**
- * The second argument is a Trigger structure allocated by the
- * fkActionTrigger() routine. This function deletes the sql_trigger
- * structure and all of its sub-components.
- *
- * The Trigger structure or any of its sub-components may be
- * allocated from the lookaside buffer belonging to database
- * handle dbMem.
- *
- * @param db Database connection.
- * @param trigger AST object.
- */
-static void
-sql_fk_trigger_delete(struct sqlite3 *db, struct sql_trigger *trigger)
-{
-	if (trigger == NULL)
-		return;
-	struct TriggerStep *trigger_step = trigger->step_list;
-	sql_expr_delete(db, trigger_step->pWhere, false);
-	sql_expr_list_delete(db, trigger_step->pExprList);
-	sql_select_delete(db, trigger_step->pSelect);
-	sql_expr_delete(db, trigger->pWhen, false);
-	sqlite3DbFree(db, trigger);
-}
-
 /*
  * The second argument points to an FKey object representing a foreign key
  * for which pTab is the child table. An UPDATE statement against pTab
@@ -1275,20 +1251,14 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 						   pWhere, 0, 0, 0, 0, 0, 0);
 			pWhere = 0;
 		}
-
-		/* Disable lookaside memory allocation */
-		db->lookaside.bDisable++;
-
-		size_t trigger_size = sizeof(struct sql_trigger) +
-				      sizeof(TriggerStep) + nFrom + 1;
-		trigger =
-			(struct sql_trigger *)sqlite3DbMallocZero(db,
-								  trigger_size);
+		trigger = (struct sql_trigger *)sqlite3DbMallocZero(db,
+								    sizeof(*trigger));
 		if (trigger != NULL) {
-			pStep = trigger->step_list = (TriggerStep *)&trigger[1];
+			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((char *)pStep->zTarget, zFrom, nFrom);
-
+			memcpy(pStep->zTarget, zFrom, nFrom);
 			pStep->pWhere =
 			    sqlite3ExprDup(db, pWhere, EXPRDUP_REDUCE);
 			pStep->pExprList =
@@ -1302,15 +1272,12 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 			}
 		}
 
-		/* Re-enable the lookaside buffer, if it was disabled earlier. */
-		db->lookaside.bDisable--;
-
 		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_fk_trigger_delete(db, trigger);
+			sql_trigger_delete(db, trigger);
 			return 0;
 		}
 		assert(pStep != 0);
@@ -1408,8 +1375,8 @@ sqlite3FkDelete(sqlite3 * db, Table * pTab)
 		assert(pFKey->isDeferred == 0 || pFKey->isDeferred == 1);
 
 		/* Delete any triggers created to implement actions for this FK. */
-		sql_fk_trigger_delete(db, pFKey->apTrigger[0]);
-		sql_fk_trigger_delete(db, pFKey->apTrigger[1]);
+		sql_trigger_delete(db, pFKey->apTrigger[0]);
+		sql_trigger_delete(db, pFKey->apTrigger[1]);
 
 		pNext = pFKey->pNextFrom;
 		sqlite3DbFree(db, pFKey);
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index e1430a398..bc61e8426 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -20,6 +20,7 @@
 #define TARANTOOL_SYS_TRUNCATE_NAME "_truncate"
 #define TARANTOOL_SYS_SQL_STAT1_NAME "_sql_stat1"
 #define TARANTOOL_SYS_SQL_STAT4_NAME "_sql_stat4"
+#define TARANTOOL_SYS_FK_CONSTRAINT_NAME "_fk_constraint"
 
 /* Max space id seen so far. */
 #define TARANTOOL_SYS_SCHEMA_MAXID_KEY "max_id"
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 53af3e8bd..3ba33ee97 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -815,6 +815,11 @@ box.space._space:select()
   - [349, 1, '_sql_stat4', 'memtx', 0, {}, [{'name': 'tbl', 'type': 'string'}, {'name': 'idx',
         'type': 'string'}, {'name': 'neq', 'type': 'string'}, {'name': 'nlt', 'type': 'string'},
       {'name': 'ndlt', 'type': 'string'}, {'name': 'sample', 'type': 'scalar'}]]
+  - [356, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
+      {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
+      {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
+      {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'},
+      {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]]
 ...
 box.space._func:select()
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index ae042664a..77a24b425 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -230,11 +230,11 @@ box.session.su('guest')
 ...
 #box.space._vspace:select{}
 ---
-- 22
+- 23
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
@@ -262,7 +262,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 50
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index c41b52f48..0d50855d2 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -107,7 +107,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 350
+- 357
 ...
 space.field_count
 ---
@@ -152,7 +152,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '350' does not exist
+- error: Space '357' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -231,6 +231,8 @@ _index:select{}
   - [348, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string']]]
   - [349, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'string'], [
         5, 'scalar']]]
+  - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
+  - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/misc.result b/test/box/misc.result
index 892851823..a680f752e 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -491,6 +491,8 @@ t;
   164: box.error.NO_SUCH_GROUP
   165: box.error.NO_SUCH_MODULE
   166: box.error.NO_SUCH_COLLATION
+  167: box.error.CREATE_FK_CONSTRAINT
+  168: box.error.DROP_FK_CONSTRAINT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/engine/iterator.result b/test/engine/iterator.result
index 98b0b3e7d..6e03fbbbd 100644
--- a/test/engine/iterator.result
+++ b/test/engine/iterator.result
@@ -4211,9 +4211,15 @@ s:replace{35}
 ---
 - [35]
 ...
-state, value = gen(param,state)
+f = function() return gen(param, state) end
+---
+...
+_, errmsg = pcall(f)
+---
+...
+errmsg:match('usage: next%(param, state%)')
 ---
-- error: 'builtin/box/schema.lua:1051: usage: next(param, state)'
+- 'usage: next(param, state)'
 ...
 value
 ---
diff --git a/test/engine/iterator.test.lua b/test/engine/iterator.test.lua
index fcf753f46..9ff51ebe1 100644
--- a/test/engine/iterator.test.lua
+++ b/test/engine/iterator.test.lua
@@ -400,7 +400,9 @@ gen,param,state = i:pairs({35})
 state, value = gen(param,state)
 value
 s:replace{35}
-state, value = gen(param,state)
+f = function() return gen(param, state) end
+_, errmsg = pcall(f)
+errmsg:match('usage: next%(param, state%)')
 value
 
 s:drop()
diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result
new file mode 100644
index 000000000..c2ec429c3
--- /dev/null
+++ b/test/sql/foreign-keys.result
@@ -0,0 +1,336 @@
+env = require('test_run')
+---
+...
+test_run = env.new()
+---
+...
+test_run:cmd('restart server default with cleanup=1')
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+---
+...
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+---
+...
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+---
+...
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+---
+...
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: Space '666' does not exist
+...
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('T2')[1]['id']
+---
+...
+view_id = box.space._space.index.name:select('V1')[1]['id']
+---
+...
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced space can''t
+    be VIEW'
+...
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space can''t
+    be VIEW'
+...
+box.sql.execute("DROP VIEW v1;")
+---
+...
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown MATCH clause'
+...
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON DELETE action'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {0}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': unknown ON UPDATE action'
+...
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referencing space must
+    be empty'
+...
+box.sql.execute("DELETE FROM t2;")
+---
+...
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {}, {}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': at least one link must
+    be specified'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1,2}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': number of referenced and
+    referencing fields must be the same'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {13}, {1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': foreign key refers to
+    nonexistent field'
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {'crash'}, {'crash'}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': value of 0 link is not
+    unsigned'
+...
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 2}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields don''t
+    compose unique index'
+...
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': field type mismatch'
+...
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 1}}
+---
+...
+box.space._fk_constraint:insert(t)
+---
+- error: 'Failed to create foreign key constraint ''fk_1'': referenced fields can
+    not contain duplicates'
+...
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+---
+- error: 'Can''t modify space ''T1'': can not drop referenced index'
+...
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+---
+- error: 'Can''t modify space ''T1'': can not alter referenced index'
+...
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}
+---
+- [512, 1, 'I2', 'tree', {'unique': true}, [{'field': 1, 'type': 'unsigned', 'is_nullable': true}]]
+...
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+---
+...
+box.space._space:delete(child_id)
+---
+- error: 'Can''t drop space ''T2'': the space has foreign key constraints'
+...
+box.space.T2:drop()
+---
+...
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+---
+- []
+...
+box.space.T1:drop()
+---
+...
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+---
+...
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+---
+...
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+---
+...
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {0}, {0}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:count()
+---
+- 5
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+box.space._fk_constraint:delete{'fk_1', child_id}
+---
+- ['fk_1', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+box.space._fk_constraint:delete{'fk_2', child_id}
+---
+- ['fk_2', 515, 516, false, 'simple', 'restrict', 'restrict', [0], [1]]
+...
+box.space._fk_constraint:delete{'self_2', parent_id}
+---
+- ['self_2', 516, 516, false, 'simple', 'restrict', 'restrict', [1], [1]]
+...
+box.space._fk_constraint:delete{'self_1', child_id}
+---
+- ['self_1', 515, 515, false, 'simple', 'restrict', 'restrict', [0], [0]]
+...
+box.space._fk_constraint:delete{'fk_3', parent_id}
+---
+- ['fk_3', 516, 515, false, 'simple', 'restrict', 'restrict', [1], [0]]
+...
+box.space._fk_constraint:count()
+---
+- 0
+...
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:insert(t)
+---
+...
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+---
+- cascade
+...
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {0}, {1}}
+---
+...
+t = box.space._fk_constraint:replace(t)
+---
+...
+box.space._fk_constraint:select({'fk_1', child_id})[1]['is_deferred']
+---
+- true
+...
+box.space.CHILD:drop()
+---
+...
+box.space.PARENT:drop()
+---
+...
+-- 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
new file mode 100644
index 000000000..a7a242bc2
--- /dev/null
+++ b/test/sql/foreign-keys.test.lua
@@ -0,0 +1,154 @@
+env = require('test_run')
+test_run = env.new()
+test_run:cmd('restart server default with cleanup=1')
+
+
+-- Check that tuple inserted into _fk_constraint is FK constrains
+-- valid data.
+--
+box.sql.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a INT, b INT);")
+box.sql.execute("CREATE UNIQUE INDEX i1 ON t1(a);")
+box.sql.execute("CREATE TABLE t2 (a INT, b INT, id INT PRIMARY KEY);")
+box.sql.execute("CREATE VIEW v1 AS SELECT * FROM t1;")
+
+-- Parent and child spaces must exist.
+--
+t = {'fk_1', 666, 777, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+parent_id = box.space._space.index.name:select('T1')[1]['id']
+child_id = box.space._space.index.name:select('T2')[1]['id']
+view_id = box.space._space.index.name:select('V1')[1]['id']
+
+-- View can't reference another space or be referenced by another space.
+--
+t = {'fk_1', child_id, view_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', view_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DROP VIEW v1;")
+
+-- Match clause can be only one of: simple, partial, full.
+--
+t = {'fk_1', child_id, parent_id, false, 'wrong_match', 'restrict', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+-- On conflict actions can be only one of: set_null, set_default,
+-- restrict, cascade, no_action.
+t = {'fk_1', child_id, parent_id, false, 'simple', 'wrong_action', 'restrict', {0}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'wrong_action', {0}, {1}}
+box.space._fk_constraint:insert(t)
+
+-- Temporary restriction (until SQL triggers work from Lua):
+-- referencing space must be empty.
+--
+box.sql.execute("INSERT INTO t2 VALUES (1, 2, 3);")
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1}}
+box.space._fk_constraint:insert(t)
+box.sql.execute("DELETE FROM t2;")
+
+-- Links must be specififed correctly.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {}, {}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {2}, {1,2}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {13}, {1}}
+box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {'crash'}, {'crash'}}
+box.space._fk_constraint:insert(t)
+
+-- Referenced fields must compose unique index.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 2}}
+box.space._fk_constraint:insert(t)
+
+-- Referencing and referenced fields must feature compatible types.
+-- Temporary, in SQL all fields except for INTEGER PRIMARY KEY
+-- are scalar.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+box.space._fk_constraint:insert(t)
+
+-- Each referenced column must appear once.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0, 1}, {1, 1}}
+box.space._fk_constraint:insert(t)
+
+-- Successful creation.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+
+-- Implicitly referenced index can't be dropped,
+-- ergo - space can't be dropped until it is referenced.
+--
+box.sql.execute("DROP INDEX i1 on t1;")
+
+-- Referenced index can't be altered as well, if alter leads to
+-- rebuild of index (e.g. index still can be renamed).
+box.space._index:replace{512, 1, 'I1', 'tree', {unique = true}, {{field = 0, type = 'unsigned', is_nullable = true}}}
+box.space._index:replace{512, 1, 'I2', 'tree', {unique = true}, {{field = 1, type = 'unsigned', is_nullable = true}}}
+
+-- Finally, can't drop space until it has FK constraints,
+-- i.e. by manual removing tuple from _space.
+-- But drop() will delete constraints.
+--
+box.space.T2.index[0]:drop()
+box.space._space:delete(child_id)
+box.space.T2:drop()
+
+-- Make sure that constraint has been successfully dropped,
+-- so we can drop now and parent space.
+--
+box.space._fk_constraint:select()
+box.space.T1:drop()
+
+-- Create several constraints to make sure that they are held
+-- as linked lists correctly including self-referencing constraints.
+--
+box.sql.execute("CREATE TABLE child (id INT PRIMARY KEY, a INT);")
+box.sql.execute("CREATE TABLE parent (a INT, id INT PRIMARY KEY);")
+
+parent_id = box.space._space.index.name:select('PARENT')[1]['id']
+child_id = box.space._space.index.name:select('CHILD')[1]['id']
+
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_3', parent_id, child_id, false, 'simple', 'restrict', 'restrict', {1}, {0}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_1', child_id, child_id, false, 'simple', 'restrict', 'restrict', {0}, {0}}
+t = box.space._fk_constraint:insert(t)
+t = {'self_2', parent_id, parent_id, false, 'simple', 'restrict', 'restrict', {1}, {1}}
+t = box.space._fk_constraint:insert(t)
+
+box.space._fk_constraint:count()
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'fk_1', child_id}
+t = {'fk_2', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+box.space._fk_constraint:delete{'fk_2', child_id}
+box.space._fk_constraint:delete{'self_2', parent_id}
+box.space._fk_constraint:delete{'self_1', child_id}
+box.space._fk_constraint:delete{'fk_3', parent_id}
+box.space._fk_constraint:count()
+
+-- Replace is also OK.
+--
+t = {'fk_1', child_id, parent_id, false, 'simple', 'restrict', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:insert(t)
+t = {'fk_1', child_id, parent_id, false, 'simple', 'cascade', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:replace(t)
+box.space._fk_constraint:select({'fk_1', child_id})[1]['on_delete']
+t = {'fk_1', child_id, parent_id, true, 'simple', 'cascade', 'restrict', {0}, {1}}
+t = box.space._fk_constraint:replace(t)
+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')
-- 
2.15.1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-07-26 20:12       ` Vladislav Shpilevoy
@ 2018-08-01 20:54         ` n.pettik
  2018-08-02 22:15           ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-08-01 20:54 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


>> 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

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-07-26 20:12       ` Vladislav Shpilevoy
@ 2018-08-01 20:54         ` n.pettik
  2018-08-02 22:15           ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-08-01 20:54 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


> 1. sql-tap/fkey2.test.lua fails on this commit. Travis fails
> as well.

Does it really matter?:) Could you forgive me these fails and
make me avoid suffering? Anyway, they are completely fixed
in second patch, but in scope of current patch they can be only
partially repaired. Hence, it would look like I fix in the second patch
fixes made in first one..

> 2. I have pushed more review fixes in a separate commit (the
> test fails before it too). Please, squash if you are ok.

Ok, I’ve squashed them.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-08-01 20:54         ` n.pettik
@ 2018-08-02 22:15           ` Vladislav Shpilevoy
  2018-08-06  0:27             ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-08-02 22:15 UTC (permalink / raw)
  To: n.pettik, tarantool-patches

Hi! Thanks for the fixes! See 1 comment below and a
separate commit on the branch.

On 01/08/2018 23:54, n.pettik wrote:
> 
>> 1. sql-tap/fkey2.test.lua fails on this commit. Travis fails
>> as well.
> 
> Does it really matter?:) Could you forgive me these fails and
> make me avoid suffering? Anyway, they are completely fixed
> in second patch, but in scope of current patch they can be only
> partially repaired. Hence, it would look like I fix in the second patch
> fixes made in first one..

It had been failing even on the last commit. So it really mattered.

On the second commit it fails now as well.

Please, make it working. It is not okay, that a non-trivial patch
breaks tests, that somehow stop failing after some non-linked
changes.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-08-01 20:54         ` n.pettik
@ 2018-08-02 22:15           ` Vladislav Shpilevoy
  2018-08-06  0:28             ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-08-02 22:15 UTC (permalink / raw)
  To: n.pettik, tarantool-patches

Thanks for the patch! See 6 comments below and a separate
commit on the branch.

> 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

1. No comment.

> +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);
> +	}
> +}
> +
> @@ -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);

2. Use after free.

>   }
>   
>   /** Return old FK and release memory for the new one. */
> @@ -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);

3. You should not update mask on commit. The mask should be updated
before the commit yielded. Else, during yield, some new requests
can arrive which will not match the mask despite of presence of the
fkey in the space.

>   	fkey_delete(fk);
>   }
>   
> 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
> @@ -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));

4. Encode_uint takes a second argument of type uint64_t. But you
cast here links to char * and then unreference it so it becomes
just char - 8 bits.

> +	}
> +	return p - buf;
> +}
> +
>   /*
>    * Format "parts" array for _index entry.
>    * Returns result size.
> 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
> @@ -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);

5. Can we use P4_STATIC and do not duplicate? I want to say, that if we
do not duplicate the memory here, then only 2 situations are possible:
1) Until the VDBE is executed nothing is happened, the memory is ok and
can be used.
2) Such alter occurred before VDBE execution so the memory is dirty and
we can not use it. But in such a case schema_version is updated and the
VDBE is expired entirely that leads to its regeneration (will lead).

> +	}
>   	/*
>   	 * Drop all _space and _index entries that refer to the
>   	 * table.
> @@ -2106,177 +2299,281 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list,
> +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)
> +{
> -
> -	/* 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);

6. You do not need to normalize it here since the name was either
auto-generated or got from sqlite3NameFromToken(). The latter already
calls normalize(). The former should generate a well-formed name.

What is more, if you call normalize only here, then all the errors
above with auto-generated names appears in lowercase, but below -
in uppercase, that leads to curious test results: somewhere
errors show fk_constraint_1_C, in another places: FK_CONSTRAINT_1_C.

> +	/*
> +	 * 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);
> +	}
>   

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-08-01 20:54         ` n.pettik
@ 2018-08-02 22:15           ` Vladislav Shpilevoy
  2018-08-06  0:28             ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-08-02 22:15 UTC (permalink / raw)
  To: n.pettik, tarantool-patches

Thanks for the patch! See 3 comments below and a separate
commit on the branch.

>     schema: add new system space for FK constraints
>     
>     This patch introduces new system space to persist foreign keys
>     constraints. Format of the space:
>     
>     _fk_constraint (space id = 358)
>     
>     [<contraint name> STR, <parent id> UINT, <child id> UINT,

1. Typo: contraint.

>      <is deferred> BOOL, <match> STR, <on delete action> STR,
>      <on update action> STR, <child cols> ARRAY<UINT>,
>      <parent cols> ARRAY<UINT>]
>     
>     FK constraint is local to space, so every pair <FK name, child id>
>     is unique (and it is PK in _fk_constraint space).
>     
>     After insertion into this space, new instance describing FK constraint
>     is created. FK constraints are held in data-dictionary as two lists
>     (for child and parent constraints) in struct space.
>     
>     There is a list of FK restrictions:
>      - At the time of FK creation parent and child spaces must exist;
>      - VIEWs can't be involved into FK processing;
>      - Child space must be empty;
>      - Types of referencing and referenced fields must be comparable;
>      - Collations of referencing and referenced fields must match;
>      - Referenced fields must compose unique index;
>      - Referenced fields can not contain duplicates.
>     
>     Until space (child) features FK constraints it isn't allowed to be
>     dropped. Implicitly referenced index also can't be dropped
>     (and that is why parent space can't be dropped). But :drop() method
>     of child space firstly deletes all FK constraint (the same as SQL
>     triggers, indexes etc) and then removes entry from _space.
>     
>     Part of #3271> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 7b6bd1a5a..5b55bfd7a 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -3459,6 +3515,395 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)> +
> +/**
> + * On rollback of creation we remove FK constraint from DD, i.e.
> + * from parent's and child's lists of constraints and
> + * release memory.
> + */
> +static void
> +on_create_fkey_rollback(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	rlist_del_entry(fk, parent_link);
> +	rlist_del_entry(fk, child_link);
> +	fkey_delete(fk);
> +}
> +
> +/** Return old FK and release memory for the new one. */
> +static void
> +on_replace_fkey_rollback(struct trigger *trigger, void *event)
> +{
> +	(void) event;
> +	struct fkey *fk = (struct fkey *)trigger->data;
> +	struct space *parent = space_by_id(fk->def->parent_id);
> +	struct space *child = space_by_id(fk->def->child_id);
> +	struct fkey *old_fkey = fkey_grab_by_name(&child->child_fkey,
> +						  fk->def->name);
> +	fkey_delete(old_fkey);
> +	rlist_add_entry(&child->child_fkey, fk, child_link);
> +	rlist_add_entry(&parent->parent_fkey, fk, parent_link);


2. In the comment you said that this function restores the old fk and
deletes the new one. But on the code we see the line:
fkey_delete(old_fkey).

Secondly, you got old_fkey from child->child_fkey, but earlier, in
on_replace trigger, you had removed old_fkey from child_fkey list
and put here new fkey. So here actually old_fkey == fkey (I tested, it is).

I think, it can be fixed quite simple: you have ability to fetch the new
fkey from child_list and have no the latter for the old one since it is
unlinked already. So lets pass here old_fkey as trigger->data and fetch
the new fkey from the list.

The test for this bug is below:

	box.sql.execute("CREATE TABLE t1 (id PRIMARY KEY, a REFERENCES t1, b INT);")
	t = box.space._fk_constraint:select{}[1]:totable()
	errinj = box.error.injection
	errinj.set("ERRINJ_WAL_IO", true)
	box.space._fk_constraint:replace(t)

But it crashes on the next commit. On the current one this function is
unreachable.

	Process 51167 stopped
	* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

> +/**
> + * ANSI SQL doesn't allow list of referenced fields to contain
> + * duplicates. Firstly, we try to follow the easiest way:
> + * if all referenced fields numbers are less than 63, we can
> + * use bit mask. Otherwise, fall through slow check where we
> + * use O(field_cont^2) simple nested cycle iterations.
> + */
> +static void
> +fkey_links_check_duplicates(struct fkey_def *fk_def)
> +{
> +	uint64_t field_mask = 0;
> +	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
> +		uint32_t parent_field = fk_def->links[i].parent_field;
> +		if (parent_field > 63)
> +			goto slow_check;
> +		if (field_mask & (((uint64_t) 1) << parent_field))
> +			goto error;
> +		column_mask_set_fieldno(&field_mask, parent_field);

3. Looks like column_mask API is not applicable here since you
anyway still use raw bit setting. I have removed column_mask API
from there on the branch.

> +	}
> +	return;
> +slow_check:
> +	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
> +		uint32_t parent_field = fk_def->links[i].parent_field;
> +		for (uint32_t j = i + 1; j < fk_def->field_count; ++j) {
> +			if (parent_field == fk_def->links[j].parent_field)
> +				goto error;
> +		}
> +	}
> +	return;
> +error:
> +	tnt_raise(ClientError, ER_CREATE_FK_CONSTRAINT, fk_def->name,
> +		  "referenced fields can not contain duplicates");
> +}
> +

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 1/5] sql: prohibit creation of FK on unexisting tables
  2018-08-02 22:15           ` Vladislav Shpilevoy
@ 2018-08-06  0:27             ` n.pettik
  0 siblings, 0 replies; 32+ messages in thread
From: n.pettik @ 2018-08-06  0:27 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy


>>> 1. sql-tap/fkey2.test.lua fails on this commit. Travis fails
>>> as well.
>> Does it really matter?:) Could you forgive me these fails and
>> make me avoid suffering? Anyway, they are completely fixed
>> in second patch, but in scope of current patch they can be only
>> partially repaired. Hence, it would look like I fix in the second patch
>> fixes made in first one..
> 
> It had been failing even on the last commit. So it really mattered.

Really? On my local machine on last commit all test seem to pass..
On Travis also I don’t see failed tests related to FK constraints.

> On the second commit it fails now as well.

These tests are fixed on third commit, not on second.

> Please, make it working. It is not okay, that a non-trivial patch
> breaks tests, that somehow stop failing after some non-linked
> changes.

But okay, I fixed them in current commit:

+++ b/test/sql-tap/fkey2.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(121)
+test:plan(117)
 
 -- This file implements regression tests for foreign keys.
 
@@ -724,14 +724,11 @@ test:do_catchsql_test(
 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 TABLE c(x PRIMARY KEY REFERENCES v(y));
-        INSERT INTO c DEFAULT VALUES;
+        CREATE VIEW v AS SELECT b AS y FROM p;
+        CREATE TABLE d(x PRIMARY KEY REFERENCES v(y));
     ]], {
         -- <fkey2-7.2>
-        1, "no such table: C"
+        1, "referenced table can't be view"
         -- </fkey2-7.2>
     })
 
@@ -740,13 +737,14 @@ test:do_catchsql_test(
     [[
         DROP VIEW v;
         DROP TABLE IF EXISTS c;
+        DROP TABLE IF EXISTS p;
         CREATE TABLE p(a COLLATE binary, b PRIMARY KEY);
         CREATE UNIQUE INDEX idx ON p(a COLLATE "unicode_ci");
         CREATE TABLE c(x PRIMARY KEY REFERENCES p(a));
         INSERT INTO c DEFAULT VALUES;
     ]], {
         -- <fkey2-7.3>
-        1, "no such view: V"
+        1, "foreign key mismatch - \"C\" referencing \"P\""
         -- </fkey2-7.3>
     })

@@ -1053,7 +1051,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>
     })
 
@@ -1080,57 +1078,17 @@ test:do_catchsql_test(
         -- </fkey2-10.8>
     })
 

@@ -1083,54 +1081,13 @@ test:do_catchsql_test(
 test:do_execsql_test(
     "fkey2-10.9",
     [[
-        DELETE FROM t2;
+        DROP TABLE t2;
         DROP TABLE t1;
     ]], {
         -- <fkey2-10.9>
         -- </fkey2-10.9>
     })

-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",
     [[
@@ -1181,7 +1139,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 +1151,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 +1162,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 TABLE t1(x PRIMARY KEY REFERENCES v);
-        DROP VIEW v;
+        CREATE VIEW v AS SELECT * FROM b1;
+        CREATE TABLE t3(x PRIMARY KEY REFERENCES v);
     ]], {
         -- <fkey2-10.20>
+        1, "referenced table can't be view"
         -- </fkey2-10.20>
     })

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-08-02 22:15           ` Vladislav Shpilevoy
@ 2018-08-06  0:28             ` n.pettik
  2018-08-06 18:24               ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-08-06  0:28 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy

Firstly, I don’t understand some of your fixes:

@@ -3855,11 +3853,9 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                memset(fkey, 0, sizeof(*fkey));
                fkey->def = fk_def;
                fkey->index_id = fk_index->def->iid;
+               rlist_add_entry(&child_space->child_fkey, fkey, child_link);
+               rlist_add_entry(&parent_space->parent_fkey, fkey, parent_link);
                if (old_tuple == NULL) {
-                       rlist_add_entry(&child_space->child_fkey, fkey,
-                                       child_link);
-                       rlist_add_entry(&parent_space->parent_fkey, fkey,
-                                       parent_link);
                        struct trigger *on_rollback =
                                txn_alter_trigger_new(on_create_fkey_rollback,
                                                      fkey);
@@ -3868,10 +3864,6 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                        struct fkey *old_fk =
                                fkey_grab_by_name(&child_space->child_fkey,
                                                  fk_def->name);
-                       rlist_add_entry(&child_space->child_fkey, fkey,
-                                       child_link);
-                       rlist_add_entry(&parent_space->parent_fkey, fkey,
-                                       parent_link);

In case of replace we must firstly remove entry and only then insert new one.
It is easy to check that the way you suggest doesn’t work (just modify test you provided):

box.sql.execute("CREATE TABLE t3 (id PRIMARY KEY, a REFERENCES t3, b INT UNIQUE);")
t = box.space._fk_constraint:select{}[1]:totable()
errinj = box.error.injection
errinj.set("ERRINJ_WAL_IO", true)
-- Make constraint reference B field instead of id.
t[9] = {2}
box.space._fk_constraint:replace(t)
box.sql.execute("INSERT INTO t3 VALUES (1, 2, 2)”)

- No error is raised.

If I discard your diff it works fine (i.e. "FOREIGN KEY constraint failed” error is raised).

>>    schema: add new system space for FK constraints
>>        This patch introduces new system space to persist foreign keys
>>    constraints. Format of the space:
>>        _fk_constraint (space id = 358)
>>        [<contraint name> STR, <parent id> UINT, <child id> UINT,
> 
> 1. Typo: contraint.

Fixed.

>> +/** Return old FK and release memory for the new one. */
>> +static void
>> +on_replace_fkey_rollback(struct trigger *trigger, void *event)
>> +{
>> +	(void) event;
>> +	struct fkey *fk = (struct fkey *)trigger->data;
>> +	struct space *parent = space_by_id(fk->def->parent_id);
>> +	struct space *child = space_by_id(fk->def->child_id);
>> +	struct fkey *old_fkey = fkey_grab_by_name(&child->child_fkey,
>> +						  fk->def->name);
>> +	fkey_delete(old_fkey);
>> +	rlist_add_entry(&child->child_fkey, fk, child_link);
>> +	rlist_add_entry(&parent->parent_fkey, fk, parent_link);
> 
> 
> 2. In the comment you said that this function restores the old fk and
> deletes the new one. But on the code we see the line:
> fkey_delete(old_fkey).
> 
> Secondly, you got old_fkey from child->child_fkey, but earlier, in
> on_replace trigger, you had removed old_fkey from child_fkey list
> and put here new fkey. So here actually old_fkey == fkey (I tested, it is).
> 
> I think, it can be fixed quite simple: you have ability to fetch the new
> fkey from child_list and have no the latter for the old one since it is
> unlinked already. So lets pass here old_fkey as trigger->data and fetch
> the new fkey from the list.

Thx for catch. Fixed:

+++ b/src/box/alter.cc
@@ -3678,14 +3678,14 @@ static void
 on_replace_fkey_rollback(struct trigger *trigger, void *event)
 {
        (void) event;
-       struct fkey *fk = (struct fkey *)trigger->data;
-       struct space *parent = space_by_id(fk->def->parent_id);
-       struct space *child = space_by_id(fk->def->child_id);
-       struct fkey *old_fkey = fkey_grab_by_name(&child->child_fkey,
-                                                 fk->def->name);
-       fkey_delete(old_fkey);
-       rlist_add_entry(&child->child_fkey, fk, child_link);
-       rlist_add_entry(&parent->parent_fkey, fk, parent_link);
+       struct fkey *old_fk = (struct fkey *)trigger->data;
+       struct space *parent = space_by_id(old_fk->def->parent_id);
+       struct space *child = space_by_id(old_fk->def->child_id);
+       struct fkey *new_fkey = fkey_grab_by_name(&child->child_fkey,
+                                                 old_fk->def->name);
+       fkey_delete(new_fkey);
+       rlist_add_entry(&child->child_fkey, old_fk, child_link);
+       rlist_add_entry(&parent->parent_fkey, old_fk, parent_link);
 }
 
 /** On rollback of drop simply return back FK to DD. */
@@ -3866,7 +3866,7 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
                                                  fk_def->name);
                        struct trigger *on_rollback =
                                txn_alter_trigger_new(on_replace_fkey_rollback,
-                                                     fkey);
+                                                     old_fk);

> 
> The test for this bug is below:
> 
> 	box.sql.execute("CREATE TABLE t1 (id PRIMARY KEY, a REFERENCES t1, b INT);")
> 	t = box.space._fk_constraint:select{}[1]:totable()
> 	errinj = box.error.injection
> 	errinj.set("ERRINJ_WAL_IO", true)
> 	box.space._fk_constraint:replace(t)

Added to the next patch:

+++ b/test/sql/foreign-keys.test.lua
@@ -160,5 +160,31 @@ box.space._fk_constraint:select{}
 box.sql.execute('DROP TABLE tc')
 box.sql.execute('DROP TABLE tp')
 
+-- Tests which are aimed at verifying work of commit/rollback
+-- triggers on _fk_constraint space.
+--
+box.sql.execute("CREATE TABLE t3 (id PRIMARY KEY, a REFERENCES t3, b INT UNIQUE);")
+t = box.space._fk_constraint:select{}[1]:totable()
+errinj = box.error.injection
+errinj.set("ERRINJ_WAL_IO", true)
+-- Make constraint reference B field instead of id.
+t[9] = {2}
+box.space._fk_constraint:replace(t)
+errinj.set("ERRINJ_WAL_IO", false)
+box.sql.execute("INSERT INTO t3 VALUES (1, 2, 2);")
+errinj.set("ERRINJ_WAL_IO", true)
+box.sql.execute("ALTER TABLE t3 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES t3;")
+errinj.set("ERRINJ_WAL_IO", false)
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+box.sql.execute("DELETE FROM t3;")
+box.snapshot()
+box.sql.execute("ALTER TABLE t3 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES t3;")
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+errinj.set("ERRINJ_WAL_IO", true)
+box.sql.execute("ALTER TABLE t3 DROP CONSTRAINT fk1;")
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+errinj.set("ERRINJ_WAL_IO", false)
+box.sql.execute("DROP TABLE t3;")
+

+++ b/test/sql/foreign-keys.result
@@ -356,5 +356,85 @@ box.sql.execute('DROP TABLE tc')
 box.sql.execute('DROP TABLE tp')
 ---
 ...
+-- Tests which are aimed at verifying work of commit/rollback
+-- triggers on _fk_constraint space.
+--
+box.sql.execute("CREATE TABLE t3 (id PRIMARY KEY, a REFERENCES t3, b INT UNIQUE);")
+---
+...
+t = box.space._fk_constraint:select{}[1]:totable()
+---
+...
+errinj = box.error.injection
+---
+...
+errinj.set("ERRINJ_WAL_IO", true)
+---
+- ok
+...
+-- Make constraint reference B field instead of id.
+t[9] = {2}
+---
+...
+box.space._fk_constraint:replace(t)
+---
+- error: Failed to write to disk
+...
+errinj.set("ERRINJ_WAL_IO", false)
+---
+- ok
+...
+box.sql.execute("INSERT INTO t3 VALUES (1, 2, 2);")
+---
+- error: FOREIGN KEY constraint failed
+...
+errinj.set("ERRINJ_WAL_IO", true)
+---
+- ok
+...
+box.sql.execute("ALTER TABLE t3 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES t3;")
+---
+- error: Failed to write to disk
+...
+errinj.set("ERRINJ_WAL_IO", false)
+---
+- ok
+...
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+---
+...
+box.sql.execute("DELETE FROM t3;")
+---
+...
+box.snapshot()
+---
+- ok
+...
+box.sql.execute("ALTER TABLE t3 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES t3;")
+---
+...
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+---
+- error: FOREIGN KEY constraint failed
+...
+errinj.set("ERRINJ_WAL_IO", true)
+---
+- ok
+...
+box.sql.execute("ALTER TABLE t3 DROP CONSTRAINT fk1;")
+---
+- error: Failed to write to disk
+...
+box.sql.execute("INSERT INTO t3 VALUES(1, 1, 3);")
+---
+- error: FOREIGN KEY constraint failed
+...
+errinj.set("ERRINJ_WAL_IO", false)
+---
+- ok
+...
+box.sql.execute("DROP TABLE t3;")
+---
+...

> But it crashes on the next commit. On the current one this function is
> unreachable.
> 
> 	Process 51167 stopped
> 	* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
> 
>> +/**
>> + * ANSI SQL doesn't allow list of referenced fields to contain
>> + * duplicates. Firstly, we try to follow the easiest way:
>> + * if all referenced fields numbers are less than 63, we can
>> + * use bit mask. Otherwise, fall through slow check where we
>> + * use O(field_cont^2) simple nested cycle iterations.
>> + */
>> +static void
>> +fkey_links_check_duplicates(struct fkey_def *fk_def)
>> +{
>> +	uint64_t field_mask = 0;
>> +	for (uint32_t i = 0; i < fk_def->field_count; ++i) {
>> +		uint32_t parent_field = fk_def->links[i].parent_field;
>> +		if (parent_field > 63)
>> +			goto slow_check;
>> +		if (field_mask & (((uint64_t) 1) << parent_field))
>> +			goto error;
>> +		column_mask_set_fieldno(&field_mask, parent_field);
> 
> 3. Looks like column_mask API is not applicable here since you
> anyway still use raw bit setting. I have removed column_mask API
> from there on the branch.

Yep, thx for fix.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-08-02 22:15           ` Vladislav Shpilevoy
@ 2018-08-06  0:28             ` n.pettik
  2018-08-06 18:24               ` Vladislav Shpilevoy
  0 siblings, 1 reply; 32+ messages in thread
From: n.pettik @ 2018-08-06  0:28 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy

I squashed your fixes. However, personally I don’t understand why you
decided to refactor endTable() routine, for instance. Ofc refactoring
is always appreciated but I would prefer separating functional part of patch and
cosmetic/codestyle changes. Initial version of this patch AFAIR included 1k +- lines.
Now (due to codestyle fixes) it is about 1.8k + and 1.6k -.. It would be quite
complicated to find smth in such giant diff.

>> 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
> 
> 1. No comment.

Comment for static inline two-lines function? Seriously?
Ok, I will add it, but I think we should consider making an exception
in our developing guide for instance for static functions less than
10 lines or sort of.

+++ b/src/box/alter.cc
@@ -3665,6 +3665,15 @@ fkey_grab_by_name(struct rlist *list, const char *fkey_name)
  */
 #define FKEY_MASK(x) (((x)>31) ? 0xffffffff : ((uint64_t)1<<(x)))
 
+/**
+ * Set bits of @mask which correspond to fields involved in
+ * given foreign key constraint.
+ *
+ * @param fk Links of this FK constraint are used to update mask.
+ * @param[out] mask Mask to be updated.
+ * @param type Type of links to be used to update mask:
+ *             parent or child.
+ */
 static inline void
 fkey_set_mask(const struct fkey *fk, uint64_t *mask, int type)

> 
>> +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);
>> +	}
>> +}
>> +
>> @@ -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);
> 
> 2. Use after free.

Skipped since you already fixed on branch.

>>  }
>>    /** Return old FK and release memory for the new one. */
>> @@ -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);
> 
> 3. You should not update mask on commit. The mask should be updated
> before the commit yielded. Else, during yield, some new requests
> can arrive which will not match the mask despite of presence of the
> fkey in the space.

Yep, it is definitely mistake. Also skipped.

>>  	fkey_delete(fk);
>>  }
>>  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
>> @@ -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));
> 
> 4. Encode_uint takes a second argument of type uint64_t. But you
> cast here links to char * and then unreference it so it becomes
> just char - 8 bits.

The same (I realised my mistake and skipped).

> 
>> +	}
>> +	return p - buf;
>> +}
>> +
>>  /*
>>   * Format "parts" array for _index entry.
>>   * Returns result size.
>> 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
>> @@ -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);
> 
> 5. Can we use P4_STATIC and do not duplicate?

No, unfortunately we can’t. P4_STATIC attribute is usable only when the same
memory is used twice during VDBE execution. STATIC attribute prevents from
memory releasing (even at the end of execution AFAIK). If we released memory
right after generating opcode (or at the end of parsing, it doesn’t matter), we will
probably get corrupted memory.

> I want to say, that if we
> do not duplicate the memory here, then only 2 situations are possible:
> 1) Until the VDBE is executed nothing is happened, the memory is ok and
> can be used.

Can be used, but wouldn’t be freed (or I misunderstand what you initially meant).

> 2) Such alter occurred before VDBE execution so the memory is dirty and
> we can not use it. But in such a case schema_version is updated and the
> VDBE is expired entirely that leads to its regeneration (will lead).
> 
>> +	}
>>  	/*
>>  	 * Drop all _space and _index entries that refer to the
>>  	 * table.
>> @@ -2106,177 +2299,281 @@ sql_drop_table(struct Parse *parse_context, struct SrcList *table_name_list,
>> +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)
>> +{
>> -
>> -	/* 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);
> 
> 6. You do not need to normalize it here since the name was either
> auto-generated or got from sqlite3NameFromToken(). The latter already
> calls normalize(). The former should generate a well-formed name.

Ok, I forgot about normalization in sqlite3NameFromToken(). Thx for fix.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-08-06  0:28             ` n.pettik
@ 2018-08-06 18:24               ` Vladislav Shpilevoy
  2018-08-06 23:43                 ` n.pettik
  0 siblings, 1 reply; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-08-06 18:24 UTC (permalink / raw)
  To: n.pettik, tarantool-patches

Thanks for the patch! The whole patchset LGTM except two
comments below about the tests.

On 06/08/2018 03:28, n.pettik wrote:
> I squashed your fixes. However, personally I don’t understand why you
> decided to refactor endTable() routine, for instance. Ofc refactoring
> is always appreciated but I would prefer separating functional part of patch and
> cosmetic/codestyle changes. Initial version of this patch AFAIR included 1k +- lines.
> Now (due to codestyle fixes) it is about 1.8k + and 1.6k -.. It would be quite
> complicated to find smth in such giant diff.

But new code amount was reduced actually. Many of the new code
(added during the review) is the tests on the bugs found during
the review.

> 
>>> 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
>>
>> 1. No comment.
> 
> Comment for static inline two-lines function? Seriously?
> Ok, I will add it, but I think we should consider making an exception
> in our developing guide for instance for static functions less than
> 10 lines or sort of.

You can propose it in a chat.

>>> +	/* 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);
>>
>> 5. Can we use P4_STATIC and do not duplicate?
> 
> No, unfortunately we can’t. P4_STATIC attribute is usable only when the same
> memory is used twice during VDBE execution. STATIC attribute prevents from
> memory releasing (even at the end of execution AFAIK). If we released memory
> right after generating opcode (or at the end of parsing, it doesn’t matter), we will
> probably get corrupted memory.

How can child_fk->def->name be freed before VDBE execution? It is drop table
code generation, so here the child_fk exists out of the parser context in a
space from the space cache.

> 
>> I want to say, that if we
>> do not duplicate the memory here, then only 2 situations are possible:
>> 1) Until the VDBE is executed nothing is happened, the memory is ok and
>> can be used.
> 
> Can be used, but wouldn’t be freed (or I misunderstand what you initially meant).

Yes, it will not be freed because it will not be duplicated. Free of the
original name together with the child_fk will occur inside alter.cc. Vdbe
after drop of the space will not touch this freed memory.

But do not mind. I see, that vdbe_emit_fkey_drop is used both to malloced
normalized names from the parser and for struct fkey names. So it is not
worth to refactor this function to use both static and dynamic memory. We
need a more comprehensive way to compact these numerous mallocs during
parsing, later.

> diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua
> index 062597e9b..7cb315fcc 100755
> --- a/test/sql-tap/fkey2.test.lua
> +++ b/test/sql-tap/fkey2.test.lua
> @@ -755,10 +753,13 @@ 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);
> +<<<<<<< HEAD
>          INSERT INTO c DEFAULT VALUES;
> +=======
> +>>>>>>> 323e96ee2... sql: introduce ADD CONSTRAINT statement

1. ???

>      ]], {
>          -- <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>
>      })
>  
> diff --git a/test/sql/foreign-keys.result b/test/sql/foreign-keys.result
> index c2ec429c3..2580221d3 100644
> --- a/test/sql/foreign-keys.result
> +++ b/test/sql/foreign-keys.result
> @@ -332,5 +332,109 @@ 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')
> +---
> +...
> +-- Tests which are aimed at verifying work of commit/rollback
> +-- triggers on _fk_constraint space.
> +--
> +box.sql.execute("CREATE TABLE t3 (id PRIMARY KEY, a REFERENCES t3, b INT UNIQUE);")
> +---
> +...
> +t = box.space._fk_constraint:select{}[1]:totable()
> +---
> +...
> +errinj = box.error.injection

2. Please, do not use errinj in regular tests. Only
in release-disabled. Errinj structures and code are
defined to nothing in C when NDEBUG is set, so they
do not work.

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 2/5] schema: add new system space for FK constraints
  2018-08-06  0:28             ` n.pettik
@ 2018-08-06 18:24               ` Vladislav Shpilevoy
  0 siblings, 0 replies; 32+ messages in thread
From: Vladislav Shpilevoy @ 2018-08-06 18:24 UTC (permalink / raw)
  To: n.pettik, tarantool-patches

Hi!

On 06/08/2018 03:28, n.pettik wrote:
> Firstly, I don’t understand some of your fixes:
> 
> @@ -3855,11 +3853,9 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>                  memset(fkey, 0, sizeof(*fkey));
>                  fkey->def = fk_def;
>                  fkey->index_id = fk_index->def->iid;
> +               rlist_add_entry(&child_space->child_fkey, fkey, child_link);
> +               rlist_add_entry(&parent_space->parent_fkey, fkey, parent_link);
>                  if (old_tuple == NULL) {
> -                       rlist_add_entry(&child_space->child_fkey, fkey,
> -                                       child_link);
> -                       rlist_add_entry(&parent_space->parent_fkey, fkey,
> -                                       parent_link);
>                          struct trigger *on_rollback =
>                                  txn_alter_trigger_new(on_create_fkey_rollback,
>                                                        fkey);
> @@ -3868,10 +3864,6 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>                          struct fkey *old_fk =
>                                  fkey_grab_by_name(&child_space->child_fkey,
>                                                    fk_def->name);
> -                       rlist_add_entry(&child_space->child_fkey, fkey,
> -                                       child_link);
> -                       rlist_add_entry(&parent_space->parent_fkey, fkey,
> -                                       parent_link);
> 
> In case of replace we must firstly remove entry and only then insert new one.
> It is easy to check that the way you suggest doesn’t work (just modify test you provided):

Yes, your are right. This was a buggy hunk. Thanks for the fix!

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 3/5] sql: introduce ADD CONSTRAINT statement
  2018-08-06 18:24               ` Vladislav Shpilevoy
@ 2018-08-06 23:43                 ` n.pettik
  0 siblings, 0 replies; 32+ messages in thread
From: n.pettik @ 2018-08-06 23:43 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Vladislav Shpilevoy

Also I’ve rebased patch-set on fresh 2.0.

>>>> +	/* 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);
>>> 
>>> 5. Can we use P4_STATIC and do not duplicate?
>> No, unfortunately we can’t. P4_STATIC attribute is usable only when the same
>> memory is used twice during VDBE execution. STATIC attribute prevents from
>> memory releasing (even at the end of execution AFAIK). If we released memory
>> right after generating opcode (or at the end of parsing, it doesn’t matter), we will
>> probably get corrupted memory.
> 
> How can child_fk->def->name be freed before VDBE execution? It is drop table
> code generation, so here the child_fk exists out of the parser context in a
> space from the space cache.
> 
>>> I want to say, that if we
>>> do not duplicate the memory here, then only 2 situations are possible:
>>> 1) Until the VDBE is executed nothing is happened, the memory is ok and
>>> can be used.
>> Can be used, but wouldn’t be freed (or I misunderstand what you initially meant).
> 
> Yes, it will not be freed because it will not be duplicated. Free of the
> original name together with the child_fk will occur inside alter.cc. Vdbe
> after drop of the space will not touch this freed memory.
> 
> But do not mind. I see, that vdbe_emit_fkey_drop is used both to malloced

Yep, I meant exactly this usage.

>> diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua
>> index 062597e9b..7cb315fcc 100755
>> --- a/test/sql-tap/fkey2.test.lua
>> +++ b/test/sql-tap/fkey2.test.lua
>> @@ -755,10 +753,13 @@ 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);
>> +<<<<<<< HEAD
>>         INSERT INTO c DEFAULT VALUES;
>> +=======
>> +>>>>>>> 323e96ee2... sql: introduce ADD CONSTRAINT statement
> 
> 1. ???

Oops, sorry, ‘haste makes waste’...

> 2. Please, do not use errinj in regular tests. Only
> in release-disabled. Errinj structures and code are
> defined to nothing in C when NDEBUG is set, so they
> do not work.

Forgot about that. So simply moved these tests to sql/erring.test.lua

^ permalink raw reply	[flat|nested] 32+ messages in thread

* [tarantool-patches] Re: [PATCH 0/5] Move FK constraints to server
  2018-07-13  2:04 [tarantool-patches] [PATCH 0/5] Move FK constraints to server Nikita Pettik
                   ` (5 preceding siblings ...)
  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
  6 siblings, 0 replies; 32+ messages in thread
From: Kirill Yukhin @ 2018-08-07 14:57 UTC (permalink / raw)
  To: tarantool-patches; +Cc: v.shpilevoy, Nikita Pettik

Hello,
On 13 июл 05:04, Nikita Pettik wrote:
> Branch: https://github.com/tarantool/tarantool/commits/np/move-fk-to-server
> Issue: https://github.com/tarantool/tarantool/issues/3271
> 
> The aim of this patch-set is to move foreign key constraint to server
> and them be closer to ANSI specification.
> 
> First patch is preliminary and enables additional restrictions
> for FK constraints (ban opportunity to drop space referenced space,
> create FK referencing VIEW etc).
> 
> In original SQLite FK constraints may appear only during CREATE TABLE
> statement. Thus, it was enough to hold string of CREATE TABLE statement
> and reparse it once on instance loading. This approach defers all
> resolutions until FK usage. For instance:
> 
> CREATE TABLE t1(id PRIMARY KEY REFERENCES t2);
> CREATE TABLE t2(id PRIMARY KEY);
> 
> We decided to use another approach - where FK constraints are always consistent
> and all DD links are kept up. For instance, if we attempted to satisfy all
> restrictions using SQLite schema - we wouldn't be able to create circular
> dependencies. To support circular dependecies, we must allow to create them
> after space itself. In turn, to create FK constraints outside CREATE STATEMENT,
> we must persist them.  To implement these steps, firstly _fk_constraint system
> space is added - it contains tuples describing FK. Then, separate SQL statement
> <ALTER TABLE name ADD CONSTRAINT ...> is introduced which processes insertion
> and deletion from this space. Finally, FK processing has been refactored to
> rely on new DD in server (struct fkey and struct fkey_def). It seems that
> perfomance of FK handling has become a little bit better: now we don't need
> find suitable index on each FK invocation - its id is held into FK struct
> itself.
> 
> The last patch is simple follow-up which removes obsolete define guard
> for FK constraints.
> 
> Nikita Pettik (5):
>   sql: prohibit creation of FK on unexisting tables
>   schema: add new system space for FK constraints
>   sql: introduce ADD CONSTRAINT statement
>   sql: display error on FK creation and drop failure
>   sql: remove SQLITE_OMIT_FOREIGN_KEY define guard
I've checked the whole patchset into 2.0 branch.

--
Regards, Kirill Yukhin

^ permalink raw reply	[flat|nested] 32+ messages in thread

end of thread, other threads:[~2018-08-07 14:57 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox