squirreling 0.10.2 → 0.11.0
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 +5 -4
- package/package.json +5 -5
- package/src/ast.d.ts +32 -15
- package/src/backend/dataSource.js +4 -3
- package/src/execute/aggregates.js +160 -19
- package/src/execute/execute.js +129 -23
- package/src/execute/join.js +20 -21
- package/src/execute/utils.js +19 -7
- package/src/expression/alias.js +3 -2
- package/src/expression/evaluate.js +87 -61
- package/src/expression/math.js +2 -0
- package/src/expression/regexp.js +11 -9
- package/src/expression/strings.js +11 -9
- package/src/index.d.ts +10 -5
- package/src/index.js +1 -1
- package/src/parse/expression.js +187 -351
- package/src/parse/functions.js +63 -51
- package/src/parse/joins.js +24 -38
- package/src/parse/parse.js +244 -200
- package/src/parse/primary.js +281 -0
- package/src/parse/state.js +11 -25
- package/src/parse/tokenize.js +77 -196
- package/src/plan/columns.js +115 -17
- package/src/plan/plan.js +121 -44
- package/src/plan/types.d.ts +11 -1
- package/src/spatial/bbox.js +3 -3
- package/src/spatial/geometry.d.ts +1 -1
- package/src/spatial/index.d.ts +6 -0
- package/src/spatial/index.js +3 -0
- package/src/spatial/spatial.js +19 -53
- package/src/types.d.ts +17 -5
- package/src/validation/executionErrors.js +20 -12
- package/src/validation/functions.js +28 -53
- package/src/validation/keywords.js +35 -0
- package/src/validation/parseErrors.js +101 -82
- package/src/validation/planErrors.js +41 -33
- package/src/parse/comparison.js +0 -233
- package/src/validation/expressionErrors.js +0 -57
package/src/plan/plan.js
CHANGED
|
@@ -1,34 +1,93 @@
|
|
|
1
|
+
import { derivedAlias } from '../expression/alias.js'
|
|
1
2
|
import { parseSql } from '../parse/parse.js'
|
|
2
3
|
import { findAggregate } from '../validation/aggregates.js'
|
|
3
|
-
import {
|
|
4
|
-
import { extractColumns } from './columns.js'
|
|
4
|
+
import { ColumnNotFoundError, TableNotFoundError } from '../validation/planErrors.js'
|
|
5
|
+
import { extractColumns, fromAlias, inferStatementColumns } from './columns.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement } from '../types.js'
|
|
8
|
+
* @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
|
|
8
9
|
* @import { QueryPlan } from './types.d.ts'
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Builds a query plan from a
|
|
13
|
+
* Builds a query plan from a statement AST.
|
|
13
14
|
* Resolves CTEs at plan time so no planning occurs during execution.
|
|
14
15
|
*
|
|
15
16
|
* @param {PlanSqlOptions} options
|
|
16
17
|
* @returns {QueryPlan} the root of the query plan tree
|
|
17
18
|
*/
|
|
18
19
|
export function planSql({ query, functions, tables }) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/** @type {Statement} */
|
|
21
|
+
const stmt = typeof query === 'string' ? parseSql({ query, functions }) : query
|
|
22
|
+
return planStatement({ stmt, tables })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Plans a Statement (SelectStatement, SetOperationStatement, or WithStatement).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {Statement} options.stmt
|
|
30
|
+
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
31
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
32
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
33
|
+
* @param {string[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
|
|
34
|
+
* @returns {QueryPlan}
|
|
35
|
+
*/
|
|
36
|
+
function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumns }) {
|
|
37
|
+
if (stmt.type === 'with') {
|
|
38
|
+
// Build CTE plans in order (each CTE can reference preceding CTEs)
|
|
39
|
+
ctePlans ??= new Map()
|
|
40
|
+
cteColumns ??= new Map()
|
|
41
|
+
for (const cte of stmt.ctes) {
|
|
42
|
+
const ctePlan = planStatement({ stmt: cte.query, ctePlans, cteColumns, tables })
|
|
27
43
|
ctePlans.set(cte.name.toLowerCase(), ctePlan)
|
|
44
|
+
cteColumns.set(cte.name.toLowerCase(), inferStatementColumns({ stmt: cte.query, cteColumns, tables }))
|
|
28
45
|
}
|
|
46
|
+
return planStatement({ stmt: stmt.query, ctePlans, cteColumns, tables, parentColumns })
|
|
47
|
+
}
|
|
48
|
+
if (stmt.type === 'compound') {
|
|
49
|
+
return planSetOperation({ compound: stmt, ctePlans, cteColumns, tables })
|
|
50
|
+
}
|
|
51
|
+
return planSelect({ select: stmt, ctePlans, cteColumns, tables, parentColumns })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Plans a SetOperationStatement (UNION/INTERSECT/EXCEPT).
|
|
56
|
+
*
|
|
57
|
+
* @param {object} options
|
|
58
|
+
* @param {SetOperationStatement} options.compound
|
|
59
|
+
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
60
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
61
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
62
|
+
* @returns {QueryPlan}
|
|
63
|
+
*/
|
|
64
|
+
function planSetOperation({ compound, ctePlans, cteColumns, tables }) {
|
|
65
|
+
const left = planStatement({ stmt: compound.left, ctePlans, cteColumns, tables })
|
|
66
|
+
const right = planStatement({ stmt: compound.right, ctePlans, cteColumns, tables })
|
|
67
|
+
const leftColumns = inferStatementColumns({ stmt: compound.left, cteColumns, tables })
|
|
68
|
+
const rightColumns = inferStatementColumns({ stmt: compound.right, cteColumns, tables })
|
|
69
|
+
|
|
70
|
+
if (leftColumns.length !== rightColumns.length || leftColumns.some((col, idx) => col !== rightColumns[idx])) {
|
|
71
|
+
throw new Error(`Set operation operands must have identical columns, got left [${leftColumns.join(', ')}] and right [${rightColumns.join(', ')}]`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {QueryPlan} */
|
|
75
|
+
let plan = {
|
|
76
|
+
type: 'SetOperation',
|
|
77
|
+
operator: compound.operator,
|
|
78
|
+
all: compound.all,
|
|
79
|
+
left,
|
|
80
|
+
right,
|
|
29
81
|
}
|
|
30
82
|
|
|
31
|
-
|
|
83
|
+
if (compound.orderBy.length) {
|
|
84
|
+
plan = { type: 'Sort', orderBy: compound.orderBy, child: plan }
|
|
85
|
+
}
|
|
86
|
+
if (compound.limit !== undefined || compound.offset) {
|
|
87
|
+
plan = { type: 'Limit', limit: compound.limit, offset: compound.offset, child: plan }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return plan
|
|
32
91
|
}
|
|
33
92
|
|
|
34
93
|
/**
|
|
@@ -36,28 +95,28 @@ export function planSql({ query, functions, tables }) {
|
|
|
36
95
|
*
|
|
37
96
|
* @param {object} options
|
|
38
97
|
* @param {SelectStatement} options.select
|
|
39
|
-
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
98
|
+
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
99
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
40
100
|
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
101
|
+
* @param {string[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
|
|
41
102
|
* @returns {QueryPlan}
|
|
42
103
|
*/
|
|
43
|
-
function planSelect({ select, ctePlans, tables }) {
|
|
104
|
+
function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
44
105
|
// Check for aggregation
|
|
45
106
|
const hasAggregate = select.columns.some(col =>
|
|
46
|
-
col.
|
|
107
|
+
col.type === 'derived' && findAggregate(col.expr)
|
|
47
108
|
)
|
|
48
109
|
const useGrouping = hasAggregate || select.groupBy.length > 0
|
|
49
110
|
const needsBuffering = useGrouping || select.orderBy.length > 0
|
|
50
111
|
|
|
51
112
|
// Source alias for FROM clause
|
|
52
|
-
const sourceAlias = select.from
|
|
53
|
-
? select.from.alias ?? select.from.table
|
|
54
|
-
: select.from.alias
|
|
113
|
+
const sourceAlias = fromAlias(select.from)
|
|
55
114
|
|
|
56
115
|
// Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
|
|
57
116
|
// included so they are only applied to fresh scans, not CTE/subquery plans)
|
|
58
117
|
/** @type {ScanOptions} */
|
|
59
118
|
const hints = {}
|
|
60
|
-
const perTableColumns = extractColumns(select)
|
|
119
|
+
const perTableColumns = extractColumns({ select, parentColumns })
|
|
61
120
|
hints.columns = perTableColumns.get(sourceAlias)
|
|
62
121
|
if (!select.joins.length) {
|
|
63
122
|
hints.where = select.where
|
|
@@ -69,11 +128,11 @@ function planSelect({ select, ctePlans, tables }) {
|
|
|
69
128
|
|
|
70
129
|
// Start with the data source (FROM clause)
|
|
71
130
|
/** @type {QueryPlan} */
|
|
72
|
-
let plan = planFrom({ select, ctePlans, hints, tables })
|
|
131
|
+
let plan = planFrom({ select, ctePlans, cteColumns, hints, tables })
|
|
73
132
|
|
|
74
133
|
// Add JOINs
|
|
75
134
|
if (select.joins.length) {
|
|
76
|
-
plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, perTableColumns, tables })
|
|
135
|
+
plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, cteColumns, perTableColumns, tables })
|
|
77
136
|
}
|
|
78
137
|
|
|
79
138
|
// Whether FROM resolved to our own direct table scan
|
|
@@ -118,7 +177,7 @@ function planSelect({ select, ctePlans, tables }) {
|
|
|
118
177
|
/** @type {Map<string, ExprNode>} */
|
|
119
178
|
const aliases = new Map()
|
|
120
179
|
for (const col of select.columns) {
|
|
121
|
-
if (col.
|
|
180
|
+
if (col.type === 'derived' && col.alias) {
|
|
122
181
|
aliases.set(col.alias, col.expr)
|
|
123
182
|
}
|
|
124
183
|
}
|
|
@@ -133,9 +192,16 @@ function planSelect({ select, ctePlans, tables }) {
|
|
|
133
192
|
// So the order is: Sort -> Project -> Distinct -> Limit
|
|
134
193
|
|
|
135
194
|
// Fast path for SELECT *
|
|
136
|
-
const isPassthrough = select.columns.length === 1 && select.columns[0].
|
|
195
|
+
const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star'
|
|
137
196
|
if (!isPassthrough) {
|
|
138
|
-
|
|
197
|
+
// When parent only needs specific columns, drop unneeded projections
|
|
198
|
+
let projectColumns = select.columns
|
|
199
|
+
if (parentColumns) {
|
|
200
|
+
projectColumns = select.columns.filter(col =>
|
|
201
|
+
col.type === 'star' || parentColumns.includes(col.alias ?? derivedAlias(col.expr))
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
plan = { type: 'Project', columns: projectColumns, child: plan }
|
|
139
205
|
}
|
|
140
206
|
|
|
141
207
|
if (select.distinct) {
|
|
@@ -153,24 +219,35 @@ function planSelect({ select, ctePlans, tables }) {
|
|
|
153
219
|
/**
|
|
154
220
|
* @param {object} options
|
|
155
221
|
* @param {SelectStatement} options.select
|
|
156
|
-
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
222
|
+
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
223
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
157
224
|
* @param {ScanOptions} options.hints
|
|
158
225
|
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
159
226
|
* @returns {QueryPlan}
|
|
160
227
|
*/
|
|
161
|
-
function planFrom({ select, ctePlans, hints, tables }) {
|
|
162
|
-
if (select.from.
|
|
163
|
-
const ctePlan = ctePlans
|
|
228
|
+
function planFrom({ select, ctePlans, cteColumns, hints, tables }) {
|
|
229
|
+
if (select.from.type === 'table') {
|
|
230
|
+
const ctePlan = ctePlans?.get(select.from.table.toLowerCase())
|
|
164
231
|
if (ctePlan) {
|
|
165
232
|
return ctePlan
|
|
166
233
|
}
|
|
167
234
|
validateScan({ ...select.from, hints, tables })
|
|
168
235
|
return { type: 'Scan', table: select.from.table, hints }
|
|
169
236
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
237
|
+
const subPlan = planStatement({ stmt: select.from.query, ctePlans, cteColumns, tables, parentColumns: hints.columns })
|
|
238
|
+
// Validate that requested columns exist in subquery output
|
|
239
|
+
const availableColumns = inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
|
|
240
|
+
if (hints.columns && availableColumns.length) {
|
|
241
|
+
const missingColumn = hints.columns.find(col => !availableColumns.includes(col))
|
|
242
|
+
if (missingColumn) {
|
|
243
|
+
throw new ColumnNotFoundError({
|
|
244
|
+
columnName: missingColumn,
|
|
245
|
+
availableColumns,
|
|
246
|
+
...select.from,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
172
249
|
}
|
|
173
|
-
return
|
|
250
|
+
return subPlan
|
|
174
251
|
}
|
|
175
252
|
}
|
|
176
253
|
|
|
@@ -179,24 +256,28 @@ function planFrom({ select, ctePlans, hints, tables }) {
|
|
|
179
256
|
* @param {QueryPlan} options.left - the left side of the join (FROM or previous joins)
|
|
180
257
|
* @param {JoinClause[]} options.joins - array of join clauses
|
|
181
258
|
* @param {string} options.leftTable - name/alias of the left table
|
|
182
|
-
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
259
|
+
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
260
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
183
261
|
* @param {Map<string, string[] | undefined>} options.perTableColumns
|
|
184
262
|
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
185
263
|
* @returns {QueryPlan}
|
|
186
264
|
*/
|
|
187
|
-
function planJoin({ left, joins, leftTable, ctePlans, perTableColumns, tables }) {
|
|
265
|
+
function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumns, tables }) {
|
|
188
266
|
let plan = left
|
|
189
267
|
let currentLeftTable = leftTable
|
|
190
268
|
|
|
191
269
|
for (const join of joins) {
|
|
192
270
|
const rightTable = join.alias ?? join.table
|
|
193
271
|
|
|
194
|
-
const ctePlan = ctePlans
|
|
272
|
+
const ctePlan = ctePlans?.get(join.table.toLowerCase())
|
|
195
273
|
/** @type {ScanOptions} */
|
|
196
274
|
const rightHints = {}
|
|
197
275
|
if (!ctePlan) {
|
|
198
276
|
rightHints.columns = perTableColumns.get(rightTable)
|
|
199
277
|
validateScan({ ...join, hints: rightHints, tables })
|
|
278
|
+
} else {
|
|
279
|
+
// For CTE joins, use CTE column metadata for hints
|
|
280
|
+
rightHints.columns = perTableColumns.get(rightTable) ?? cteColumns?.get(join.table.toLowerCase())
|
|
200
281
|
}
|
|
201
282
|
/** @type {QueryPlan} */
|
|
202
283
|
const rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
|
|
@@ -300,13 +381,9 @@ function resolveAliases(node, aliases) {
|
|
|
300
381
|
* @returns {{ leftKey: ExprNode, rightKey: ExprNode } | undefined}
|
|
301
382
|
*/
|
|
302
383
|
function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
|
|
303
|
-
if (condition.type !== 'binary' || condition.op !== '=')
|
|
304
|
-
return undefined
|
|
305
|
-
}
|
|
384
|
+
if (condition.type !== 'binary' || condition.op !== '=') return
|
|
306
385
|
const { left, right } = condition
|
|
307
|
-
if (left.type !== 'identifier' || right.type !== 'identifier')
|
|
308
|
-
return undefined
|
|
309
|
-
}
|
|
386
|
+
if (left.type !== 'identifier' || right.type !== 'identifier') return
|
|
310
387
|
|
|
311
388
|
// Check if keys are in swapped order (right table ref on left side)
|
|
312
389
|
const leftRefsRight = left.name.startsWith(`${rightTable}.`)
|
|
@@ -333,11 +410,11 @@ function validateScan({ table, hints, tables, positionStart, positionEnd }) {
|
|
|
333
410
|
if (!tables) return
|
|
334
411
|
const resolved = tables[table]
|
|
335
412
|
if (!resolved) {
|
|
336
|
-
throw
|
|
413
|
+
throw new TableNotFoundError({ table, tables, positionStart, positionEnd })
|
|
337
414
|
}
|
|
338
415
|
const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
|
|
339
416
|
if (missingColumn) {
|
|
340
|
-
throw
|
|
417
|
+
throw new ColumnNotFoundError({
|
|
341
418
|
columnName: missingColumn,
|
|
342
419
|
availableColumns: resolved.columns,
|
|
343
420
|
positionStart,
|
|
@@ -355,7 +432,7 @@ function validateScan({ table, hints, tables, positionStart, positionEnd }) {
|
|
|
355
432
|
function isAllCountStar(columns) {
|
|
356
433
|
if (columns.length === 0) return false
|
|
357
434
|
return columns.every(col =>
|
|
358
|
-
col.
|
|
435
|
+
col.type === 'derived' &&
|
|
359
436
|
col.expr.type === 'function' &&
|
|
360
437
|
col.expr.funcName.toUpperCase() === 'COUNT' &&
|
|
361
438
|
col.expr.args.length === 1 &&
|
package/src/plan/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DerivedColumn, ExprNode, JoinType, OrderByItem, ScanOptions, SelectColumn } from '../types.js'
|
|
1
|
+
import { DerivedColumn, ExprNode, JoinType, OrderByItem, ScanOptions, SelectColumn, SetOperator } from '../types.js'
|
|
2
2
|
|
|
3
3
|
export type QueryPlan =
|
|
4
4
|
| ScanNode
|
|
@@ -13,6 +13,7 @@ export type QueryPlan =
|
|
|
13
13
|
| HashJoinNode
|
|
14
14
|
| NestedLoopJoinNode
|
|
15
15
|
| PositionalJoinNode
|
|
16
|
+
| SetOperationNode
|
|
16
17
|
|
|
17
18
|
// Scan node
|
|
18
19
|
export interface ScanNode {
|
|
@@ -104,3 +105,12 @@ export interface PositionalJoinNode {
|
|
|
104
105
|
left: QueryPlan
|
|
105
106
|
right: QueryPlan
|
|
106
107
|
}
|
|
108
|
+
|
|
109
|
+
// Set operation node (UNION, INTERSECT, EXCEPT)
|
|
110
|
+
export interface SetOperationNode {
|
|
111
|
+
type: 'SetOperation'
|
|
112
|
+
operator: SetOperator
|
|
113
|
+
all: boolean
|
|
114
|
+
left: QueryPlan
|
|
115
|
+
right: QueryPlan
|
|
116
|
+
}
|
package/src/spatial/bbox.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import {
|
|
2
|
+
* @import { BoundingBox, SimpleGeometry } from './geometry.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export const EPSILON = 1e-10
|
|
6
6
|
export const EPSILON_SQ = EPSILON * EPSILON
|
|
7
7
|
|
|
8
|
-
/** @type {WeakMap<SimpleGeometry,
|
|
8
|
+
/** @type {WeakMap<SimpleGeometry, BoundingBox>} */
|
|
9
9
|
const bboxCache = new WeakMap()
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -26,7 +26,7 @@ export function bboxOverlap(a, b) {
|
|
|
26
26
|
* Results are cached per geometry object.
|
|
27
27
|
*
|
|
28
28
|
* @param {SimpleGeometry} geom
|
|
29
|
-
* @returns {
|
|
29
|
+
* @returns {BoundingBox}
|
|
30
30
|
*/
|
|
31
31
|
export function bbox(geom) {
|
|
32
32
|
let b = bboxCache.get(geom)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BoundingBox, Geometry, SimpleGeometry } from './geometry.js'
|
|
2
|
+
|
|
3
|
+
export function decompose(geom: Geometry): SimpleGeometry[]
|
|
4
|
+
export function bbox(geom: SimpleGeometry): BoundingBox
|
|
5
|
+
export function bboxOverlap(a: SimpleGeometry, b: SimpleGeometry): boolean
|
|
6
|
+
export function parseWkt(wkt: string): Geometry | null
|
package/src/spatial/spatial.js
CHANGED
|
@@ -68,6 +68,25 @@ export function evaluateSpatialFunc({ funcName, args }) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Decompose Multi* and GeometryCollection into simple geometries.
|
|
73
|
+
*
|
|
74
|
+
* @param {Geometry} geom
|
|
75
|
+
* @returns {SimpleGeometry[]}
|
|
76
|
+
*/
|
|
77
|
+
export function decompose(geom) {
|
|
78
|
+
if (geom.type === 'MultiPoint') {
|
|
79
|
+
return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
|
|
80
|
+
} else if (geom.type === 'MultiLineString') {
|
|
81
|
+
return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
|
|
82
|
+
} else if (geom.type === 'MultiPolygon') {
|
|
83
|
+
return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
|
|
84
|
+
} else if (geom.type === 'GeometryCollection') {
|
|
85
|
+
return geom.geometries.flatMap(decompose)
|
|
86
|
+
}
|
|
87
|
+
return [geom]
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/**
|
|
72
91
|
* Normalize a geometry value. Accepts GeoJSON objects.
|
|
73
92
|
* Returns null if the value is not a valid geometry.
|
|
@@ -90,10 +109,6 @@ function toGeometry(val) {
|
|
|
90
109
|
return null
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
// ============================================================================
|
|
94
|
-
// Minimum distance between geometries
|
|
95
|
-
// ============================================================================
|
|
96
|
-
|
|
97
112
|
/**
|
|
98
113
|
* Get all line segments from a geometry.
|
|
99
114
|
*
|
|
@@ -165,35 +180,6 @@ function stDWithin(a, b, distance) {
|
|
|
165
180
|
return false
|
|
166
181
|
}
|
|
167
182
|
|
|
168
|
-
// ============================================================================
|
|
169
|
-
// Spatial predicate dispatch - decompose to primitive type pairs
|
|
170
|
-
// ============================================================================
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Decompose Multi* and GeometryCollection into simple geometries.
|
|
174
|
-
*
|
|
175
|
-
* @param {Geometry} geom
|
|
176
|
-
* @returns {SimpleGeometry[]}
|
|
177
|
-
*/
|
|
178
|
-
function decompose(geom) {
|
|
179
|
-
switch (geom.type) {
|
|
180
|
-
case 'MultiPoint':
|
|
181
|
-
return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
|
|
182
|
-
case 'MultiLineString':
|
|
183
|
-
return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
|
|
184
|
-
case 'MultiPolygon':
|
|
185
|
-
return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
|
|
186
|
-
case 'GeometryCollection':
|
|
187
|
-
return geom.geometries.flatMap(decompose)
|
|
188
|
-
default:
|
|
189
|
-
return [geom]
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ============================================================================
|
|
194
|
-
// ST_Contains
|
|
195
|
-
// ============================================================================
|
|
196
|
-
|
|
197
183
|
/**
|
|
198
184
|
* @param {SimpleGeometry[]} a
|
|
199
185
|
* @param {SimpleGeometry[]} b
|
|
@@ -204,10 +190,6 @@ function stContains(a, b) {
|
|
|
204
190
|
return b.every(pb => a.some(pa => pairContainment(pa, pb) !== 'OUTSIDE'))
|
|
205
191
|
}
|
|
206
192
|
|
|
207
|
-
// ============================================================================
|
|
208
|
-
// ST_ContainsProperly
|
|
209
|
-
// ============================================================================
|
|
210
|
-
|
|
211
193
|
/**
|
|
212
194
|
* @param {SimpleGeometry[]} a
|
|
213
195
|
* @param {SimpleGeometry[]} b
|
|
@@ -218,10 +200,6 @@ function stContainsProperly(a, b) {
|
|
|
218
200
|
return b.every(pb => a.some(pa => pairContainment(pa, pb) === 'INSIDE'))
|
|
219
201
|
}
|
|
220
202
|
|
|
221
|
-
// ============================================================================
|
|
222
|
-
// ST_Touches
|
|
223
|
-
// ============================================================================
|
|
224
|
-
|
|
225
203
|
/**
|
|
226
204
|
* @param {SimpleGeometry[]} a
|
|
227
205
|
* @param {SimpleGeometry[]} b
|
|
@@ -239,10 +217,6 @@ function stTouches(a, b) {
|
|
|
239
217
|
return intersects
|
|
240
218
|
}
|
|
241
219
|
|
|
242
|
-
// ============================================================================
|
|
243
|
-
// ST_Overlaps
|
|
244
|
-
// ============================================================================
|
|
245
|
-
|
|
246
220
|
/**
|
|
247
221
|
* @param {SimpleGeometry[]} a
|
|
248
222
|
* @param {SimpleGeometry[]} b
|
|
@@ -281,10 +255,6 @@ function geometryDimension(parts) {
|
|
|
281
255
|
return max
|
|
282
256
|
}
|
|
283
257
|
|
|
284
|
-
// ============================================================================
|
|
285
|
-
// ST_Equals
|
|
286
|
-
// ============================================================================
|
|
287
|
-
|
|
288
258
|
/**
|
|
289
259
|
* @param {SimpleGeometry[]} a
|
|
290
260
|
* @param {SimpleGeometry[]} b
|
|
@@ -310,10 +280,6 @@ function stEquals(a, b) {
|
|
|
310
280
|
return true
|
|
311
281
|
}
|
|
312
282
|
|
|
313
|
-
// ============================================================================
|
|
314
|
-
// ST_Crosses
|
|
315
|
-
// ============================================================================
|
|
316
|
-
|
|
317
283
|
/**
|
|
318
284
|
* @param {SimpleGeometry[]} a
|
|
319
285
|
* @param {SimpleGeometry[]} b
|
package/src/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExprNode, SelectStatement, SqlPrimitive } from './ast.js'
|
|
1
|
+
import type { ExprNode, SelectStatement, SqlPrimitive, Statement } from './ast.js'
|
|
2
2
|
|
|
3
3
|
export * from './ast.js'
|
|
4
4
|
export { ParserState, Token, TokenType } from './parse/types.js'
|
|
@@ -13,14 +13,14 @@ export interface ParseSqlOptions {
|
|
|
13
13
|
// executeSql(options)
|
|
14
14
|
export interface ExecuteSqlOptions {
|
|
15
15
|
tables: Record<string, Row | AsyncDataSource>
|
|
16
|
-
query: string |
|
|
16
|
+
query: string | Statement
|
|
17
17
|
functions?: Record<string, UserDefinedFunction>
|
|
18
18
|
signal?: AbortSignal
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// planSql(options)
|
|
22
22
|
export interface PlanSqlOptions {
|
|
23
|
-
query: string |
|
|
23
|
+
query: string | Statement
|
|
24
24
|
functions?: Record<string, UserDefinedFunction>
|
|
25
25
|
tables?: Record<string, AsyncDataSource>
|
|
26
26
|
}
|
|
@@ -49,6 +49,8 @@ export interface AsyncDataSource {
|
|
|
49
49
|
numRows?: number
|
|
50
50
|
columns: string[]
|
|
51
51
|
scan(options: ScanOptions): ScanResults
|
|
52
|
+
// Optional method for fast column scans
|
|
53
|
+
scanColumn?(options: ScanColumnOptions): AsyncIterable<ArrayLike<SqlPrimitive>>
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -77,6 +79,14 @@ export interface ScanOptions {
|
|
|
77
79
|
signal?: AbortSignal
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Options for scanning a single column.
|
|
84
|
+
*/
|
|
85
|
+
export interface ScanColumnOptions {
|
|
86
|
+
column: string
|
|
87
|
+
signal?: AbortSignal
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
export interface FunctionSignature {
|
|
81
91
|
min: number
|
|
82
92
|
max?: number
|
|
@@ -88,9 +98,9 @@ export interface UserDefinedFunction {
|
|
|
88
98
|
arguments: FunctionSignature
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP'
|
|
101
|
+
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE'
|
|
92
102
|
|
|
93
|
-
export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_REPLACE'
|
|
103
|
+
export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE'
|
|
94
104
|
|
|
95
105
|
export type MathFunc =
|
|
96
106
|
| 'FLOOR'
|
|
@@ -131,6 +141,8 @@ export type StringFunc =
|
|
|
131
141
|
| 'LEFT'
|
|
132
142
|
| 'RIGHT'
|
|
133
143
|
| 'INSTR'
|
|
144
|
+
| 'POSITION'
|
|
145
|
+
| 'STRPOS'
|
|
134
146
|
|
|
135
147
|
export type SpatialFunc =
|
|
136
148
|
| 'ST_INTERSECTS'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FUNCTION_SIGNATURES } from './functions.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Structured execution error with position range and optional row number.
|
|
3
5
|
*/
|
|
@@ -12,7 +14,7 @@ export class ExecutionError extends Error {
|
|
|
12
14
|
constructor({ message, positionStart, positionEnd, rowIndex }) {
|
|
13
15
|
const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
|
|
14
16
|
super(message + rowSuffix)
|
|
15
|
-
this.name =
|
|
17
|
+
this.name = this.constructor.name
|
|
16
18
|
this.positionStart = positionStart
|
|
17
19
|
this.positionEnd = positionEnd
|
|
18
20
|
this.rowIndex = rowIndex
|
|
@@ -20,16 +22,22 @@ export class ExecutionError extends Error {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* Error for invalid
|
|
24
|
-
*
|
|
25
|
-
* @param {Object} options
|
|
26
|
-
* @param {string} options.item - What was used incorrectly
|
|
27
|
-
* @param {string} options.validContext - Where it can be used
|
|
28
|
-
* @param {number} options.positionStart
|
|
29
|
-
* @param {number} options.positionEnd
|
|
30
|
-
* @param {number} options.rowIndex - 1-based row number where error occurred
|
|
31
|
-
* @returns {ExecutionError}
|
|
25
|
+
* Error for invalid argument type or value.
|
|
32
26
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
27
|
+
export class ArgValueError extends ExecutionError {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {string} options.funcName - The function name
|
|
31
|
+
* @param {string} options.message - Specific error message
|
|
32
|
+
* @param {number} options.positionStart
|
|
33
|
+
* @param {number} options.positionEnd
|
|
34
|
+
* @param {string} [options.hint] - Recovery hint
|
|
35
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
36
|
+
*/
|
|
37
|
+
constructor({ funcName, message, positionStart, positionEnd, hint, rowIndex }) {
|
|
38
|
+
const funcNameUpper = funcName.toUpperCase()
|
|
39
|
+
const signature = FUNCTION_SIGNATURES[funcNameUpper]?.signature ?? ''
|
|
40
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
41
|
+
super({ message: `${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowIndex })
|
|
42
|
+
}
|
|
35
43
|
}
|