From 9c6c44835321a6331cd68080327899349083b25a Mon Sep 17 00:00:00 2001
From: AKhatskevich <avkhatskevich@tarantool.org>
Date: Mon, 8 Oct 2018 19:26:59 +0300
Subject: [PATCH] sql: fix fk set null clause

After changing behavior of the `IS` operator (#b3a3ddb571),
`SET NULL` was rewritten to use `EQ` instead. Which doesn't respect
NULLs.

This commit fixes the null related behavior by emitting logical
constructions equivalent for this case to old `IS`.
The new expression works differently than old `IS` for nulls, however
the difference doesn't change anything, because matched rows are then
searched in a child table with `EQ` expression which do not match nulls.
Before:
`oldval` old_is `newval`
Now:
`oldval` is_null or (`newval` is_not_null and `oldval` eq `newval`)

Closes #3645
---
 src/box/sql/fkey.c          | 43 +++++++++++++++++++++++++------------
 test/sql-tap/fkey1.test.lua | 25 ++++++++++++++++++++-
 2 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index 091778fc85..56885e448f 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -787,24 +787,39 @@ fkey_action_trigger(struct Parse *pParse, struct Table *pTab, struct fkey *fkey,
 
 		/*
 		 * For ON UPDATE, construct the next term of the
-		 * WHEN clause. The final WHEN clause will be like
+		 * WHEN clause, which should return false in case
+		 * there is a reason to for a broken constrant in
+		 * a parent table:
+		 *     no_action_needed := `oldval` IS NULL OR
+		 *         (`newval` IS NOT NULL AND
+		 *             `newval` = `oldval`)
+		 *
+		 * The final WHEN clause will be like
 		 * this:
 		 *
-		 *    WHEN NOT(old.col1 = new.col1 AND ... AND
-		 *             old.colN = new.colN)
+		 *    WHEN NOT( no_action_needed(col1) AND ...
+		 *        no_action_needed(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);
+			struct Expr *old_val = sqlite3PExpr(pParse, TK_DOT,
+				sqlite3ExprAlloc(db, TK_ID, &t_old, 0),
+				sqlite3ExprAlloc(db, TK_ID, &t_to_col, 0));
+			struct Expr *new_val = sqlite3PExpr(pParse, TK_DOT,
+				sqlite3ExprAlloc(db, TK_ID, &t_new, 0),
+				sqlite3ExprAlloc(db, TK_ID, &t_to_col, 0));
+			struct Expr *old_is_null = sqlite3PExpr(
+				pParse, TK_ISNULL,
+				sqlite3ExprDup(db, old_val, 0), NULL);
+			eq = sqlite3PExpr(pParse, TK_EQ, old_val,
+				sqlite3ExprDup(db, new_val, 0));
+			struct Expr *new_non_null =
+				sqlite3PExpr(pParse, TK_NOTNULL, new_val, NULL);
+			struct Expr *non_null_eq =
+				sqlite3PExpr(pParse, TK_AND, new_non_null, eq);
+			struct Expr *no_action_needed =
+				sqlite3PExpr(pParse, TK_OR, old_is_null,
+					     non_null_eq);
+			when = sqlite3ExprAnd(db, when, no_action_needed);
 		}
 
 		if (action != FKEY_ACTION_RESTRICT &&
diff --git a/test/sql-tap/fkey1.test.lua b/test/sql-tap/fkey1.test.lua
index 3c29b097df..b419e4d591 100755
--- a/test/sql-tap/fkey1.test.lua
+++ b/test/sql-tap/fkey1.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(18)
+test:plan(25)
 
 -- This file implements regression tests for foreign keys.
 
@@ -233,4 +233,27 @@ test:do_execsql_test(
         -- </fkey1-6.3>
     })
 
+-- gh-3645: update col=Null do not activates ON UPDATE trigger.
+
+test:do_select_tests(
+    "fkey1-7",
+    {
+        {"0",
+            [[
+                CREATE TABLE T12 (A INTEGER PRIMARY KEY,
+                    B CHAR(5) UNIQUE);
+                CREATE TABLE T13 (A INTEGER PRIMARY KEY,
+                    B CHAR(5) UNIQUE,
+                    FOREIGN KEY (B) REFERENCES T12 (B) ON UPDATE SET NULL);
+                INSERT INTO T12 VALUES (1,'a');
+                INSERT INTO T13 VALUES (1,'a');
+            ]], {}},
+        {"1", "UPDATE T12 SET B = NULL", {}},
+        {"2", "SELECT * FROM T12", {1, ""}},
+        {"3", "SELECT * FROM T13", {1, ""}},
+        {"4", "UPDATE T12 SET B = 'a'", {}},
+        {"5", "SELECT * FROM T12", {1, "a"}},
+        {"6", "SELECT * FROM T13", {1, ""}},
+    })
+
 test:finish_test()
-- 
GitLab