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.
@@ -0,0 +1,365 @@
1
+ import { geomToWkt, parseWkt } from './wkt.js'
2
+ import { distSq, pointInPolygon, pointLineRelation, pointToSegmentDistSq, simpleIntersects, simplePairContainment, simplePairRelation } from './spatial.geometry.js'
3
+ import { simpleGeomEqual } from './spatial.equality.js'
4
+
5
+ /**
6
+ * @import { SpatialFunc, SqlPrimitive } from '../types.js'
7
+ * @import { Geometry, Point, SimpleGeometry } from './geometry.js'
8
+ */
9
+
10
+ /**
11
+ * Evaluate a spatial predicate function.
12
+ *
13
+ * @param {Object} options
14
+ * @param {SpatialFunc} options.funcName
15
+ * @param {SqlPrimitive[]} options.args
16
+ * @returns {SqlPrimitive}
17
+ */
18
+ export function evaluateSpatialFunc({ funcName, args }) {
19
+ // Singleton functions
20
+ if (funcName === 'ST_GEOMFROMTEXT') {
21
+ if (args[0] == null) return null
22
+ return parseWkt(String(args[0]))
23
+ }
24
+
25
+ if (funcName === 'ST_MAKEENVELOPE') {
26
+ if (args[0] == null || args[1] == null || args[2] == null || args[3] == null) return null
27
+ const xmin = Number(args[0])
28
+ const ymin = Number(args[1])
29
+ const xmax = Number(args[2])
30
+ const ymax = Number(args[3])
31
+ return {
32
+ type: 'Polygon',
33
+ coordinates: [[[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax], [xmin, ymin]]],
34
+ }
35
+ }
36
+
37
+ const geomA = toGeometry(args[0])
38
+ if (funcName === 'ST_ASTEXT') {
39
+ if (geomA == null) return null
40
+ return geomToWkt(geomA)
41
+ }
42
+
43
+ // Predicate functions (require two geometries)
44
+ const geomB = toGeometry(args[1])
45
+ if (geomA == null || geomB == null) return null
46
+ const a = decompose(geomA)
47
+ const b = decompose(geomB)
48
+
49
+ switch (funcName) {
50
+ case 'ST_INTERSECTS': return simpleIntersects(a, b)
51
+ case 'ST_CONTAINS': return stContains(a, b)
52
+ case 'ST_CONTAINSPROPERLY': return stContainsProperly(a, b)
53
+ case 'ST_WITHIN': return stContains(b, a) // inverse of contains
54
+ case 'ST_OVERLAPS': return stOverlaps(a, b)
55
+ case 'ST_TOUCHES': return stTouches(a, b)
56
+ case 'ST_EQUALS': return stEquals(a, b)
57
+ case 'ST_CROSSES': return stCrosses(a, b)
58
+ case 'ST_COVERS': return stContains(a, b) // TODO: handle boundary
59
+ case 'ST_COVEREDBY': return stContains(b, a) // inverse of covers
60
+ case 'ST_DWITHIN': {
61
+ if (args[2] == null) return null
62
+ const dist = Number(args[2])
63
+ return stDWithin(a, b, dist)
64
+ }
65
+ default: return null
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Normalize a geometry value. Accepts GeoJSON objects.
71
+ * Returns null if the value is not a valid geometry.
72
+ *
73
+ * @param {SqlPrimitive} val
74
+ * @returns {Geometry | null}
75
+ */
76
+ function toGeometry(val) {
77
+ if (typeof val === 'object' && val != null && 'type' in val) {
78
+ if (val.type === 'GeometryCollection' && Array.isArray(val.geometries)) {
79
+ // eslint-disable-next-line no-extra-parens
80
+ return /** @type {Geometry} */ (val)
81
+ }
82
+ const geometryTypes = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon']
83
+ if (geometryTypes.includes(val.type) && Array.isArray(val.coordinates)) {
84
+ // eslint-disable-next-line no-extra-parens
85
+ return /** @type {Geometry} */ (val)
86
+ }
87
+ }
88
+ return null
89
+ }
90
+
91
+ // ============================================================================
92
+ // Minimum distance between geometries
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Get all line segments from a geometry.
97
+ *
98
+ * @param {SimpleGeometry[]} geoms
99
+ * @returns {{ segments: Array<[number[], number[]]>, points: number[][] }}
100
+ */
101
+ function getSegments(geoms) {
102
+ /** @type {Array<[number[], number[]]>} */
103
+ const segments = []
104
+ /** @type {number[][]} */
105
+ const points = []
106
+
107
+ /**
108
+ * @param {number[][]} coords
109
+ */
110
+ function addLine(coords) {
111
+ for (let i = 0; i < coords.length - 1; i++) {
112
+ segments.push([coords[i], coords[i + 1]])
113
+ }
114
+ points.push(...coords)
115
+ }
116
+
117
+ for (const geom of geoms) {
118
+ if (geom.type === 'Point') points.push(geom.coordinates)
119
+ else if (geom.type === 'LineString') addLine(geom.coordinates)
120
+ else if (geom.type === 'Polygon') geom.coordinates.forEach(addLine)
121
+ }
122
+
123
+ return { segments, points }
124
+ }
125
+
126
+ /**
127
+ * Test whether two geometries are within a given distance of each other.
128
+ * Intersecting geometries have distance 0. For non-intersecting geometries,
129
+ * the minimum distance is always at an endpoint, so point-to-segment suffices.
130
+ *
131
+ * @param {SimpleGeometry[]} a
132
+ * @param {SimpleGeometry[]} b
133
+ * @param {number} distance
134
+ * @returns {boolean}
135
+ */
136
+ function stDWithin(a, b, distance) {
137
+ if (distance < 0) return false
138
+ if (simpleIntersects(a, b)) return true
139
+
140
+ const distanceSq = distance * distance
141
+ const { points: ptsA, segments: segsA } = getSegments(a)
142
+ const { points: ptsB, segments: segsB } = getSegments(b)
143
+
144
+ // Point-to-point
145
+ for (const pa of ptsA) {
146
+ for (const pb of ptsB) {
147
+ if (distSq(pa, pb) <= distanceSq) return true
148
+ }
149
+ }
150
+
151
+ // Point-to-segment
152
+ for (const pt of ptsA) {
153
+ for (const [b1, b2] of segsB) {
154
+ if (pointToSegmentDistSq(pt, b1, b2) <= distanceSq) return true
155
+ }
156
+ }
157
+ for (const pt of ptsB) {
158
+ for (const [a1, a2] of segsA) {
159
+ if (pointToSegmentDistSq(pt, a1, a2) <= distanceSq) return true
160
+ }
161
+ }
162
+
163
+ return false
164
+ }
165
+
166
+ // ============================================================================
167
+ // Spatial predicate dispatch - decompose to primitive type pairs
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Decompose Multi* and GeometryCollection into simple geometries.
172
+ *
173
+ * @param {Geometry} geom
174
+ * @returns {SimpleGeometry[]}
175
+ */
176
+ function decompose(geom) {
177
+ switch (geom.type) {
178
+ case 'MultiPoint':
179
+ return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
180
+ case 'MultiLineString':
181
+ return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
182
+ case 'MultiPolygon':
183
+ return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
184
+ case 'GeometryCollection':
185
+ return geom.geometries.flatMap(decompose)
186
+ default:
187
+ return [geom]
188
+ }
189
+ }
190
+
191
+ // ============================================================================
192
+ // ST_Contains
193
+ // ============================================================================
194
+
195
+ /**
196
+ * @param {SimpleGeometry[]} a
197
+ * @param {SimpleGeometry[]} b
198
+ * @returns {boolean}
199
+ */
200
+ function stContains(a, b) {
201
+ // Every part of b must be inside some part of a
202
+ return b.every(pb => a.some(pa => simplePairContainment(pa, pb) !== 'OUTSIDE'))
203
+ }
204
+
205
+ // ============================================================================
206
+ // ST_ContainsProperly
207
+ // ============================================================================
208
+
209
+ /**
210
+ * @param {SimpleGeometry[]} a
211
+ * @param {SimpleGeometry[]} b
212
+ * @returns {boolean}
213
+ */
214
+ function stContainsProperly(a, b) {
215
+ // Every part of b must be strictly inside some part of a
216
+ return b.every(pb => a.some(pa => simplePairContainment(pa, pb) === 'INSIDE'))
217
+ }
218
+
219
+ // ============================================================================
220
+ // ST_Touches
221
+ // ============================================================================
222
+
223
+ /**
224
+ * @param {SimpleGeometry[]} a
225
+ * @param {SimpleGeometry[]} b
226
+ * @returns {boolean}
227
+ */
228
+ function stTouches(a, b) {
229
+ let intersects = false
230
+ for (const pa of a) {
231
+ for (const pb of b) {
232
+ const rel = simplePairRelation(pa, pb)
233
+ if (rel === 'INSIDE') return false
234
+ if (rel === 'BOUNDARY') intersects = true
235
+ }
236
+ }
237
+ return intersects
238
+ }
239
+
240
+ // ============================================================================
241
+ // ST_Overlaps
242
+ // ============================================================================
243
+
244
+ /**
245
+ * @param {SimpleGeometry[]} a
246
+ * @param {SimpleGeometry[]} b
247
+ * @returns {boolean}
248
+ */
249
+ function stOverlaps(a, b) {
250
+ // Overlaps requires same dimension, and that each geometry has some part
251
+ // inside the other and some part outside
252
+ const dimA = geometryDimension(a)
253
+ const dimB = geometryDimension(b)
254
+ if (dimA !== dimB) return false
255
+ if (!simpleIntersects(a, b)) return false
256
+ if (stEquals(a, b)) return false
257
+ // Must not be containment
258
+ if (stContains(a, b) || stContains(b, a)) return false
259
+ return true
260
+ }
261
+
262
+ /**
263
+ * @param {SimpleGeometry[]} parts
264
+ * @returns {number}
265
+ */
266
+ function geometryDimension(parts) {
267
+ let max = 0
268
+ for (const geom of parts) {
269
+ switch (geom.type) {
270
+ case 'Point':
271
+ break
272
+ case 'LineString':
273
+ if (max < 1) max = 1
274
+ break
275
+ case 'Polygon':
276
+ return 2
277
+ }
278
+ }
279
+ return max
280
+ }
281
+
282
+ // ============================================================================
283
+ // ST_Equals
284
+ // ============================================================================
285
+
286
+ /**
287
+ * @param {SimpleGeometry[]} a
288
+ * @param {SimpleGeometry[]} b
289
+ * @returns {boolean}
290
+ */
291
+ function stEquals(a, b) {
292
+ if (a.length !== b.length) return false
293
+
294
+ // For each simple geometry in a, find a matching one in b
295
+ const used = new Set()
296
+ for (const pa of a) {
297
+ let found = false
298
+ for (let i = 0; i < b.length; i++) {
299
+ if (used.has(i)) continue
300
+ if (simpleGeomEqual(pa, b[i])) {
301
+ used.add(i)
302
+ found = true
303
+ break
304
+ }
305
+ }
306
+ if (!found) return false
307
+ }
308
+ return true
309
+ }
310
+
311
+ // ============================================================================
312
+ // ST_Crosses
313
+ // ============================================================================
314
+
315
+ /**
316
+ * @param {SimpleGeometry[]} a
317
+ * @param {SimpleGeometry[]} b
318
+ * @returns {boolean}
319
+ */
320
+ function stCrosses(a, b) {
321
+ // Crosses: interiors intersect, and the intersection has lower dimension
322
+ // than the maximum of the two geometries' dimensions
323
+ const dimA = geometryDimension(a)
324
+ const dimB = geometryDimension(b)
325
+
326
+ if (!simpleIntersects(a, b)) return false
327
+
328
+ // Point/Point or Polygon/Polygon cannot cross
329
+ if (dimA === dimB && dimA !== 1) return false
330
+
331
+ // Line/Line: they cross if they intersect at a point (not overlap)
332
+ if (dimA === 1 && dimB === 1) {
333
+ // They cross if they intersect but neither contains the other
334
+ // and the intersection is a set of points (not line segments)
335
+ return !stContains(a, b) && !stContains(b, a) && !stTouches(a, b)
336
+ }
337
+
338
+ // Point/Line, Point/Polygon: point "in interior"
339
+ if (dimA === 0 && dimB >= 1) {
340
+ for (const pa of a) {
341
+ // eslint-disable-next-line no-extra-parens
342
+ const point = /** @type {Point} */ (pa)
343
+ for (const pb of b) {
344
+ if (pb.type === 'LineString') {
345
+ if (pointLineRelation(point.coordinates, pb.coordinates) === 'INSIDE') return true
346
+ }
347
+ if (pb.type === 'Polygon') {
348
+ if (pointInPolygon(point.coordinates, pb.coordinates) === 'INSIDE') return true
349
+ }
350
+ }
351
+ }
352
+ return false
353
+ }
354
+
355
+ // Line/Polygon: line crosses polygon if part of line is inside and part is outside
356
+ if (dimA === 1 && dimB === 2) {
357
+ return !stContains(b, a)
358
+ }
359
+
360
+ // Symmetric cases
361
+ if (dimA >= 1 && dimB === 0) return stCrosses(b, a)
362
+ if (dimA === 2 && dimB === 1) return stCrosses(b, a)
363
+
364
+ return false
365
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @import { Geometry } from './geometry.js'
3
+ */
4
+
5
+ /**
6
+ * Parse a WKT string into a GeoJSON geometry.
7
+ *
8
+ * @param {string} wkt
9
+ * @returns {Geometry | null}
10
+ */
11
+ export function parseWkt(wkt) {
12
+ const s = wkt.trim()
13
+ const upper = s.toUpperCase()
14
+
15
+ if (upper.startsWith('POINT')) {
16
+ const coords = parseWktCoordinate(s.slice(5).trim())
17
+ if (!coords) return null
18
+ return { type: 'Point', coordinates: coords }
19
+ }
20
+
21
+ if (upper.startsWith('MULTIPOINT')) {
22
+ const inner = extractParens(s.slice(10).trim())
23
+ if (inner == null) return null
24
+ const coords = parseWktCoordinateList(inner)
25
+ if (!coords) return null
26
+ return { type: 'MultiPoint', coordinates: coords }
27
+ }
28
+
29
+ if (upper.startsWith('MULTILINESTRING')) {
30
+ const inner = extractParens(s.slice(15).trim())
31
+ if (inner == null) return null
32
+ const rings = parseWktRingList(inner)
33
+ if (!rings) return null
34
+ return { type: 'MultiLineString', coordinates: rings }
35
+ }
36
+
37
+ if (upper.startsWith('MULTIPOLYGON')) {
38
+ const inner = extractParens(s.slice(12).trim())
39
+ if (inner == null) return null
40
+ const polys = parseWktPolygonList(inner)
41
+ if (!polys) return null
42
+ return { type: 'MultiPolygon', coordinates: polys }
43
+ }
44
+
45
+ if (upper.startsWith('LINESTRING')) {
46
+ const inner = extractParens(s.slice(10).trim())
47
+ if (inner == null) return null
48
+ const coords = parseWktCoordinateList(inner)
49
+ if (!coords) return null
50
+ return { type: 'LineString', coordinates: coords }
51
+ }
52
+
53
+ if (upper.startsWith('POLYGON')) {
54
+ const inner = extractParens(s.slice(7).trim())
55
+ if (inner == null) return null
56
+ const rings = parseWktRingList(inner)
57
+ if (!rings) return null
58
+ return { type: 'Polygon', coordinates: rings }
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ /**
65
+ * Convert a GeoJSON geometry to WKT.
66
+ *
67
+ * @param {Geometry} geom
68
+ * @returns {string}
69
+ */
70
+ export function geomToWkt(geom) {
71
+ switch (geom.type) {
72
+ case 'Point':
73
+ return `POINT (${coordToWkt(geom.coordinates)})`
74
+ case 'MultiPoint':
75
+ return `MULTIPOINT (${geom.coordinates.map(c => `(${coordToWkt(c)})`).join(', ')})`
76
+ case 'LineString':
77
+ return `LINESTRING (${coordListToWkt(geom.coordinates)})`
78
+ case 'MultiLineString':
79
+ return `MULTILINESTRING (${geom.coordinates.map(l => `(${coordListToWkt(l)})`).join(', ')})`
80
+ case 'Polygon':
81
+ return `POLYGON (${geom.coordinates.map(r => `(${coordListToWkt(r)})`).join(', ')})`
82
+ case 'MultiPolygon':
83
+ return `MULTIPOLYGON (${geom.coordinates.map(p => `(${p.map(r => `(${coordListToWkt(r)})`).join(', ')})`).join(', ')})`
84
+ case 'GeometryCollection':
85
+ return `GEOMETRYCOLLECTION (${(geom.geometries || []).map(g => geomToWkt(g)).join(', ')})`
86
+ default:
87
+ return ''
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extract content inside outer parentheses.
93
+ *
94
+ * @param {string} s
95
+ * @returns {string | null}
96
+ */
97
+ function extractParens(s) {
98
+ const trimmed = s.trim()
99
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return null
100
+ return trimmed.slice(1, -1).trim()
101
+ }
102
+
103
+ /**
104
+ * Parse a single coordinate like "(1 2)" or "1 2".
105
+ *
106
+ * @param {string} s
107
+ * @returns {number[] | null}
108
+ */
109
+ function parseWktCoordinate(s) {
110
+ const inner = s.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
111
+ const parts = inner.split(/\s+/)
112
+ if (parts.length < 2) return null
113
+ const x = Number(parts[0])
114
+ const y = Number(parts[1])
115
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
116
+ return [x, y]
117
+ }
118
+
119
+ /**
120
+ * Parse a comma-separated list of coordinates like "1 2, 3 4, 5 6".
121
+ *
122
+ * @param {string} s
123
+ * @returns {number[][] | null}
124
+ */
125
+ function parseWktCoordinateList(s) {
126
+ const parts = s.split(',')
127
+ /** @type {number[][]} */
128
+ const coords = []
129
+ for (const part of parts) {
130
+ const trimmed = part.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
131
+ const nums = trimmed.split(/\s+/)
132
+ if (nums.length < 2) return null
133
+ const x = Number(nums[0])
134
+ const y = Number(nums[1])
135
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
136
+ coords.push([x, y])
137
+ }
138
+ return coords.length ? coords : null
139
+ }
140
+
141
+ /**
142
+ * Parse a list of rings like "(1 2, 3 4), (5 6, 7 8)".
143
+ *
144
+ * @param {string} s
145
+ * @returns {number[][][] | null}
146
+ */
147
+ function parseWktRingList(s) {
148
+ /** @type {number[][][]} */
149
+ const rings = []
150
+ const ringStrs = splitTopLevel(s)
151
+ for (const ringStr of ringStrs) {
152
+ const inner = extractParens(ringStr.trim())
153
+ if (inner == null) return null
154
+ const coords = parseWktCoordinateList(inner)
155
+ if (!coords) return null
156
+ rings.push(coords)
157
+ }
158
+ return rings.length ? rings : null
159
+ }
160
+
161
+ /**
162
+ * Parse a list of polygons like "((ring1), (ring2)), ((ring3))".
163
+ *
164
+ * @param {string} s
165
+ * @returns {number[][][][] | null}
166
+ */
167
+ function parseWktPolygonList(s) {
168
+ /** @type {number[][][][]} */
169
+ const polys = []
170
+ const polyStrs = splitTopLevel(s)
171
+ for (const polyStr of polyStrs) {
172
+ const inner = extractParens(polyStr.trim())
173
+ if (inner == null) return null
174
+ const rings = parseWktRingList(inner)
175
+ if (!rings) return null
176
+ polys.push(rings)
177
+ }
178
+ return polys.length ? polys : null
179
+ }
180
+
181
+ /**
182
+ * Split a string by commas at the top-level (not inside parentheses).
183
+ *
184
+ * @param {string} s
185
+ * @returns {string[]}
186
+ */
187
+ function splitTopLevel(s) {
188
+ /** @type {string[]} */
189
+ const parts = []
190
+ let depth = 0
191
+ let start = 0
192
+ for (let i = 0; i < s.length; i++) {
193
+ if (s[i] === '(') depth++
194
+ else if (s[i] === ')') depth--
195
+ else if (s[i] === ',' && depth === 0) {
196
+ parts.push(s.slice(start, i))
197
+ start = i + 1
198
+ }
199
+ }
200
+ parts.push(s.slice(start))
201
+ return parts
202
+ }
203
+
204
+ /**
205
+ * Format a single coordinate to WKT.
206
+ *
207
+ * @param {number[]} coord
208
+ * @returns {string}
209
+ */
210
+ function coordToWkt(coord) {
211
+ return `${coord[0]} ${coord[1]}`
212
+ }
213
+
214
+ /**
215
+ * Format a coordinate list to WKT.
216
+ *
217
+ * @param {number[][]} coords
218
+ * @returns {string}
219
+ */
220
+ function coordListToWkt(coords) {
221
+ return coords.map(coordToWkt).join(', ')
222
+ }
package/src/types.d.ts CHANGED
@@ -276,6 +276,22 @@ export type StringFunc =
276
276
  | 'RIGHT'
277
277
  | 'INSTR'
278
278
 
279
+ export type SpatialFunc =
280
+ | 'ST_INTERSECTS'
281
+ | 'ST_CONTAINS'
282
+ | 'ST_CONTAINSPROPERLY'
283
+ | 'ST_WITHIN'
284
+ | 'ST_OVERLAPS'
285
+ | 'ST_TOUCHES'
286
+ | 'ST_EQUALS'
287
+ | 'ST_CROSSES'
288
+ | 'ST_COVERS'
289
+ | 'ST_COVEREDBY'
290
+ | 'ST_DWITHIN'
291
+ | 'ST_GEOMFROMTEXT'
292
+ | 'ST_MAKEENVELOPE'
293
+ | 'ST_ASTEXT'
294
+
279
295
  export interface StarColumn {
280
296
  kind: 'star'
281
297
  table?: string
package/src/validation.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ParseError } from './parseErrors.js'
2
2
 
3
3
  /**
4
- * @import { AggregateFunc, BinaryOp, ExprNode, FunctionNode, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction } from './types.js'
4
+ * @import { AggregateFunc, BinaryOp, ExprNode, FunctionNode, IntervalUnit, MathFunc, SpatialFunc, StringFunc, UserDefinedFunction } from './types.js'
5
5
  * @param {string} name
6
6
  * @returns {name is AggregateFunc}
7
7
  */
@@ -79,6 +79,19 @@ export function isRegexpFunc(name) {
79
79
  return ['REGEXP_SUBSTR', 'REGEXP_REPLACE'].includes(name)
80
80
  }
81
81
 
82
+ /**
83
+ * @param {string} name
84
+ * @returns {name is SpatialFunc}
85
+ */
86
+ export function isSpatialFunc(name) {
87
+ return [
88
+ 'ST_INTERSECTS', 'ST_CONTAINS', 'ST_CONTAINSPROPERLY', 'ST_WITHIN',
89
+ 'ST_OVERLAPS', 'ST_TOUCHES', 'ST_EQUALS', 'ST_CROSSES',
90
+ 'ST_COVERS', 'ST_COVEREDBY', 'ST_DWITHIN',
91
+ 'ST_GEOMFROMTEXT', 'ST_MAKEENVELOPE', 'ST_ASTEXT',
92
+ ].includes(name)
93
+ }
94
+
82
95
  /**
83
96
  * @param {string} name
84
97
  * @returns {name is MathFunc}
@@ -178,6 +191,12 @@ export const FUNCTION_ARG_COUNTS = {
178
191
  JSON_OBJECT: { min: 0 },
179
192
  JSON_ARRAYAGG: { min: 1, max: 1 },
180
193
 
194
+ // Array functions
195
+ ARRAY_LENGTH: { min: 1, max: 1 },
196
+ ARRAY_POSITION: { min: 2, max: 2 },
197
+ ARRAY_SORT: { min: 1, max: 1 },
198
+ CARDINALITY: { min: 1, max: 1 },
199
+
181
200
  // Conditional functions
182
201
  COALESCE: { min: 1 },
183
202
  NULLIF: { min: 2, max: 2 },
@@ -190,6 +209,22 @@ export const FUNCTION_ARG_COUNTS = {
190
209
  MAX: { min: 1, max: 1 },
191
210
  STDDEV_SAMP: { min: 1, max: 1 },
192
211
  STDDEV_POP: { min: 1, max: 1 },
212
+
213
+ // Spatial predicate functions
214
+ ST_INTERSECTS: { min: 2, max: 2 },
215
+ ST_CONTAINS: { min: 2, max: 2 },
216
+ ST_CONTAINSPROPERLY: { min: 2, max: 2 },
217
+ ST_WITHIN: { min: 2, max: 2 },
218
+ ST_OVERLAPS: { min: 2, max: 2 },
219
+ ST_TOUCHES: { min: 2, max: 2 },
220
+ ST_EQUALS: { min: 2, max: 2 },
221
+ ST_CROSSES: { min: 2, max: 2 },
222
+ ST_COVERS: { min: 2, max: 2 },
223
+ ST_COVEREDBY: { min: 2, max: 2 },
224
+ ST_DWITHIN: { min: 3, max: 3 },
225
+ ST_GEOMFROMTEXT: { min: 1, max: 1 },
226
+ ST_MAKEENVELOPE: { min: 4, max: 4 },
227
+ ST_ASTEXT: { min: 1, max: 1 },
193
228
  }
194
229
 
195
230
  /**
@@ -249,7 +284,8 @@ export function isKnownFunction(funcName, functions) {
249
284
  isAggregateFunc(funcName) ||
250
285
  isMathFunc(funcName) ||
251
286
  isStringFunc(funcName) ||
252
- isRegexpFunc(funcName)
287
+ isRegexpFunc(funcName) ||
288
+ isSpatialFunc(funcName)
253
289
  ) {
254
290
  return true
255
291
  }
@@ -258,6 +294,7 @@ export function isKnownFunction(funcName, functions) {
258
294
  if ([
259
295
  'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
260
296
  'JSON_VALUE', 'JSON_QUERY', 'JSON_OBJECT',
297
+ 'ARRAY_LENGTH', 'ARRAY_POSITION', 'ARRAY_SORT', 'CARDINALITY',
261
298
  'COALESCE', 'NULLIF', 'CAST',
262
299
  ].includes(funcName)) {
263
300
  return true
@@ -68,6 +68,22 @@ export const FUNCTION_SIGNATURES = {
68
68
  MAX: 'expression',
69
69
  STDDEV_SAMP: 'expression',
70
70
  STDDEV_POP: 'expression',
71
+
72
+ // Spatial predicate functions
73
+ ST_INTERSECTS: 'geometry, geometry',
74
+ ST_CONTAINS: 'geometry, geometry',
75
+ ST_CONTAINSPROPERLY: 'geometry, geometry',
76
+ ST_WITHIN: 'geometry, geometry',
77
+ ST_OVERLAPS: 'geometry, geometry',
78
+ ST_TOUCHES: 'geometry, geometry',
79
+ ST_EQUALS: 'geometry, geometry',
80
+ ST_CROSSES: 'geometry, geometry',
81
+ ST_COVERS: 'geometry, geometry',
82
+ ST_COVEREDBY: 'geometry, geometry',
83
+ ST_DWITHIN: 'geometry, geometry, distance',
84
+ ST_GEOMFROMTEXT: 'wkt',
85
+ ST_MAKEENVELOPE: 'xmin, ymin, xmax, ymax',
86
+ ST_ASTEXT: 'geometry',
71
87
  }
72
88
 
73
89
  /**