From 6565a5e2ee24c51a313fff4495b57cfc79048555 Mon Sep 17 00:00:00 2001 From: komal0000 Date: Mon, 26 Oct 2026 12:42:21 +0545 Subject: [PATCH 1/3] Compile IN and NOT IN lists with non-literal items to close #2874 Co-authored-by: Mathias Wulff <1063454+mathiasrw@users.noreply.github.com> --- src/50expression.js | 8 ++++++-- test/test2874.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/test2874.js diff --git a/src/50expression.js b/src/50expression.js index b6a627be4d..a8dca5dc1f 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -385,7 +385,9 @@ skipNullCheck = true; // Result is deterministic even with null operands } else if ( !alasql.options.cache || - this.right.some(value => value instanceof yy.ParamValue) + this.right.some( + value => !(value instanceof yy.NumValue || value instanceof yy.StringValue) + ) ) { // Leverage JS Set for faster lookups than arrays s = `(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()})))`; @@ -420,7 +422,9 @@ skipNullCheck = true; // Result is deterministic even with null operands } else if ( !alasql.options.cache || - this.right.some(value => value instanceof yy.ParamValue) + this.right.some( + value => !(value instanceof yy.NumValue || value instanceof yy.StringValue) + ) ) { // Leverage JS Set for faster lookups than arrays s = `(!(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()}))))`; diff --git a/test/test2874.js b/test/test2874.js new file mode 100644 index 0000000000..2d47a4bfda --- /dev/null +++ b/test/test2874.js @@ -0,0 +1,41 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +let testId = '2874'; // Use the ID of the issue being fixed by this PR + +describe(`Test ${testId} - IN and NOT IN with items that are not plain literals`, function () { + const data = [ + {a: -1, b: 0}, + {a: 5, b: 0}, + {a: 3, b: 3}, + ]; + + it('A) IN with a negative number', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (-1, 5)', [data]), [{a: -1}, {a: 5}]); + }); + + it('B) IN with an expression', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (1 + 2, 5)', [data]), [ + {a: 5}, + {a: 3}, + ]); + }); + + it('C) IN with a column', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (b, 5)', [data]), [{a: 5}, {a: 3}]); + }); + + it('D) NOT IN with a negative number', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a NOT IN (-1, 5)', [data]), [{a: 3}]); + }); + + it('E) NOT IN with a column', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a NOT IN (b, 5)', [data]), [{a: -1}]); + }); + + it('F) IN with plain literals still works', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (3, 5)', [data]), [{a: 5}, {a: 3}]); + }); +}); From c0748b691bf817a69aad30489212616fd3ac677b Mon Sep 17 00:00:00 2001 From: "M. Wulff" Date: Tue, 27 Oct 2026 13:00:34 +1100 Subject: [PATCH 2/3] Keep negative numbers on the cached IN list path --- src/50expression.js | 26 ++++++++++++-------------- test/test2874.js | 5 +++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/50expression.js b/src/50expression.js index a8dca5dc1f..ade2658b1c 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -187,6 +187,14 @@ } } + // The parser reads -1 as a minus in front of 1, so a negative number counts as a literal here + // too: an IN list with a non-literal builds its Set again for every row + const isInListLiteral = item => + item instanceof yy.NumValue || + item instanceof yy.StringValue || + (item instanceof yy.UniOp && item.op === '-' && item.right instanceof yy.NumValue); + const inListValue = item => (item instanceof yy.UniOp ? -item.right.value : item.value); + const toTypeNumberOps = new Set(['-', '*', '/', '%', '^', '<<', '>>', '&', '|']); const toTypeStringOps = new Set(['||']); // Regex to detect identifiers that need bracket wrapping (spaces, dots, hyphens, square brackets) @@ -383,19 +391,14 @@ leftJS(); s = 'false'; skipNullCheck = true; // Result is deterministic even with null operands - } else if ( - !alasql.options.cache || - this.right.some( - value => !(value instanceof yy.NumValue || value instanceof yy.StringValue) - ) - ) { + } else if (!alasql.options.cache || !this.right.every(isInListLiteral)) { // Leverage JS Set for faster lookups than arrays s = `(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()})))`; } else { // Use a cache to avoid re-creating the Set on every identical query. The key keeps the // type of each value, so IN (5) and IN ('5') get their own Set (#2912) alasql.sets = alasql.sets || {}; - const allValues = this.right.map(value => value.value); + const allValues = this.right.map(inListValue); const key = JSON.stringify(allValues); alasql.sets[key] = alasql.sets[key] || new Set(allValues); s = `alasql.sets[${JSON.stringify(key)}].has(alasql.utils.getValueOf(${leftJS()}))`; @@ -420,18 +423,13 @@ leftJS(); s = 'true'; skipNullCheck = true; // Result is deterministic even with null operands - } else if ( - !alasql.options.cache || - this.right.some( - value => !(value instanceof yy.NumValue || value instanceof yy.StringValue) - ) - ) { + } else if (!alasql.options.cache || !this.right.every(isInListLiteral)) { // Leverage JS Set for faster lookups than arrays s = `(!(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()}))))`; } else { // Use a cache to avoid re-creating the Set on every identical query, keyed as for IN alasql.sets = alasql.sets || {}; - const allValues = this.right.map(value => value.value); + const allValues = this.right.map(inListValue); const key = JSON.stringify(allValues); alasql.sets[key] = alasql.sets[key] || new Set(allValues); s = `!alasql.sets[${JSON.stringify(key)}].has(alasql.utils.getValueOf(${leftJS()}))`; diff --git a/test/test2874.js b/test/test2874.js index 2d47a4bfda..9275da829f 100644 --- a/test/test2874.js +++ b/test/test2874.js @@ -38,4 +38,9 @@ describe(`Test ${testId} - IN and NOT IN with items that are not plain literals` it('F) IN with plain literals still works', function () { assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (3, 5)', [data]), [{a: 5}, {a: 3}]); }); + + it('G) A negative number keeps the cached list', function () { + assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (-1, 3)', [data]), [{a: -1}, {a: 3}]); + assert.ok(alasql.sets['-1,3'] instanceof Set); + }); }); From 628d77b8bccba7b53988e3d3c76d9283fb09fec3 Mon Sep 17 00:00:00 2001 From: komal0000 Date: Thu, 29 Oct 2026 21:40:27 +0545 Subject: [PATCH 3/3] Match the JSON key of the IN list cache from #2913 in test2874 --- test/test2874.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test2874.js b/test/test2874.js index 9275da829f..4d2daafaf5 100644 --- a/test/test2874.js +++ b/test/test2874.js @@ -41,6 +41,6 @@ describe(`Test ${testId} - IN and NOT IN with items that are not plain literals` it('G) A negative number keeps the cached list', function () { assert.deepStrictEqual(alasql('SELECT a FROM ? WHERE a IN (-1, 3)', [data]), [{a: -1}, {a: 3}]); - assert.ok(alasql.sets['-1,3'] instanceof Set); + assert.ok(alasql.sets['[-1,3]'] instanceof Set); }); });