squirreling 0.9.3 → 0.9.5
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 +2 -2
- package/src/expression/date.js +60 -0
- package/src/expression/evaluate.js +10 -2
- package/src/parse/expression.js +42 -1
- package/src/spatial/bbox.js +53 -0
- package/src/spatial/equality.js +98 -0
- package/src/spatial/geometry.d.ts +78 -0
- package/src/spatial/operations.js +371 -0
- package/src/spatial/pointRelations.js +102 -0
- package/src/spatial/primitives.js +27 -0
- package/src/spatial/segments.js +139 -0
- package/src/spatial/spatial.js +367 -0
- package/src/spatial/wkt.js +222 -0
- package/src/validation.js +24 -1
- package/src/expression/spatial.js +0 -1471
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
147
147
|
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`
|
|
148
148
|
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
149
149
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
150
|
-
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
150
|
+
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_PART`, `DATE_TRUNC`, `EXTRACT`, `INTERVAL`
|
|
151
151
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
152
152
|
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_SORT`, `CARDINALITY`
|
|
153
153
|
- Regex: `REGEXP_SUBSTR`, `REGEXP_REPLACE`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.3.
|
|
40
|
+
"@types/node": "25.3.3",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.18",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
43
|
"eslint-plugin-jsdoc": "62.7.1",
|
package/src/expression/date.js
CHANGED
|
@@ -40,6 +40,66 @@ export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Truncate a date to the given precision
|
|
45
|
+
* @param {SqlPrimitive} precision - the unit to truncate to (year, month, day, hour, minute, second)
|
|
46
|
+
* @param {SqlPrimitive} dateVal - the date value to truncate
|
|
47
|
+
* @returns {Date | string | null}
|
|
48
|
+
*/
|
|
49
|
+
export function dateTrunc(precision, dateVal) {
|
|
50
|
+
if (precision == null || dateVal == null) return null
|
|
51
|
+
const date = toDate(dateVal)
|
|
52
|
+
if (date == null) return null
|
|
53
|
+
|
|
54
|
+
const unit = String(precision).toUpperCase()
|
|
55
|
+
if (unit === 'YEAR') {
|
|
56
|
+
date.setUTCMonth(0, 1)
|
|
57
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
58
|
+
} else if (unit === 'MONTH') {
|
|
59
|
+
date.setUTCDate(1)
|
|
60
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
61
|
+
} else if (unit === 'DAY') {
|
|
62
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
63
|
+
} else if (unit === 'HOUR') {
|
|
64
|
+
date.setUTCMinutes(0, 0, 0)
|
|
65
|
+
} else if (unit === 'MINUTE') {
|
|
66
|
+
date.setUTCSeconds(0, 0)
|
|
67
|
+
} else if (unit === 'SECOND') {
|
|
68
|
+
date.setUTCMilliseconds(0)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return in same format as input
|
|
72
|
+
if (dateVal instanceof Date) return date
|
|
73
|
+
if (String(dateVal).includes('T')) {
|
|
74
|
+
return date.toISOString()
|
|
75
|
+
} else {
|
|
76
|
+
return date.toISOString().split('T')[0]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract a field from a date value
|
|
82
|
+
* @param {SqlPrimitive} field - the field to extract (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)
|
|
83
|
+
* @param {SqlPrimitive} dateVal - the date value to extract from
|
|
84
|
+
* @returns {number | null}
|
|
85
|
+
*/
|
|
86
|
+
export function extractField(field, dateVal) {
|
|
87
|
+
if (field == null || dateVal == null) return null
|
|
88
|
+
const date = toDate(dateVal)
|
|
89
|
+
if (date == null) return null
|
|
90
|
+
|
|
91
|
+
const unit = String(field).toUpperCase()
|
|
92
|
+
if (unit === 'YEAR') return date.getUTCFullYear()
|
|
93
|
+
if (unit === 'MONTH') return date.getUTCMonth() + 1
|
|
94
|
+
if (unit === 'DAY') return date.getUTCDate()
|
|
95
|
+
if (unit === 'HOUR') return date.getUTCHours()
|
|
96
|
+
if (unit === 'MINUTE') return date.getUTCMinutes()
|
|
97
|
+
if (unit === 'SECOND') return date.getUTCSeconds()
|
|
98
|
+
if (unit === 'DOW') return date.getUTCDay()
|
|
99
|
+
if (unit === 'EPOCH') return date.getTime() / 1000
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
43
103
|
/**
|
|
44
104
|
* @param {SqlPrimitive} val
|
|
45
105
|
* @returns {Date | null}
|
|
@@ -6,10 +6,10 @@ import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc
|
|
|
6
6
|
import { aggregateError, argValueError, castError } from '../validationErrors.js'
|
|
7
7
|
import { derivedAlias } from './alias.js'
|
|
8
8
|
import { applyBinaryOp } from './binary.js'
|
|
9
|
-
import { applyIntervalToDate } from './date.js'
|
|
9
|
+
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
|
10
10
|
import { evaluateMathFunc } from './math.js'
|
|
11
11
|
import { evaluateRegexpFunc } from './regexp.js'
|
|
12
|
-
import { evaluateSpatialFunc } from '
|
|
12
|
+
import { evaluateSpatialFunc } from '../spatial/spatial.js'
|
|
13
13
|
import { evaluateStringFunc } from './strings.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -271,6 +271,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
271
271
|
return val1 == val2 ? null : val1
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
if (funcName === 'DATE_TRUNC') {
|
|
275
|
+
return dateTrunc(args[0], args[1])
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (funcName === 'EXTRACT' || funcName === 'DATE_PART') {
|
|
279
|
+
return extractField(args[0], args[1])
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
if (funcName === 'CURRENT_DATE') {
|
|
275
283
|
return new Date().toISOString().split('T')[0]
|
|
276
284
|
}
|
package/src/parse/expression.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
syntaxError,
|
|
5
5
|
unknownFunctionError,
|
|
6
6
|
} from '../parseErrors.js'
|
|
7
|
-
import { isIntervalUnit, isKnownFunction } from '../validation.js'
|
|
7
|
+
import { RESERVED_KEYWORDS, isExtractField, isIntervalUnit, isKnownFunction } from '../validation.js'
|
|
8
8
|
import { parseComparison } from './comparison.js'
|
|
9
9
|
import { parseFunctionCall } from './functions.js'
|
|
10
10
|
import { parseSelectInternal } from './parse.js'
|
|
@@ -70,6 +70,36 @@ export function parsePrimary(state) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// EXTRACT(field FROM expr)
|
|
74
|
+
if (tok.value === 'EXTRACT' && next.type === 'paren' && next.value === '(') {
|
|
75
|
+
consume(state) // EXTRACT
|
|
76
|
+
consume(state) // '('
|
|
77
|
+
const fieldTok = current(state)
|
|
78
|
+
const isValidType = fieldTok.type === 'keyword' || fieldTok.type === 'identifier'
|
|
79
|
+
if (!isValidType || !isExtractField(fieldTok.value)) {
|
|
80
|
+
throw syntaxError({
|
|
81
|
+
expected: 'extract field (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)',
|
|
82
|
+
received: `"${fieldTok.value}"`,
|
|
83
|
+
positionStart: fieldTok.positionStart,
|
|
84
|
+
positionEnd: fieldTok.positionEnd,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
consume(state) // field
|
|
88
|
+
expect(state, 'keyword', 'FROM')
|
|
89
|
+
const expr = parseExpression(state)
|
|
90
|
+
expect(state, 'paren', ')')
|
|
91
|
+
return {
|
|
92
|
+
type: 'function',
|
|
93
|
+
name: 'EXTRACT',
|
|
94
|
+
args: [
|
|
95
|
+
{ type: 'literal', value: fieldTok.value, positionStart: fieldTok.positionStart, positionEnd: fieldTok.positionEnd },
|
|
96
|
+
expr,
|
|
97
|
+
],
|
|
98
|
+
positionStart,
|
|
99
|
+
positionEnd: state.lastPos,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
// function call
|
|
74
104
|
if (next.type === 'paren' && next.value === '(') {
|
|
75
105
|
const funcName = tok.value
|
|
@@ -220,6 +250,17 @@ export function parsePrimary(state) {
|
|
|
220
250
|
if (tok.value === 'INTERVAL') {
|
|
221
251
|
return parseInterval(state)
|
|
222
252
|
}
|
|
253
|
+
|
|
254
|
+
// Non-reserved keywords can be used as identifiers (e.g. column aliases)
|
|
255
|
+
if (!RESERVED_KEYWORDS.has(tok.value)) {
|
|
256
|
+
consume(state)
|
|
257
|
+
return {
|
|
258
|
+
type: 'identifier',
|
|
259
|
+
name: tok.originalValue ?? tok.value,
|
|
260
|
+
positionStart,
|
|
261
|
+
positionEnd: state.lastPos,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
223
264
|
}
|
|
224
265
|
|
|
225
266
|
if (tok.type === 'operator' && tok.value === '-') {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BBox, SimpleGeometry } from './geometry.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const EPSILON = 1e-10
|
|
6
|
+
export const EPSILON_SQ = EPSILON * EPSILON
|
|
7
|
+
|
|
8
|
+
/** @type {WeakMap<SimpleGeometry, BBox>} */
|
|
9
|
+
const bboxCache = new WeakMap()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test whether two bounding boxes overlap.
|
|
13
|
+
*
|
|
14
|
+
* @param {SimpleGeometry} a
|
|
15
|
+
* @param {SimpleGeometry} b
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function bboxOverlap(a, b) {
|
|
19
|
+
const aBox = bbox(a)
|
|
20
|
+
const bBox = bbox(b)
|
|
21
|
+
return aBox.minX <= bBox.maxX && aBox.maxX >= bBox.minX && aBox.minY <= bBox.maxY && aBox.maxY >= bBox.minY
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute the axis-aligned bounding box of a simple geometry.
|
|
26
|
+
* Results are cached per geometry object.
|
|
27
|
+
*
|
|
28
|
+
* @param {SimpleGeometry} geom
|
|
29
|
+
* @returns {BBox}
|
|
30
|
+
*/
|
|
31
|
+
function bbox(geom) {
|
|
32
|
+
let b = bboxCache.get(geom)
|
|
33
|
+
if (b) return b
|
|
34
|
+
if (geom.type === 'Point') {
|
|
35
|
+
const [x, y] = geom.coordinates
|
|
36
|
+
b = { minX: x, minY: y, maxX: x, maxY: y }
|
|
37
|
+
} else {
|
|
38
|
+
/** @type {number[][]} */
|
|
39
|
+
const points = geom.type === 'LineString'
|
|
40
|
+
? geom.coordinates
|
|
41
|
+
: geom.coordinates[0] // outer ring
|
|
42
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
43
|
+
for (const p of points) {
|
|
44
|
+
if (p[0] < minX) minX = p[0]
|
|
45
|
+
if (p[1] < minY) minY = p[1]
|
|
46
|
+
if (p[0] > maxX) maxX = p[0]
|
|
47
|
+
if (p[1] > maxY) maxY = p[1]
|
|
48
|
+
}
|
|
49
|
+
b = { minX, minY, maxX, maxY }
|
|
50
|
+
}
|
|
51
|
+
bboxCache.set(geom, b)
|
|
52
|
+
return b
|
|
53
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { SimpleGeometry } from './geometry.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EPSILON, EPSILON_SQ, distSq } from './primitives.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {SimpleGeometry} a
|
|
9
|
+
* @param {SimpleGeometry} b
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function geometryEqual(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
|
+
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
|
+
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
|
+
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
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
export interface BBox {
|
|
19
|
+
minX: number
|
|
20
|
+
minY: number
|
|
21
|
+
maxX: number
|
|
22
|
+
maxY: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Boundary relationship between two geometries.
|
|
27
|
+
*/
|
|
28
|
+
export type Relation = 'OUTSIDE' | 'BOUNDARY' | 'INSIDE'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Position is an array of at least two numbers.
|
|
32
|
+
* The order should be [longitude, latitude] with optional properties (eg- altitude).
|
|
33
|
+
*/
|
|
34
|
+
export type Position = number[]
|
|
35
|
+
|
|
36
|
+
export interface Point {
|
|
37
|
+
type: 'Point'
|
|
38
|
+
coordinates: Position
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MultiPoint {
|
|
42
|
+
type: 'MultiPoint'
|
|
43
|
+
coordinates: Position[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LineString {
|
|
47
|
+
type: 'LineString'
|
|
48
|
+
coordinates: Position[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Each element is one LineString.
|
|
53
|
+
*/
|
|
54
|
+
export interface MultiLineString {
|
|
55
|
+
type: 'MultiLineString'
|
|
56
|
+
coordinates: Position[][]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Each element is a linear ring.
|
|
61
|
+
*/
|
|
62
|
+
export interface Polygon {
|
|
63
|
+
type: 'Polygon'
|
|
64
|
+
coordinates: Position[][]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Each element is one Polygon.
|
|
69
|
+
*/
|
|
70
|
+
export interface MultiPolygon {
|
|
71
|
+
type: 'MultiPolygon'
|
|
72
|
+
coordinates: Position[][][]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface GeometryCollection {
|
|
76
|
+
type: 'GeometryCollection'
|
|
77
|
+
geometries: Geometry[]
|
|
78
|
+
}
|