From 934b0ac08de838b2325382d02330687925c9473c Mon Sep 17 00:00:00 2001
Message-Id: <934b0ac08de838b2325382d02330687925c9473c.1525533484.git.korablev@tarantool.org>
In-Reply-To: <cover.1525533484.git.korablev@tarantool.org>
References: <cover.1525533484.git.korablev@tarantool.org>
From: Nikita Pettik <korablev@tarantool.org>
Date: Thu, 26 Apr 2018 16:55:11 +0300
Subject: [PATCH 2/4] sql: allow transitive Lua <-> SQL transactions

This patch makes possible to start transaction in Lua and continue
operations in SQL as well, and vice versa. Previously, such transactions
result in assertion fault. To support them, it is required to hold deferred
foreign keys constraints as attributes of transaction, not particular VDBE.
Thus, deferred foreign keys counters have been completely removed from
VDBE and transfered to sql_txn struct. In its turn, if there is at least
one deferred foreign key violation, error will be raised alongside with
rollback - that is what ANSI SQL says. Note that in SQLite rollback
doesn't occur: transaction remains open untill explicit rollback or
resolving all FK violations.

Also, 'PRAGMA defer_foreign_keys' has been slightly changed: now it is not
automatically turned off after trasaction's rollback or commit. It
can be turned off by explicit PRAGMA statement only. It was made owing
to the fact that execution of PRAGMA statement occurs in auto-commit
mode, so it ends with COMMIT. Hence, it turns off right after turning on
(outside the transaction).

Closes #3237
---
 src/box/errcode.h                         |   1 +
 src/box/sql/fkey.c                        |  66 ++++++----------
 src/box/sql/main.c                        |   5 --
 src/box/sql/pragma.c                      |   3 -
 src/box/sql/select.c                      |   7 +-
 src/box/sql/sqliteInt.h                   |   2 -
 src/box/sql/status.c                      |   3 +-
 src/box/sql/vdbe.c                        |  25 +++---
 src/box/sql/vdbe.h                        |  23 ++++++
 src/box/sql/vdbeInt.h                     |  26 +-----
 src/box/sql/vdbeapi.c                     |   3 -
 src/box/sql/vdbeaux.c                     |  79 ++++++++++++-------
 src/box/txn.c                             |  17 +++-
 src/box/txn.h                             |  22 +++++-
 test/box/misc.result                      |   1 +
 test/sql/transitive-transactions.result   | 126 ++++++++++++++++++++++++++++++
 test/sql/transitive-transactions.test.lua |  65 +++++++++++++++
 17 files changed, 345 insertions(+), 129 deletions(-)
 create mode 100644 test/sql/transitive-transactions.result
 create mode 100644 test/sql/transitive-transactions.test.lua

diff --git a/src/box/errcode.h b/src/box/errcode.h
index e5d882df8..ba5288059 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -214,6 +214,7 @@ struct errcode_record {
 	/*159 */_(ER_SQL_BIND_NOT_FOUND,	"Parameter %s was not found in the statement") \
 	/*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") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index fb9a3101a..55edf6ba5 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -748,23 +748,19 @@ fkTriggerDelete(sqlite3 * dbMem, Trigger * p)
 	}
 }
 
-/*
- * 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 either
- *
- *   (a) The table is the parent table of a FK constraint, or
- *   (b) The table is the child table of a deferred FK constraint and it is
- *       determined at runtime that there are outstanding deferred FK
- *       constraint violations in the database,
- *
- * 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.
+/**
+ * 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 * pParse, SrcList * pName, Table * pTab)
@@ -774,31 +770,17 @@ sqlite3FkDropTable(Parse * pParse, SrcList * pName, Table * pTab)
 
 	if ((user_session->sql_flags & SQLITE_ForeignKeys) &&
 	    !space_is_view(pTab)) {
-		int iSkip = 0;
 		Vdbe *v = sqlite3GetVdbe(pParse);
-
-		assert(v);	/* VDBE has already been allocated */
-		if (sqlite3FkReferences(pTab) == 0) {
-			/* Search for a deferred foreign key constraint for which this table
-			 * is the child table. If one cannot be found, return without
-			 * generating any VDBE code. If one can be found, then jump over
-			 * the entire DELETE if there are no outstanding deferred constraints
-			 * when this statement is run.
-			 */
-			FKey *p;
-			for (p = pTab->pFKey; p; p = p->pNextFrom) {
-				if (p->isDeferred
-				    || (user_session->
-					sql_flags & SQLITE_DeferFKs))
-					break;
-			}
-			if (!p)
+		assert(v != NULL);
+		/*
+		 * Search for a foreign key constraint for which
+		 * this table is the parent table. If one can't be
+		 * found, return without generating any VDBE code,
+		 * since it makes no sense starting transaction
+		 * to test FK violations.
+		 */
+		if (sqlite3FkReferences(pTab) == NULL)
 				return;
-			iSkip = sqlite3VdbeMakeLabel(v);
-			sqlite3VdbeAddOp2(v, OP_FkIfZero, 1, iSkip);
-			VdbeCoverage(v);
-		}
-
 		pParse->disableTriggers = 1;
 		/* Staring new transaction before DELETE FROM <tbl> */
 		sqlite3VdbeAddOp0(v, OP_TTransaction);
@@ -813,10 +795,6 @@ sqlite3FkDropTable(Parse * pParse, SrcList * pName, Table * pTab)
 		 * commit changes.
 		 */
 		sqlite3VdbeAddOp0(v, OP_FkCheckCommit);
-
-		if (iSkip) {
-			sqlite3VdbeResolveLabel(v, iSkip);
-		}
 	}
 }
 
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index 8775ef3ad..0acf7bc64 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -770,11 +770,6 @@ sqlite3RollbackAll(Vdbe * pVdbe, int tripCode)
 	assert((user_session->sql_flags & SQLITE_InternChanges) == 0
 	       || db->init.busy == 1);
 
