From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id 41F8628757 for ; Wed, 1 Aug 2018 14:14:47 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id hXoGn45Ze2kk for ; Wed, 1 Aug 2018 14:14:47 -0400 (EDT) Received: from mail-lj1-f194.google.com (mail-lj1-f194.google.com [209.85.208.194]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id 795BF2874D for ; Wed, 1 Aug 2018 14:14:46 -0400 (EDT) Received: by mail-lj1-f194.google.com with SMTP id s12-v6so17677758ljj.0 for ; Wed, 01 Aug 2018 11:14:46 -0700 (PDT) MIME-Version: 1.0 References: <20180718024314.be245cmsgklxuvnk@tkn_work_nb> <20180727130601.b2oby7dleapd5upg@tkn_work_nb> <20180727202219.ikwbax7tysfnmgr4@tkn_work_nb> <20180731134705.3pij4hwyyirhiwr7@tkn_work_nb> In-Reply-To: From: Nikita Tatunov Date: Wed, 1 Aug 2018 21:14:32 +0300 Message-ID: Subject: [tarantool-patches] Re: [PATCH] sql: LIKE & GLOB pattern comparison issue Content-Type: multipart/alternative; boundary="0000000000004d45fd057263abab" Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-help: List-unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-subscribe: List-owner: List-post: List-archive: To: avkhatskevich@tarantool.org Cc: Alexander Turenko , tarantool-patches@freelists.org --0000000000004d45fd057263abab Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable commit 5f2c0b8aa78d3638473a2a12b6d1059657144d58 Author: N.Tatunov Date: Thu Jun 28 15:17:32 2018 +0300 sql: LIKE & GLOB pattern comparison issue Currently function that compares pattern and string for GLOB & LIKE operators doesn't work properly. It uses ICU reading function which was assumed having other return codes and the implementation for the comparison ending isn't paying attention to some special cases, hence in those cases it works improperly. With the patch applied an error will be returned in case there's an invalid UTF-8 symbol in pattern & pattern containing only valid UTF-8 symbols will not be matched with the string that contains invalid symbol. =D0=A1loses #3251 =D0=A1loses #3334 Part of #3572 diff --git a/src/box/sql/func.c b/src/box/sql/func.c index c06e3bd..27c6f42 100644 --- a/src/box/sql/func.c +++ b/src/box/sql/func.c @@ -617,13 +617,16 @@ struct compareInfo { u8 noCase; /* true to ignore case differences */ }; -/* - * For LIKE and GLOB matching on EBCDIC machines, assume that every - * character is exactly one byte in size. Also, provde the Utf8Read() - * macro for fast reading of the next character in the common case where - * the next character is ASCII. +/** + * Providing there are symbols in string s this + * macro returns UTF-8 code of character and + * promotes pointer to the next symbol in the string. + * Otherwise return code is SQL_END_OF_STRING. */ -#define Utf8Read(s, e) ucnv_getNextUChar(pUtf8conv, &s, e, &status) +#define Utf8Read(s, e) ucnv_getNextUChar(pUtf8conv, &(s), (e), &(status)) + +#define SQL_END_OF_STRING 0xffff +#define SQL_INVALID_UTF8_SYMBOL 0xfffd static const struct compareInfo globInfo =3D { '*', '?', '[', 0 }; @@ -638,19 +641,16 @@ static const struct compareInfo likeInfoNorm =3D { '%= ', '_', 0, 1 }; static const struct compareInfo likeInfoAlt =3D { '%', '_', 0, 0 }; /* - * Possible error returns from patternMatch() + * Possible error returns from sql_utf8_pattern_compare() */ #define SQLITE_MATCH 0 #define SQLITE_NOMATCH 1 #define SQLITE_NOWILDCARDMATCH 2 +#define SQL_INVALID_PATTERN 3 -/* - * Compare two UTF-8 strings for equality where the first string is - * a GLOB or LIKE expression. Return values: - * - * SQLITE_MATCH: Match - * SQLITE_NOMATCH: No match - * SQLITE_NOWILDCARDMATCH: No match in spite of having * or % wildcards. +/** + * Compare two UTF-8 strings for equality where the first string + * is a GLOB or LIKE expression. * * Globbing rules: * @@ -663,92 +663,136 @@ static const struct compareInfo likeInfoAlt =3D { '%= ', '_', 0, 0 }; * * [^...] Matches one character not in the enclosed list. * - * With the [...] and [^...] matching, a ']' character can be included - * in the list by making it the first character after '[' or '^'. A - * range of characters can be specified using '-'. Example: - * "[a-z]" matches any single lower-case letter. To match a '-', make - * it the last character in the list. + * With the [...] and [^...] matching, a ']' character can be + * included in the list by making it the first character after + * '[' or '^'. A range of characters can be specified using '-'. + * Example: "[a-z]" matches any single lower-case letter. + * To match a '-', make it the last character in the list. * * Like matching rules: * - * '%' Matches any sequence of zero or more characters + * '%' Matches any sequence of zero or more characters. * - ** '_' Matches any one character + ** '_' Matches any one character. * * Ec Where E is the "esc" character and c is any other - * character, including '%', '_', and esc, match exactly c. + * character, including '%', '_', and esc, match + * exactly c. * * The comments within this routine usually assume glob matching. * - * This routine is usually quick, but can be N**2 in the worst case. + * This routine is usually quick, but can be N**2 in the worst + * case. + * + * @param pattern String containing comparison pattern. + * @param string String being compared. + * @param compareInfo Information about how to compare. + * @param matchOther The escape char (LIKE) or '[' (GLOB). + * + * @retval SQLITE_MATCH: Match. + * SQLITE_NOMATCH: No match. + * SQLITE_NOWILDCARDMATCH: No match in spite of having * + * or % wildcards. + * SQL_INVALID_PATTERN: Pattern contains invalid + * symbol. */ static int -patternCompare(const char * pattern, /* The glob pattern */ - const char * string, /* The string to compare against the glob */ - const struct compareInfo *pInfo, /* Information about how to do the compare */ - UChar32 matchOther /* The escape char (LIKE) or '[' (GLOB) */ - ) +sql_utf8_pattern_compare(const char *pattern, + const char *string, + const struct compareInfo *pInfo, + UChar32 matchOther) { - UChar32 c, c2; /* Next pattern and input string chars */ - UChar32 matchOne =3D pInfo->matchOne; /* "?" or "_" */ - UChar32 matchAll =3D pInfo->matchAll; /* "*" or "%" */ - UChar32 noCase =3D pInfo->noCase; /* True if uppercase=3D=3Dlowercase */ - const char *zEscaped =3D 0; /* One past the last escaped input char */ + /* Next pattern and input string chars */ + UChar32 c, c2; + /* "?" or "_" */ + UChar32 matchOne =3D pInfo->matchOne; + /* "*" or "%" */ + UChar32 matchAll =3D pInfo->matchAll; + /* True if uppercase=3D=3Dlowercase */ + UChar32 noCase =3D pInfo->noCase; + /* One past the last escaped input char */ + const char *zEscaped =3D 0; const char * pattern_end =3D pattern + strlen(pattern); const char * string_end =3D string + strlen(string); UErrorCode status =3D U_ZERO_ERROR; - while (pattern < pattern_end){ - c =3D Utf8Read(pattern, pattern_end); + while ((c =3D Utf8Read(pattern, pattern_end)) !=3D SQL_END_OF_STRING) { + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; if (c =3D=3D matchAll) { /* Match "*" */ - /* Skip over multiple "*" characters in the pattern. If there - * are also "?" characters, skip those as well, but consume a - * single character of the input string for each "?" skipped + /* Skip over multiple "*" characters in + * the pattern. If there are also "?" + * characters, skip those as well, but + * consume a single character of the + * input string for each "?" skipped. */ - while (pattern < pattern_end){ - c =3D Utf8Read(pattern, pattern_end); + while ((c =3D Utf8Read(pattern, pattern_end)) !=3D + SQL_END_OF_STRING) { + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; if (c !=3D matchAll && c !=3D matchOne) break; - if (c =3D=3D matchOne - && Utf8Read(string, string_end) =3D=3D 0) { + if (c =3D=3D matchOne && + (c2 =3D Utf8Read(string, string_end)) =3D=3D + SQL_END_OF_STRING) return SQLITE_NOWILDCARDMATCH; - } + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; } - /* "*" at the end of the pattern matches */ - if (pattern =3D=3D pattern_end) + /* + * "*" at the end of the pattern matches. + */ + if (c =3D=3D SQL_END_OF_STRING) { + while ((c2 =3D Utf8Read(string, string_end)) !=3D + SQL_END_OF_STRING) + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; return SQLITE_MATCH; + } if (c =3D=3D matchOther) { if (pInfo->matchSet =3D=3D 0) { c =3D Utf8Read(pattern, pattern_end); - if (c =3D=3D 0) + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; + if (c =3D=3D SQL_END_OF_STRING) return SQLITE_NOWILDCARDMATCH; } else { - /* "[...]" immediately follows the "*". We have to do a slow - * recursive search in this case, but it is an unusual case. + /* "[...]" immediately + * follows the "*". We + * have to do a slow + * recursive search in + * this case, but it is + * an unusual case. */ - assert(matchOther < 0x80); /* '[' is a single-byte character */ - while (string < string_end) { + assert(matchOther < 0x80); + while (c !=3D SQL_END_OF_STRING) { int bMatch =3D - patternCompare(&pattern[-1], - string, - pInfo, - matchOther); + sql_utf8_pattern_compare( + &pattern[-1], + string, + pInfo, + matchOther); if (bMatch !=3D SQLITE_NOMATCH) return bMatch; - Utf8Read(string, string_end); + c =3D Utf8Read(string, string_end); + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; } return SQLITE_NOWILDCARDMATCH; } } - /* At this point variable c contains the first character of the - * pattern string past the "*". Search in the input string for the - * first matching character and recursively continue the match from - * that point. + /* At this point variable c contains the + * first character of the pattern string + * past the "*". Search in the input + * string for the first matching + * character and recursively continue the + * match from that point. * - * For a case-insensitive search, set variable cx to be the same as - * c but in the other case and search the input string for either - * c or cx. + * For a case-insensitive search, set + * variable cx to be the same as c but in + * the other case and search the input + * string for either c or cx. */ int bMatch; @@ -756,14 +800,18 @@ patternCompare(const char * pattern, /* The glob pattern */ c =3D u_tolower(c); while (string < string_end){ /** - * This loop could have been implemented - * without if converting c2 to lower case - * (by holding c_upper and c_lower), however - * it is implemented this way because lower - * works better with German and Turkish - * languages. + * This loop could have been + * implemented without if + * converting c2 to lower case + * by holding c_upper and + * c_lower,however it is + * implemented this way because + * lower works better with German + * and Turkish languages. */ c2 =3D Utf8Read(string, string_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; if (!noCase) { if (c2 !=3D c) continue; @@ -771,9 +819,10 @@ patternCompare(const char * pattern, /* The glob pattern */ if (c2 !=3D c && u_tolower(c2) !=3D c) continue; } - bMatch =3D - patternCompare(pattern, string, - pInfo, matchOther); + bMatch =3D sql_utf8_pattern_compare(pattern, + string, + pInfo, + matchOther); if (bMatch !=3D SQLITE_NOMATCH) return bMatch; } @@ -782,7 +831,9 @@ patternCompare(const char * pattern, /* The glob pattern */ if (c =3D=3D matchOther) { if (pInfo->matchSet =3D=3D 0) { c =3D Utf8Read(pattern, pattern_end); - if (c =3D=3D 0) + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; + if (c =3D=3D SQL_END_OF_STRING) return SQLITE_NOMATCH; zEscaped =3D pattern; } else { @@ -790,23 +841,33 @@ patternCompare(const char * pattern, /* The glob pattern */ int seen =3D 0; int invert =3D 0; c =3D Utf8Read(string, string_end); + if (c =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; if (string =3D=3D string_end) return SQLITE_NOMATCH; c2 =3D Utf8Read(pattern, pattern_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; if (c2 =3D=3D '^') { invert =3D 1; c2 =3D Utf8Read(pattern, pattern_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; } if (c2 =3D=3D ']') { if (c =3D=3D ']') seen =3D 1; c2 =3D Utf8Read(pattern, pattern_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; } - while (c2 && c2 !=3D ']') { + while (c2 !=3D SQL_END_OF_STRING && c2 !=3D ']') { if (c2 =3D=3D '-' && pattern[0] !=3D ']' && pattern < pattern_end && prior_c > 0) { c2 =3D Utf8Read(pattern, pattern_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; if (c >=3D prior_c && c <=3D c2) seen =3D 1; prior_c =3D 0; @@ -817,29 +878,36 @@ patternCompare(const char * pattern, /* The glob pattern */ prior_c =3D c2; } c2 =3D Utf8Read(pattern, pattern_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQL_INVALID_PATTERN; } - if (pattern =3D=3D pattern_end || (seen ^ invert) =3D=3D 0) { + if (pattern =3D=3D pattern_end || + (seen ^ invert) =3D=3D 0) { return SQLITE_NOMATCH; } continue; } } c2 =3D Utf8Read(string, string_end); + if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL) + return SQLITE_NOMATCH; if (c =3D=3D c2) continue; if (noCase){ /** - * Small optimisation. Reduce number of calls - * to u_tolower function. - * SQL standards suggest use to_upper for symbol - * normalisation. However, using to_lower allows to - * respect Turkish '=C4=B0' in default locale. + * Small optimisation. Reduce number of + * calls to u_tolower function. SQL + * standards suggest use to_upper for + * symbol normalisation. However, using + * to_lower allows to respect Turkish '=C4=B0' + * in default locale. */ if (u_tolower(c) =3D=3D c2 || c =3D=3D u_tolower(c2)) continue; } - if (c =3D=3D matchOne && pattern !=3D zEscaped && c2 !=3D 0) + if (c =3D=3D matchOne && pattern !=3D zEscaped && + c2 !=3D SQL_END_OF_STRING) continue; return SQLITE_NOMATCH; } @@ -853,8 +921,7 @@ patternCompare(const char * pattern, /* The glob pattern */ int sqlite3_strglob(const char *zGlobPattern, const char *zString) { - return patternCompare(zGlobPattern, zString, &globInfo, - '['); + return sql_utf8_pattern_compare(zGlobPattern, zString, &globInfo, '['); } /* @@ -864,7 +931,7 @@ sqlite3_strglob(const char *zGlobPattern, const char *zString) int sqlite3_strlike(const char *zPattern, const char *zStr, unsigned int esc) { - return patternCompare(zPattern, zStr, &likeInfoNorm, esc); + return sql_utf8_pattern_compare(zPattern, zStr, &likeInfoNorm, esc); } /* @@ -910,8 +977,9 @@ likeFunc(sqlite3_context * context, int argc, sqlite3_value ** argv) zB =3D (const char *) sqlite3_value_text(argv[0]); zA =3D (const char *) sqlite3_value_text(argv[1]); - /* Limit the length of the LIKE or GLOB pattern to avoid problems - * of deep recursion and N*N behavior in patternCompare(). + /* Limit the length of the LIKE or GLOB pattern to avoid + * problems of deep recursion and N*N behavior in + * sql_utf8_pattern_compare(). */ nPat =3D sqlite3_value_bytes(argv[0]); testcase(nPat =3D=3D db->aLimit[SQLITE_LIMIT_LIKE_PATTERN_LENGTH]); @@ -947,7 +1015,12 @@ likeFunc(sqlite3_context * context, int argc, sqlite3_value ** argv) sqlite3_like_count++; #endif int res; - res =3D patternCompare(zB, zA, pInfo, escape); + res =3D sql_utf8_pattern_compare(zB, zA, pInfo, escape); + if (res =3D=3D SQL_INVALID_PATTERN) { + sqlite3_result_error(context, "LIKE or GLOB pattern can only" + " contain UTF-8 characters", -1); + return; + } sqlite3_result_int(context, res =3D=3D SQLITE_MATCH); } diff --git a/test/sql-tap/e_expr.test.lua b/test/sql-tap/e_expr.test.lua index 13d3a96..9780d2c 100755 --- a/test/sql-tap/e_expr.test.lua +++ b/test/sql-tap/e_expr.test.lua @@ -1,6 +1,6 @@ #!/usr/bin/env tarantool test =3D require("sqltester") -test:plan(12431) +test:plan(10665) --!./tcltestrunner.lua -- 2010 July 16 @@ -77,8 +77,10 @@ local operations =3D { {"<>", "ne1"}, {"!=3D", "ne2"}, {"IS", "is"}, - {"LIKE", "like"}, - {"GLOB", "glob"}, +-- NOTE: This test needs refactoring after deletion of GLOB & +-- type restrictions for LIKE. (See #3572) +-- {"LIKE", "like"}, +-- {"GLOB", "glob"}, {"AND", "and"}, {"OR", "or"}, {"MATCH", "match"}, @@ -96,7 +98,12 @@ operations =3D { {"+", "-"}, {"<<", ">>", "&", "|"}, {"<", "<=3D", ">", ">=3D"}, - {"=3D", "=3D=3D", "!=3D", "<>", "LIKE", "GLOB"}, --"MATCH", "REGEXP"}, +-- NOTE: This test needs refactoring after deletion of GLOB & +-- type restrictions for LIKE. (See #3572) +-- Another NOTE: MATCH & REGEXP aren't supported in Tarantool & +-- are waiting for their hour, don't confuse them +-- being commented with ticket above. + {"=3D", "=3D=3D", "!=3D", "<>"}, --"LIKE", "GLOB"}, --"MATCH", "REGEXP= "}, {"AND"}, {"OR"}, } @@ -475,6 +482,7 @@ for _, op in ipairs(oplist) do end end end + --------------------------------------------------------------------------= - -- Test the IS and IS NOT operators. -- diff --git a/test/sql-tap/gh-3251-string-pattern-comparison.test.lua b/test/sql-tap/gh-3251-string-pattern-comparison.test.lua new file mode 100755 index 0000000..addf0e3 --- /dev/null +++ b/test/sql-tap/gh-3251-string-pattern-comparison.test.lua @@ -0,0 +1,213 @@ +#!/usr/bin/env tarantool +test =3D require("sqltester") +test:plan(128) + +local prefix =3D "like-test-" + +local like_test_cases =3D +{ + {"1.1", + "SELECT 'AB' LIKE '_B';", + {0, {1}} }, + {"1.2", + "SELECT 'CD' LIKE '_B';", + {0, {0}} }, + {"1.3", + "SELECT '' LIKE '_B';", + {0, {0}} }, + {"1.4", + "SELECT 'AB' LIKE '%B';", + {0, {1}} }, + {"1.5", + "SELECT 'CD' LIKE '%B';", + {0, {0}} }, + {"1.6", + "SELECT '' LIKE '%B';", + {0, {0}} }, + {"1.7", + "SELECT 'AB' LIKE 'A__';", + {0, {0}} }, + {"1.8", + "SELECT 'CD' LIKE 'A__';", + {0, {0}} }, + {"1.9", + "SELECT '' LIKE 'A__';", + {0, {0}} }, + {"1.10", + "SELECT 'AB' LIKE 'A_';", + {0, {1}} }, + {"1.11", + "SELECT 'CD' LIKE 'A_';", + {0, {0}} }, + {"1.12", + "SELECT '' LIKE 'A_';", + {0, {0}} }, + {"1.13", + "SELECT 'AB' LIKE 'A';", + {0, {0}} }, + {"1.14", + "SELECT 'CD' LIKE 'A';", + {0, {0}} }, + {"1.15", + "SELECT '' LIKE 'A';", + {0, {0}} }, + {"1.16", + "SELECT 'AB' LIKE '_';", + {0, {0}} }, + {"1.17", + "SELECT 'CD' LIKE '_';", + {0, {0}} }, + {"1.18", + "SELECT '' LIKE '_';", + {0, {0}} }, + {"1.19", + "SELECT 'AB' LIKE '__';", + {0, {1}} }, + {"1.20", + "SELECT 'CD' LIKE '__';", + {0, {1}} }, + {"1.21", + "SELECT '' LIKE '__';", + {0, {0}} }, + {"1.22", + "SELECT 'AB' LIKE '%A';", + {0, {0}} }, + {"1.23", + "SELECT 'AB' LIKE '%C';", + {0, {0}} }, + {"1.24", + "SELECT '=D1=91=D1=84' LIKE '%=C5=93=D8=B4';", + {0, {0}} }, + {"1.25", + "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '%=C5=93=D8=B4';"= , + {0, {1}} }, + {"1.26", + "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '%=C5=93=D8=B4';", + {0, {1}} }, + {"1.27", + "SELECT '=D1=91=D1=84' LIKE '=D1=91_';", + {0, {1}} }, + {"1.28", + "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_';", + {0, {0}} }, + {"1.29", + "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_';", + {0, {0}} }, + {"1.30", + "SELECT '=D1=91=D1=84' LIKE '=D1=91=D1=84%';", + {0, {1}} }, + {"1.31", + "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91=D1=84%';"= , + {0, {1}} }, + {"1.32", + "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91=D1=84%';", + {0, {0}} }, + {"1.33", + "SELECT '=D1=91=D1=84' LIKE '=D1=91=D1=84=E2=84=AB%';", + {0, {0}} }, + {"1.34", + "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91=D1=84=E2= =84=AB%';", + {0, {1}} }, + {"1.35", + "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91=D1=84=D8=B4%';", + {0, {0}} }, + {"1.36", + "SELECT '=D1=91=D1=84' LIKE '=D1=91_%';", + {0, {1}} }, + {"1.37", + "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_%';", + {0, {1}} }, + {"1.38", + "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_%';", + {0, {0}} }, +} + +test:do_catchsql_set_test(like_test_cases, prefix) + +-- Non-Unicode byte sequences. +local invalid_testcases =3D { + '\xE2\x80', + '\xFE\xFF', + '\xC2', + '\xED\xB0\x80', + '\xD0', +} + +-- Invalid testcases. +for i, tested_string in ipairs(invalid_testcases) do + + -- We should raise an error in case + -- pattern contains invalid characters. + + local test_name =3D prefix .. "2." .. tostring(i) + local test_itself =3D "SELECT 'abc' LIKE 'ab" .. tested_string .. "';" + test:do_catchsql_test(test_name, test_itself, + {1, "LIKE or GLOB pattern can only contain UTF-8 characters"}) + + test_name =3D prefix .. "3." .. tostring(i) + test_itself =3D "SELECT 'abc' LIKE 'abc" .. tested_string .. "';" + test:do_catchsql_test(test_name, test_itself, + {1, "LIKE or GLOB pattern can only contain UTF-8 characters"}) + + test_name =3D prefix .. "4." .. tostring(i) + test_itself =3D "SELECT 'abc' LIKE 'ab" .. tested_string .. "c';" + test:do_catchsql_test(test_name, test_itself, + {1, "LIKE or GLOB pattern can only contain UTF-8 characters"}) + + -- Just skipping if row value predicand contains invalid character. + + test_name =3D prefix .. "5." .. tostring(i) + test_itself =3D "SELECT 'ab" .. tested_string .. "' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "6." .. tostring(i) + test_itself =3D "SELECT 'abc" .. tested_string .. "' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "7." .. tostring(i) + test_itself =3D "SELECT 'ab" .. tested_string .. "c' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) +end + +-- Unicode byte sequences. +local valid_testcases =3D { + '\x01', + '\x09', + '\x1F', + '\x7F', + '\xC2\x80', + '\xC2\x90', + '\xC2\x9F', + '\xE2\x80\xA8', + '\x20\x0B', + '\xE2\x80\xA9', +} + +-- Valid testcases. +for i, tested_string in ipairs(valid_testcases) do + test_name =3D prefix .. "8." .. tostring(i) + local test_itself =3D "SELECT 'abc' LIKE 'ab" .. tested_string .. "';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "9." .. tostring(i) + test_itself =3D "SELECT 'abc' LIKE 'abc" .. tested_string .. "';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "10." .. tostring(i) + test_itself =3D "SELECT 'abc' LIKE 'ab" .. tested_string .. "c';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "11." .. tostring(i) + test_itself =3D "SELECT 'ab" .. tested_string .. "' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "12." .. tostring(i) + test_itself =3D "SELECT 'abc" .. tested_string .. "' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) + + test_name =3D prefix .. "13." .. tostring(i) + test_itself =3D "SELECT 'ab" .. tested_string .. "c' LIKE 'abc';" + test:do_execsql_test(test_name, test_itself, {0}) +end + +test:finish_test() --0000000000004d45fd057263abab Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
commit 5f2c0b8aa78d3638473a2a12b6d1059657144d58<= /div>
Author: N.Tatunov <holl= ow653@gmail.com>
Date:=C2=A0 =C2=A0Thu Jun 28 15:17:32 201= 8 +0300

=C2=A0 =C2=A0 sql: LIKE & GLOB pattern= comparison issue
=C2=A0 =C2=A0=C2=A0
=C2=A0 =C2=A0 Cur= rently function that compares pattern and string for GLOB & LIKE
<= div>=C2=A0 =C2=A0 operators doesn't work properly. It uses ICU reading = function which
=C2=A0 =C2=A0 was assumed having other return code= s and the implementation for the
=C2=A0 =C2=A0 comparison ending = isn't paying attention to some special cases, hence
=C2=A0 = =C2=A0 in those cases it works improperly.
=C2=A0 =C2=A0=C2=A0
=C2=A0 =C2=A0 With the patch applied an error will be returned in c= ase there's an
=C2=A0 =C2=A0 invalid UTF-8 symbol in pattern = & pattern containing only valid UTF-8
=C2=A0 =C2=A0 symbols w= ill not be matched with the string that contains invalid
=C2=A0 = =C2=A0 symbol.
=C2=A0 =C2=A0=C2=A0
=C2=A0 =C2=A0 =D0=A1= loses #3251
=C2=A0 =C2=A0 =D0=A1loses #3334
=C2=A0 =C2= =A0 Part of #3572

diff --git a/src/box/sql/func.c = b/src/box/sql/func.c
index c06e3bd..27c6f42 100644
--- = a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -617,1= 3 +617,16 @@ struct compareInfo {
=C2=A0 u8 noCase; /* true t= o ignore case differences */
=C2=A0};
=C2=A0
= -/*
- * For LIKE and GLOB matching on EBCDIC machines, assume tha= t every
- * character is exactly one byte in size.=C2=A0 Also, pr= ovde the Utf8Read()
- * macro for fast reading of the next charac= ter in the common case where
- * the next character is ASCII.
+/**
+ * Providing there are symbols in string s this
+ * macro returns UTF-8 code of character and
+ * promotes = pointer to the next symbol in the string.
+ * Otherwise return co= de is SQL_END_OF_STRING.
=C2=A0 */
-#define Utf8Read(s,= e)=C2=A0 =C2=A0 ucnv_getNextUChar(pUtf8conv, &s, e, &status)
=
+#define Utf8Read(s, e) ucnv_getNextUChar(pUtf8conv, &(s), (e), &a= mp;(status))
+
+#define SQL_END_OF_STRING=C2=A0 =C2=A0 = =C2=A0 =C2=A0 0xffff
+#define SQL_INVALID_UTF8_SYMBOL=C2=A0 0xfff= d
=C2=A0
=C2=A0static const struct compareInfo globInfo= =3D { '*', '?', '[', 0 };
=C2=A0
@@ -638,19 +641,16 @@ static const struct compareInfo likeInfoNorm =3D {= '%', '_', 0, 1 };
=C2=A0static const struct comp= areInfo likeInfoAlt =3D { '%', '_', 0, 0 };
=C2= =A0
=C2=A0/*
- * Possible error returns from patternMat= ch()
+ * Possible error returns from sql_utf8_pattern_compare()
=C2=A0 */
=C2=A0#define SQLITE_MATCH=C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A00
=C2=A0#define SQLITE_NOMATCH=C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A01
=C2=A0#define SQLITE_NOWILDCA= RDMATCH=C2=A0 =C2=A02
+#define SQL_INVALID_PATTERN=C2=A0 =C2=A0 = =C2=A0 3
=C2=A0
-/*
- * Compare two UTF-8 str= ings for equality where the first string is
- * a GLOB or LIKE ex= pression.=C2=A0 Return values:
- *
- *=C2=A0 =C2=A0 SQL= ITE_MATCH:=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Match
- *=C2= =A0 =C2=A0 SQLITE_NOMATCH:=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 No match
=
- *=C2=A0 =C2=A0 SQLITE_NOWILDCARDMATCH:=C2=A0 No match in spite of ha= ving * or % wildcards.
+/**
+ * Compare two UTF-8 strin= gs for equality where the first string
+ * is a GLOB or LIKE expr= ession.
=C2=A0 *
=C2=A0 * Globbing rules:
=C2= =A0 *
@@ -663,92 +663,136 @@ static const struct compareInfo like= InfoAlt =3D { '%', '_', 0, 0 };
=C2=A0 *
=C2=A0 *=C2=A0 =C2=A0 =C2=A0[^...]=C2=A0 =C2=A0 =C2=A0Matches one charact= er not in the enclosed list.
=C2=A0 *
- * With the [...= ] and [^...] matching, a ']' character can be included
- = * in the list by making it the first character after '[' or '^&= #39;.=C2=A0 A
- * range of characters can be specified using '= ;-'.=C2=A0 Example:
- * "[a-z]" matches any single = lower-case letter.=C2=A0 To match a '-', make
- * it the = last character in the list.
+ * With the [...] and [^...] matchin= g, a ']' character can be
+ * included in the list by mak= ing it the first character after
+ * '[' or '^'. = A range of characters can be specified using '-'.
+ * Exa= mple: "[a-z]" matches any single lower-case letter.
+ *= To match a '-', make it the last character in the list.
= =C2=A0 *
=C2=A0 * Like matching rules:
=C2=A0 *
- *=C2=A0 =C2=A0 =C2=A0 '%'=C2=A0 =C2=A0 =C2=A0 =C2=A0Matches an= y sequence of zero or more characters
+ *=C2=A0 =C2=A0 =C2=A0 = 9;%'=C2=A0 =C2=A0 =C2=A0 =C2=A0Matches any sequence of zero or more cha= racters.
=C2=A0 *
- **=C2=A0 =C2=A0 =C2=A0'_'= =C2=A0 =C2=A0 =C2=A0 =C2=A0Matches any one character
+ **=C2=A0 = =C2=A0 =C2=A0'_'=C2=A0 =C2=A0 =C2=A0 =C2=A0Matches any one characte= r.
=C2=A0 *
=C2=A0 *=C2=A0 =C2=A0 =C2=A0 Ec=C2=A0 =C2= =A0 =C2=A0 =C2=A0 Where E is the "esc" character and c is any oth= er
- *=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 cha= racter, including '%', '_', and esc, match exactly c.
=
+ *=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 character, = including '%', '_', and esc, match
+ *=C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 exactly c.
=C2=A0 *=
=C2=A0 * The comments within this routine usually assume glob ma= tching.
=C2=A0 *
- * This routine is usually quick, but= can be N**2 in the worst case.
+ * This routine is usually quick= , but can be N**2 in the worst
+ * case.
+ *
= + * @param pattern String containing comparison pattern.
+ * @par= am string String being compared.
+ * @param compareInfo Informati= on about how to compare.
+ * @param matchOther The escape char (L= IKE) or '[' (GLOB).
+ *
+ * @retval SQLITE_MATC= H:=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Match.
+ * =C2=A0 =C2=A0SQLITE_NOMATCH:=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 No match.
+ * <= /span>=C2=A0 =C2=A0SQLITE_NOWILDCARDMATCH:=C2=A0 No match in spite of havin= g *
+ * =C2=A0 =C2=A0 o= r % wildcards.
+ * =C2=A0 = =C2=A0SQL_INVALID_PATTERN:=C2=A0 =C2=A0 =C2=A0Pattern contains invalid
+ * =C2=A0 =C2=A0 symbol.
=C2=A0 */
=C2=A0static int
-patternCompare(con= st char * pattern, /* The glob patte= rn */
- =C2=A0 =C2=A0 =C2= =A0 =C2=A0const char * string, /* Th= e string to compare against the glob */
- =C2=A0 =C2=A0 =C2=A0 =C2=A0const struct compareInfo *pInfo,= /* Information about how to do the = compare */
- =C2=A0 =C2=A0= =C2=A0 =C2=A0UChar32 matchOther /* = The escape char (LIKE) or '[' (GLOB) */
-=C2=A0 =C2=A0 )<= /div>
+sql_utf8_pattern_compare(const char *pattern,
+ const char *string,
+ const struct compareInfo *pInfo,
+ UChar32 matchOther)
=C2=A0{
- UChar32 c, c2;= /* Next pattern and input string c= hars */
- UChar32 matchOne= =3D pInfo->matchOne; /* "?&= quot; or "_" */
- UChar32 matchAll =3D pInfo->matchAll; = /* "*" or "%" */
- UChar32 noCase =3D pInfo->noCase; /* True if uppercase=3D=3Dlowercase */
- const char *zEscaped =3D 0; /* One past the last escaped input char */
+ /* Next pattern and input s= tring chars */
+ UChar32 c= , c2;
+ /* "?" o= r "_" */
+ UChar= 32 matchOne =3D pInfo->matchOne;
+ /* "*" or "%" */
+ UChar32 matchAll =3D pInfo->matchAll;
+ /* True if uppercase=3D=3Dlowerca= se */
+ UChar32 noCase =3D= pInfo->noCase;
+ /* On= e past the last escaped input char */
+ const char *zEscaped =3D 0;
=C2=A0 const char * pattern_end =3D pattern + strlen(patter= n);
=C2=A0 const char * st= ring_end =3D string + strlen(string);
=C2=A0 UErrorCode status =3D U_ZERO_ERROR;
=C2=A0
- while (pattern < pattern= _end){
- c =3D Utf8Read(p= attern, pattern_end);
+ wh= ile ((c =3D Utf8Read(pattern, pattern_end)) !=3D SQL_END_OF_STRING) {
=
+ if (c =3D=3D SQL_INVALID_UTF= 8_SYMBOL)
+ return SQL_I= NVALID_PATTERN;
=C2=A0 if= (c =3D=3D matchAll) { /* Match &quo= t;*" */
- /* Skip o= ver multiple "*" characters in the pattern.=C2=A0 If there
<= div>- * are also "?" ch= aracters, skip those as well, but consume a
- * single character of the input string for each &quo= t;?" skipped
+ /* S= kip over multiple "*" characters in
+ * the pattern. If there are also "?"
+ * characters, skip those= as well, but
+ * consu= me a single character of the
+ = * input string for each "?" skipped.
=C2=A0 */
- while (pattern < pattern_end){
- c =3D Utf8Read(pattern, pattern_end);
+ while ((c =3D Utf8Read(pat= tern, pattern_end)) !=3D
+ =C2=A0 =C2=A0 =C2=A0 =C2=A0SQL_END_OF_STRING) {
+ if (c =3D=3D SQL_INVALID_UTF8_SYMBOL)
=
+ return SQL_INVALID_PATTER= N;
=C2=A0 if (c !=3D ma= tchAll && c !=3D matchOne)
=C2=A0 break;
- <= /span>if (c =3D=3D matchOne
- = =C2=A0 =C2=A0 && Utf8Read(string, string_end) =3D=3D 0) {
+ if (c =3D=3D matchOne &= amp;&
+ =C2=A0 =C2= =A0 (c2 =3D Utf8Read(string, string_end)) =3D=3D
+ =C2=A0 =C2=A0 SQL_END_OF_STRING)
=C2= =A0 return SQLITE_NOWILDCARDMATC= H;
- }
+ if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL= )
+ return SQLITE_NOMA= TCH;
=C2=A0 }
= - /* "*" at the end of t= he pattern matches */
- = if (pattern =3D=3D pattern_end)
+= /*
+ * "= *" at the end of the pattern matches.
+ */
+ if (c =3D=3D SQL_END_OF_STRING) {
+ while ((c2 =3D Utf8Read(string, string_end)) !=3D
+ =C2=A0 =C2=A0 =C2=A0 =C2=A0SQL= _END_OF_STRING)
+ if (= c2 =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQLITE_NOMATCH;
=C2=A0 return SQLITE_MATCH;
+ }
=C2=A0 = if (c =3D=3D matchOther) {
=C2=A0 if (pInfo->matchSet =3D=3D 0) {
=C2=A0 c =3D Utf8Read(pattern, pattern_end);=
- if (c =3D=3D 0)
+ if (c =3D=3D SQL_INVALI= D_UTF8_SYMBOL)
+ retu= rn SQL_INVALID_PATTERN;
+ if (c =3D=3D SQL_END_OF_STRING)
=C2=A0 return SQLITE_NOWILDCARDMATCH;
=C2=A0 } else {
- /* "[...]" immediately follows the &quo= t;*".=C2=A0 We have to do a slow
- * recursive search in this case, but it is an unusual ca= se.
+ /* "[...]&q= uot; immediately
+ * = follows the "*". We
+ = * have to do a slow
+ = * recursive search in
+ * this case, but it is
+ * an unusual case.
=C2=A0 */
- = assert(matchOther < 0x80); /* '[' is a single-byte character */
- while (string < string_end) {
+ assert(matchOther < 0x80);
<= div>+ while (c !=3D SQL_END_OF_S= TRING) {
=C2=A0 int b= Match =3D
- =C2=A0 = =C2=A0 patternCompare(&pattern[-1],
- =C2=A0 =C2=A0string,
- =C2=A0 =C2=A0pInfo,
- =C2=A0 =C2=A0matchOther);
+ =C2=A0 =C2=A0 sql_utf8_pattern_compare= (
+ &pattern[-1= ],
+ string,
<= div>+ pInfo,
+ matchOther);
=C2=A0 if (bMatch !=3D SQLITE_NOMATCH)
=C2=A0 return bMatch;<= /div>
- Utf8Read(string, st= ring_end);
+ c =3D Ut= f8Read(string, string_end);
+ = if (c =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQLITE_NOMATCH;
=C2=A0 }
=C2=A0 return SQLITE_NOWILDCARDMATCH;
=C2=A0 }
=C2=A0 }
=C2=A0
- /* At this point variable c contains the first charact= er of the
- * pattern s= tring past the "*".=C2=A0 Search in the input string for the
- * first matching characte= r and recursively continue the match from
- * that point.
+ /* At this point variable c contains the
+ * first character of the pattern string
+ * past the "*"= ;. Search in the input
+ * string for the first matching
+ * character and recursively continue the
+ * match from that point.
=C2=A0<= span style=3D"white-space:pre"> *
- * For a case-insensitive search, set variable cx to = be the same as
- * c bu= t in the other case and search the input string for either
- * c or cx.
+ * For a case-insensitive search, set
= + * variable cx to be the same as= c but in
+ * the other= case and search the input
+ * string for either c or cx.
=C2=A0 */
=C2=A0
=C2=A0 int bMatch;
@@ -756,14 +800,18 @@ patternCo= mpare(const char * pattern, /* The g= lob pattern */
=C2=A0 c= =3D u_tolower(c);
=C2=A0 while (string < string_end){
=C2=A0 /**
- * This loop could have been implemented
- * without if converting c2 to lower case
-<= span style=3D"white-space:pre"> * (by holding c_upper and c_lowe= r), however
- * it is = implemented this way because lower
- * works better with German and Turkish
- * languages.
+ * This loop could have been
+ * implemented without if
+ * converting c2 to lower case
=
+ * by holding c_upper and<= /div>
+ * c_lower,however it= is
+ * implemented th= is way because
+ * low= er works better with German
+ = * and Turkish languages.
=C2=A0 */
=C2=A0 c2 =3D Utf8Read(string, string_end);
+ if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQLITE_NOMATCH;
= =C2=A0 if (!noCase) {
= =C2=A0 if (c2 !=3D c)
= =C2=A0 continue;
@@ -= 771,9 +819,10 @@ patternCompare(const char * pattern, /* The glob pattern */
=C2=A0 if (c2 !=3D c && u_tolower(c2) !=3D c)
=C2=A0 continue;
<= div>=C2=A0 }
- bMatch =3D
- =C2=A0 =C2=A0 patternCompare(pattern, string,
=
- =C2=A0 =C2=A0pInfo, matc= hOther);
+ bMatch =3D s= ql_utf8_pattern_compare(pattern,
+ =C2=A0 string,
+ = =C2=A0 pInfo,
+ = =C2=A0 matchOther);
=C2=A0 if (bMatch !=3D SQLITE_NOMATCH)
=C2=A0 return bMatch;
=C2=A0 }
@@ -782,7 +831,9 @@ patternCompa= re(const char * pattern, /* The glob= pattern */
=C2=A0 if (c = =3D=3D matchOther) {
=C2=A0 if (pInfo->matchSet =3D=3D 0) {
=C2=A0 c =3D Utf8Read(pattern, pattern_end);
- if (c =3D=3D 0)
+ if (c =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQL_INVALID_PAT= TERN;
+ if (c =3D=3D SQ= L_END_OF_STRING)
=C2=A0 return SQLITE_NOMATCH;
=C2=A0 = zEscaped =3D pattern;
=C2=A0 } else {
@@ -790,23 +841,33 @@ patternCompare(const= char * pattern, /* The glob pattern= */
=C2=A0 int seen =3D= 0;
=C2=A0 int invert = =3D 0;
=C2=A0 c =3D Utf= 8Read(string, string_end);
+ <= /span>if (c =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQLITE_NOMATCH;
=C2=A0 if (string =3D=3D string_end)
=C2= =A0 return SQLITE_NOMATCH;
=
=C2=A0 c2 =3D Utf8Read(patte= rn, pattern_end);
+ if = (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQL_INVALID_PATTERN;
=C2=A0 if (c2 =3D=3D '^') {
=C2=A0 invert =3D 1;
=C2=A0 c2 =3D Utf8Read(pattern, pattern_e= nd);
+ if (c2 =3D=3D S= QL_INVALID_UTF8_SYMBOL)
+ return SQL_INVALID_PATTERN;
=C2=A0 }
=C2=A0 if (c2 =3D=3D ']') {
=C2=A0 if (c =3D=3D ']')
=C2=A0 seen =3D 1;
=C2=A0 c2 =3D Utf8Read(pattern, pattern_end);
= + if (c2 =3D=3D SQL_INVALID_UTF8= _SYMBOL)
+ return SQL= _INVALID_PATTERN;
=C2=A0 }
- while (c2 &&a= mp; c2 !=3D ']') {
+ <= /span>while (c2 !=3D SQL_END_OF_STRING && c2 !=3D ']') {
=C2=A0 if (c2 =3D=3D = 9;-' && pattern[0] !=3D ']'
=C2=A0 =C2=A0 =C2=A0 && pattern < patt= ern_end
=C2=A0 =C2=A0 = =C2=A0 && prior_c > 0) {
=C2=A0 c2 =3D Utf8Read(pattern, pattern_end);
+ if (c2 =3D=3D SQL_INVALID_UTF8_SY= MBOL)
+ return SQL_I= NVALID_PATTERN;
=C2=A0 if (c >=3D prior_c && c <=3D c2)
=C2=A0 seen =3D 1;
=C2=A0 prior_c =3D 0;
@@ -817,29 +878,= 36 @@ patternCompare(const char * pattern, = /* The glob pattern */
=C2=A0 prior_c =3D c2;
=C2=A0 }
=C2=A0 c2 =3D Utf8Read(pattern, pattern_end);
+ if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQL_INVALID_PATTERN;
=C2=A0 }
- if (pattern =3D=3D pattern_end || (seen= ^ invert) =3D=3D 0) {
+ if (pattern =3D=3D pattern_end ||
+ =C2=A0 =C2=A0 (seen ^ invert) =3D=3D 0) {
=C2=A0 return SQLITE_NOMATCH;
= =C2=A0 }
=C2=A0 continue;
=C2=A0 }
=C2=A0 }
=C2=A0 c2 = =3D Utf8Read(string, string_end);
+ if (c2 =3D=3D SQL_INVALID_UTF8_SYMBOL)
+ return SQLITE_NOMATCH;
=C2=A0 if (c =3D=3D c2)
=C2=A0 continue;
=C2=A0 if (noCase){
=C2=A0 /**
- * Small optimisation. Reduce number of calls
- * to u_tolower function.
- * SQL standards suggest use to_upper for sy= mbol
- * normalisation.= However, using to_lower allows to
- * respect Turkish '=C4=B0' in default locale.
+ * Small optimisation. Reduce= number of
+ * calls to= u_tolower function. SQL
+ * standards suggest use to_upper for
+ * symbol normalisation. However, using
+ * to_lower allows to respect Turkish = '=C4=B0'
+ * in= default locale.
=C2=A0 = */
=C2=A0 if (u_tolower= (c) =3D=3D c2 ||
=C2=A0 = =C2=A0 =C2=A0 c =3D=3D u_tolower(c2))
=C2=A0 continue;
=C2=A0 }
- if (c = =3D=3D matchOne && pattern !=3D zEscaped && c2 !=3D 0)
+ if (c =3D=3D matchOne &= & pattern !=3D zEscaped &&
+ =C2=A0 =C2=A0 c2 !=3D SQL_END_OF_STRING)
=C2=A0 continue;
=C2=A0 return SQLITE_NOMATCH;
=C2=A0 }
@@ -853,8 +921,7 @@ pattern= Compare(const char * pattern, /* The= glob pattern */
=C2=A0int
=C2=A0sqlite3_strglob(const = char *zGlobPattern, const char *zString)
=C2=A0{
- return patternCompare(zGlobPattern, zStr= ing, &globInfo,
- = =C2=A0 =C2=A0 =C2=A0 '[');
+ return sql_utf8_pattern_compare(zGlobPattern, zString, &glob= Info, '[');
=C2=A0}
=C2=A0
=C2=A0/*
@@ -864,7 +931,7 @@ sqlite3_strglob(const char *zGlobPattern, cons= t char *zString)
=C2=A0int
=C2=A0sqlite3_strlike(const = char *zPattern, const char *zStr, unsigned int esc)
=C2=A0{
=
- return patternCompare(zPatter= n, zStr, &likeInfoNorm, esc);
+ return sql_utf8_pattern_compare(zPattern, zStr, &likeInfoNorm= , esc);
=C2=A0}
=C2=A0
=C2=A0/*
@@ = -910,8 +977,9 @@ likeFunc(sqlite3_context * context, int argc, sqlite3_valu= e ** argv)
=C2=A0 zB =3D (= const char *) sqlite3_value_text(argv[0]);
=C2=A0 zA =3D (const char *) sqlite3_value_text(argv[1]);<= /div>
=C2=A0
- /* Limi= t the length of the LIKE or GLOB pattern to avoid problems
- * of deep recursion and N*N behavior in= patternCompare().
+ /* Li= mit the length of the LIKE or GLOB pattern to avoid
+ * problems of deep recursion and N*N behavior= in
+ * sql_utf8_pattern_= compare().
=C2=A0 */
=C2=A0 nPat =3D sqlite3_value_= bytes(argv[0]);
=C2=A0 tes= tcase(nPat =3D=3D db->aLimit[SQLITE_LIMIT_LIKE_PATTERN_LENGTH]);
@@ -947,7 +1015,12 @@ likeFunc(sqlite3_context * context, int argc, sqli= te3_value ** argv)
=C2=A0 = sqlite3_like_count++;
=C2=A0#endif
=C2=A0 int res;
- res =3D patternCompare(zB, zA, pInfo, escape);
+ res =3D sql_utf8_pattern_compare(zB, zA, = pInfo, escape);
+ if (res = =3D=3D SQL_INVALID_PATTERN) {
+ = sqlite3_result_error(context, "LIKE or GLOB pattern can only&qu= ot;
+ =C2=A0 =C2=A0 =C2= =A0" contain UTF-8 characters", -1);
+ return;
+= }
=C2=A0 sqlite3_r= esult_int(context, res =3D=3D SQLITE_MATCH);
=C2=A0}
= =C2=A0
diff --git a/test/sql-tap/e_expr.test.lua b/test/sql-tap/e= _expr.test.lua
index 13d3a96..9780d2c 100755
--- a/test= /sql-tap/e_expr.test.lua
+++ b/test/sql-tap/e_expr.test.lua
=
@@ -1,6 +1,6 @@
=C2=A0#!/usr/bin/env tarantool
=C2= =A0test =3D require("sqltester")
-test:plan(12431)
+test:plan(10665)
=C2=A0
=C2=A0--!./tcltestrunner= .lua
=C2=A0-- 2010 July 16
@@ -77,8 +77,10 @@ local ope= rations =3D {
=C2=A0 =C2=A0 =C2=A0{"<>", "ne= 1"},
=C2=A0 =C2=A0 =C2=A0{"!=3D", "ne2"}= ,
=C2=A0 =C2=A0 =C2=A0{"IS", "is"},
-=C2=A0 =C2=A0 {"LIKE", "like"},
-=C2=A0 =C2= =A0 {"GLOB", "glob"},
+-- NOTE: This test nee= ds refactoring after deletion of GLOB &
+-- type restrictions for LIKE. (See #3572)
+-= -=C2=A0 =C2=A0 {"LIKE", "like"},
+--=C2=A0 = =C2=A0 {"GLOB", "glob"},
=C2=A0 =C2=A0 =C2=A0= {"AND", "and"},
=C2=A0 =C2=A0 =C2=A0{"OR= ", "or"},
=C2=A0 =C2=A0 =C2=A0{"MATCH", = "match"},
@@ -96,7 +98,12 @@ operations =3D {
=C2=A0 =C2=A0 =C2=A0{"+", "-"},
=C2=A0 =C2= =A0 =C2=A0{"<<", ">>", "&", &= quot;|"},
=C2=A0 =C2=A0 =C2=A0{"<", "<= =3D", ">", ">=3D"},
-=C2=A0 =C2=A0= {"=3D", "=3D=3D", "!=3D", "<>&quo= t;, "LIKE", "GLOB"}, --"MATCH", "REGEXP&= quot;},
+-- NOTE: This test needs refactoring after deletion of G= LOB &
+-- type restri= ctions for LIKE. (See #3572)
+-- Another NOTE: MATCH & REGEXP= aren't supported in Tarantool &
+-- are waiting for their hour, don't confuse them
+-- being commented with t= icket above.
+=C2=A0 =C2=A0 {"=3D", "=3D=3D",= "!=3D", "<>"}, --"LIKE", "GLOB&qu= ot;}, --"MATCH", "REGEXP"},
=C2=A0 =C2=A0 =C2= =A0{"AND"},
=C2=A0 =C2=A0 =C2=A0{"OR"},
=
=C2=A0}
@@ -475,6 +482,7 @@ for _, op in ipairs(oplist) do
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0end
=C2=A0 =C2=A0 =C2= =A0end
=C2=A0end
+
=C2=A0--------------------= -------------------------------------------------------
=C2=A0-- = Test the IS and IS NOT operators.
=C2=A0--
diff --git a= /test/sql-tap/gh-3251-string-pattern-comparison.test.lua b/test/sql-tap/gh-= 3251-string-pattern-comparison.test.lua
new file mode 100755
index 0000000..addf0e3
--- /dev/null
+++ b/test/s= ql-tap/gh-3251-string-pattern-comparison.test.lua
@@ -0,0 +1,213 = @@
+#!/usr/bin/env tarantool
+test =3D require("sq= ltester")
+test:plan(128)
+
+local prefi= x =3D "like-test-"
+
+local like_test_cases = =3D
+{
+=C2=A0 =C2=A0 {"1.1",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'AB' LIKE '_B';"= ,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2= =A0 {"1.2",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT &= #39;CD' LIKE '_B';",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.3",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '' LIKE '_B';",<= /div>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0= {"1.4",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '= ;AB' LIKE '%B';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {= 0, {1}} },
+=C2=A0 =C2=A0 {"1.5",
+=C2=A0 =C2= =A0 =C2=A0 =C2=A0 "SELECT 'CD' LIKE '%B';",
=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {&qu= ot;1.6",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT ''= ; LIKE '%B';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}}= },
+=C2=A0 =C2=A0 {"1.7",
+=C2=A0 =C2=A0 =C2= =A0 =C2=A0 "SELECT 'AB' LIKE 'A__';",
+= =C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.8= ",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'CD' LI= KE 'A__';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },=
+=C2=A0 =C2=A0 {"1.9",
+=C2=A0 =C2=A0 =C2=A0= =C2=A0 "SELECT '' LIKE 'A__';",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.10&q= uot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'AB' LIKE= 'A_';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.11",
+=C2=A0 =C2=A0 =C2=A0 = =C2=A0 "SELECT 'CD' LIKE 'A_';",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.12&q= uot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '' LIKE &= #39;A_';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.13",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 "SELECT 'AB' LIKE 'A';",
+=C2=A0 = =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.14"= ,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'CD' LIKE = 9;A';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.15",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0= "SELECT '' LIKE 'A';",
+=C2=A0 =C2=A0 = =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.16",
=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'AB' LIKE '_'= ;;",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2= =A0 =C2=A0 {"1.17",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "= SELECT 'CD' LIKE '_';",
+=C2=A0 =C2=A0 =C2= =A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.18",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '' LIKE '_';&qu= ot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 = =C2=A0 {"1.19",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELE= CT 'AB' LIKE '__';",
+=C2=A0 =C2=A0 =C2=A0 = =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.20",
+= =C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'CD' LIKE '__';&qu= ot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 = =C2=A0 {"1.21",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELE= CT '' LIKE '__';",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.22",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT 'AB' LIKE '%A';"= ,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2= =A0 {"1.23",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT = 'AB' LIKE '%C';",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.24",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84' LIKE '%=C5= =93=D8=B4';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.25",
+=C2=A0 =C2=A0 =C2=A0 = =C2=A0 "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '%= =C5=93=D8=B4';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} }= ,
+=C2=A0 =C2=A0 {"1.26",
+=C2=A0 =C2=A0 =C2= =A0 =C2=A0 "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '%=C5=93= =D8=B4';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.27",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 "SELECT '=D1=91=D1=84' LIKE '=D1=91_';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {&q= uot;1.28",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '= =D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_';",
=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {&qu= ot;1.29",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=E2= =84=AB=C5=92=D8=B4' LIKE '=D1=91_';",
+=C2=A0 = =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.30"= ,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84'= ; LIKE '=D1=91=D1=84%';",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.31",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8= =B4' LIKE '=D1=91=D1=84%';",
+=C2=A0 =C2=A0 =C2= =A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.32",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=E2=84=AB=C5=92=D8=B4'= LIKE '=D1=91=D1=84%';",
+=C2=A0 =C2=A0 =C2=A0 =C2= =A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.33",
+=C2= =A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84' LIKE '=D1= =91=D1=84=E2=84=AB%';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0,= {0}} },
+=C2=A0 =C2=A0 {"1.34",
+=C2=A0 =C2= =A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' = LIKE '=D1=91=D1=84=E2=84=AB%';",
+=C2=A0 =C2=A0 =C2= =A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.35",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=E2=84=AB=C5=92=D8=B4'= LIKE '=D1=91=D1=84=D8=B4%';",
+=C2=A0 =C2=A0 =C2=A0= =C2=A0 {0, {0}} },
+=C2=A0 =C2=A0 {"1.36",
+= =C2=A0 =C2=A0 =C2=A0 =C2=A0 "SELECT '=D1=91=D1=84' LIKE '= =D1=91_%';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.37",
+=C2=A0 =C2=A0 =C2=A0 = =C2=A0 "SELECT '=D1=91=D1=84=E2=84=AB=C5=92=D8=B4' LIKE '= =D1=91_%';",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {1}} },
+=C2=A0 =C2=A0 {"1.38",
+=C2=A0 =C2=A0 =C2=A0 = =C2=A0 "SELECT '=E2=84=AB=C5=92=D8=B4' LIKE '=D1=91_%'= ;",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {0, {0}} },
+}
+
+test:do_catchsql_set_test(like_test_cases, prefix)
+
+-- Non-Unicode byte sequences.
+local invalid= _testcases =3D {
+=C2=A0 =C2=A0 '\xE2\x80',
+= =C2=A0 =C2=A0 '\xFE\xFF',
+=C2=A0 =C2=A0 '\xC2',<= /div>
+=C2=A0 =C2=A0 '\xED\xB0\x80',
+=C2=A0 =C2=A0 &= #39;\xD0',
+}
+
+-- Invalid testcases.
+for i, tested_string in ipairs(invalid_testcases) do
+
+=C2=A0 =C2=A0 -- We should raise an error in case
+=C2= =A0 =C2=A0 -- pattern contains invalid characters.
+
+= =C2=A0 =C2=A0 local test_name =3D prefix .. "2." .. tostring(i)
+=C2=A0 =C2=A0 local test_itself =3D "SELECT 'abc' LI= KE 'ab" .. tested_string .. "';"
+=C2=A0 = =C2=A0 test:do_catchsql_test(test_name, test_itself,
+=C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 {1, "LIKE or GLOB pattern can only contain UTF-8 characters&quo= t;})
+
+=C2=A0 =C2=A0 test_name =3D prefix .. "3.&= quot; .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT = 'abc' LIKE 'abc" .. tested_string .. "';"
+=C2=A0 =C2=A0 test:do_catchsql_test(test_name, test_itself,
<= div>+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 {1, "LIKE or GLOB pattern can only contain UTF-8 = characters"})
+
+=C2=A0 =C2=A0 test_name =3D prefi= x .. "4." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D= "SELECT 'abc' LIKE 'ab" .. tested_string .. "c&= #39;;"
+=C2=A0 =C2=A0 test:do_catchsql_test(test_name, test_= itself,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 {1, "LIKE or GLOB pattern can only = contain UTF-8 characters"})
+
+=C2=A0 =C2=A0 -- Ju= st skipping if row value predicand contains invalid character.
+<= /div>
+=C2=A0 =C2=A0 test_name =3D prefix .. "5." .. tostring= (i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT 'ab" .. = tested_string .. "' LIKE 'abc';"
+=C2=A0 = =C2=A0 test:do_execsql_test(test_name, test_itself, {0})
+
<= div>+=C2=A0 =C2=A0 test_name =3D prefix .. "6." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT 'abc" .. teste= d_string .. "' LIKE 'abc';"
+=C2=A0 =C2=A0 = test:do_execsql_test(test_name, test_itself, {0})
+
+= =C2=A0 =C2=A0 test_name =3D prefix .. "7." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT 'ab" .. tested_stri= ng .. "c' LIKE 'abc';"
+=C2=A0 =C2=A0 test:= do_execsql_test(test_name, test_itself, {0})
+end
+
+-- Unicode byte sequences.
+local valid_testcases =3D {
+=C2=A0 =C2=A0 '\x01',
+=C2=A0 =C2=A0 '\x09&= #39;,
+=C2=A0 =C2=A0 '\x1F',
+=C2=A0 =C2=A0 = 9;\x7F',
+=C2=A0 =C2=A0 '\xC2\x80',
+=C2=A0= =C2=A0 '\xC2\x90',
+=C2=A0 =C2=A0 '\xC2\x9F',
+=C2=A0 =C2=A0 '\xE2\x80\xA8',
+=C2=A0 =C2=A0 = 9;\x20\x0B',
+=C2=A0 =C2=A0 '\xE2\x80\xA9',
+}
+
+-- Valid testcases.
+for i, tested_str= ing in ipairs(valid_testcases) do
+=C2=A0 =C2=A0 test_name =3D pr= efix .. "8." .. tostring(i)
+=C2=A0 =C2=A0 local test_i= tself =3D "SELECT 'abc' LIKE 'ab" .. tested_string ..= "';"
+=C2=A0 =C2=A0 test:do_execsql_test(test_name= , test_itself, {0})
+
+=C2=A0 =C2=A0 test_name =3D pref= ix .. "9." .. tostring(i)
+=C2=A0 =C2=A0 test_itself = =3D "SELECT 'abc' LIKE 'abc" .. tested_string .. &quo= t;';"
+=C2=A0 =C2=A0 test:do_execsql_test(test_name, tes= t_itself, {0})
+
+=C2=A0 =C2=A0 test_name =3D prefix ..= "10." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D &q= uot;SELECT 'abc' LIKE 'ab" .. tested_string .. "c'= ;;"
+=C2=A0 =C2=A0 test:do_execsql_test(test_name, test_itself, {0})
+
+=C2= =A0 =C2=A0 test_name =3D prefix .. "11." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT 'ab" .. tested_string= .. "' LIKE 'abc';"
+=C2=A0 =C2=A0 test:do_= execsql_test(test_name, test_itself,= {0})
+
+=C2=A0 =C2=A0 test_name =3D prefix .. "12= ." .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELEC= T 'abc" .. tested_string .. "' LIKE 'abc';"<= /div>
+=C2=A0 =C2=A0 test:do_execsql_test(test_name, test_itself, {0})<= /div>
+
+=C2=A0 =C2=A0 test_name =3D prefix .. "13."= ; .. tostring(i)
+=C2=A0 =C2=A0 test_itself =3D "SELECT '= ;ab" .. tested_string .. "c' LIKE 'abc';"
<= div>+=C2=A0 =C2=A0 test:do_execsql_test(test_name, test_itself, {0})
<= div>+end
+
+test:finish_test()

--0000000000004d45fd057263abab--