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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -29,12 +29,25 @@ export function applyBinaryOp(op, a, b) {
29
29
  return String(a) + String(b)
30
30
  }
31
31
 
32
- // Comparison and logical operators
33
- if (a == null || b == null) {
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
- if (op === 'AND') return Boolean(a) && Boolean(b)
37
- if (op === 'OR') return Boolean(a) || Boolean(b)
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
- if (node.op === 'NOT') return !val
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
- if (node.op === 'AND' && !left) return false
191
- if (node.op === 'OR' && left) return true
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 (sqlEquals(exprVal, val)) return true
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 (sqlEquals(exprVal, value)) return true
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