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 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.19",
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.6.2",
43
- "@vitest/coverage-v8": "4.1.5",
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.5"
47
+ "vitest": "4.1.6"
48
48
  }
49
49
  }
@@ -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 (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
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
  *
@@ -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 == val) return true
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 == value) return true
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 == whenValue : whenValue) {
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