squirreling 0.12.27 → 0.14.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 +3 -2
- package/package.json +4 -4
- package/src/ast.d.ts +9 -2
- package/src/execute/aggregates.js +3 -3
- package/src/execute/execute.js +35 -28
- package/src/execute/join.js +13 -13
- package/src/execute/sort.js +1 -1
- package/src/execute/streamingAggregate.js +13 -9
- package/src/execute/window.js +8 -8
- package/src/expression/alias.js +7 -0
- package/src/expression/binary.js +6 -0
- package/src/expression/date.js +1 -1
- package/src/expression/evaluate.js +26 -1
- package/src/expression/strings.js +38 -0
- package/src/index.d.ts +1 -1
- package/src/parse/expression.js +36 -9
- package/src/parse/extractTables.js +4 -0
- package/src/parse/functions.js +12 -1
- package/src/parse/parse.js +8 -0
- package/src/parse/primary.js +87 -11
- package/src/parse/tokenize.js +12 -0
- package/src/plan/columns.js +3 -0
- package/src/plan/plan.js +21 -0
- package/src/types.d.ts +3 -0
- package/src/validation/aggregates.js +3 -0
- package/src/validation/functions.js +6 -3
- package/src/validation/tables.js +6 -0
package/README.md
CHANGED
|
@@ -146,7 +146,8 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
146
146
|
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `CROSS JOIN`, `POSITIONAL JOIN`, `LATERAL VIEW [OUTER] EXPLODE(...)`, with `ON` or `USING (col, ...)` conditions
|
|
147
147
|
- `GROUP BY` and `HAVING` clauses
|
|
148
148
|
- Set operations: `UNION`, `UNION ALL`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, `EXCEPT ALL`
|
|
149
|
-
- Expressions: `CASE`, `CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL
|
|
149
|
+
- Expressions: `CASE`, `CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL`, string concatenation `||`
|
|
150
|
+
- Subscript access: zero-based array indexing `col[0]`, struct field access `col['field']`, and chains like `col[0].field`
|
|
150
151
|
|
|
151
152
|
### Quoting
|
|
152
153
|
|
|
@@ -158,7 +159,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
158
159
|
|
|
159
160
|
- Aggregate: `COUNT`, `COUNTIF`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `ARRAY_AGG`, `JSON_ARRAYAGG`, `STRING_AGG`
|
|
160
161
|
- Window: `ROW_NUMBER`, `LAG`, `LEAD`
|
|
161
|
-
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`, `POSITION`, `STRPOS`
|
|
162
|
+
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `OCTET_LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`, `POSITION`, `STRPOS`, `SPLIT_PART`, `STRING_SPLIT`
|
|
162
163
|
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
163
164
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
164
165
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_DIFF`, `DATEDIFF`, `DATE_PART`, `DATE_TRUNC`, `EPOCH`, `EXTRACT`, `INTERVAL`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "26.1.0",
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
43
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "63.0.
|
|
45
|
+
"eslint-plugin-jsdoc": "63.0.12",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.10"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/ast.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ export interface FromFunction extends AstBase {
|
|
|
70
70
|
|
|
71
71
|
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
72
72
|
|
|
73
|
-
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
73
|
+
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | '||' | ComparisonOp | ArithmeticOp
|
|
74
74
|
|
|
75
75
|
export type ComparisonOp = '=' | '==' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
76
76
|
|
|
@@ -114,7 +114,7 @@ export interface WindowFunctionNode extends AstBase {
|
|
|
114
114
|
orderBy: OrderByItem[]
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL'
|
|
117
|
+
export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL' | 'TIMESTAMP'
|
|
118
118
|
|
|
119
119
|
export interface CastNode extends AstBase {
|
|
120
120
|
type: 'cast'
|
|
@@ -168,6 +168,12 @@ export interface StarNode extends AstBase {
|
|
|
168
168
|
type: 'star'
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
export interface SubscriptNode extends AstBase {
|
|
172
|
+
type: 'subscript'
|
|
173
|
+
expr: ExprNode
|
|
174
|
+
index: ExprNode
|
|
175
|
+
}
|
|
176
|
+
|
|
171
177
|
export type ExprNode =
|
|
172
178
|
| LiteralNode
|
|
173
179
|
| IdentifierNode
|
|
@@ -183,6 +189,7 @@ export type ExprNode =
|
|
|
183
189
|
| SubqueryNode
|
|
184
190
|
| IntervalNode
|
|
185
191
|
| StarNode
|
|
192
|
+
| SubscriptNode
|
|
186
193
|
|
|
187
194
|
export interface StarColumn extends AstBase {
|
|
188
195
|
type: 'star'
|
|
@@ -101,7 +101,7 @@ export function executeHashAggregate(plan, context) {
|
|
|
101
101
|
for await (const row of child.rows()) {
|
|
102
102
|
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
103
103
|
await yieldToEventLoop()
|
|
104
|
-
|
|
104
|
+
context.signal?.throwIfAborted()
|
|
105
105
|
}
|
|
106
106
|
allRows.push(row)
|
|
107
107
|
}
|
|
@@ -121,7 +121,7 @@ export function executeHashAggregate(plan, context) {
|
|
|
121
121
|
for (let chunkStart = 0; chunkStart < allRows.length; chunkStart += YIELD_INTERVAL) {
|
|
122
122
|
if (chunkStart > 0) {
|
|
123
123
|
await yieldToEventLoop()
|
|
124
|
-
|
|
124
|
+
context.signal?.throwIfAborted()
|
|
125
125
|
}
|
|
126
126
|
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, allRows.length)
|
|
127
127
|
const chunkLen = chunkEnd - chunkStart
|
|
@@ -227,7 +227,7 @@ export function executeScalarAggregate(plan, context) {
|
|
|
227
227
|
for await (const row of child.rows()) {
|
|
228
228
|
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
229
229
|
await yieldToEventLoop()
|
|
230
|
-
|
|
230
|
+
context.signal?.throwIfAborted()
|
|
231
231
|
}
|
|
232
232
|
group.push(row)
|
|
233
233
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -174,7 +174,7 @@ function executeUnnest(plan, context) {
|
|
|
174
174
|
const value = await evaluateExpr({ node: plan.args[0], row, rowIndex: 1, context })
|
|
175
175
|
if (!Array.isArray(value)) return
|
|
176
176
|
for (const element of value) {
|
|
177
|
-
|
|
177
|
+
context.signal?.throwIfAborted()
|
|
178
178
|
yield {
|
|
179
179
|
columns,
|
|
180
180
|
cells: { [columnName]: () => Promise.resolve(element) },
|
|
@@ -209,7 +209,7 @@ function executeJsonEach(plan, context) {
|
|
|
209
209
|
}
|
|
210
210
|
if (Array.isArray(parsed)) {
|
|
211
211
|
for (let i = 0; i < parsed.length; i++) {
|
|
212
|
-
|
|
212
|
+
context.signal?.throwIfAborted()
|
|
213
213
|
const k = i
|
|
214
214
|
const v = parsed[i]
|
|
215
215
|
yield {
|
|
@@ -224,7 +224,7 @@ function executeJsonEach(plan, context) {
|
|
|
224
224
|
}
|
|
225
225
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
226
226
|
for (const [k, v] of Object.entries(parsed)) {
|
|
227
|
-
|
|
227
|
+
context.signal?.throwIfAborted()
|
|
228
228
|
yield {
|
|
229
229
|
columns,
|
|
230
230
|
cells: {
|
|
@@ -294,7 +294,7 @@ function executeScan(plan, context) {
|
|
|
294
294
|
async *rows() {
|
|
295
295
|
const columns = [column]
|
|
296
296
|
for await (const chunk of chunks) {
|
|
297
|
-
|
|
297
|
+
signal?.throwIfAborted()
|
|
298
298
|
for (let i = 0; i < chunk.length; i++) {
|
|
299
299
|
const value = chunk[i]
|
|
300
300
|
yield {
|
|
@@ -303,6 +303,9 @@ function executeScan(plan, context) {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
+
// A data source may end its stream cooperatively on abort; surface
|
|
307
|
+
// the abort so a truncated scan is not mistaken for a complete one
|
|
308
|
+
signal?.throwIfAborted()
|
|
306
309
|
},
|
|
307
310
|
}
|
|
308
311
|
}
|
|
@@ -335,6 +338,10 @@ function executeScan(plan, context) {
|
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
yield* result
|
|
341
|
+
|
|
342
|
+
// A data source may end its stream cooperatively on abort; surface
|
|
343
|
+
// the abort so a truncated scan is not mistaken for a complete one
|
|
344
|
+
signal?.throwIfAborted()
|
|
338
345
|
},
|
|
339
346
|
}
|
|
340
347
|
}
|
|
@@ -420,10 +427,10 @@ async function* filterRows(rows, condition, context, limit) {
|
|
|
420
427
|
let buffer = []
|
|
421
428
|
|
|
422
429
|
for await (const row of rows) {
|
|
423
|
-
|
|
430
|
+
context.signal?.throwIfAborted()
|
|
424
431
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
425
432
|
await yieldToEventLoop()
|
|
426
|
-
|
|
433
|
+
context.signal?.throwIfAborted()
|
|
427
434
|
}
|
|
428
435
|
rowIndex++
|
|
429
436
|
buffer.push({ row, rowIndex })
|
|
@@ -466,10 +473,10 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
466
473
|
let yielded = 0
|
|
467
474
|
let innerCount = 0
|
|
468
475
|
for await (const row of rows) {
|
|
469
|
-
|
|
476
|
+
signal?.throwIfAborted()
|
|
470
477
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
471
478
|
await yieldToEventLoop()
|
|
472
|
-
|
|
479
|
+
signal?.throwIfAborted()
|
|
473
480
|
}
|
|
474
481
|
if (skipped < offset) {
|
|
475
482
|
skipped++
|
|
@@ -521,10 +528,10 @@ function executeProject(plan, context) {
|
|
|
521
528
|
let innerCount = 0
|
|
522
529
|
|
|
523
530
|
for await (const row of child.rows()) {
|
|
524
|
-
|
|
531
|
+
context.signal?.throwIfAborted()
|
|
525
532
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
526
533
|
await yieldToEventLoop()
|
|
527
|
-
|
|
534
|
+
context.signal?.throwIfAborted()
|
|
528
535
|
}
|
|
529
536
|
rowIndex++
|
|
530
537
|
const currentRowIndex = rowIndex
|
|
@@ -614,10 +621,10 @@ function executeDistinct(plan, context) {
|
|
|
614
621
|
let innerCount = 0
|
|
615
622
|
|
|
616
623
|
for await (const row of child.rows()) {
|
|
617
|
-
|
|
624
|
+
signal?.throwIfAborted()
|
|
618
625
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
619
626
|
await yieldToEventLoop()
|
|
620
|
-
|
|
627
|
+
signal?.throwIfAborted()
|
|
621
628
|
}
|
|
622
629
|
buffer.push(row)
|
|
623
630
|
|
|
@@ -701,10 +708,10 @@ function executeSetOperation(plan, context) {
|
|
|
701
708
|
const seen = new Set()
|
|
702
709
|
let count = 0
|
|
703
710
|
for await (const row of left.rows()) {
|
|
704
|
-
|
|
711
|
+
signal?.throwIfAborted()
|
|
705
712
|
if (++count % YIELD_INTERVAL === 0) {
|
|
706
713
|
await yieldToEventLoop()
|
|
707
|
-
|
|
714
|
+
signal?.throwIfAborted()
|
|
708
715
|
}
|
|
709
716
|
const key = await stableRowKey(row)
|
|
710
717
|
if (!seen.has(key)) {
|
|
@@ -713,10 +720,10 @@ function executeSetOperation(plan, context) {
|
|
|
713
720
|
}
|
|
714
721
|
}
|
|
715
722
|
for await (const row of right.rows()) {
|
|
716
|
-
|
|
723
|
+
signal?.throwIfAborted()
|
|
717
724
|
if (++count % YIELD_INTERVAL === 0) {
|
|
718
725
|
await yieldToEventLoop()
|
|
719
|
-
|
|
726
|
+
signal?.throwIfAborted()
|
|
720
727
|
}
|
|
721
728
|
const key = await stableRowKey(row)
|
|
722
729
|
if (!seen.has(key)) {
|
|
@@ -739,10 +746,10 @@ function executeSetOperation(plan, context) {
|
|
|
739
746
|
const rightKeys = new Map()
|
|
740
747
|
let tick = 0
|
|
741
748
|
for await (const row of right.rows()) {
|
|
742
|
-
|
|
749
|
+
signal?.throwIfAborted()
|
|
743
750
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
744
751
|
await yieldToEventLoop()
|
|
745
|
-
|
|
752
|
+
signal?.throwIfAborted()
|
|
746
753
|
}
|
|
747
754
|
const key = await stableRowKey(row)
|
|
748
755
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
@@ -751,10 +758,10 @@ function executeSetOperation(plan, context) {
|
|
|
751
758
|
if (plan.all) {
|
|
752
759
|
// INTERSECT ALL: yield each left row that matches, consuming right counts
|
|
753
760
|
for await (const row of left.rows()) {
|
|
754
|
-
|
|
761
|
+
signal?.throwIfAborted()
|
|
755
762
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
756
763
|
await yieldToEventLoop()
|
|
757
|
-
|
|
764
|
+
signal?.throwIfAborted()
|
|
758
765
|
}
|
|
759
766
|
const key = await stableRowKey(row)
|
|
760
767
|
const count = rightKeys.get(key)
|
|
@@ -767,10 +774,10 @@ function executeSetOperation(plan, context) {
|
|
|
767
774
|
// INTERSECT: yield deduplicated rows present in both
|
|
768
775
|
const seen = new Set()
|
|
769
776
|
for await (const row of left.rows()) {
|
|
770
|
-
|
|
777
|
+
signal?.throwIfAborted()
|
|
771
778
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
772
779
|
await yieldToEventLoop()
|
|
773
|
-
|
|
780
|
+
signal?.throwIfAborted()
|
|
774
781
|
}
|
|
775
782
|
const key = await stableRowKey(row)
|
|
776
783
|
if (rightKeys.has(key) && !seen.has(key)) {
|
|
@@ -794,10 +801,10 @@ function executeSetOperation(plan, context) {
|
|
|
794
801
|
const rightKeys = new Map()
|
|
795
802
|
let tick = 0
|
|
796
803
|
for await (const row of right.rows()) {
|
|
797
|
-
|
|
804
|
+
signal?.throwIfAborted()
|
|
798
805
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
799
806
|
await yieldToEventLoop()
|
|
800
|
-
|
|
807
|
+
signal?.throwIfAborted()
|
|
801
808
|
}
|
|
802
809
|
const key = await stableRowKey(row)
|
|
803
810
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
@@ -806,10 +813,10 @@ function executeSetOperation(plan, context) {
|
|
|
806
813
|
if (plan.all) {
|
|
807
814
|
// EXCEPT ALL: yield left rows, consuming right counts
|
|
808
815
|
for await (const row of left.rows()) {
|
|
809
|
-
|
|
816
|
+
signal?.throwIfAborted()
|
|
810
817
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
811
818
|
await yieldToEventLoop()
|
|
812
|
-
|
|
819
|
+
signal?.throwIfAborted()
|
|
813
820
|
}
|
|
814
821
|
const key = await stableRowKey(row)
|
|
815
822
|
const count = rightKeys.get(key)
|
|
@@ -823,10 +830,10 @@ function executeSetOperation(plan, context) {
|
|
|
823
830
|
// EXCEPT: yield deduplicated left rows not in right
|
|
824
831
|
const seen = new Set()
|
|
825
832
|
for await (const row of left.rows()) {
|
|
826
|
-
|
|
833
|
+
signal?.throwIfAborted()
|
|
827
834
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
828
835
|
await yieldToEventLoop()
|
|
829
|
-
|
|
836
|
+
signal?.throwIfAborted()
|
|
830
837
|
}
|
|
831
838
|
const key = await stableRowKey(row)
|
|
832
839
|
if (!rightKeys.has(key) && !seen.has(key)) {
|
package/src/execute/join.js
CHANGED
|
@@ -56,7 +56,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
56
56
|
/** @type {AsyncRow[]} */
|
|
57
57
|
const innerRows = []
|
|
58
58
|
for await (const row of inner.rows()) {
|
|
59
|
-
|
|
59
|
+
context.signal?.throwIfAborted()
|
|
60
60
|
innerRows.push(row)
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -69,7 +69,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
69
69
|
|
|
70
70
|
let innerCount = 0
|
|
71
71
|
for await (const outerRow of outer.rows()) {
|
|
72
|
-
|
|
72
|
+
context.signal?.throwIfAborted()
|
|
73
73
|
|
|
74
74
|
if (!outerCols) {
|
|
75
75
|
outerCols = outerRow.columns
|
|
@@ -80,7 +80,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
80
80
|
for (const innerRow of innerRows) {
|
|
81
81
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
82
82
|
await yieldToEventLoop()
|
|
83
|
-
|
|
83
|
+
context.signal?.throwIfAborted()
|
|
84
84
|
}
|
|
85
85
|
const tempMerged = merge(outerRow, innerRow)
|
|
86
86
|
const matches = await evaluateExpr({
|
|
@@ -101,7 +101,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
context.signal?.throwIfAborted()
|
|
105
105
|
|
|
106
106
|
// Unmatched inner rows for outer joins on the buffered side
|
|
107
107
|
if (matchedInnerRows) {
|
|
@@ -134,7 +134,7 @@ function executeLateralJoin(plan, context) {
|
|
|
134
134
|
const rightTable = plan.rightAlias
|
|
135
135
|
|
|
136
136
|
for await (const leftRow of left.rows()) {
|
|
137
|
-
|
|
137
|
+
context.signal?.throwIfAborted()
|
|
138
138
|
|
|
139
139
|
// When nested inside a correlated subquery, preserve the enclosing
|
|
140
140
|
// outer row so UNNEST args can reference its columns (e.g. o.arr).
|
|
@@ -146,7 +146,7 @@ function executeLateralJoin(plan, context) {
|
|
|
146
146
|
|
|
147
147
|
let hasMatch = false
|
|
148
148
|
for await (const rightRow of right.rows()) {
|
|
149
|
-
|
|
149
|
+
context.signal?.throwIfAborted()
|
|
150
150
|
const merged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
151
151
|
const matches = plan.condition === undefined
|
|
152
152
|
? true
|
|
@@ -199,10 +199,10 @@ export function executePositionalJoin(plan, context) {
|
|
|
199
199
|
while (true) {
|
|
200
200
|
const [leftResult, rightResult] = await Promise.all([leftRows.next(), rightRows.next()])
|
|
201
201
|
if (leftResult.done && rightResult.done) return
|
|
202
|
-
|
|
202
|
+
signal?.throwIfAborted()
|
|
203
203
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
204
204
|
await yieldToEventLoop()
|
|
205
|
-
|
|
205
|
+
signal?.throwIfAborted()
|
|
206
206
|
}
|
|
207
207
|
if (!leftResult.done && !leftCols.length) leftCols = leftResult.value.columns
|
|
208
208
|
if (!rightResult.done && !rightCols.length) rightCols = rightResult.value.columns
|
|
@@ -266,10 +266,10 @@ export function executeHashJoin(plan, context) {
|
|
|
266
266
|
let buildCols = []
|
|
267
267
|
let innerCount = 0
|
|
268
268
|
for await (const buildRow of build.rows()) {
|
|
269
|
-
|
|
269
|
+
context.signal?.throwIfAborted()
|
|
270
270
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
271
271
|
await yieldToEventLoop()
|
|
272
|
-
|
|
272
|
+
context.signal?.throwIfAborted()
|
|
273
273
|
}
|
|
274
274
|
if (!buildCols.length) {
|
|
275
275
|
buildCols = buildRow.columns
|
|
@@ -297,7 +297,7 @@ export function executeHashJoin(plan, context) {
|
|
|
297
297
|
|
|
298
298
|
// Probe phase: stream the other side
|
|
299
299
|
for await (const probeRow of probe.rows()) {
|
|
300
|
-
|
|
300
|
+
context.signal?.throwIfAborted()
|
|
301
301
|
|
|
302
302
|
if (!probeCols) {
|
|
303
303
|
probeCols = probeRow.columns
|
|
@@ -314,7 +314,7 @@ export function executeHashJoin(plan, context) {
|
|
|
314
314
|
for (const buildRow of candidates) {
|
|
315
315
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
316
316
|
await yieldToEventLoop()
|
|
317
|
-
|
|
317
|
+
context.signal?.throwIfAborted()
|
|
318
318
|
}
|
|
319
319
|
const merged = merge(probeRow, buildRow)
|
|
320
320
|
if (residual) {
|
|
@@ -333,7 +333,7 @@ export function executeHashJoin(plan, context) {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
|
|
336
|
+
context.signal?.throwIfAborted()
|
|
337
337
|
|
|
338
338
|
// Unmatched build rows for outer joins on the build side
|
|
339
339
|
if (buildRows && matchedBuildRows) {
|
package/src/execute/sort.js
CHANGED
|
@@ -146,7 +146,7 @@ export function executeSort(plan, context) {
|
|
|
146
146
|
/** @type {SortEntry[]} */
|
|
147
147
|
let entries = []
|
|
148
148
|
for await (const row of child.rows()) {
|
|
149
|
-
|
|
149
|
+
context.signal?.throwIfAborted()
|
|
150
150
|
entries.push({ row })
|
|
151
151
|
if (entries.length >= bufferLimit) {
|
|
152
152
|
// Sort keys are cached on the rows, so survivors of one truncation
|
|
@@ -161,6 +161,8 @@ export function planStreamingAggregates({ columns, having, orderBy, groupBy }, c
|
|
|
161
161
|
case 'in valuelist':
|
|
162
162
|
// values after the first are skipped once an earlier value matches
|
|
163
163
|
return walk(node.expr, lazy) && node.values.every((v, i) => walk(v, lazy || i > 0))
|
|
164
|
+
case 'subscript':
|
|
165
|
+
return walk(node.expr, lazy) && walk(node.index, lazy)
|
|
164
166
|
case 'function': {
|
|
165
167
|
const funcName = node.funcName.toUpperCase()
|
|
166
168
|
if (!isAggregateFunc(funcName)) {
|
|
@@ -257,6 +259,8 @@ function isScalarExpr(node) {
|
|
|
257
259
|
(!node.elseResult || isScalarExpr(node.elseResult))
|
|
258
260
|
case 'in valuelist':
|
|
259
261
|
return isScalarExpr(node.expr) && node.values.every(v => isScalarExpr(v))
|
|
262
|
+
case 'subscript':
|
|
263
|
+
return isScalarExpr(node.expr) && isScalarExpr(node.index)
|
|
260
264
|
case 'function':
|
|
261
265
|
return !isAggregateFunc(node.funcName.toUpperCase()) && node.args.every(arg => isScalarExpr(arg))
|
|
262
266
|
default:
|
|
@@ -316,6 +320,11 @@ function substituteValues(node, values) {
|
|
|
316
320
|
const valueNodes = node.values.map(v => substituteValues(v, values))
|
|
317
321
|
return { ...node, expr, values: valueNodes }
|
|
318
322
|
}
|
|
323
|
+
case 'subscript': {
|
|
324
|
+
const expr = substituteValues(node.expr, values)
|
|
325
|
+
const index = substituteValues(node.index, values)
|
|
326
|
+
return expr === node.expr && index === node.index ? node : { ...node, expr, index }
|
|
327
|
+
}
|
|
319
328
|
case 'function': {
|
|
320
329
|
const args = node.args.map(arg => substituteValues(arg, values))
|
|
321
330
|
return args.every((arg, i) => arg === node.args[i]) ? node : { ...node, args }
|
|
@@ -409,8 +418,8 @@ async function accumulateChunk({ chunk, groupBy, specs, groups, needsRow, contex
|
|
|
409
418
|
|
|
410
419
|
/**
|
|
411
420
|
* Consumes the child rows into per-group accumulators, holding at most one
|
|
412
|
-
* chunk of rows at a time.
|
|
413
|
-
*
|
|
421
|
+
* chunk of rows at a time. Throws when aborted so partial accumulators are
|
|
422
|
+
* never finalized into results.
|
|
414
423
|
*
|
|
415
424
|
* @param {object} options
|
|
416
425
|
* @param {QueryResults} options.child
|
|
@@ -418,7 +427,7 @@ async function accumulateChunk({ chunk, groupBy, specs, groups, needsRow, contex
|
|
|
418
427
|
* @param {StreamingAggSpec[]} options.specs
|
|
419
428
|
* @param {boolean} options.needsRow
|
|
420
429
|
* @param {ExecuteContext} options.context
|
|
421
|
-
* @returns {Promise<Map<unknown, StreamingGroup
|
|
430
|
+
* @returns {Promise<Map<unknown, StreamingGroup>>}
|
|
422
431
|
*/
|
|
423
432
|
async function accumulateGroups({ child, groupBy, specs, needsRow, context }) {
|
|
424
433
|
/** @type {Map<unknown, StreamingGroup>} */
|
|
@@ -431,14 +440,11 @@ async function accumulateGroups({ child, groupBy, specs, needsRow, context }) {
|
|
|
431
440
|
await accumulateChunk({ chunk, groupBy, specs, groups, needsRow, context })
|
|
432
441
|
chunk = []
|
|
433
442
|
await yieldToEventLoop()
|
|
434
|
-
|
|
443
|
+
context.signal?.throwIfAborted()
|
|
435
444
|
}
|
|
436
445
|
}
|
|
437
446
|
if (chunk.length) {
|
|
438
447
|
await accumulateChunk({ chunk, groupBy, specs, groups, needsRow, context })
|
|
439
|
-
// An abort during the final partial chunk ends the stream silently,
|
|
440
|
-
// consistent with the full-chunk check above
|
|
441
|
-
if (context.signal?.aborted) return
|
|
442
448
|
}
|
|
443
449
|
context.signal?.throwIfAborted()
|
|
444
450
|
return groups
|
|
@@ -531,7 +537,6 @@ export function streamingHashAggregateRows({ plan, streaming, child, context })
|
|
|
531
537
|
const { specs, keyRefs, needsRow } = streaming
|
|
532
538
|
return async function* () {
|
|
533
539
|
const groups = await accumulateGroups({ child, groupBy: plan.groupBy, specs, needsRow, context })
|
|
534
|
-
if (!groups) return
|
|
535
540
|
const { orderBy, having } = plan
|
|
536
541
|
|
|
537
542
|
// Without ORDER BY, groups finalize and yield one at a time so output
|
|
@@ -585,7 +590,6 @@ export function streamingScalarAggregateRows({ plan, streaming, child, context }
|
|
|
585
590
|
const { specs, keyRefs, needsRow } = streaming
|
|
586
591
|
return async function* () {
|
|
587
592
|
const groups = await accumulateGroups({ child, groupBy: [], specs, needsRow, context })
|
|
588
|
-
if (!groups) return
|
|
589
593
|
/** @type {StreamingGroup} */
|
|
590
594
|
const group = groups.get(true) ?? { firstRow: undefined, keyValues: [], accumulators: specs.map(spec => newAccumulator(spec.funcName, spec.node.distinct)) }
|
|
591
595
|
|
package/src/execute/window.js
CHANGED
|
@@ -42,7 +42,7 @@ export function executeWindow(plan, context) {
|
|
|
42
42
|
for await (const row of child.rows()) {
|
|
43
43
|
if (++i % YIELD_INTERVAL === 0) {
|
|
44
44
|
await yieldToEventLoop()
|
|
45
|
-
|
|
45
|
+
context.signal?.throwIfAborted()
|
|
46
46
|
}
|
|
47
47
|
const cells = { ...row.cells }
|
|
48
48
|
for (const w of plan.windows) {
|
|
@@ -69,7 +69,7 @@ export function executeWindow(plan, context) {
|
|
|
69
69
|
for await (const row of child.rows()) {
|
|
70
70
|
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
71
71
|
await yieldToEventLoop()
|
|
72
|
-
|
|
72
|
+
context.signal?.throwIfAborted()
|
|
73
73
|
}
|
|
74
74
|
rows.push(row)
|
|
75
75
|
}
|
|
@@ -81,14 +81,14 @@ export function executeWindow(plan, context) {
|
|
|
81
81
|
|
|
82
82
|
for (let w = 0; w < plan.windows.length; w++) {
|
|
83
83
|
await computeWindow(plan.windows[w], rows, windowValues[w], context)
|
|
84
|
-
|
|
84
|
+
context.signal?.throwIfAborted()
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
let emitCount = 0
|
|
88
88
|
for (let i = 0; i < rows.length; i++) {
|
|
89
89
|
if (++emitCount % YIELD_INTERVAL === 0) {
|
|
90
90
|
await yieldToEventLoop()
|
|
91
|
-
|
|
91
|
+
context.signal?.throwIfAborted()
|
|
92
92
|
}
|
|
93
93
|
const row = rows[i]
|
|
94
94
|
const cells = { ...row.cells }
|
|
@@ -122,7 +122,7 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
122
122
|
for (let chunkStart = 0; chunkStart < rows.length; chunkStart += YIELD_INTERVAL) {
|
|
123
123
|
if (chunkStart > 0) {
|
|
124
124
|
await yieldToEventLoop()
|
|
125
|
-
|
|
125
|
+
context.signal?.throwIfAborted()
|
|
126
126
|
}
|
|
127
127
|
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, rows.length)
|
|
128
128
|
const chunkKeys = await Promise.all(
|
|
@@ -142,7 +142,7 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
for (const bucket of partitions.values()) {
|
|
145
|
-
|
|
145
|
+
context.signal?.throwIfAborted()
|
|
146
146
|
|
|
147
147
|
// Order within the partition. Empty ORDER BY → input order.
|
|
148
148
|
/** @type {number[]} */
|
|
@@ -153,7 +153,7 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
153
153
|
for (let chunkStart = 0; chunkStart < bucket.length; chunkStart += YIELD_INTERVAL) {
|
|
154
154
|
if (chunkStart > 0) {
|
|
155
155
|
await yieldToEventLoop()
|
|
156
|
-
|
|
156
|
+
context.signal?.throwIfAborted()
|
|
157
157
|
}
|
|
158
158
|
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, bucket.length)
|
|
159
159
|
const chunkValues = await Promise.all(
|
|
@@ -205,7 +205,7 @@ async function applyWindowFunction(spec, ordered, rows, output, context) {
|
|
|
205
205
|
for (let k = 0; k < ordered.length; k++) {
|
|
206
206
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
207
207
|
await yieldToEventLoop()
|
|
208
|
-
|
|
208
|
+
context.signal?.throwIfAborted()
|
|
209
209
|
}
|
|
210
210
|
const idx = ordered[k]
|
|
211
211
|
const row = rows[idx]
|
package/src/expression/alias.js
CHANGED
|
@@ -37,5 +37,12 @@ export function derivedAlias(expr) {
|
|
|
37
37
|
if (expr.type === 'interval') {
|
|
38
38
|
return `interval_${expr.value}_${expr.unit.toLowerCase()}`
|
|
39
39
|
}
|
|
40
|
+
if (expr.type === 'subscript') {
|
|
41
|
+
// string subscript is struct field access, alias to the field name
|
|
42
|
+
if (expr.index.type === 'literal' && typeof expr.index.value === 'string') {
|
|
43
|
+
return expr.index.value
|
|
44
|
+
}
|
|
45
|
+
return `${derivedAlias(expr.expr)}[${derivedAlias(expr.index)}]`
|
|
46
|
+
}
|
|
40
47
|
return 'expr'
|
|
41
48
|
}
|
package/src/expression/binary.js
CHANGED
|
@@ -23,6 +23,12 @@ export function applyBinaryOp(op, a, b) {
|
|
|
23
23
|
if (op === '%') return numB === 0 ? null : numA % numB
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// String concatenation returns null if either operand is null
|
|
27
|
+
if (op === '||') {
|
|
28
|
+
if (a == null || b == null) return null
|
|
29
|
+
return String(a) + String(b)
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// Comparison and logical operators
|
|
27
33
|
if (a == null || b == null) {
|
|
28
34
|
return false
|
package/src/expression/date.js
CHANGED
|
@@ -130,7 +130,7 @@ export function dateDiff(unit, startVal, endVal) {
|
|
|
130
130
|
* @param {SqlPrimitive} val
|
|
131
131
|
* @returns {Date | null}
|
|
132
132
|
*/
|
|
133
|
-
function toDate(val) {
|
|
133
|
+
export function toDate(val) {
|
|
134
134
|
if (val instanceof Date) return val
|
|
135
135
|
const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
|
|
136
136
|
if (typeof val === 'string' && dateOrTime.test(val)) {
|
|
@@ -7,7 +7,7 @@ import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
|
7
7
|
import { ColumnNotFoundError } from '../validation/tables.js'
|
|
8
8
|
import { derivedAlias } from './alias.js'
|
|
9
9
|
import { applyBinaryOp } from './binary.js'
|
|
10
|
-
import { applyIntervalToDate, dateDiff, dateTrunc, extractField } from './date.js'
|
|
10
|
+
import { applyIntervalToDate, dateDiff, dateTrunc, extractField, toDate } from './date.js'
|
|
11
11
|
import { evaluateMathFunc } from './math.js'
|
|
12
12
|
import { evaluateRegexpFunc } from './regexp.js'
|
|
13
13
|
import { evaluateSpatialFunc } from '../spatial/spatial.js'
|
|
@@ -130,6 +130,23 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
130
130
|
})
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Subscript access: array indexing (arr[0]) or struct field access (obj['key'])
|
|
134
|
+
if (node.type === 'subscript') {
|
|
135
|
+
const value = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
136
|
+
if (value == null) return null
|
|
137
|
+
const index = await evaluateExpr({ node: node.index, row, rowIndex, rows, context })
|
|
138
|
+
if (index == null) return null
|
|
139
|
+
if (typeof index === 'number' || typeof index === 'bigint') {
|
|
140
|
+
if (!Array.isArray(value)) return null
|
|
141
|
+
return value[Number(index)] ?? null
|
|
142
|
+
}
|
|
143
|
+
const key = String(index)
|
|
144
|
+
if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, key)) {
|
|
145
|
+
return value[key]
|
|
146
|
+
}
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
// Scalar subquery - returns a single value
|
|
134
151
|
if (node.type === 'subquery') {
|
|
135
152
|
const outerScope = context.scope
|
|
@@ -710,6 +727,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
710
727
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
711
728
|
return Boolean(val)
|
|
712
729
|
}
|
|
730
|
+
if (toType === 'TIMESTAMP') {
|
|
731
|
+
if (typeof val === 'number' || typeof val === 'bigint') {
|
|
732
|
+
const date = new Date(Number(val))
|
|
733
|
+
if (isNaN(date.getTime())) return null
|
|
734
|
+
return date
|
|
735
|
+
}
|
|
736
|
+
return toDate(val)
|
|
737
|
+
}
|
|
713
738
|
}
|
|
714
739
|
|
|
715
740
|
// IN and NOT IN with value lists
|
|
@@ -4,6 +4,8 @@ import { ArgValueError } from '../validation/executionErrors.js'
|
|
|
4
4
|
* @import { FunctionNode, SqlPrimitive, StringFunc } from '../types.js'
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const textEncoder = new TextEncoder()
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Evaluate a string function
|
|
9
11
|
*
|
|
@@ -43,6 +45,16 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
43
45
|
})
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
if (funcName === 'OCTET_LENGTH') {
|
|
49
|
+
if (typeof val === 'string') return textEncoder.encode(val).length
|
|
50
|
+
throw new ArgValueError({
|
|
51
|
+
...node,
|
|
52
|
+
message: `expected string, got ${typeof val === 'object' ? val instanceof Date ? 'date' : 'object' : typeof val}`,
|
|
53
|
+
hint: 'Use CAST to convert to a string first.',
|
|
54
|
+
rowIndex,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
if (typeof val === 'object' && !(val instanceof Date)) {
|
|
47
59
|
throw new ArgValueError({
|
|
48
60
|
...node,
|
|
@@ -131,6 +143,32 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
131
143
|
return str.substring(str.length - len)
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
if (funcName === 'SPLIT_PART') {
|
|
147
|
+
const delimiter = args[1]
|
|
148
|
+
if (delimiter == null) return null
|
|
149
|
+
const index = Number(args[2])
|
|
150
|
+
if (!Number.isInteger(index) || index === 0) {
|
|
151
|
+
throw new ArgValueError({
|
|
152
|
+
...node,
|
|
153
|
+
message: `index must be a non-zero integer, got ${args[2]}`,
|
|
154
|
+
hint: 'Field indexes are 1-based.',
|
|
155
|
+
rowIndex,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
const delim = String(delimiter)
|
|
159
|
+
const parts = delim === '' ? [str] : str.split(delim)
|
|
160
|
+
// Negative index counts from the end
|
|
161
|
+
const partIdx = index < 0 ? parts.length + index : index - 1
|
|
162
|
+
return parts[partIdx] ?? ''
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (funcName === 'STRING_SPLIT') {
|
|
166
|
+
const delimiter = args[1]
|
|
167
|
+
if (delimiter == null) return null
|
|
168
|
+
const delim = String(delimiter)
|
|
169
|
+
return delim === '' ? [str] : str.split(delim)
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
if (funcName === 'INSTR' || funcName === 'POSITION' || funcName === 'STRPOS') {
|
|
135
173
|
const search = args[1]
|
|
136
174
|
if (search == null) return null
|
package/src/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type {
|
|
|
28
28
|
* @param options.tables - source data as a list of objects or an AsyncDataSource
|
|
29
29
|
* @param options.query - SQL query string
|
|
30
30
|
* @param options.functions - user-defined functions available in the SQL context
|
|
31
|
-
* @param options.signal - AbortSignal to cancel the query
|
|
31
|
+
* @param options.signal - AbortSignal to cancel the query; an aborted query rejects with the signal's reason
|
|
32
32
|
* @returns async generator yielding rows matching the query
|
|
33
33
|
*/
|
|
34
34
|
export function executeSql(options: ExecuteSqlOptions): QueryResults
|
package/src/parse/expression.js
CHANGED
|
@@ -9,7 +9,7 @@ import { consume, current, expect, match } from './state.js'
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// Precedence (lowest to highest):
|
|
12
|
-
// OR, AND, NOT, Comparison, Additive, Multiplicative, Primary
|
|
12
|
+
// OR, AND, NOT, Comparison, Concat, Additive, Multiplicative, Primary
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {ParserState} state
|
|
@@ -88,7 +88,7 @@ function parseNot(state) {
|
|
|
88
88
|
* @returns {ExprNode}
|
|
89
89
|
*/
|
|
90
90
|
function parseComparison(state) {
|
|
91
|
-
const left =
|
|
91
|
+
const left = parseConcat(state)
|
|
92
92
|
const { positionStart } = left
|
|
93
93
|
|
|
94
94
|
// IS [NOT] NULL
|
|
@@ -108,7 +108,7 @@ function parseComparison(state) {
|
|
|
108
108
|
if (match(state, 'keyword', 'NOT')) {
|
|
109
109
|
// NOT LIKE
|
|
110
110
|
if (match(state, 'keyword', 'LIKE')) {
|
|
111
|
-
const right =
|
|
111
|
+
const right = parseConcat(state)
|
|
112
112
|
return {
|
|
113
113
|
type: 'unary',
|
|
114
114
|
op: 'NOT',
|
|
@@ -127,9 +127,9 @@ function parseComparison(state) {
|
|
|
127
127
|
|
|
128
128
|
// NOT BETWEEN - convert to range comparison
|
|
129
129
|
if (match(state, 'keyword', 'BETWEEN')) {
|
|
130
|
-
const lower =
|
|
130
|
+
const lower = parseConcat(state)
|
|
131
131
|
expect(state, 'keyword', 'AND')
|
|
132
|
-
const upper =
|
|
132
|
+
const upper = parseConcat(state)
|
|
133
133
|
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
134
134
|
return {
|
|
135
135
|
type: 'binary',
|
|
@@ -163,7 +163,7 @@ function parseComparison(state) {
|
|
|
163
163
|
|
|
164
164
|
// LIKE
|
|
165
165
|
if (match(state, 'keyword', 'LIKE')) {
|
|
166
|
-
const right =
|
|
166
|
+
const right = parseConcat(state)
|
|
167
167
|
return {
|
|
168
168
|
type: 'binary',
|
|
169
169
|
op: 'LIKE',
|
|
@@ -176,9 +176,9 @@ function parseComparison(state) {
|
|
|
176
176
|
|
|
177
177
|
// BETWEEN - convert to range comparison
|
|
178
178
|
if (match(state, 'keyword', 'BETWEEN')) {
|
|
179
|
-
const lower =
|
|
179
|
+
const lower = parseConcat(state)
|
|
180
180
|
expect(state, 'keyword', 'AND')
|
|
181
|
-
const upper =
|
|
181
|
+
const upper = parseConcat(state)
|
|
182
182
|
// BETWEEN -> expr >= lower AND expr <= upper
|
|
183
183
|
return {
|
|
184
184
|
type: 'binary',
|
|
@@ -198,7 +198,7 @@ function parseComparison(state) {
|
|
|
198
198
|
const opTok = current(state)
|
|
199
199
|
if (opTok.type === 'operator' && isBinaryOp(opTok.value)) {
|
|
200
200
|
consume(state)
|
|
201
|
-
const right =
|
|
201
|
+
const right = parseConcat(state)
|
|
202
202
|
return {
|
|
203
203
|
type: 'binary',
|
|
204
204
|
op: opTok.value,
|
|
@@ -212,6 +212,33 @@ function parseComparison(state) {
|
|
|
212
212
|
return left
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* @param {ParserState} state
|
|
217
|
+
* @returns {ExprNode}
|
|
218
|
+
*/
|
|
219
|
+
export function parseConcat(state) {
|
|
220
|
+
let left = parseAdditive(state)
|
|
221
|
+
while (true) {
|
|
222
|
+
const tok = current(state)
|
|
223
|
+
if (tok.type === 'operator' && tok.value === '||') {
|
|
224
|
+
consume(state)
|
|
225
|
+
const right = parseAdditive(state)
|
|
226
|
+
// Recursive left-associative binary operator
|
|
227
|
+
left = {
|
|
228
|
+
type: 'binary',
|
|
229
|
+
op: '||',
|
|
230
|
+
left,
|
|
231
|
+
right,
|
|
232
|
+
positionStart: left.positionStart,
|
|
233
|
+
positionEnd: right.positionEnd,
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return left
|
|
240
|
+
}
|
|
241
|
+
|
|
215
242
|
/**
|
|
216
243
|
* @param {ParserState} state
|
|
217
244
|
* @returns {ExprNode}
|
|
@@ -110,6 +110,10 @@ function walkExpr(expr, cteScope, refs) {
|
|
|
110
110
|
walkExpr(expr.expr, cteScope, refs)
|
|
111
111
|
for (const v of expr.values) walkExpr(v, cteScope, refs)
|
|
112
112
|
return
|
|
113
|
+
case 'subscript':
|
|
114
|
+
walkExpr(expr.expr, cteScope, refs)
|
|
115
|
+
walkExpr(expr.index, cteScope, refs)
|
|
116
|
+
return
|
|
113
117
|
case 'exists':
|
|
114
118
|
case 'not exists':
|
|
115
119
|
walkStatement(expr.subquery, cteScope, refs)
|
package/src/parse/functions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAggregateFunc, isKnownFunction, isWindowFunc, niladicFuncs, validateFunctionArgs } from '../validation/functions.js'
|
|
2
2
|
import { ParseError, UnknownFunctionError } from '../validation/parseErrors.js'
|
|
3
|
-
import { parseExpression } from './expression.js'
|
|
3
|
+
import { parseConcat, parseExpression } from './expression.js'
|
|
4
4
|
import { consume, current, expect, match } from './state.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -60,6 +60,17 @@ export function parseFunctionCall(state, positionStart) {
|
|
|
60
60
|
positionStart: next.positionStart,
|
|
61
61
|
positionEnd: state.lastPos,
|
|
62
62
|
})
|
|
63
|
+
} else if (funcNameUpper === 'POSITION' && !args.length) {
|
|
64
|
+
// ANSI syntax: POSITION(needle IN haystack), stored as (haystack, needle).
|
|
65
|
+
// The needle is parsed below comparison precedence so IN is not consumed
|
|
66
|
+
// as the set-membership operator.
|
|
67
|
+
const needle = parseConcat(state)
|
|
68
|
+
if (match(state, 'keyword', 'IN')) {
|
|
69
|
+
const haystack = parseExpression(state)
|
|
70
|
+
args.push(haystack, needle)
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
args.push(needle)
|
|
63
74
|
} else {
|
|
64
75
|
args.push(parseExpression(state))
|
|
65
76
|
}
|
package/src/parse/parse.js
CHANGED
|
@@ -48,6 +48,14 @@ export function parseSql({ query, functions }) {
|
|
|
48
48
|
export function parseStatement(state) {
|
|
49
49
|
const positionStart = state.lastPos
|
|
50
50
|
if (match(state, 'keyword', 'WITH')) {
|
|
51
|
+
const recursiveTok = current(state)
|
|
52
|
+
if (recursiveTok.type === 'identifier' && recursiveTok.value.toUpperCase() === 'RECURSIVE') {
|
|
53
|
+
throw new ParseError({
|
|
54
|
+
message: `WITH RECURSIVE is not supported at position ${recursiveTok.positionStart}`,
|
|
55
|
+
...recursiveTok,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/** @type {CTEDefinition[]} */
|
|
52
60
|
const ctes = []
|
|
53
61
|
/** @type {Set<string>} */
|
package/src/parse/primary.js
CHANGED
|
@@ -11,12 +11,65 @@ import { consume, current, expect, match, parseError, peekToken } from './state.
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Parse a primary expression, which is the innermost order of operations
|
|
14
|
+
* Parse a primary expression, which is the innermost order of operations,
|
|
15
|
+
* followed by any postfix subscript operators.
|
|
15
16
|
*
|
|
16
17
|
* @param {ParserState} state
|
|
17
18
|
* @returns {ExprNode}
|
|
18
19
|
*/
|
|
19
20
|
export function parsePrimary(state) {
|
|
21
|
+
return parsePostfix(state, parsePrimaryBase(state))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parses postfix subscript operators after a primary expression: bracket
|
|
26
|
+
* indexing (`expr[0]`, `expr['key']`) and struct field access on a subscript
|
|
27
|
+
* result (`expr[0].field`).
|
|
28
|
+
*
|
|
29
|
+
* @param {ParserState} state
|
|
30
|
+
* @param {ExprNode} expr
|
|
31
|
+
* @returns {ExprNode}
|
|
32
|
+
*/
|
|
33
|
+
function parsePostfix(state, expr) {
|
|
34
|
+
while (true) {
|
|
35
|
+
if (match(state, 'bracket', '[')) {
|
|
36
|
+
const index = parseExpression(state)
|
|
37
|
+
expect(state, 'bracket', ']')
|
|
38
|
+
expr = {
|
|
39
|
+
type: 'subscript',
|
|
40
|
+
expr,
|
|
41
|
+
index,
|
|
42
|
+
positionStart: expr.positionStart,
|
|
43
|
+
positionEnd: state.lastPos,
|
|
44
|
+
}
|
|
45
|
+
} else if (expr.type === 'subscript' && current(state).type === 'dot') {
|
|
46
|
+
consume(state)
|
|
47
|
+
const fieldTok = expect(state, 'identifier')
|
|
48
|
+
expr = {
|
|
49
|
+
type: 'subscript',
|
|
50
|
+
expr,
|
|
51
|
+
index: {
|
|
52
|
+
type: 'literal',
|
|
53
|
+
value: fieldTok.value,
|
|
54
|
+
positionStart: fieldTok.positionStart,
|
|
55
|
+
positionEnd: fieldTok.positionEnd,
|
|
56
|
+
},
|
|
57
|
+
positionStart: expr.positionStart,
|
|
58
|
+
positionEnd: state.lastPos,
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
return expr
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a primary expression without postfix operators.
|
|
68
|
+
*
|
|
69
|
+
* @param {ParserState} state
|
|
70
|
+
* @returns {ExprNode}
|
|
71
|
+
*/
|
|
72
|
+
function parsePrimaryBase(state) {
|
|
20
73
|
const tok = current(state)
|
|
21
74
|
const { positionStart } = tok
|
|
22
75
|
|
|
@@ -82,7 +135,7 @@ export function parsePrimary(state) {
|
|
|
82
135
|
const toType = typeTok.value.toUpperCase()
|
|
83
136
|
if (!isCastType(toType)) {
|
|
84
137
|
throw new SyntaxError({
|
|
85
|
-
expected: 'cast type (STRING, INT, BIGINT, FLOAT, BOOL)',
|
|
138
|
+
expected: 'cast type (STRING, INT, BIGINT, FLOAT, BOOL, TIMESTAMP)',
|
|
86
139
|
after: 'AS',
|
|
87
140
|
...typeTok,
|
|
88
141
|
})
|
|
@@ -97,6 +150,24 @@ export function parsePrimary(state) {
|
|
|
97
150
|
}
|
|
98
151
|
}
|
|
99
152
|
|
|
153
|
+
// TIMESTAMP '2026-01-01 00:00:00' typed literal
|
|
154
|
+
if (funcNameUpper === 'TIMESTAMP' && next.type === 'string') {
|
|
155
|
+
consume(state) // TIMESTAMP
|
|
156
|
+
const strTok = consume(state) // string literal
|
|
157
|
+
return {
|
|
158
|
+
type: 'cast',
|
|
159
|
+
expr: {
|
|
160
|
+
type: 'literal',
|
|
161
|
+
value: strTok.value,
|
|
162
|
+
positionStart: strTok.positionStart,
|
|
163
|
+
positionEnd: strTok.positionEnd,
|
|
164
|
+
},
|
|
165
|
+
toType: 'TIMESTAMP',
|
|
166
|
+
positionStart,
|
|
167
|
+
positionEnd: state.lastPos,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
100
171
|
// EXTRACT(field FROM expr)
|
|
101
172
|
if (funcNameUpper === 'EXTRACT' && next.type === 'paren' && next.value === '(') {
|
|
102
173
|
consume(state) // EXTRACT
|
|
@@ -157,16 +228,21 @@ export function parsePrimary(state) {
|
|
|
157
228
|
if (match(state, 'dot')) {
|
|
158
229
|
prefix = name
|
|
159
230
|
name = expect(state, 'identifier').value
|
|
160
|
-
} else
|
|
161
|
-
// table['column'] string subscript is equivalent to dot access
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
231
|
+
} else {
|
|
232
|
+
// table['column'] string subscript is equivalent to dot access; other
|
|
233
|
+
// subscripts (numeric, expression) are left for the postfix parser
|
|
234
|
+
const open = current(state)
|
|
235
|
+
const fieldTok = peekToken(state, 1)
|
|
236
|
+
const close = peekToken(state, 2)
|
|
237
|
+
if (open.type === 'bracket' && open.value === '[' &&
|
|
238
|
+
fieldTok.type === 'string' &&
|
|
239
|
+
close.type === 'bracket' && close.value === ']') {
|
|
240
|
+
consume(state) // '['
|
|
241
|
+
consume(state) // string
|
|
242
|
+
consume(state) // ']'
|
|
243
|
+
prefix = name
|
|
244
|
+
name = fieldTok.value
|
|
165
245
|
}
|
|
166
|
-
consume(state)
|
|
167
|
-
expect(state, 'bracket', ']')
|
|
168
|
-
prefix = name
|
|
169
|
-
name = fieldTok.value
|
|
170
246
|
}
|
|
171
247
|
|
|
172
248
|
return {
|
package/src/parse/tokenize.js
CHANGED
|
@@ -172,6 +172,18 @@ export function tokenizeSql(query) {
|
|
|
172
172
|
continue
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// string concatenation operator
|
|
176
|
+
if (ch === '|' && next === '|') {
|
|
177
|
+
i += 2
|
|
178
|
+
tokens.push({
|
|
179
|
+
type: 'operator',
|
|
180
|
+
value: '||',
|
|
181
|
+
positionStart,
|
|
182
|
+
positionEnd: i,
|
|
183
|
+
})
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
175
187
|
// operators
|
|
176
188
|
if ('<>!=+-*/%'.includes(ch)) {
|
|
177
189
|
let op = nextChar()
|
package/src/plan/columns.js
CHANGED
|
@@ -255,6 +255,9 @@ export function collectColumnsFromExpr(expr, columns, aliases) {
|
|
|
255
255
|
}
|
|
256
256
|
} else if (expr.type === 'in') {
|
|
257
257
|
collectColumnsFromExpr(expr.expr, columns, aliases)
|
|
258
|
+
} else if (expr.type === 'subscript') {
|
|
259
|
+
collectColumnsFromExpr(expr.expr, columns, aliases)
|
|
260
|
+
collectColumnsFromExpr(expr.index, columns, aliases)
|
|
258
261
|
} else if (expr.type === 'case') {
|
|
259
262
|
if (expr.caseExpr) {
|
|
260
263
|
collectColumnsFromExpr(expr.caseExpr, columns, aliases)
|
package/src/plan/plan.js
CHANGED
|
@@ -637,6 +637,9 @@ function resolveAliases(node, aliases) {
|
|
|
637
637
|
const values = node.values.map(v => resolveAliases(v, aliases))
|
|
638
638
|
return { ...node, expr, values }
|
|
639
639
|
}
|
|
640
|
+
if (node.type === 'subscript') {
|
|
641
|
+
return { ...node, expr: resolveAliases(node.expr, aliases), index: resolveAliases(node.index, aliases) }
|
|
642
|
+
}
|
|
640
643
|
if (node.type === 'case') {
|
|
641
644
|
const caseExpr = resolveAliases(node.caseExpr, aliases)
|
|
642
645
|
const whenClauses = node.whenClauses.map(w => {
|
|
@@ -697,6 +700,13 @@ function normalizeIdentifiers(node, sourceColumns) {
|
|
|
697
700
|
values: node.values.map(v => normalizeIdentifiers(v, sourceColumns)),
|
|
698
701
|
}
|
|
699
702
|
}
|
|
703
|
+
if (node.type === 'subscript') {
|
|
704
|
+
return {
|
|
705
|
+
...node,
|
|
706
|
+
expr: normalizeIdentifiers(node.expr, sourceColumns),
|
|
707
|
+
index: normalizeIdentifiers(node.index, sourceColumns),
|
|
708
|
+
}
|
|
709
|
+
}
|
|
700
710
|
if (node.type === 'case') {
|
|
701
711
|
return {
|
|
702
712
|
...node,
|
|
@@ -872,6 +882,9 @@ function validateLateralSubqueries({ expr, ctePlans, cteColumns, tables, outerSc
|
|
|
872
882
|
for (const val of expr.values) {
|
|
873
883
|
validateLateralSubqueries({ expr: val, ctePlans, cteColumns, tables, outerScope })
|
|
874
884
|
}
|
|
885
|
+
} else if (expr.type === 'subscript') {
|
|
886
|
+
validateLateralSubqueries({ expr: expr.expr, ctePlans, cteColumns, tables, outerScope })
|
|
887
|
+
validateLateralSubqueries({ expr: expr.index, ctePlans, cteColumns, tables, outerScope })
|
|
875
888
|
} else if (expr.type === 'case') {
|
|
876
889
|
validateLateralSubqueries({ expr: expr.caseExpr, ctePlans, cteColumns, tables, outerScope })
|
|
877
890
|
for (const w of expr.whenClauses) {
|
|
@@ -930,6 +943,13 @@ function collectWindows(expr, windows) {
|
|
|
930
943
|
values: expr.values.map(v => collectWindows(v, windows)),
|
|
931
944
|
}
|
|
932
945
|
}
|
|
946
|
+
if (expr.type === 'subscript') {
|
|
947
|
+
return {
|
|
948
|
+
...expr,
|
|
949
|
+
expr: collectWindows(expr.expr, windows),
|
|
950
|
+
index: collectWindows(expr.index, windows),
|
|
951
|
+
}
|
|
952
|
+
}
|
|
933
953
|
if (expr.type === 'case') {
|
|
934
954
|
return {
|
|
935
955
|
...expr,
|
|
@@ -979,6 +999,7 @@ function findWindow(expr) {
|
|
|
979
999
|
return undefined
|
|
980
1000
|
}
|
|
981
1001
|
if (expr.type === 'cast') return findWindow(expr.expr)
|
|
1002
|
+
if (expr.type === 'subscript') return findWindow(expr.expr) || findWindow(expr.index)
|
|
982
1003
|
if (expr.type === 'in valuelist') {
|
|
983
1004
|
const found = findWindow(expr.expr)
|
|
984
1005
|
if (found) return found
|
package/src/types.d.ts
CHANGED
|
@@ -166,6 +166,7 @@ export type StringFunc =
|
|
|
166
166
|
| 'LOWER'
|
|
167
167
|
| 'CONCAT'
|
|
168
168
|
| 'LENGTH'
|
|
169
|
+
| 'OCTET_LENGTH'
|
|
169
170
|
| 'SUBSTRING'
|
|
170
171
|
| 'SUBSTR'
|
|
171
172
|
| 'TRIM'
|
|
@@ -175,6 +176,8 @@ export type StringFunc =
|
|
|
175
176
|
| 'INSTR'
|
|
176
177
|
| 'POSITION'
|
|
177
178
|
| 'STRPOS'
|
|
179
|
+
| 'SPLIT_PART'
|
|
180
|
+
| 'STRING_SPLIT'
|
|
178
181
|
|
|
179
182
|
export type SpatialFunc =
|
|
180
183
|
| 'ST_INTERSECTS'
|
|
@@ -31,6 +31,9 @@ export function findAggregate(expr) {
|
|
|
31
31
|
if (expr.type === 'cast') {
|
|
32
32
|
return findAggregate(expr.expr)
|
|
33
33
|
}
|
|
34
|
+
if (expr.type === 'subscript') {
|
|
35
|
+
return findAggregate(expr.expr) || findAggregate(expr.index)
|
|
36
|
+
}
|
|
34
37
|
if (expr.type === 'case') {
|
|
35
38
|
if (expr.caseExpr) {
|
|
36
39
|
const found = findAggregate(expr.caseExpr)
|
|
@@ -86,7 +86,7 @@ export function isExtractField(name) {
|
|
|
86
86
|
* @returns {name is CastType}
|
|
87
87
|
*/
|
|
88
88
|
export function isCastType(name) {
|
|
89
|
-
return ['TEXT', 'STRING', 'VARCHAR', 'INTEGER', 'INT', 'BIGINT', 'FLOAT', 'REAL', 'DOUBLE', 'BOOLEAN', 'BOOL'].includes(name)
|
|
89
|
+
return ['TEXT', 'STRING', 'VARCHAR', 'INTEGER', 'INT', 'BIGINT', 'FLOAT', 'REAL', 'DOUBLE', 'BOOLEAN', 'BOOL', 'TIMESTAMP'].includes(name)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -95,8 +95,8 @@ export function isCastType(name) {
|
|
|
95
95
|
*/
|
|
96
96
|
export function isStringFunc(name) {
|
|
97
97
|
return [
|
|
98
|
-
'UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM',
|
|
99
|
-
'REPLACE', 'LEFT', 'RIGHT', 'INSTR', 'POSITION', 'STRPOS',
|
|
98
|
+
'UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'OCTET_LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM',
|
|
99
|
+
'REPLACE', 'LEFT', 'RIGHT', 'INSTR', 'POSITION', 'STRPOS', 'SPLIT_PART', 'STRING_SPLIT',
|
|
100
100
|
].includes(name)
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -117,6 +117,7 @@ export const FUNCTION_SIGNATURES = {
|
|
|
117
117
|
UPPER: { min: 1, max: 1, signature: 'string' },
|
|
118
118
|
LOWER: { min: 1, max: 1, signature: 'string' },
|
|
119
119
|
LENGTH: { min: 1, max: 1, signature: 'string' },
|
|
120
|
+
OCTET_LENGTH: { min: 1, max: 1, signature: 'string' },
|
|
120
121
|
TRIM: { min: 1, max: 1, signature: 'string' },
|
|
121
122
|
REPLACE: { min: 3, max: 3, signature: 'string, search, replacement' },
|
|
122
123
|
SUBSTRING: { min: 2, max: 3, signature: 'string, start[, length]' },
|
|
@@ -127,6 +128,8 @@ export const FUNCTION_SIGNATURES = {
|
|
|
127
128
|
INSTR: { min: 2, max: 2, signature: 'string, substring' },
|
|
128
129
|
POSITION: { min: 2, max: 2, signature: 'string, substring' },
|
|
129
130
|
STRPOS: { min: 2, max: 2, signature: 'string, substring' },
|
|
131
|
+
SPLIT_PART: { min: 3, max: 3, signature: 'string, delimiter, index' },
|
|
132
|
+
STRING_SPLIT: { min: 2, max: 2, signature: 'string, delimiter' },
|
|
130
133
|
REGEXP_SUBSTR: { min: 2, max: 4, signature: 'string, pattern[, position[, occurrence]]' },
|
|
131
134
|
REGEXP_EXTRACT: { min: 2, max: 4, signature: 'string, pattern[, position[, occurrence]]' },
|
|
132
135
|
REGEXP_REPLACE: { min: 3, max: 5, signature: 'string, pattern, replacement[, position[, occurrence]]' },
|
package/src/validation/tables.js
CHANGED
|
@@ -90,6 +90,9 @@ export function validateNoIdentifiers(expr, context, outerScope) {
|
|
|
90
90
|
} else if (expr.type === 'in') {
|
|
91
91
|
// LHS is in our scope; subquery is self-contained and planned separately.
|
|
92
92
|
validateNoIdentifiers(expr.expr, context, outerScope)
|
|
93
|
+
} else if (expr.type === 'subscript') {
|
|
94
|
+
validateNoIdentifiers(expr.expr, context, outerScope)
|
|
95
|
+
validateNoIdentifiers(expr.index, context, outerScope)
|
|
93
96
|
} else if (expr.type === 'case') {
|
|
94
97
|
validateNoIdentifiers(expr.caseExpr, context, outerScope)
|
|
95
98
|
for (const w of expr.whenClauses) {
|
|
@@ -144,6 +147,9 @@ export function validateTableRefs(expr, tables, scopeColumns) {
|
|
|
144
147
|
for (const val of expr.values) {
|
|
145
148
|
validateTableRefs(val, tables, scopeColumns)
|
|
146
149
|
}
|
|
150
|
+
} else if (expr.type === 'subscript') {
|
|
151
|
+
validateTableRefs(expr.expr, tables, scopeColumns)
|
|
152
|
+
validateTableRefs(expr.index, tables, scopeColumns)
|
|
147
153
|
} else if (expr.type === 'case') {
|
|
148
154
|
validateTableRefs(expr.caseExpr, tables, scopeColumns)
|
|
149
155
|
for (const w of expr.whenClauses) {
|