squirreling 0.12.19 → 0.12.20
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/README.md +1 -1
- package/package.json +4 -4
- package/src/execute/utils.js +18 -1
- package/src/expression/binary.js +12 -0
- package/src/expression/evaluate.js +19 -7
- package/src/validation/functions.js +8 -0
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
161
161
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
162
162
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_DIFF`, `DATEDIFF`, `DATE_PART`, `DATE_TRUNC`, `EPOCH`, `EXTRACT`, `INTERVAL`
|
|
163
163
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`
|
|
164
|
-
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `CARDINALITY`, `SIZE`
|
|
164
|
+
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `ARRAY_APPEND`, `ARRAY_CONCAT`, `LEN`, `CARDINALITY`, `SIZE`
|
|
165
165
|
- Table functions: `UNNEST`, `EXPLODE`, `JSON_EACH`
|
|
166
166
|
- Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
|
|
167
167
|
- Spatial: `ST_GeomFromText`, `ST_MakeEnvelope`, `ST_AsText`, `ST_Intersects`, `ST_Contains`, `ST_ContainsProperly`, `ST_Within`, `ST_Overlaps`, `ST_Touches`, `ST_Equals`, `ST_Crosses`, `ST_Covers`, `ST_CoveredBy`, `ST_DWithin`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.20",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "25.
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
42
|
+
"@types/node": "25.8.0",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
45
|
"eslint-plugin-jsdoc": "62.9.0",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.6"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/execute/utils.js
CHANGED
|
@@ -27,7 +27,11 @@ export function compareForTerm(a, b, term) {
|
|
|
27
27
|
if (a == b) return 0
|
|
28
28
|
|
|
29
29
|
let cmp
|
|
30
|
-
if (
|
|
30
|
+
if (a instanceof Date && b instanceof Date) {
|
|
31
|
+
const at = a.getTime()
|
|
32
|
+
const bt = b.getTime()
|
|
33
|
+
cmp = at < bt ? -1 : at > bt ? 1 : 0
|
|
34
|
+
} else if (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
|
|
31
35
|
cmp = a < b ? -1 : 1
|
|
32
36
|
} else {
|
|
33
37
|
const aa = String(a)
|
|
@@ -121,6 +125,19 @@ export function maxBounds(a, b) {
|
|
|
121
125
|
return a ?? b
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
/**
|
|
129
|
+
* SQL equality for primitives. Two Date instances for the same instant compare
|
|
130
|
+
* equal (JS `==` would compare by identity).
|
|
131
|
+
*
|
|
132
|
+
* @param {SqlPrimitive} a
|
|
133
|
+
* @param {SqlPrimitive} b
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
export function sqlEquals(a, b) {
|
|
137
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
|
|
138
|
+
return a == b
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
/**
|
|
125
142
|
* Returns true for plain object SqlPrimitive values, excluding null, arrays, and Dates.
|
|
126
143
|
*
|
package/src/expression/binary.js
CHANGED
|
@@ -29,6 +29,18 @@ export function applyBinaryOp(op, a, b) {
|
|
|
29
29
|
}
|
|
30
30
|
if (op === 'AND') return Boolean(a) && Boolean(b)
|
|
31
31
|
if (op === 'OR') return Boolean(a) || Boolean(b)
|
|
32
|
+
// Compare Date values by their time so distinct instances for the same
|
|
33
|
+
// instant are equal, matching SQL TIMESTAMP semantics rather than JS identity.
|
|
34
|
+
if (a instanceof Date && b instanceof Date) {
|
|
35
|
+
const at = a.getTime()
|
|
36
|
+
const bt = b.getTime()
|
|
37
|
+
if (op === '!=' || op === '<>') return at !== bt
|
|
38
|
+
if (op === '=' || op === '==') return at === bt
|
|
39
|
+
if (op === '<') return at < bt
|
|
40
|
+
if (op === '<=') return at <= bt
|
|
41
|
+
if (op === '>') return at > bt
|
|
42
|
+
if (op === '>=') return at >= bt
|
|
43
|
+
}
|
|
32
44
|
if (op === '!=' || op === '<>') return a != b
|
|
33
45
|
if (op === '=' || op === '==') return a == b
|
|
34
46
|
if (op === '<') return a < b
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { executeStatement } from '../execute/execute.js'
|
|
2
|
-
import { isPlainObject, keyify, stringify } from '../execute/utils.js'
|
|
2
|
+
import { isPlainObject, keyify, sqlEquals, stringify } from '../execute/utils.js'
|
|
3
3
|
import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
|
|
4
4
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
5
5
|
import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
@@ -516,7 +516,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
516
516
|
return arr.length
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
if (funcName === 'ARRAY_LENGTH' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
|
|
519
|
+
if (funcName === 'ARRAY_LENGTH' || funcName === 'LIST_LENGTH' || funcName === 'LEN' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
|
|
520
520
|
const arr = args[0]
|
|
521
521
|
if (!Array.isArray(arr)) return null
|
|
522
522
|
if (funcName === 'ARRAY_LENGTH' && args.length === 2) {
|
|
@@ -539,19 +539,31 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
539
539
|
return arr.length
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
if (funcName === 'ARRAY_POSITION') {
|
|
542
|
+
if (funcName === 'ARRAY_POSITION' || funcName === 'LIST_POSITION') {
|
|
543
543
|
const [arr, target] = args
|
|
544
544
|
if (!Array.isArray(arr)) return null
|
|
545
545
|
const index = arr.indexOf(target)
|
|
546
546
|
return index === -1 ? null : index + 1
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
if (funcName === 'ARRAY_CONTAINS') {
|
|
549
|
+
if (funcName === 'ARRAY_CONTAINS' || funcName === 'LIST_CONTAINS') {
|
|
550
550
|
const [arr, target] = args
|
|
551
551
|
if (!Array.isArray(arr)) return null
|
|
552
552
|
return arr.includes(target)
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
if (funcName === 'ARRAY_APPEND' || funcName === 'LIST_APPEND') {
|
|
556
|
+
const [arr, element] = args
|
|
557
|
+
if (!Array.isArray(arr)) return null
|
|
558
|
+
return [...arr, element]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (funcName === 'ARRAY_CONCAT' || funcName === 'LIST_CONCAT') {
|
|
562
|
+
const [a, b] = args
|
|
563
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return null
|
|
564
|
+
return [...a, ...b]
|
|
565
|
+
}
|
|
566
|
+
|
|
555
567
|
if (funcName === 'ARRAY_SORT') {
|
|
556
568
|
const arr = args[0]
|
|
557
569
|
if (!Array.isArray(arr)) return null
|
|
@@ -667,7 +679,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
667
679
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
668
680
|
for (const valueNode of node.values) {
|
|
669
681
|
const val = await evaluateExpr({ node: valueNode, row, rowIndex, rows, context })
|
|
670
|
-
if (exprVal
|
|
682
|
+
if (sqlEquals(exprVal, val)) return true
|
|
671
683
|
}
|
|
672
684
|
return false
|
|
673
685
|
}
|
|
@@ -677,7 +689,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
677
689
|
const subResult = executeStatement({ query: node.subquery, context })
|
|
678
690
|
for await (const resRow of subResult.rows()) {
|
|
679
691
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
680
|
-
if (exprVal
|
|
692
|
+
if (sqlEquals(exprVal, value)) return true
|
|
681
693
|
}
|
|
682
694
|
return false
|
|
683
695
|
}
|
|
@@ -703,7 +715,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
703
715
|
for (const whenClause of node.whenClauses) {
|
|
704
716
|
const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
|
|
705
717
|
// compare caseValue with condition or evaluate as boolean
|
|
706
|
-
if (caseValue !== undefined ? caseValue
|
|
718
|
+
if (caseValue !== undefined ? sqlEquals(caseValue, whenValue) : whenValue) {
|
|
707
719
|
return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
|
|
708
720
|
}
|
|
709
721
|
}
|
|
@@ -184,9 +184,17 @@ export const FUNCTION_SIGNATURES = {
|
|
|
184
184
|
|
|
185
185
|
// Array functions
|
|
186
186
|
ARRAY_LENGTH: { min: 1, max: 2, signature: 'array[, dimension]' },
|
|
187
|
+
LIST_LENGTH: { min: 1, max: 1, signature: 'array' },
|
|
188
|
+
LEN: { min: 1, max: 1, signature: 'array' },
|
|
187
189
|
ARRAY_POSITION: { min: 2, max: 2, signature: 'array, element' },
|
|
190
|
+
LIST_POSITION: { min: 2, max: 2, signature: 'array, element' },
|
|
188
191
|
ARRAY_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
|
|
192
|
+
LIST_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
|
|
189
193
|
ARRAY_SORT: { min: 1, max: 1, signature: 'array' },
|
|
194
|
+
ARRAY_APPEND: { min: 2, max: 2, signature: 'array, element' },
|
|
195
|
+
LIST_APPEND: { min: 2, max: 2, signature: 'array, element' },
|
|
196
|
+
ARRAY_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
|
|
197
|
+
LIST_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
|
|
190
198
|
CARDINALITY: { min: 1, max: 1, signature: 'array' },
|
|
191
199
|
SIZE: { min: 1, max: 1, signature: 'array' },
|
|
192
200
|
|