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 +2 -0
- package/package.json +3 -3
- package/src/backend/dataSource.js +2 -2
- package/src/expression/evaluate.js +32 -1
- package/src/expression/geometry.d.ts +71 -0
- package/src/expression/spatial.equality.js +98 -0
- package/src/expression/spatial.geometry.js +620 -0
- package/src/expression/spatial.js +365 -0
- package/src/expression/wkt.js +222 -0
- package/src/types.d.ts +16 -0
- package/src/validation.js +39 -2
- package/src/validationErrors.js +16 -0
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
+
}
|