squirreling 0.4.5 → 0.4.6

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.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -1,4 +1,5 @@
1
1
  import { executeSelect } from './execute.js'
2
+ import { applyBinaryOp } from './utils.js'
2
3
 
3
4
  /**
4
5
  * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
@@ -59,55 +60,24 @@ export async function evaluateExpr({ node, row, tables }) {
59
60
  if (node.op === '-') {
60
61
  const val = await evaluateExpr({ node: node.argument, row, tables })
61
62
  if (val == null) return null
62
- return -Number(val)
63
+ return -val
63
64
  }
64
65
  }
65
66
 
66
67
  // Binary operators
67
68
  if (node.type === 'binary') {
69
+ const left = await evaluateExpr({ node: node.left, row, tables })
70
+
71
+ // Short-circuit evaluation for AND and OR
68
72
  if (node.op === 'AND') {
69
- const leftVal = await evaluateExpr({ node: node.left, row, tables })
70
- if (!leftVal) return false
71
- return Boolean(await evaluateExpr({ node: node.right, row, tables }))
73
+ if (!left) return false
72
74
  }
73
-
74
75
  if (node.op === 'OR') {
75
- const leftVal = await evaluateExpr({ node: node.left, row, tables })
76
- if (leftVal) return true
77
- return Boolean(await evaluateExpr({ node: node.right, row, tables }))
76
+ if (left) return true
78
77
  }
79
78
 
80
- const left = await evaluateExpr({ node: node.left, row, tables })
81
79
  const right = await evaluateExpr({ node: node.right, row, tables })
82
-
83
- // In SQL, NULL comparisons with =, !=, <> always return false (unknown)
84
- // You must use IS NULL or IS NOT NULL to check for NULL
85
- if (left == null || right == null) {
86
- if (node.op === '=' || node.op === '!=' || node.op === '<>') {
87
- return false
88
- }
89
- }
90
-
91
- if (node.op === '=') return left === right
92
- if (node.op === '!=' || node.op === '<>') return left !== right
93
- if (node.op === '<') return left < right
94
- if (node.op === '>') return left > right
95
- if (node.op === '<=') return left <= right
96
- if (node.op === '>=') return left >= right
97
-
98
- if (node.op === 'LIKE') {
99
- const str = String(left)
100
- const pattern = String(right)
101
- // Convert SQL LIKE pattern to regex
102
- // % matches zero or more characters
103
- // _ matches exactly one character
104
- const regexPattern = pattern
105
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
106
- .replace(/%/g, '.*') // Replace % with .*
107
- .replace(/_/g, '.') // Replace _ with .
108
- const regex = new RegExp('^' + regexPattern + '$', 'i')
109
- return regex.test(str)
110
- }
80
+ return applyBinaryOp(node.op, left, right)
111
81
  }
112
82
 
113
83
  // Function calls
@@ -1,5 +1,6 @@
1
1
  import { isAggregateFunc } from '../validation.js'
2
2
  import { evaluateExpr } from './expression.js'
3
+ import { applyBinaryOp } from './utils.js'
3
4
 
4
5
  /**
5
6
  * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
@@ -30,38 +31,17 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
30
31
 
31
32
  if (expr.type === 'binary') {
32
33
  const left = await evaluateHavingValue(expr.left, context, group, tables)
33
- const right = await evaluateHavingValue(expr.right, context, group, tables)
34
34
 
35
+ // Short-circuit evaluation for AND and OR
35
36
  if (expr.op === 'AND') {
36
- return Boolean(left && right)
37
+ if (!left) return false
37
38
  }
38
39
  if (expr.op === 'OR') {
39
- return Boolean(left || right)
40
- }
41
-
42
- // Handle NULL comparisons
43
- if (left == null || right == null) {
44
- if (expr.op === '=' || expr.op === '!=' || expr.op === '<>') {
45
- return false
46
- }
40
+ if (left) return true
47
41
  }
48
42
 
49
- if (expr.op === '=') return left === right
50
- if (expr.op === '!=' || expr.op === '<>') return left !== right
51
- if (expr.op === '<') return left < right
52
- if (expr.op === '>') return left > right
53
- if (expr.op === '<=') return left <= right
54
- if (expr.op === '>=') return left >= right
55
- if (expr.op === 'LIKE') {
56
- const str = String(left)
57
- const pattern = String(right)
58
- const regexPattern = pattern
59
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
60
- .replace(/%/g, '.*')
61
- .replace(/_/g, '.')
62
- const regex = new RegExp('^' + regexPattern + '$', 'i')
63
- return regex.test(str)
64
- }
43
+ const right = await evaluateHavingValue(expr.right, context, group, tables)
44
+ return applyBinaryOp(expr.op, left, right)
65
45
  }
66
46
 
67
47
  if (expr.type === 'unary') {
@@ -1,14 +1,47 @@
1
1
  /**
2
- * @import {AsyncRow, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
2
+ * @import {AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
3
3
  */
4
4
 
5
+ /**
6
+ * Compares two values with the given operator, handling nulls according to SQL semantics
7
+ *
8
+ * @param {BinaryOp} op
9
+ * @param {SqlPrimitive} a
10
+ * @param {SqlPrimitive} b
11
+ * @returns {boolean}
12
+ */
13
+ export function applyBinaryOp(op, a, b) {
14
+ if (a == null || b == null) {
15
+ return false
16
+ }
17
+ if (op === 'AND') return Boolean(a) && Boolean(b)
18
+ if (op === 'OR') return Boolean(a) || Boolean(b)
19
+ if (op === '!=' || op === '<>') return a != b
20
+ if (op === '=') return a == b
21
+ if (op === '<') return a < b
22
+ if (op === '<=') return a <= b
23
+ if (op === '>') return a > b
24
+ if (op === '>=') return a >= b
25
+
26
+ if (op === 'LIKE') {
27
+ const str = String(a)
28
+ const pattern = String(b)
29
+ const regexPattern = pattern
30
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
31
+ .replace(/%/g, '.*')
32
+ .replace(/_/g, '.')
33
+ const regex = new RegExp(`^${regexPattern}$`, 'i')
34
+ return regex.test(str)
35
+ }
36
+ }
37
+
5
38
  /**
6
39
  * Compares two values for a single ORDER BY term, handling nulls and direction
7
40
  *
8
41
  * @param {SqlPrimitive} a
9
42
  * @param {SqlPrimitive} b
10
43
  * @param {OrderByItem} term
11
- * @returns {number} comparison result
44
+ * @returns {number}
12
45
  */
13
46
  export function compareForTerm(a, b, term) {
14
47
  const aIsNull = a == null
@@ -22,7 +55,7 @@ export function compareForTerm(a, b, term) {
22
55
  }
23
56
 
24
57
  // Compare non-null values
25
- if (a === b) return 0
58
+ if (a == b) return 0
26
59
 
27
60
  const primitives = ['number', 'bigint', 'boolean', 'string']
28
61
  let cmp
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, StringFunc} from './types.js'
3
+ * @import {AggregateFunc, BinaryOp, ComparisonOp, StringFunc} from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */
@@ -21,7 +21,7 @@ export function isStringFunc(name) {
21
21
  * @returns {op is BinaryOp}
22
22
  */
23
23
  export function isBinaryOp(op) {
24
- return ['=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
24
+ return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
25
25
  }
26
26
 
27
27
  // Keywords that cannot be used as implicit aliases after a column