From 86e80544436407387e7503cc1d1c8811675c5d38 Mon Sep 17 00:00:00 2001
From: Ivan Koptelov <ivan.koptelov@tarantool.org>
Date: Mon, 18 Mar 2019 14:49:33 +0300
Subject: [PATCH] sql: add better collation determination in functions

Before the patch determination of collation in SQL functions
was too narrow: the arguments were scanned
from left to right, till the argument with collation was
encountered, then its collation was used.
Now every arguments collation is considered. The right collation
which would be used in function is determined using ANSI
compatibility rules ("type combination" rules).
---
 src/box/sql/expr.c          |  46 +++++++++++--
 src/box/sql/sqlInt.h        |   8 ++-
 test/sql-tap/func5.test.lua | 132 +++++++++++++++++++++++++++++++++++-
 3 files changed, 177 insertions(+), 9 deletions(-)

diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 838fbd21ad..88c4a9f537 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -4081,16 +4081,48 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 					testcase(i == 31);
 					constMask |= MASKBIT32(i);
 				}
-				if ((pDef->funcFlags & SQL_FUNC_NEEDCOLL) !=
-				    0 && coll == NULL) {
-					bool unused;
-					uint32_t id;
+			}
+			/*
+			 * Function arguments may have different
+			 * collations. The following code
+			 * checks if they are compatible and
+			 * finds the collation to be used. This
+			 * is done using ANSI rules from
+			 * collations_check_compatibility().
+			 */
+			if ((pDef->funcFlags & SQL_FUNC_NEEDCOLL) != 0) {
+				struct coll *unused = NULL;
+				uint32_t curr_id = COLL_NONE;
+				bool is_curr_forced = false;
+
+				uint32_t next_id = COLL_NONE;
+				bool is_next_forced = false;
+
+				if (sql_expr_coll(pParse, pFarg->a[0].pExpr,
+						  &is_curr_forced, &curr_id,
+						  &unused) != 0)
+					return 0;
+
+				for (int j = 1; j < nFarg; j++) {
 					if (sql_expr_coll(pParse,
-							  pFarg->a[i].pExpr,
-							  &unused, &id,
-							  &coll) != 0)
+							  pFarg->a[j].pExpr,
+							  &is_next_forced,
+							  &next_id,
+							  &unused) != 0)
 						return 0;
+
+					if (collations_check_compatibility(
+						curr_id, is_curr_forced,
+						next_id, is_next_forced,
+						&curr_id) != 0) {
+						pParse->is_aborted = true;
+						return 0;
+					}
+					is_curr_forced = curr_id == next_id ?
+							 is_next_forced :
+							 is_curr_forced;
 				}
