Repository navigation
Fix NOT IN with NULL in the subquery or list to close #1414 - #2580
Open
joshuablac wants to merge 2 commits into
Open
joshuablac wants to merge 2 commits into
joshuablac wants to merge 2 commits into
Conversation
In SQL, x NOT IN (...) is UNKNOWN when x is not found and the values contain NULL, so such rows are filtered out. AlaSQL returned true, e.g. SELECT a FROM R WHERE a NOT IN (SELECT b FROM S) returned a = 1 when S holds a NULL. A utils.notIn(set, value) helper now returns false when the value is found, undefined (NULL) when it is not found but the set contains NULL (null, undefined or NaN, like the operator null check), otherwise true. It is used by the cached and correlated subquery paths and by both literal list paths of NOT IN. IN is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cache of literal value lists was keyed by values.join(','), so lists
like (2, NULL) and (2, '') or (1, 3) and ('1', 3) shared one Set. With
NOT IN now treating a NULL in the set as UNKNOWN, a NOT IN (2, '') run
after a IN (2, NULL) filtered out every row. The key now includes the
type of each value, and is quoted with JSON.stringify in the generated
code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1414
Problem
In SQL,
x NOT IN (...)is UNKNOWN whenxisn't found and the values contain a NULL, so the row is filtered out. AlaSQL returnstrueinstead:developa NOT IN (SELECT b FROM S),R = [1, NULL],S = [NULL][{a: 1}][]a NOT IN (SELECT b ...)withb = [NULL, 2],a = [1, 2, NULL][{a: 1}][]a NOT IN (SELECT b FROM S2 WHERE S2.id = R2.id)where one row's set is{NULL}a NOT IN (2, NULL)[{a: 1}][]1 NOT IN (2, NULL)trueundefined(NULL)Cause
All four
NOT INcode paths inOp.toJScompute plain set membership (!set.has(x)orindexOf(x) < 0): the cached subquery, the correlated subquery, and both literal-list paths (with and withoutalasql.options.cache). The operator's null check only covers the operands (x), not the values inside the subquery or list.Fix
A small helper,
utils.notIn(set, value), returns:falsewhen the value is in the setundefined(AlaSQL's NULL) when it isn't, but the set contains NULL (null,undefinedorNaN, the same values the operator null check treats as NULL; a NULL inserted into anINTcolumn comes back asNaN)trueotherwiseThe subquery paths (cached and correlated) and both literal-list paths use it. The correlated path now builds a
Setlike the cached path does.INkeeps its results: when nothing matches it returnsfalseinstead of UNKNOWN, but both exclude the row in aWHEREclause.Cache key for literal lists (affects
INandNOT IN). Withalasql.options.cacheon (the default), value lists were cached inalasql.setsundervalues.join(','). Lists such as(2, NULL)and(2, ''), or(1, 3)and('1', 3), therefore shared oneSet. This was already a bug ondevelop:a IN ('1', 3)run aftera IN (1, 3)doesn't match'1'. With this PR's NULL handling it got worse:a NOT IN (2, '')run aftera IN (2, NULL)would filter out every row. The key now includes each value's type (JSON.stringify(values.map(v => [typeof v, v]))) and is quoted withJSON.stringifyin the generated code, which also avoids broken code for list values that contain a".This takes over from #2315, the earlier Copilot attempt, which was closed without merging. That PR ran into the subquery cache, so this one covers both the cached and uncached paths, plus the cached literal-list path.
Not changed here:
NULL NOT IN (<empty subquery>)is still filtered out, although SQL defines it as TRUE.a NOT IN @(?)with an array parameter) still usesindexOf, so a NULL in that array isn't treated as UNKNOWN.Tests
test/test1414.js:undefined/false)NOT INandINwithout NULL in the set are unchanged''or type (1vs'1') are kept apartdevelopand pass with this change.bun test, same command asyarn test-only): 2256 pass, 213 skip. The only failures are the 2 CLI tests intest2149.js, which fail the same way on unmodifieddevelopin my local setup (a space in my checkout path, and the version-injection step ofbuild.shnot running).This fix was written with AI assistance (Claude Code) and verified locally as described above.
🤖 Generated with Claude Code