-	/* Any deferred constraint violations have now been resolved. */
-	pVdbe->nDeferredCons = 0;
-	pVdbe->nDeferredImmCons = 0;
-	user_session->sql_flags &= ~SQLITE_DeferFKs;
-
 	/* If one has been configured, invoke the rollback-hook callback */
 	if (db->xRollbackCallback && (!pVdbe->auto_commit)) {
 		db->xRollbackCallback(db->pRollbackArg);
diff --git a/src/box/sql/pragma.c b/src/box/sql/pragma.c
index e41f69b02..73cd34cc0 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -320,9 +320,6 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 					user_session->sql_flags |= mask;
 				} else {
 					user_session->sql_flags &= ~mask;
-					if (mask == SQLITE_DeferFKs) {
-						v->nDeferredImmCons = 0;
-					}
 				}
 
 				/* Many of the flag-pragmas modify the code
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 5a50413a6..b8863a850 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -1994,13 +1994,16 @@ static SQLITE_NOINLINE Vdbe *
 allocVdbe(Parse * pParse)
 {
 	Vdbe *v = pParse->pVdbe = sqlite3VdbeCreate(pParse);
-	if (v)
-		sqlite3VdbeAddOp2(v, OP_Init, 0, 1);
+	if (v == NULL)
+		return NULL;
+	sqlite3VdbeAddOp2(v, OP_Init, 0, 1);
 	if (pParse->pToplevel == 0
 	    && OptimizationEnabled(pParse->db, SQLITE_FactorOutConst)
 	    ) {
 		pParse->okConstFactor = 1;
 	}
+	if (sql_vdbe_prepare(v) != 0)
+		return NULL;
 	return v;
 }
 
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 6bfe1352d..27d421b7e 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -1849,8 +1849,6 @@ struct FuncDestructor {
 struct Savepoint {
 	box_txn_savepoint_t *tnt_savepoint; /* Tarantool's savepoint struct */
 	char *zName;		/* Savepoint name (nul-terminated) */
-	i64 nDeferredCons;	/* Number of deferred fk violations */
-	i64 nDeferredImmCons;	/* Number of deferred imm fk. */
 	Savepoint *pNext;	/* Parent savepoint (if any) */
 };
 