+				coll = coll_by_id(curr_id)->coll;
 			}
 			if (pFarg) {
 				if (constMask) {
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 72bd4ee0fe..2c07492c14 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1661,7 +1661,13 @@ struct FuncDestructor {
 #define SQL_FUNC_LIKE     0x0004	/* Candidate for the LIKE optimization */
 #define SQL_FUNC_CASE     0x0008	/* Case-sensitive LIKE-type function */
 #define SQL_FUNC_EPHEM    0x0010	/* Ephemeral.  Delete with VDBE */
-#define SQL_FUNC_NEEDCOLL 0x0020	/* sqlGetFuncCollSeq() might be called */
+#define SQL_FUNC_NEEDCOLL 0x0020	/* sqlGetFuncCollSeq() might be called.
+					 * The flag is set when the collation
+					 * of function arguments should be
+					 * determined, using rules in
+					 * collations_check_compatibility()
+					 * function.
+					 */
 #define SQL_FUNC_LENGTH   0x0040	/* Built-in length() function */
 #define SQL_FUNC_TYPEOF   0x0080	/* Built-in typeof() function */
 #define SQL_FUNC_COUNT    0x0100	/* Built-in count(*) aggregate */
diff --git a/test/sql-tap/func5.test.lua b/test/sql-tap/func5.test.lua
index bb9e11a049..337bd3b949 100755
--- a/test/sql-tap/func5.test.lua
+++ b/test/sql-tap/func5.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(5)
+test:plan(15)
 
 --!./tcltestrunner.lua
 -- 2010 August 27
@@ -98,5 +98,135 @@ test:do_execsql_test(
         -- </func5-2.2>
     })
 
+-- The following tests ensures that max() and min() functions
+-- raise error if argument's collations are incompatible.
+
+test:do_catchsql_test(
+    "func-5-3.1",
+    [[
+        SELECT max('a' COLLATE "unicode", 'A' COLLATE "unicode_ci");
+    ]],
+    {
+        -- <func5-3.1>
+        1, "Illegal mix of collations"
+        -- </func5-3.1>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5-3.2",
+    [[
+        CREATE TABLE test1 (s1 VARCHAR(5) PRIMARY KEY COLLATE "unicode");
+        CREATE TABLE test2 (s2 VARCHAR(5) PRIMARY KEY COLLATE "unicode_ci");
+        INSERT INTO test1 VALUES ('a');
+        INSERT INTO test2 VALUES ('a');
+        SELECT max(s1, s2) FROM test1 JOIN test2;
+    ]],
+    {
+        -- <func5-3.2>
+        1, "Illegal mix of collations"
+        -- </func5-3.2>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5-3.3",
+    [[
+        SELECT max ('abc', 'asd' COLLATE "binary", 'abc' COLLATE "unicode")
+    ]],
+    {
+        -- <func5-3.3>
+        1, "Illegal mix of collations"
+        -- </func5-3.3>
+    }
+)
+
+test:do_execsql_test(
+    "func-5-3.4",
+    [[
+        SELECT max (s1, 'asd' COLLATE "binary", s2) FROM test1 JOIN test2;
+    ]], {
+        -- <func5-3.4>
+        "asd"
+        -- </func5-3.4>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5.3.5",
+    [[
+        CREATE TABLE test3 (s3 VARCHAR(5) PRIMARY KEY COLLATE "unicode");
+        CREATE TABLE test4 (s4 VARCHAR(5) PRIMARY KEY COLLATE "unicode");
+        CREATE TABLE test5 (s5 VARCHAR(5) PRIMARY KEY COLLATE "binary");
+        INSERT INTO test3 VALUES ('a');
+        INSERT INTO test4 VALUES ('a');
+        INSERT INTO test5 VALUES ('a');
+        SELECT max(s3, s4, s5) FROM test3 JOIN test4 JOIN test5;
+    ]],
+    {
+        -- <func5-3.5>
+        1, "Illegal mix of collations"
+        -- </func5-3.5>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5-3.6",
+    [[
+        SELECT min('a' COLLATE "unicode", 'A' COLLATE "unicode_ci");
+    ]],
+    {
+        -- <func5-3.6>
+        1, "Illegal mix of collations"
+        -- </func5-3.6>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5-3.7",
+    [[
+        SELECT min(s1, s2) FROM test1 JOIN test2;
+    ]],
+    {
+        -- <func5-3.7>
+        1, "Illegal mix of collations"
+        -- </func5-3.7>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5-3.8",
+    [[
+        SELECT min ('abc', 'asd' COLLATE "binary", 'abc' COLLATE "unicode")
+    ]],
+    {
+        -- <func5-3.8>
+        1, "Illegal mix of collations"
+        -- </func5-3.8>
+    }
+)
+
+test:do_execsql_test(
+    "func-5-3.9",
+    [[
+        SELECT min (s1, 'asd' COLLATE "binary", s2) FROM test1 JOIN test2;
+    ]], {
+        -- <func5-3.9>
+        "a"
+        -- </func5-3.9>
+    }
+)
+
+test:do_catchsql_test(
+    "func-5.3.10",
+    [[
+        SELECT min(s3, s4, s5) FROM test3 JOIN test4 JOIN test5;
+    ]],
+    {
+        -- <func5-3.10>
+        1, "Illegal mix of collations"
+        -- <func5-3.10>
+    }
+)
 
 test:finish_test()
-- 
GitLab