Skip to content

Commit bc5dc5d

Browse files
Copilotmathiasrw
andauthored
Fix behavior of null in IN and NOT IN operators with empty sets (#2310)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mathiasrw <[email protected]> Co-authored-by: Mathias Wulff <[email protected]>
1 parent 2cfd8fb commit bc5dc5d

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

src/50expression.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@
294294
var s;
295295
let refs = [];
296296
let op = this.op;
297+
let skipNullCheck = false; // Flag to skip null checking for deterministic empty set operations
297298
let _this = this;
298299
let ref = function (expr) {
299300
if (expr.toJS) {
@@ -375,7 +376,17 @@
375376
const uncachedLookup = `(alasql.utils.flatArray(this.queriesfn[${this.queriesidx}](params, null, ${context})).indexOf(alasql.utils.getValueOf(${leftJS()})) > -1)`;
376377
s = `(${checkCorrelated} ? ${uncachedLookup} : ${cachedLookup})`;
377378
} else if (Array.isArray(this.right)) {
378-
if (!alasql.options.cache || this.right.some(value => value instanceof yy.ParamValue)) {
379+
// Empty array: nothing is IN an empty set, always false
380+
if (this.right.length === 0) {
381+
// Must call leftJS() to populate the refs array for the declareRefs statement,
382+
// even though the result is not used in the final expression
383+
leftJS();
384+
s = 'false';
385+
skipNullCheck = true; // Result is deterministic even with null operands
386+
} else if (
387+
!alasql.options.cache ||
388+
this.right.some(value => value instanceof yy.ParamValue)
389+
) {
379390
// Leverage JS Set for faster lookups than arrays
380391
s = `(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()})))`;
381392
} else {
@@ -399,7 +410,17 @@
399410
const uncachedLookup = `(alasql.utils.flatArray(this.queriesfn[${this.queriesidx}](params, null, ${context})).indexOf(alasql.utils.getValueOf(${leftJS()})) < 0)`;
400411
s = `(${checkCorrelated} ? ${uncachedLookup} : ${cachedLookup})`;
401412
} else if (Array.isArray(this.right)) {
402-
if (!alasql.options.cache || this.right.some(value => value instanceof yy.ParamValue)) {
413+
// Empty array: everything is NOT IN an empty set, always true
414+
if (this.right.length === 0) {
415+
// Must call leftJS() to populate the refs array for the declareRefs statement,
416+
// even though the result is not used in the final expression
417+
leftJS();
418+
s = 'true';
419+
skipNullCheck = true; // Result is deterministic even with null operands
420+
} else if (
421+
!alasql.options.cache ||
422+
this.right.some(value => value instanceof yy.ParamValue)
423+
) {
403424
// Leverage JS Set for faster lookups than arrays
404425
s = `(!(new Set([${this.right.map(ref).join(',')}]).has(alasql.utils.getValueOf(${leftJS()}))))`;
405426
} else {
@@ -475,7 +496,14 @@
475496
var expr = s || '(' + leftJS() + op + rightJS() + ')';
476497

477498
var declareRefs = 'y=[(' + refs.join('), (') + ')]';
478-
if (op === '&&' || op === '||' || op === 'IS' || op === 'IS NULL' || op === 'IS NOT NULL') {
499+
if (
500+
skipNullCheck ||
501+
op === '&&' ||
502+
op === '||' ||
503+
op === 'IS' ||
504+
op === 'IS NULL' ||
505+
op === 'IS NOT NULL'
506+
) {
479507
return '(' + declareRefs + ', ' + expr + ')';
480508
}
481509

test/test1529.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var alasql = require('..');
4+
}
5+
6+
describe('Test 1529 - SELECT null [not] IN ()', function () {
7+
it('1. null IN () should return false', function (done) {
8+
var res = alasql('SELECT VALUE null IN ()');
9+
assert.strictEqual(res, false, 'null IN () should return false, got ' + res);
10+
done();
11+
});
12+
13+
it('2. null NOT IN () should return true', function (done) {
14+
var res = alasql('SELECT VALUE null NOT IN ()');
15+
assert.strictEqual(res, true, 'null NOT IN () should return true, got ' + res);
16+
done();
17+
});
18+
19+
it('3. Verify non-null values still work correctly', function (done) {
20+
var res1 = alasql('SELECT VALUE 1 IN ()');
21+
assert.strictEqual(res1, false, '1 IN () should return false');
22+
23+
var res2 = alasql('SELECT VALUE 1 NOT IN ()');
24+
assert.strictEqual(res2, true, '1 NOT IN () should return true');
25+
26+
done();
27+
});
28+
});

0 commit comments

Comments
 (0)