diff --git a/src/15utility.js b/src/15utility.js index 5f2c40559b..ff6f860046 100755 --- a/src/15utility.js +++ b/src/15utility.js @@ -1196,6 +1196,15 @@ var domEmptyChildren = (utils.domEmptyChildren = function (container) { */ var patternCache = {}; var like = (utils.like = function (pattern, value, escape = '') { + // Normalize the escape character: the SQL ESCAPE clause specifies a single + // character, but due to JavaScript string escaping layers in the parser + // pipeline, a backslash escape can arrive as multiple backslash characters + // (e.g. '\\\\' for '\\', etc.). If the escape string consists entirely + // of backslashes, collapse it to a single '\\'. + if (escape.length > 1 && /^\\+$/.test(escape)) { + escape = '\\'; + } + if (!patternCache[pattern]) { var regexStr = '^'; // Start regex pattern to match from the beginning. var i = 0; // Index for traversing the pattern string. @@ -1205,7 +1214,7 @@ var like = (utils.like = function (pattern, value, escape = '') { var nextChar = i < pattern.length - 1 ? pattern[i + 1] : ''; // Handle escape character. - if (currentChar === escape) { + if (escape && currentChar === escape) { regexStr += '\\' + nextChar; i++; // Skip next character as it's escaped. } diff --git a/test/test1978.js b/test/test1978.js new file mode 100644 index 0000000000..a5df50b981 --- /dev/null +++ b/test/test1978.js @@ -0,0 +1,98 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} else { + __dirname = '.'; +} + +describe('Test 1978 - LIKE ESCAPE with backslash', function () { + it('supports backslash as ESCAPE character in utils.like', function (done) { + // Single backslash escape (the correct runtime value) + assert(alasql.utils.like('%\\%%', '100%', '\\')); + assert(!alasql.utils.like('%\\%%', '100', '\\')); + + // Double backslash (arrives from parser escaping layer) + assert(alasql.utils.like('%\\%%', '100%', '\\\\')); + assert(!alasql.utils.like('%\\%%', '1000', '\\\\')); + + // Triple and quadruple backslashes (additional escaping layers) + assert(alasql.utils.like('%\\%%', '100%', '\\\\\\')); + assert(!alasql.utils.like('%\\%%', '1000', '\\\\\\')); + + assert(alasql.utils.like('%\\%%', '100%', '\\\\\\\\')); + assert(!alasql.utils.like('%\\%%', '1000', '\\\\\\\\')); + + // Underscore wildcard escaping with backslash + assert(alasql.utils.like('%\\_%', '100_', '\\')); + assert(!alasql.utils.like('%\\_%', '1000', '\\')); + + assert(alasql.utils.like('%\\_%', '100_', '\\\\')); + assert(!alasql.utils.like('%\\_%', '1000', '\\\\')); + + done(); + }); + + it('supports non-backslash ESCAPE characters in utils.like', function (done) { + // Caret escape + assert(alasql.utils.like('%^%%', '100%', '^')); + assert(!alasql.utils.like('%^%%', '100', '^')); + + // Exclamation mark escape + assert(alasql.utils.like('%!%%', '100%', '!')); + assert(!alasql.utils.like('%!%%', '100', '!')); + + // Tilde escape + assert(alasql.utils.like('%~_%', '100_', '~')); + assert(!alasql.utils.like('%~_%', '1000', '~')); + + done(); + }); + + it('supports backslash ESCAPE in SQL queries', function (done) { + alasql('CREATE TABLE cities1978 (city string, population string)'); + + alasql( + "INSERT INTO cities1978 VALUES ('%','1'), ('1%','2'), ('%1','3'), ('1%1','4'), ('_1','5'), ('1^1','6'), ('1!1','7')" + ); + + // Using backslash ESCAPE in SQL with two LIKE columns (the original issue) + var res = alasql( + "SELECT * FROM cities1978 WHERE (city LIKE '%\\%%' ESCAPE '\\\\') OR (population LIKE '2\\%' ESCAPE '\\\\') ORDER BY population DESC" + ); + + assert.deepStrictEqual(res, [ + {city: '1%1', population: '4'}, + {city: '%1', population: '3'}, + {city: '1%', population: '2'}, + {city: '%', population: '1'}, + ]); + + // Also verify with quadruple backslashes in SQL ESCAPE + var resQuad = alasql( + "SELECT * FROM cities1978 WHERE (city LIKE '%\\%%' ESCAPE '\\\\\\\\') OR (population LIKE '2\\%' ESCAPE '\\\\\\\\') ORDER BY population DESC" + ); + + assert.deepStrictEqual(resQuad, [ + {city: '1%1', population: '4'}, + {city: '%1', population: '3'}, + {city: '1%', population: '2'}, + {city: '%', population: '1'}, + ]); + + // Using caret ESCAPE (should also work with multiple LIKE columns) + var res2 = alasql( + "SELECT * FROM cities1978 WHERE (city LIKE '%^%%' ESCAPE '^') OR (population LIKE '2^%' ESCAPE '^') ORDER BY population DESC" + ); + + assert.deepStrictEqual(res2, [ + {city: '1%1', population: '4'}, + {city: '%1', population: '3'}, + {city: '1%', population: '2'}, + {city: '%', population: '1'}, + ]); + + alasql('DROP TABLE cities1978'); + + done(); + }); +});