diff --git a/src/box/sql/status.c b/src/box/sql/status.c
index 8742bbf8b..84f07cc68 100644
--- a/src/box/sql/status.c
+++ b/src/box/sql/status.c
@@ -333,8 +333,7 @@ sqlite3_db_status(sqlite3 * db,	/* The database connection whose status is desir
 				break;
 			}
 			const struct sql_txn *psql_txn = ptxn->psql_txn;
-			*pCurrent = psql_txn->nDeferredImmConsSave > 0 ||
-				    psql_txn->nDeferredConsSave > 0;
+			*pCurrent = psql_txn->fk_deferred_count > 0;
 			break;
 		}
 
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 43572583f..207410c7f 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -1029,7 +1029,6 @@ case OP_Halt: {
 		p->rc = SQLITE_BUSY;
 	} else {
 		assert(rc==SQLITE_OK || (p->rc&0xff)==SQLITE_CONSTRAINT);
-		assert(rc==SQLITE_OK || p->nDeferredCons>0 || p->nDeferredImmCons>0);
 		rc = p->rc ? SQLITE_ERROR : SQLITE_DONE;
 	}
 	goto vdbe_return;
@@ -2950,8 +2949,8 @@ case OP_Savepoint: {
 				assert(pSavepoint == psql_txn->pSavepoint);
 				psql_txn->pSavepoint = pSavepoint->pNext;
 			} else {
-				p->nDeferredCons = pSavepoint->nDeferredCons;
-				p->nDeferredImmCons = pSavepoint->nDeferredImmCons;
+				p->psql_txn->fk_deferred_count =
+					pSavepoint->tnt_savepoint->fk_deferred_count;
 			}
 		}
 	}
@@ -5058,10 +5057,10 @@ case OP_Param: {           /* out2 */
  * statement counter is incremented (immediate foreign key constraints).
  */
 case OP_FkCounter: {
-	if (user_session->sql_flags & SQLITE_DeferFKs) {
-		p->nDeferredImmCons += pOp->p2;
-	} else if (pOp->p1) {
-		p->nDeferredCons += pOp->p2;
+	if ((user_session->sql_flags & SQLITE_DeferFKs || pOp->p1 != 0) &&
+	    !p->auto_commit) {
+		assert(p->psql_txn != NULL);
+		p->psql_txn->fk_deferred_count += pOp->p2;
 	} else {
 		p->nFkConstraint += pOp->p2;
 	}
@@ -5081,12 +5080,14 @@ case OP_FkCounter: {
  * (immediate foreign key constraint violations).
  */
 case OP_FkIfZero: {         /* jump */
-	if (pOp->p1) {
-		VdbeBranchTaken(db->nDeferredCons == 0 && db->nDeferredImmCons == 0, 2);
-		if (p->nDeferredCons==0 && p->nDeferredImmCons==0) goto jump_to_p2;
+	if ((user_session->sql_flags & SQLITE_DeferFKs || pOp->p1) &&
+	    !p->auto_commit) {
+		assert(p->psql_txn != NULL);
+		if (p->psql_txn->fk_deferred_count == 0)
+			goto jump_to_p2;
 	} else {
-		VdbeBranchTaken(p->nFkConstraint == 0 && db->nDeferredImmCons == 0, 2);
-		if (p->nFkConstraint==0 && p->nDeferredImmCons==0) goto jump_to_p2;
+		if (p->nFkConstraint == 0)
+			goto jump_to_p2;
 	}
 	break;
 }
diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
index 340ddc766..c31983f21 100644
--- a/src/box/sql/vdbe.h
+++ b/src/box/sql/vdbe.h
@@ -191,6 +191,29 @@ typedef struct VdbeOpList VdbeOpList;
  * for a description of what each of these routines does.
  */
 Vdbe *sqlite3VdbeCreate(Parse *);
+
+/**
+ * Allocate and initialize SQL-specific struct which completes
+ * original Tarantool's txn struct using region allocator.
+ *
+ * @retval NULL on OOM, new psql_txn struct on success.
+ **/
+struct sql_txn *
+sql_alloc_txn();
+
+/**
+ * Prepare given VDBE to execution: initialize structs connected
+ * with transaction routine: autocommit mode, deferred foreign
+ * keys counter, struct representing SQL savepoint.
+ * If execution context is already within active transaction,
+ * just transfer transaction data to VDBE.
+ *
+ * @param vdbe VDBE to be prepared.
+ * @retval -1 on out of memory, 0 otherwise.
+ */
+int
+sql_vdbe_prepare(struct Vdbe *vdbe);
+
 int sqlite3VdbeAddOp0(Vdbe *, int);
 int sqlite3VdbeAddOp1(Vdbe *, int, int);
 int sqlite3VdbeAddOp2(Vdbe *, int, int, int);
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 4b1767e24..3a907cd93 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -332,19 +332,6 @@ struct ScanStatus {
 	char *zName;		/* Name of table or index */
 };
 
-
-struct sql_txn {
-	Savepoint *pSavepoint;	/* List of active savepoints */
-	/*
-	 * This variables transfer deferred constraints from one
-	 * VDBE to the next in the same transaction.
-	 * We have to do it this way because some VDBE execute ddl and do not
-	 * have a transaction which disallows to always store this vars here.
-	 */
-	i64 nDeferredConsSave;
-	i64 nDeferredImmConsSave;
-};
-
 /*
  * An instance of the virtual machine.  This structure contains the complete
  * state of the virtual machine.
@@ -367,8 +354,6 @@ struct Vdbe {
 	int iStatement;		/* Statement number (or 0 if has not opened stmt) */
 	i64 iCurrentTime;	/* Value of julianday('now') for this statement */
 	i64 nFkConstraint;	/* Number of imm. FK constraints this VM */
-	i64 nStmtDefCons;	/* Number of def. constraints when stmt started */
-	i64 nStmtDefImmCons;	/* Number of def. imm constraints when stmt started */
 	uint32_t schema_ver;	/* Schema version at the moment of VDBE creation. */
 
 	/*
@@ -379,17 +364,10 @@ struct Vdbe {
 	 * ignoreRaised variable helps to track such situations
 	 */
 	u8 ignoreRaised;	/* Flag for ON CONFLICT IGNORE for triggers */
-
-	struct sql_txn *psql_txn; /* Data related to current transaction */
+	/** Data related to current transaction. */
+	struct sql_txn *psql_txn;
 	/** The auto-commit flag. */
 	bool auto_commit;
-	/*
-	 * `nDeferredCons` and `nDeferredImmCons` are stored in vdbe during
-	 * vdbe execution and moved to sql_txn when it needs to be saved until
-	 * execution of the next vdbe in the same transaction.
-	 */
-	i64 nDeferredCons;	/* Number of deferred fk violations */
-	i64 nDeferredImmCons;	/* Number of deferred imm fk. */
 
 	/* When allocating a new Vdbe object, all of the fields below should be
 	 * initialized to zero or NULL
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index d8cdefa7c..32af463f6 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -560,9 +560,6 @@ sqlite3Step(Vdbe * p)
 			db->u1.isInterrupted = 0;
 		}
 
-		assert(box_txn() ||
-		       (p->nDeferredCons == 0 && p->nDeferredImmCons == 0));
-
 #ifndef SQLITE_OMIT_TRACE
 		if ((db->xProfile || (db->mTrace & SQLITE_TRACE_PROFILE) != 0)
 		    && !db->init.busy && p->zSql) {
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index 775e1aae7..e0f665c37 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -65,17 +65,7 @@ sqlite3VdbeCreate(Parse * pParse)
 	db->pVdbe = p;
 	p->magic = VDBE_MAGIC_INIT;
 	p->pParse = pParse;
-	p->auto_commit = !box_txn();
 	p->schema_ver = box_schema_version();
-	if (!p->auto_commit) {
-		p->psql_txn = in_txn()->psql_txn;
-		p->nDeferredCons = p->psql_txn->nDeferredConsSave;
-		p->nDeferredImmCons = p->psql_txn->nDeferredImmConsSave;
-	} else{
-		p->psql_txn = NULL;
-		p->nDeferredCons = 0;
-		p->nDeferredImmCons = 0;
-	}
 	assert(pParse->aLabel == 0);
 	assert(pParse->nLabel == 0);
 	assert(pParse->nOpAlloc == 0);
@@ -83,6 +73,48 @@ sqlite3VdbeCreate(Parse * pParse)
 	return p;
 }
 
+struct sql_txn *
+sql_alloc_txn()
+{
+	struct sql_txn *txn = region_alloc_object(&fiber()->gc,
+						  struct sql_txn);
+	if (txn == NULL) {
+		diag_set(OutOfMemory, sizeof(struct sql_txn), "region",
+			 "struct sql_txn");
+		return NULL;
+	}
+	txn->pSavepoint = NULL;
+	txn->fk_deferred_count = 0;
+	return txn;
+}
+
+int
+sql_vdbe_prepare(struct Vdbe *vdbe)
+{
+	assert(vdbe != NULL);
+	struct txn *txn = in_txn();
+	vdbe->auto_commit = txn == NULL;
+	if (txn != NULL) {
+		/*
+		 * If transaction has been started in Lua, then
+		 * sql_txn is NULL. On the other hand, it is not
+		 * critical, since in Lua it is impossible to
+		 * check FK violations, at least now.
+		 */
+		if (txn->psql_txn == NULL) {
+			txn->psql_txn = sql_alloc_txn();
+			if (txn->psql_txn == NULL) {
+				sqlite3DbFree(vdbe->db, vdbe);
+				return -1;
+			}
+		}
+		vdbe->psql_txn = txn->psql_txn;
+	} else {
+		vdbe->psql_txn = NULL;
+	}
+	return 0;
+}
+
 /*
  * Change the error string stored in Vdbe.zErrMsg
  */
@@ -2439,11 +2471,8 @@ sqlite3VdbeCloseStatement(Vdbe * p, int eOp)
 	/*
 	 * If we have an anonymous transaction opened -> perform eOp.
 	 */
-	if (savepoint && eOp == SAVEPOINT_ROLLBACK) {
+	if (savepoint && eOp == SAVEPOINT_ROLLBACK)
 		rc = box_txn_rollback_to_savepoint(savepoint->tnt_savepoint);
-		p->nDeferredCons = savepoint->nDeferredCons;
-		p->nDeferredImmCons = savepoint->nDeferredImmCons;
-	}
 	p->anonymous_savepoint = NULL;
 	return rc;
 }
@@ -2462,9 +2491,9 @@ sqlite3VdbeCloseStatement(Vdbe * p, int eOp)
 int
 sqlite3VdbeCheckFk(Vdbe * p, int deferred)
 {
-	if ((deferred && (p->nDeferredCons + p->nDeferredImmCons) > 0)
-	    || (!deferred && p->nFkConstraint > 0)
-	    ) {
+	if ((deferred && p->psql_txn != NULL &&
+	     p->psql_txn->fk_deferred_count > 0) ||
+	    (!deferred && p->nFkConstraint > 0)) {
 		p->rc = SQLITE_CONSTRAINT_FOREIGNKEY;
 		p->errorAction = ON_CONFLICT_ACTION_ABORT;
 		sqlite3VdbeError(p, "FOREIGN KEY constraint failed");
@@ -2484,14 +2513,11 @@ sql_txn_begin(Vdbe *p)
 	struct txn *ptxn = txn_begin(false);
 	if (ptxn == NULL)
 		return -1;
-	ptxn->psql_txn = region_alloc_object(&fiber()->gc, struct sql_txn);
+	ptxn->psql_txn = sql_alloc_txn();
 	if (ptxn->psql_txn == NULL) {
 		box_txn_rollback();
-		diag_set(OutOfMemory, sizeof(struct sql_txn), "region",
-			 "struct sql_txn");
 		return -1;
 	}
-	memset(ptxn->psql_txn, 0, sizeof(struct sql_txn));
 	p->psql_txn = ptxn->psql_txn;
 	return 0;
 }
@@ -2518,9 +2544,7 @@ sql_savepoint(Vdbe *p, const char *zName)
 	if (zName) {
 		pNew->zName = (char *)&pNew[1];
 		memcpy(pNew->zName, zName, nName);
-	}
-	pNew->nDeferredCons = p->nDeferredCons;
-	pNew->nDeferredImmCons = p->nDeferredImmCons;
+	};
 	return pNew;
 }
 
@@ -2542,7 +2566,6 @@ sqlite3VdbeHalt(Vdbe * p)
 {
 	int rc;			/* Used to store transient return codes */
 	sqlite3 *db = p->db;
-	struct session *user_session = current_session();
 
 	/* This function contains the logic that determines if a statement or
 	 * transaction will be committed or rolled back as a result of the
@@ -2659,10 +2682,6 @@ sqlite3VdbeHalt(Vdbe * p)
 					sqlite3RollbackAll(p, SQLITE_OK);
 					p->nChange = 0;
 				} else {
-					p->nDeferredCons = 0;
-					p->nDeferredImmCons = 0;
-					user_session->sql_flags &=
-					    ~SQLITE_DeferFKs;
 					sqlite3CommitInternalChanges();
 				}
 			} else {
@@ -2741,7 +2760,7 @@ sqlite3VdbeHalt(Vdbe * p)
 		fiber_gc();
 
 	assert(db->nVdbeActive > 0 || box_txn() ||
-		       p->anonymous_savepoint == NULL);
+	       p->anonymous_savepoint == NULL);
 	return (p->rc == SQLITE_BUSY ? SQLITE_BUSY : SQLITE_OK);
 }
 
diff --git a/src/box/txn.c b/src/box/txn.c
index c5aaa8e0b..a97a04250 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -289,7 +289,18 @@ txn_commit(struct txn *txn)
 	assert(txn == in_txn());
 
 	assert(stailq_empty(&txn->stmts) || txn->engine);
-
+	/*
+	 * If transaction has been started in SQL, deferred
+	 * foreign key constraints must not be violated.
+	 * If not so, just rollback transaction.
+	 */
+	if (txn->psql_txn != NULL) {
+		struct sql_txn *sql_txn = txn->psql_txn;
+		if (sql_txn->fk_deferred_count != 0) {
+			diag_set(ClientError, ER_FOREIGN_KEY_CONSTRAINT);
+			goto fail;
+		}
+	}
 	/* Do transaction conflict resolving */
 	if (txn->engine) {
 		if (engine_prepare(txn->engine, txn) != 0)
@@ -458,6 +469,8 @@ box_txn_savepoint()
 	}
 	svp->stmt = stailq_last(&txn->stmts);
 	svp->in_sub_stmt = txn->in_sub_stmt;
+	if (txn->psql_txn != NULL)
+		svp->fk_deferred_count = txn->psql_txn->fk_deferred_count;
 	return svp;
 }
 
@@ -484,5 +497,7 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *svp)
 		return -1;
 	}
 	txn_rollback_to_svp(txn, svp->stmt);
