squirreling 0.9.2 → 0.9.4

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
@@ -149,6 +149,8 @@ Squirreling mostly follows the SQL standard. The following features are supporte
149
149
  - Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
150
150
  - Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
151
151
  - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
152
+ - Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_SORT`, `CARDINALITY`
152
153
  - Regex: `REGEXP_SUBSTR`, `REGEXP_REPLACE`
154
+ - 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`
153
155
  - Conditional: `COALESCE`, `NULLIF`
154
156
  - User-defined functions (UDFs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -37,10 +37,10 @@
37
37
  "test": "vitest run"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "25.3.0",
40
+ "@types/node": "25.3.2",
41
41
  "@vitest/coverage-v8": "4.0.18",
42
42
  "eslint": "9.39.2",
43
- "eslint-plugin-jsdoc": "62.6.1",
43
+ "eslint-plugin-jsdoc": "62.7.1",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.18"
46
46
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { AsyncCells, AsyncDataSource, AsyncRow, ScanOptions, ScanResults, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  /**
@@ -8,7 +8,7 @@
8
8
  * @param {Record<string, SqlPrimitive>} obj - the plain object
9
9
  * @returns {AsyncRow} a row accessor interface
10
10
  */
11
- function asyncRow(obj) {
11
+ export function asyncRow(obj) {
12
12
  /** @type {AsyncCells} */
13
13
  const cells = {}
14
14
  for (const [key, value] of Object.entries(obj)) {
@@ -2,13 +2,14 @@ import { executeSelect } from '../execute/execute.js'
2
2
  import { stringify } from '../execute/utils.js'
3
3
  import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
4
4
  import { unknownFunctionError } from '../parseErrors.js'
5
- import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
5
+ import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation.js'
6
6
  import { aggregateError, argValueError, castError } from '../validationErrors.js'
7
7
  import { derivedAlias } from './alias.js'
8
8
  import { applyBinaryOp } from './binary.js'
9
9
  import { applyIntervalToDate } from './date.js'
10
10
  import { evaluateMathFunc } from './math.js'
11
11
  import { evaluateRegexpFunc } from './regexp.js'
12
+ import { evaluateSpatialFunc } from './spatial.js'
12
13
  import { evaluateStringFunc } from './strings.js'
13
14
 
14
15
  /**
@@ -250,6 +251,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
250
251
  return evaluateMathFunc({ funcName, args })
251
252
  }
252
253
 
254
+ if (isSpatialFunc(funcName)) {
255
+ return evaluateSpatialFunc({ funcName, args })
256
+ }
257
+
253
258
  if (funcName === 'COALESCE') {
254
259
  // Short-circuit: evaluate args one at a time, return first non-null
255
260
  for (const arg of node.args) {
@@ -308,6 +313,32 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
308
313
  return result
309
314
  }
310
315
 
316
+ if (funcName === 'ARRAY_LENGTH' || funcName === 'CARDINALITY') {
317
+ const arr = args[0]
318
+ if (!Array.isArray(arr)) return null
319
+ return arr.length
320
+ }
321
+
322
+ if (funcName === 'ARRAY_POSITION') {
323
+ const [arr, target] = args
324
+ if (!Array.isArray(arr)) return null
325
+ const index = arr.indexOf(target)
326
+ return index === -1 ? null : index + 1
327
+ }
328
+
329
+ if (funcName === 'ARRAY_SORT') {
330
+ const arr = args[0]
331
+ if (!Array.isArray(arr)) return null
332
+ return [...arr].sort((a, b) => {
333
+ if (a == null && b == null) return 0
334
+ if (a == null) return 1
335
+ if (b == null) return -1
336
+ if (a < b) return -1
337
+ if (a > b) return 1
338
+ return 0
339
+ })
340
+ }
341
+
311
342
  if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
312
343
  let jsonArg = args[0]
313
344
  const pathArg = args[1]
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Geometry types based on the GeoJSON specification (RFC 7946)
3
+ */
4
+ export type Geometry =
5
+ | Point
6
+ | MultiPoint
7
+ | LineString
8
+ | MultiLineString
9
+ | Polygon
10
+ | MultiPolygon
11
+ | GeometryCollection
12
+
13
+ /**
14
+ * Simple geometries that are not collections.
15
+ */
16
+ export type SimpleGeometry = Point | LineString | Polygon
17
+
18
+ /**
19
+ * Boundary relationship between two geometries.
20
+ */
21
+ export type Relation = 'OUTSIDE' | 'BOUNDARY' | 'INSIDE'
22
+
23
+ /**
24
+ * Position is an array of at least two numbers.
25
+ * The order should be [longitude, latitude] with optional properties (eg- altitude).
26
+ */
27
+ export type Position = number[]
28
+
29
+ export interface Point {
30
+ type: 'Point'
31
+ coordinates: Position
32
+ }
33
+
34
+ export interface MultiPoint {
35
+ type: 'MultiPoint'
36
+ coordinates: Position[]
37
+ }
38
+
39
+ export interface LineString {
40
+ type: 'LineString'
41
+ coordinates: Position[]
42
+ }
43
+
44
+ /**
45
+ * Each element is one LineString.
46
+ */
47
+ export interface MultiLineString {
48
+ type: 'MultiLineString'
49
+ coordinates: Position[][]
50
+ }
51
+
52
+ /**
53
+ * Each element is a linear ring.
54
+ */
55
+ export interface Polygon {
56
+ type: 'Polygon'
57
+ coordinates: Position[][]
58
+ }
59
+
60
+ /**
61
+ * Each element is one Polygon.
62
+ */
63
+ export interface MultiPolygon {
64
+ type: 'MultiPolygon'
65
+ coordinates: Position[][][]
66
+ }
67
+
68
+ export interface GeometryCollection {
69
+ type: 'GeometryCollection'
70
+ geometries: Geometry[]
71
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @import { SimpleGeometry } from './geometry.js'
3
+ */
4
+
5
+ import { EPSILON, EPSILON_SQ, distSq } from './spatial.geometry.js'
6
+
7
+ /**
8
+ * @param {SimpleGeometry} a
9
+ * @param {SimpleGeometry} b
10
+ * @returns {boolean}
11
+ */
12
+ export function simpleGeomEqual(a, b) {
13
+ if (a.type === 'Point' && b.type === 'Point') {
14
+ return distSq(a.coordinates, b.coordinates) < EPSILON_SQ
15
+ } else if (a.type === 'LineString' && b.type === 'LineString') {
16
+ return lineEqual(a.coordinates, b.coordinates)
17
+ } else if (a.type === 'Polygon' && b.type === 'Polygon') {
18
+ return polygonEqual(a.coordinates, b.coordinates)
19
+ }
20
+ return false
21
+ }
22
+
23
+ /**
24
+ * @param {number[][]} a
25
+ * @param {number[][]} b
26
+ * @returns {boolean}
27
+ */
28
+ export function lineEqual(a, b) {
29
+ if (a.length !== b.length) return false
30
+ // Forward
31
+ let forward = true
32
+ for (let i = 0; i < a.length; i++) {
33
+ if (Math.abs(a[i][0] - b[i][0]) > EPSILON || Math.abs(a[i][1] - b[i][1]) > EPSILON) {
34
+ forward = false
35
+ break
36
+ }
37
+ }
38
+ if (forward) return true
39
+ // Reverse
40
+ for (let i = 0; i < a.length; i++) {
41
+ if (Math.abs(a[i][0] - b[a.length - 1 - i][0]) > EPSILON || Math.abs(a[i][1] - b[a.length - 1 - i][1]) > EPSILON) {
42
+ return false
43
+ }
44
+ }
45
+ return true
46
+ }
47
+
48
+ /**
49
+ * @param {number[][][]} a
50
+ * @param {number[][][]} b
51
+ * @returns {boolean}
52
+ */
53
+ export function polygonEqual(a, b) {
54
+ if (a.length !== b.length) return false
55
+ for (let i = 0; i < a.length; i++) {
56
+ if (!ringsEqual(a[i], b[i])) return false
57
+ }
58
+ return true
59
+ }
60
+
61
+ /**
62
+ * Test if two rings are equal (same vertices, possibly different starting point).
63
+ *
64
+ * @param {number[][]} ring1
65
+ * @param {number[][]} ring2
66
+ * @returns {boolean}
67
+ */
68
+ export function ringsEqual(ring1, ring2) {
69
+ if (ring1.length !== ring2.length) return false
70
+ // Try every rotation
71
+ const n = ring1.length - 1 // closed ring, last = first
72
+ for (let offset = 0; offset < n; offset++) {
73
+ let match = true
74
+ for (let i = 0; i < n; i++) {
75
+ const j = (i + offset) % n
76
+ if (Math.abs(ring1[i][0] - ring2[j][0]) > EPSILON ||
77
+ Math.abs(ring1[i][1] - ring2[j][1]) > EPSILON) {
78
+ match = false
79
+ break
80
+ }
81
+ }
82
+ if (match) return true
83
+ }
84
+ // Try reverse direction
85
+ for (let offset = 0; offset < n; offset++) {
86
+ let match = true
87
+ for (let i = 0; i < n; i++) {
88
+ const j = (n - i + offset) % n
89
+ if (Math.abs(ring1[i][0] - ring2[j][0]) > EPSILON ||
90
+ Math.abs(ring1[i][1] - ring2[j][1]) > EPSILON) {
91
+ match = false
92
+ break
93
+ }
94
+ }
95
+ if (match) return true
96
+ }
97
+ return false
98
+ }