squirreling 0.15.2 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/expression/binary.js +17 -4
- package/src/expression/evaluate.js +27 -10
package/package.json
CHANGED
package/src/expression/binary.js
CHANGED
|
@@ -29,12 +29,25 @@ export function applyBinaryOp(op, a, b) {
|
|
|
29
29
|
return String(a) + String(b)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
|
|
32
|
+
// Logical operators use Kleene three-valued logic: null is UNKNOWN, which
|
|
33
|
+
// only resolves when the other operand decides the answer on its own
|
|
34
|
+
if (op === 'AND') {
|
|
35
|
+
if (a != null && !a || b != null && !b) return false
|
|
36
|
+
if (a == null || b == null) return null
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
if (op === 'OR') {
|
|
40
|
+
if (a != null && Boolean(a) || b != null && Boolean(b)) return true
|
|
41
|
+
if (a == null || b == null) return null
|
|
34
42
|
return false
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
|
|
45
|
+
// A comparison with a null operand is UNKNOWN, not false. The difference is
|
|
46
|
+
// invisible to a WHERE (both exclude the row) but not to a NOT above it,
|
|
47
|
+
// which must keep UNKNOWN as UNKNOWN rather than flip false to true
|
|
48
|
+
if (a == null || b == null) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
38
51
|
// Compare Date values by their time so distinct instances for the same
|
|
39
52
|
// instant are equal, matching SQL TIMESTAMP semantics rather than JS identity.
|
|
40
53
|
if (a instanceof Date && b instanceof Date) {
|
|
@@ -167,7 +167,9 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
167
167
|
if (val == null) return null
|
|
168
168
|
return -val
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
// NOT UNKNOWN is UNKNOWN: JS ! would flip null to true and return rows
|
|
171
|
+
// SQL excludes, e.g. WHERE NOT (nullable < 300)
|
|
172
|
+
if (node.op === 'NOT') return val == null ? null : !val
|
|
171
173
|
if (node.op === 'IS NULL') return val == null
|
|
172
174
|
if (node.op === 'IS NOT NULL') return val != null
|
|
173
175
|
}
|
|
@@ -186,9 +188,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
186
188
|
|
|
187
189
|
const left = await evaluateExpr({ node: node.left, row, rowIndex, rows, context })
|
|
188
190
|
|
|
189
|
-
// Short-circuit evaluation for AND and OR
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
// Short-circuit evaluation for AND and OR. A null left cannot
|
|
192
|
+
// short-circuit AND: NULL AND FALSE is false but NULL AND TRUE is null,
|
|
193
|
+
// so the right side decides
|
|
194
|
+
if (node.op === 'AND' && left != null && !left) return false
|
|
195
|
+
if (node.op === 'OR' && left != null && Boolean(left)) return true
|
|
192
196
|
|
|
193
197
|
const right = await evaluateExpr({ node: node.right, row, rowIndex, rows, context })
|
|
194
198
|
return applyBinaryOp(node.op, left, right)
|
|
@@ -776,19 +780,28 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
776
780
|
}
|
|
777
781
|
}
|
|
778
782
|
|
|
779
|
-
// IN and NOT IN with value lists
|
|
783
|
+
// IN and NOT IN with value lists. IN is a chain of OR'd equalities, so it
|
|
784
|
+
// inherits their three-valued logic: a null on either side of any equality
|
|
785
|
+
// makes a non-match UNKNOWN rather than false, and NOT above it must not
|
|
786
|
+
// turn that into true
|
|
780
787
|
if (node.type === 'in valuelist') {
|
|
781
788
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
789
|
+
let sawNull = exprVal == null
|
|
782
790
|
for (const valueNode of node.values) {
|
|
783
791
|
const val = await evaluateExpr({ node: valueNode, row, rowIndex, rows, context })
|
|
784
|
-
if (
|
|
792
|
+
if (val == null) {
|
|
793
|
+
sawNull = true
|
|
794
|
+
} else if (exprVal != null && sqlEquals(exprVal, val)) {
|
|
795
|
+
return true
|
|
796
|
+
}
|
|
785
797
|
}
|
|
786
|
-
return false
|
|
798
|
+
return sawNull ? null : false
|
|
787
799
|
}
|
|
788
|
-
// IN with subqueries
|
|
800
|
+
// IN with subqueries, same three-valued logic as the value-list form
|
|
789
801
|
if (node.type === 'in') {
|
|
790
802
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
791
803
|
const subResult = executeStatement({ query: node.subquery, context })
|
|
804
|
+
let sawNull = false
|
|
792
805
|
let innerCount = 0
|
|
793
806
|
for await (const resRow of subResult.rows()) {
|
|
794
807
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
@@ -796,9 +809,13 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
796
809
|
context.signal?.throwIfAborted()
|
|
797
810
|
}
|
|
798
811
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
799
|
-
if (
|
|
812
|
+
if (exprVal == null || value == null) {
|
|
813
|
+
sawNull = true
|
|
814
|
+
} else if (exprVal != null && sqlEquals(exprVal, value)) {
|
|
815
|
+
return true
|
|
816
|
+
}
|
|
800
817
|
}
|
|
801
|
-
return false
|
|
818
|
+
return sawNull ? null : false
|
|
802
819
|
}
|
|
803
820
|
|
|
804
821
|
// EXISTS and NOT EXISTS with subqueries
|