+	if (txn->psql_txn != NULL)
+		txn->psql_txn->fk_deferred_count = svp->fk_deferred_count;
 	return 0;
 }
diff --git a/src/box/txn.h b/src/box/txn.h
index 9aedd9452..00cf34cdd 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -43,7 +43,7 @@ extern "C" {
 
 /** box statistics */
 extern struct rmean *rmean_box;
-struct sql_txn;
+struct Savepoint;
 
 struct engine;
 struct space;
@@ -95,10 +95,30 @@ struct txn_savepoint {
 	 * an empty transaction.
 	 */
 	struct stailq_entry *stmt;
+	/**
+	 * Each foreign key constraint is classified as either
+	 * immediate (by default) or deferred. In autocommit mode
+	 * they mean the same. Inside separate transaction,
+	 * deferred FK constraints are not checked until the
+	 * transaction tries to commit. For as long as
+	 * a transaction is open, it is allowed to exist in a
+	 * state violating any number of deferred FK constraints.
+	 */
+	uint32_t fk_deferred_count;
 };
 
 extern double too_long_threshold;
 
+struct sql_txn {
+	/** List of active SQL savepoints. */
+	struct Savepoint *pSavepoint;
+	/**
+	 * This variables transfer deferred constraints from one
+	 * VDBE to the next in the same transaction.
+	 */
+	uint32_t fk_deferred_count;
+};
+
 struct txn {
 	/**
 	 * A sequentially growing transaction id, assigned when
diff --git a/test/box/misc.result b/test/box/misc.result
index 4e437d88f..840abbb66 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -389,6 +389,7 @@ t;
   - 'box.error.LOCAL_INSTANCE_ID_IS_READ_ONLY : 128'
   - 'box.error.FUNCTION_EXISTS : 52'
   - 'box.error.UPDATE_ARG_TYPE : 26'
+  - 'box.error.FOREIGN_KEY_CONSTRAINT : 162'
   - 'box.error.CROSS_ENGINE_TRANSACTION : 81'
   - 'box.error.ACTION_MISMATCH : 160'
   - 'box.error.FORMAT_MISMATCH_INDEX_PART : 27'
diff --git a/test/sql/transitive-transactions.result b/test/sql/transitive-transactions.result
new file mode 100644
index 000000000..37b563a17
--- /dev/null
+++ b/test/sql/transitive-transactions.result
@@ -0,0 +1,126 @@
+test_run = require('test_run').new()
+---
+...
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+-- These tests are aimed at checking transitive transactions
+-- between SQL and Lua. In particular, make sure that deferred foreign keys
+-- violations are passed correctly.
+--
+box.begin() box.sql.execute('COMMIT');
+---
+...
+box.begin() box.sql.execute('ROLLBACK');
+---
+...
+box.sql.execute('BEGIN;') box.commit();
+---
+...
+box.sql.execute('BEGIN;') box.rollback();
+---
+...
+box.sql.execute('pragma foreign_keys = 1;');
+---
+...
+box.sql.execute('CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);');
+---
+...
+box.sql.execute('CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y) DEFERRABLE INITIALLY DEFERRED);');
+---
+...
+fk_violation_1 = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.sql.execute('COMMIT;')
+end;
+---
+...
+fk_violation_1();
+---
+- error: 'Can not commit transaction: deferred foreign keys violations are not resolved'
+...
+box.space.CHILD:select();
+---
+- []
+...
+fk_violation_2 = function()
+    box.sql.execute('BEGIN;')
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.commit()
+end;
+---
+...
+fk_violation_2();
+---
+- error: 'Can not commit transaction: deferred foreign keys violations are not resolved'
+...
+box.space.CHILD:select();
+---
+- []
+...
+fk_violation_3 = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.sql.execute('INSERT INTO parent VALUES (1, 1);')
+    box.commit()
+end;
+---
+...
+fk_violation_3();
+---
+...
+box.space.CHILD:select();
+---
+- - [1, 1]
+...
+box.space.PARENT:select();
+---
+- - [1, 1]
+...
+-- Make sure that 'PRAGMA defer_foreign_keys' works.
+--
+box.sql.execute('DROP TABLE child;')
+box.sql.execute('CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y))')
+
+fk_defer = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 2);')
+    box.sql.execute('INSERT INTO parent VALUES (2, 2);')
+    box.commit()
+end;
+---
+...
+fk_defer();
+---
+- error: FOREIGN KEY constraint failed
+...
+box.space.CHILD:select();
+---
+- []
+...
+box.space.PARENT:select();
+---
+- - [1, 1]
+...
+box.sql.execute('PRAGMA defer_foreign_keys = 1;')
+fk_defer();
+---
+...
+box.space.CHILD:select();
+---
+- - [1, 2]
+...
+box.space.PARENT:select();
+---
+- - [1, 1]
+  - [2, 2]
+...
+-- Cleanup
+box.sql.execute('DROP TABLE child;');
+---
+...
+box.sql.execute('DROP TABLE parent;');
+---
+...
diff --git a/test/sql/transitive-transactions.test.lua b/test/sql/transitive-transactions.test.lua
new file mode 100644
index 000000000..14a1e8c6b
--- /dev/null
+++ b/test/sql/transitive-transactions.test.lua
@@ -0,0 +1,65 @@
+test_run = require('test_run').new()
+test_run:cmd("setopt delimiter ';'")
+
+-- These tests are aimed at checking transitive transactions
+-- between SQL and Lua. In particular, make sure that deferred foreign keys
+-- violations are passed correctly.
+--
+
+box.begin() box.sql.execute('COMMIT');
+box.begin() box.sql.execute('ROLLBACK');
+box.sql.execute('BEGIN;') box.commit();
+box.sql.execute('BEGIN;') box.rollback();
+
+box.sql.execute('pragma foreign_keys = 1;');
+box.sql.execute('CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);');
+box.sql.execute('CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y) DEFERRABLE INITIALLY DEFERRED);');
+
+fk_violation_1 = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.sql.execute('COMMIT;')
+end;
+fk_violation_1();
+box.space.CHILD:select();
+
+fk_violation_2 = function()
+    box.sql.execute('BEGIN;')
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.commit()
+end;
+fk_violation_2();
+box.space.CHILD:select();
+
+fk_violation_3 = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 1);')
+    box.sql.execute('INSERT INTO parent VALUES (1, 1);')
+    box.commit()
+end;
+fk_violation_3();
+box.space.CHILD:select();
+box.space.PARENT:select();
+
+-- Make sure that 'PRAGMA defer_foreign_keys' works.
+--
+box.sql.execute('DROP TABLE child;')
+box.sql.execute('CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y))')
+
+fk_defer = function()
+    box.begin()
+    box.sql.execute('INSERT INTO child VALUES (1, 2);')
+    box.sql.execute('INSERT INTO parent VALUES (2, 2);')
+    box.commit()
+end;
+fk_defer();
+box.space.CHILD:select();
+box.space.PARENT:select();
+box.sql.execute('PRAGMA defer_foreign_keys = 1;')
+fk_defer();
+box.space.CHILD:select();
+box.space.PARENT:select();
+
+-- Cleanup
+box.sql.execute('DROP TABLE child;');
+box.sql.execute('DROP TABLE parent;');
-- 
2.15.1

