squirreling 0.12.19 → 0.12.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +5 -5
- package/src/execute/aggregates.js +33 -9
- package/src/execute/execute.js +59 -1
- package/src/execute/join.js +13 -0
- package/src/execute/sort.js +1 -1
- package/src/execute/utils.js +18 -1
- package/src/execute/window.js +58 -19
- package/src/expression/binary.js +12 -0
- package/src/expression/evaluate.js +65 -35
- package/src/parse/primary.js +2 -2
- package/src/validation/functions.js +8 -0
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
161
161
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
162
162
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_DIFF`, `DATEDIFF`, `DATE_PART`, `DATE_TRUNC`, `EPOCH`, `EXTRACT`, `INTERVAL`
|
|
163
163
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`
|
|
164
|
-
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `CARDINALITY`, `SIZE`
|
|
164
|
+
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `ARRAY_APPEND`, `ARRAY_CONCAT`, `LEN`, `CARDINALITY`, `SIZE`
|
|
165
165
|
- Table functions: `UNNEST`, `EXPLODE`, `JSON_EACH`
|
|
166
166
|
- Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
|
|
167
167
|
- Spatial: `ST_GeomFromText`, `ST_MakeEnvelope`, `ST_AsText`, `ST_Intersects`, `ST_Contains`, `ST_ContainsProperly`, `ST_Within`, `ST_Overlaps`, `ST_Touches`, `ST_Equals`, `ST_Crosses`, `ST_Covers`, `ST_CoveredBy`, `ST_DWithin`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.21",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "25.
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
42
|
+
"@types/node": "25.9.1",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.7",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "
|
|
45
|
+
"eslint-plugin-jsdoc": "63.0.0",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.7"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -9,6 +9,9 @@ import { keyify } from './utils.js'
|
|
|
9
9
|
* @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// Yield to the event loop every this many iterations so that aborts can actually fire
|
|
13
|
+
const YIELD_INTERVAL = 4000
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Projects aggregate columns from a group of rows
|
|
14
17
|
*
|
|
@@ -83,8 +86,12 @@ export function executeHashAggregate(plan, context) {
|
|
|
83
86
|
// Collect all rows
|
|
84
87
|
/** @type {AsyncRow[]} */
|
|
85
88
|
const allRows = []
|
|
89
|
+
let collectCount = 0
|
|
86
90
|
for await (const row of child.rows()) {
|
|
87
|
-
if (
|
|
91
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
93
|
+
if (context.signal?.aborted) return
|
|
94
|
+
}
|
|
88
95
|
allRows.push(row)
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -92,14 +99,27 @@ export function executeHashAggregate(plan, context) {
|
|
|
92
99
|
/** @type {Map<any, AsyncRow[]>} */
|
|
93
100
|
const groups = new Map()
|
|
94
101
|
|
|
95
|
-
for (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
for (let chunkStart = 0; chunkStart < allRows.length; chunkStart += YIELD_INTERVAL) {
|
|
103
|
+
if (chunkStart > 0) {
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
105
|
+
if (context.signal?.aborted) return
|
|
106
|
+
}
|
|
107
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, allRows.length)
|
|
108
|
+
const chunkKeys = await Promise.all(
|
|
109
|
+
allRows.slice(chunkStart, chunkEnd).map(row =>
|
|
110
|
+
Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context })))
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
for (let j = 0; j < chunkKeys.length; j++) {
|
|
114
|
+
const key = keyify(...chunkKeys[j])
|
|
115
|
+
const row = allRows[chunkStart + j]
|
|
116
|
+
let group = groups.get(key)
|
|
117
|
+
if (!group) {
|
|
118
|
+
group = []
|
|
119
|
+
groups.set(key, group)
|
|
120
|
+
}
|
|
121
|
+
group.push(row)
|
|
101
122
|
}
|
|
102
|
-
group.push(row)
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/** @type {{ row: AsyncRow, rows: AsyncRow[], outputRow: AsyncRow }[]} */
|
|
@@ -166,8 +186,12 @@ export function executeScalarAggregate(plan, context) {
|
|
|
166
186
|
// Collect all rows into single group
|
|
167
187
|
/** @type {AsyncRow[]} */
|
|
168
188
|
const group = []
|
|
189
|
+
let collectCount = 0
|
|
169
190
|
for await (const row of child.rows()) {
|
|
170
|
-
if (
|
|
191
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
193
|
+
if (context.signal?.aborted) return
|
|
194
|
+
}
|
|
171
195
|
group.push(row)
|
|
172
196
|
}
|
|
173
197
|
|
package/src/execute/execute.js
CHANGED
|
@@ -16,6 +16,9 @@ import { executeWindow } from './window.js'
|
|
|
16
16
|
* @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode, TableFunctionNode } from '../plan/types.js'
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
20
|
+
const YIELD_INTERVAL = 4000
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* Executes a SQL SELECT query against tables
|
|
21
24
|
*
|
|
@@ -406,12 +409,17 @@ async function* filterRows(rows, condition, context, limit) {
|
|
|
406
409
|
let chunkSize = limit ?? 1
|
|
407
410
|
const grow = limit !== undefined
|
|
408
411
|
let rowIndex = 0
|
|
412
|
+
let innerCount = 0
|
|
409
413
|
|
|
410
414
|
/** @type {{ row: AsyncRow, rowIndex: number }[]} */
|
|
411
415
|
let buffer = []
|
|
412
416
|
|
|
413
417
|
for await (const row of rows) {
|
|
414
418
|
if (context.signal?.aborted) return
|
|
419
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
420
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
421
|
+
if (context.signal?.aborted) return
|
|
422
|
+
}
|
|
415
423
|
rowIndex++
|
|
416
424
|
buffer.push({ row, rowIndex })
|
|
417
425
|
|
|
@@ -451,8 +459,13 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
451
459
|
if (limit <= 0) return
|
|
452
460
|
let skipped = 0
|
|
453
461
|
let yielded = 0
|
|
462
|
+
let innerCount = 0
|
|
454
463
|
for await (const row of rows) {
|
|
455
464
|
if (signal?.aborted) return
|
|
465
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
466
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
467
|
+
if (signal?.aborted) return
|
|
468
|
+
}
|
|
456
469
|
if (skipped < offset) {
|
|
457
470
|
skipped++
|
|
458
471
|
continue
|
|
@@ -500,16 +513,21 @@ function executeProject(plan, context) {
|
|
|
500
513
|
maxRows: child.maxRows,
|
|
501
514
|
async *rows() {
|
|
502
515
|
let rowIndex = 0
|
|
516
|
+
let innerCount = 0
|
|
503
517
|
|
|
504
518
|
for await (const row of child.rows()) {
|
|
505
519
|
if (context.signal?.aborted) return
|
|
520
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
521
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
522
|
+
if (context.signal?.aborted) return
|
|
523
|
+
}
|
|
506
524
|
rowIndex++
|
|
507
525
|
const currentRowIndex = rowIndex
|
|
508
526
|
|
|
509
527
|
/** @type {AsyncCells} */
|
|
510
528
|
const cells = {}
|
|
511
529
|
// Only safe to propagate resolved when every output column comes from
|
|
512
|
-
// the star branch
|
|
530
|
+
// the star branch. Derived expressions evaluate lazily and can't be
|
|
513
531
|
// pre-materialized here, and a partial resolved would make
|
|
514
532
|
// collect()/downstream identifier fast paths read undefined.
|
|
515
533
|
const source = resolveable ? row.resolved : undefined
|
|
@@ -588,9 +606,14 @@ function executeDistinct(plan, context) {
|
|
|
588
606
|
|
|
589
607
|
/** @type {AsyncRow[]} */
|
|
590
608
|
let buffer = []
|
|
609
|
+
let innerCount = 0
|
|
591
610
|
|
|
592
611
|
for await (const row of child.rows()) {
|
|
593
612
|
if (signal?.aborted) return
|
|
613
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
614
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
615
|
+
if (signal?.aborted) return
|
|
616
|
+
}
|
|
594
617
|
buffer.push(row)
|
|
595
618
|
|
|
596
619
|
if (buffer.length >= MAX_CHUNK) {
|
|
@@ -671,8 +694,13 @@ function executeSetOperation(plan, context) {
|
|
|
671
694
|
async *rows() {
|
|
672
695
|
// UNION: yield deduplicated rows from both sides
|
|
673
696
|
const seen = new Set()
|
|
697
|
+
let count = 0
|
|
674
698
|
for await (const row of left.rows()) {
|
|
675
699
|
if (signal?.aborted) return
|
|
700
|
+
if (++count % YIELD_INTERVAL === 0) {
|
|
701
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
702
|
+
if (signal?.aborted) return
|
|
703
|
+
}
|
|
676
704
|
const key = await stableRowKey(row)
|
|
677
705
|
if (!seen.has(key)) {
|
|
678
706
|
seen.add(key)
|
|
@@ -681,6 +709,10 @@ function executeSetOperation(plan, context) {
|
|
|
681
709
|
}
|
|
682
710
|
for await (const row of right.rows()) {
|
|
683
711
|
if (signal?.aborted) return
|
|
712
|
+
if (++count % YIELD_INTERVAL === 0) {
|
|
713
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
714
|
+
if (signal?.aborted) return
|
|
715
|
+
}
|
|
684
716
|
const key = await stableRowKey(row)
|
|
685
717
|
if (!seen.has(key)) {
|
|
686
718
|
seen.add(key)
|
|
@@ -700,8 +732,13 @@ function executeSetOperation(plan, context) {
|
|
|
700
732
|
// Materialize right side keys
|
|
701
733
|
/** @type {Map<any, number>} */
|
|
702
734
|
const rightKeys = new Map()
|
|
735
|
+
let tick = 0
|
|
703
736
|
for await (const row of right.rows()) {
|
|
704
737
|
if (signal?.aborted) return
|
|
738
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
739
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
740
|
+
if (signal?.aborted) return
|
|
741
|
+
}
|
|
705
742
|
const key = await stableRowKey(row)
|
|
706
743
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
707
744
|
}
|
|
@@ -710,6 +747,10 @@ function executeSetOperation(plan, context) {
|
|
|
710
747
|
// INTERSECT ALL: yield each left row that matches, consuming right counts
|
|
711
748
|
for await (const row of left.rows()) {
|
|
712
749
|
if (signal?.aborted) return
|
|
750
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
751
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
752
|
+
if (signal?.aborted) return
|
|
753
|
+
}
|
|
713
754
|
const key = await stableRowKey(row)
|
|
714
755
|
const count = rightKeys.get(key)
|
|
715
756
|
if (count) {
|
|
@@ -722,6 +763,10 @@ function executeSetOperation(plan, context) {
|
|
|
722
763
|
const seen = new Set()
|
|
723
764
|
for await (const row of left.rows()) {
|
|
724
765
|
if (signal?.aborted) return
|
|
766
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
767
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
768
|
+
if (signal?.aborted) return
|
|
769
|
+
}
|
|
725
770
|
const key = await stableRowKey(row)
|
|
726
771
|
if (rightKeys.has(key) && !seen.has(key)) {
|
|
727
772
|
seen.add(key)
|
|
@@ -742,8 +787,13 @@ function executeSetOperation(plan, context) {
|
|
|
742
787
|
// Materialize right side keys
|
|
743
788
|
/** @type {Map<any, number>} */
|
|
744
789
|
const rightKeys = new Map()
|
|
790
|
+
let tick = 0
|
|
745
791
|
for await (const row of right.rows()) {
|
|
746
792
|
if (signal?.aborted) return
|
|
793
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
794
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
795
|
+
if (signal?.aborted) return
|
|
796
|
+
}
|
|
747
797
|
const key = await stableRowKey(row)
|
|
748
798
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
749
799
|
}
|
|
@@ -752,6 +802,10 @@ function executeSetOperation(plan, context) {
|
|
|
752
802
|
// EXCEPT ALL: yield left rows, consuming right counts
|
|
753
803
|
for await (const row of left.rows()) {
|
|
754
804
|
if (signal?.aborted) return
|
|
805
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
806
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
807
|
+
if (signal?.aborted) return
|
|
808
|
+
}
|
|
755
809
|
const key = await stableRowKey(row)
|
|
756
810
|
const count = rightKeys.get(key)
|
|
757
811
|
if (count) {
|
|
@@ -765,6 +819,10 @@ function executeSetOperation(plan, context) {
|
|
|
765
819
|
const seen = new Set()
|
|
766
820
|
for await (const row of left.rows()) {
|
|
767
821
|
if (signal?.aborted) return
|
|
822
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
823
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
824
|
+
if (signal?.aborted) return
|
|
825
|
+
}
|
|
768
826
|
const key = await stableRowKey(row)
|
|
769
827
|
if (!rightKeys.has(key) && !seen.has(key)) {
|
|
770
828
|
seen.add(key)
|
package/src/execute/join.js
CHANGED
|
@@ -7,6 +7,9 @@ import { executePlan } from './execute.js'
|
|
|
7
7
|
* @import { HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
11
|
+
const YIELD_INTERVAL = 4000
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Executes a nested loop join operation
|
|
12
15
|
*
|
|
@@ -41,6 +44,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
41
44
|
/** @type {Set<AsyncRow> | undefined} */
|
|
42
45
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
43
46
|
|
|
47
|
+
let innerCount = 0
|
|
44
48
|
for await (const leftRow of left.rows()) {
|
|
45
49
|
if (context.signal?.aborted) break
|
|
46
50
|
|
|
@@ -51,6 +55,10 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
51
55
|
let hasMatch = false
|
|
52
56
|
|
|
53
57
|
for (const rightRow of rightRows) {
|
|
58
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
60
|
+
if (context.signal?.aborted) return
|
|
61
|
+
}
|
|
54
62
|
const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
55
63
|
const matches = await evaluateExpr({
|
|
56
64
|
node: plan.condition,
|
|
@@ -237,6 +245,7 @@ export function executeHashJoin(plan, context) {
|
|
|
237
245
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
238
246
|
|
|
239
247
|
// Probe phase: stream left rows
|
|
248
|
+
let innerCount = 0
|
|
240
249
|
for await (const leftRow of left.rows()) {
|
|
241
250
|
if (context.signal?.aborted) break
|
|
242
251
|
|
|
@@ -253,6 +262,10 @@ export function executeHashJoin(plan, context) {
|
|
|
253
262
|
const candidates = hashMap.get(key)
|
|
254
263
|
if (candidates?.length) {
|
|
255
264
|
for (const rightRow of candidates) {
|
|
265
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
266
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
267
|
+
if (context.signal?.aborted) return
|
|
268
|
+
}
|
|
256
269
|
const merged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
257
270
|
if (residual) {
|
|
258
271
|
const ok = await evaluateExpr({ node: residual, row: merged, context })
|
package/src/execute/sort.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function sortEntriesByTerms({ entries, orderBy, context, cacheValue
|
|
|
59
59
|
let chunkSize = 1
|
|
60
60
|
let start = 0
|
|
61
61
|
while (start < missing.length) {
|
|
62
|
-
|
|
62
|
+
context.signal?.throwIfAborted()
|
|
63
63
|
const chunk = missing.slice(start, start + chunkSize)
|
|
64
64
|
const values = await Promise.all(chunk.map(idx =>
|
|
65
65
|
evaluateExpr({
|
package/src/execute/utils.js
CHANGED
|
@@ -27,7 +27,11 @@ export function compareForTerm(a, b, term) {
|
|
|
27
27
|
if (a == b) return 0
|
|
28
28
|
|
|
29
29
|
let cmp
|
|
30
|
-
if (
|
|
30
|
+
if (a instanceof Date && b instanceof Date) {
|
|
31
|
+
const at = a.getTime()
|
|
32
|
+
const bt = b.getTime()
|
|
33
|
+
cmp = at < bt ? -1 : at > bt ? 1 : 0
|
|
34
|
+
} else if (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
|
|
31
35
|
cmp = a < b ? -1 : 1
|
|
32
36
|
} else {
|
|
33
37
|
const aa = String(a)
|
|
@@ -121,6 +125,19 @@ export function maxBounds(a, b) {
|
|
|
121
125
|
return a ?? b
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
/**
|
|
129
|
+
* SQL equality for primitives. Two Date instances for the same instant compare
|
|
130
|
+
* equal (JS `==` would compare by identity).
|
|
131
|
+
*
|
|
132
|
+
* @param {SqlPrimitive} a
|
|
133
|
+
* @param {SqlPrimitive} b
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
export function sqlEquals(a, b) {
|
|
137
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
|
|
138
|
+
return a == b
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
/**
|
|
125
142
|
* Returns true for plain object SqlPrimitive values, excluding null, arrays, and Dates.
|
|
126
143
|
*
|
package/src/execute/window.js
CHANGED
|
@@ -7,6 +7,9 @@ import { compareForTerm, keyify } from './utils.js'
|
|
|
7
7
|
* @import { WindowNode, WindowSpec } from '../plan/types.js'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
11
|
+
const YIELD_INTERVAL = 4000
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Executes a Window plan node: buffers the child's rows, assigns each window
|
|
12
15
|
* function's output per partition, and yields rows in input order with the
|
|
@@ -36,8 +39,10 @@ export function executeWindow(plan, context) {
|
|
|
36
39
|
async *rows() {
|
|
37
40
|
let i = 0
|
|
38
41
|
for await (const row of child.rows()) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
42
|
+
if (++i % YIELD_INTERVAL === 0) {
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
44
|
+
if (context.signal?.aborted) return
|
|
45
|
+
}
|
|
41
46
|
const cells = { ...row.cells }
|
|
42
47
|
for (const w of plan.windows) {
|
|
43
48
|
const value = i
|
|
@@ -59,8 +64,12 @@ export function executeWindow(plan, context) {
|
|
|
59
64
|
async *rows() {
|
|
60
65
|
/** @type {AsyncRow[]} */
|
|
61
66
|
const rows = []
|
|
67
|
+
let collectCount = 0
|
|
62
68
|
for await (const row of child.rows()) {
|
|
63
|
-
if (
|
|
69
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
71
|
+
if (context.signal?.aborted) return
|
|
72
|
+
}
|
|
64
73
|
rows.push(row)
|
|
65
74
|
}
|
|
66
75
|
if (rows.length === 0) return
|
|
@@ -74,8 +83,12 @@ export function executeWindow(plan, context) {
|
|
|
74
83
|
if (context.signal?.aborted) return
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
let emitCount = 0
|
|
77
87
|
for (let i = 0; i < rows.length; i++) {
|
|
78
|
-
if (
|
|
88
|
+
if (++emitCount % YIELD_INTERVAL === 0) {
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
90
|
+
if (context.signal?.aborted) return
|
|
91
|
+
}
|
|
79
92
|
const row = rows[i]
|
|
80
93
|
const cells = { ...row.cells }
|
|
81
94
|
for (let w = 0; w < plan.windows.length; w++) {
|
|
@@ -105,17 +118,26 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
105
118
|
// Bucket row indices by partition key.
|
|
106
119
|
/** @type {Map<string | number | bigint | boolean, number[]>} */
|
|
107
120
|
const partitions = new Map()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
for (let chunkStart = 0; chunkStart < rows.length; chunkStart += YIELD_INTERVAL) {
|
|
122
|
+
if (chunkStart > 0) {
|
|
123
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
124
|
+
if (context.signal?.aborted) return
|
|
125
|
+
}
|
|
126
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, rows.length)
|
|
127
|
+
const chunkKeys = await Promise.all(
|
|
128
|
+
rows.slice(chunkStart, chunkEnd).map(row =>
|
|
129
|
+
Promise.all(spec.partitionBy.map(expr => evaluateExpr({ node: expr, row, context })))
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
for (let j = 0; j < chunkKeys.length; j++) {
|
|
133
|
+
const key = keyify(...chunkKeys[j])
|
|
134
|
+
let bucket = partitions.get(key)
|
|
135
|
+
if (!bucket) {
|
|
136
|
+
bucket = []
|
|
137
|
+
partitions.set(key, bucket)
|
|
138
|
+
}
|
|
139
|
+
bucket.push(chunkStart + j)
|
|
117
140
|
}
|
|
118
|
-
bucket.push(i)
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
for (const bucket of partitions.values()) {
|
|
@@ -125,11 +147,24 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
125
147
|
/** @type {number[]} */
|
|
126
148
|
let ordered
|
|
127
149
|
if (spec.orderBy.length) {
|
|
128
|
-
const orderValues = await Promise.all(bucket.map(idx =>
|
|
129
|
-
Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
|
|
130
|
-
))
|
|
131
150
|
/** @type {{ idx: number, values: SqlPrimitive[], pos: number }[]} */
|
|
132
|
-
const entries = bucket.
|
|
151
|
+
const entries = new Array(bucket.length)
|
|
152
|
+
for (let chunkStart = 0; chunkStart < bucket.length; chunkStart += YIELD_INTERVAL) {
|
|
153
|
+
if (chunkStart > 0) {
|
|
154
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
155
|
+
if (context.signal?.aborted) return
|
|
156
|
+
}
|
|
157
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, bucket.length)
|
|
158
|
+
const chunkValues = await Promise.all(
|
|
159
|
+
bucket.slice(chunkStart, chunkEnd).map(idx =>
|
|
160
|
+
Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
for (let j = 0; j < chunkValues.length; j++) {
|
|
164
|
+
const k = chunkStart + j
|
|
165
|
+
entries[k] = { idx: bucket[k], values: chunkValues[j], pos: k }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
133
168
|
entries.sort((a, b) => {
|
|
134
169
|
for (let i = 0; i < spec.orderBy.length; i++) {
|
|
135
170
|
const cmp = compareForTerm(a.values[i], b.values[i], spec.orderBy[i])
|
|
@@ -165,8 +200,12 @@ async function applyWindowFunction(spec, ordered, rows, output, context) {
|
|
|
165
200
|
if (spec.funcName === 'LAG' || spec.funcName === 'LEAD') {
|
|
166
201
|
const direction = spec.funcName === 'LAG' ? -1 : 1
|
|
167
202
|
const [valueExpr, offsetExpr, defaultExpr] = spec.args
|
|
203
|
+
let tick = 0
|
|
168
204
|
for (let k = 0; k < ordered.length; k++) {
|
|
169
|
-
if (
|
|
205
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
206
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
207
|
+
if (context.signal?.aborted) return
|
|
208
|
+
}
|
|
170
209
|
const idx = ordered[k]
|
|
171
210
|
const row = rows[idx]
|
|
172
211
|
const offset = offsetExpr
|
package/src/expression/binary.js
CHANGED
|
@@ -29,6 +29,18 @@ export function applyBinaryOp(op, a, b) {
|
|
|
29
29
|
}
|
|
30
30
|
if (op === 'AND') return Boolean(a) && Boolean(b)
|
|
31
31
|
if (op === 'OR') return Boolean(a) || Boolean(b)
|
|
32
|
+
// Compare Date values by their time so distinct instances for the same
|
|
33
|
+
// instant are equal, matching SQL TIMESTAMP semantics rather than JS identity.
|
|
34
|
+
if (a instanceof Date && b instanceof Date) {
|
|
35
|
+
const at = a.getTime()
|
|
36
|
+
const bt = b.getTime()
|
|
37
|
+
if (op === '!=' || op === '<>') return at !== bt
|
|
38
|
+
if (op === '=' || op === '==') return at === bt
|
|
39
|
+
if (op === '<') return at < bt
|
|
40
|
+
if (op === '<=') return at <= bt
|
|
41
|
+
if (op === '>') return at > bt
|
|
42
|
+
if (op === '>=') return at >= bt
|
|
43
|
+
}
|
|
32
44
|
if (op === '!=' || op === '<>') return a != b
|
|
33
45
|
if (op === '=' || op === '==') return a == b
|
|
34
46
|
if (op === '<') return a < b
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { executeStatement } from '../execute/execute.js'
|
|
2
|
-
import { isPlainObject, keyify, stringify } from '../execute/utils.js'
|
|
2
|
+
import { isPlainObject, keyify, sqlEquals, stringify } from '../execute/utils.js'
|
|
3
3
|
import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
|
|
4
4
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
5
5
|
import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
@@ -16,6 +16,35 @@ import { evaluateStringFunc } from './strings.js'
|
|
|
16
16
|
* @import { ExprNode, AsyncRow, ExecuteContext, SqlPrimitive } from '../types.js'
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// Yield to the event loop every this many iterations so that aborts can actually fire
|
|
20
|
+
const YIELD_INTERVAL = 4000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Evaluates an expression for each row, yielding to the event loop every
|
|
24
|
+
* YIELD_INTERVAL rows so signal-based aborts can fire mid-evaluation.
|
|
25
|
+
*
|
|
26
|
+
* @param {ExprNode} node
|
|
27
|
+
* @param {AsyncRow[]} rows
|
|
28
|
+
* @param {ExecuteContext} context
|
|
29
|
+
* @returns {Promise<SqlPrimitive[]>}
|
|
30
|
+
*/
|
|
31
|
+
async function evaluateAll(node, rows, context) {
|
|
32
|
+
/** @type {SqlPrimitive[]} */
|
|
33
|
+
const results = new Array(rows.length)
|
|
34
|
+
for (let i = 0; i < rows.length; i += YIELD_INTERVAL) {
|
|
35
|
+
if (i > 0) {
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
37
|
+
context.signal?.throwIfAborted()
|
|
38
|
+
}
|
|
39
|
+
const end = Math.min(i + YIELD_INTERVAL, rows.length)
|
|
40
|
+
const chunk = await Promise.all(rows.slice(i, end).map(row =>
|
|
41
|
+
evaluateExpr({ node, row, context })
|
|
42
|
+
))
|
|
43
|
+
for (let j = 0; j < chunk.length; j++) results[i + j] = chunk[j]
|
|
44
|
+
}
|
|
45
|
+
return results
|
|
46
|
+
}
|
|
47
|
+
|
|
19
48
|
/**
|
|
20
49
|
* Evaluates an expression node against a row of data
|
|
21
50
|
*
|
|
@@ -177,10 +206,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
177
206
|
// Apply FILTER clause if present
|
|
178
207
|
let filteredRows = rows
|
|
179
208
|
if (node.filter) {
|
|
180
|
-
const
|
|
181
|
-
const passes = await Promise.all(rows.map(row =>
|
|
182
|
-
evaluateExpr({ node: filterNode, row, context })
|
|
183
|
-
))
|
|
209
|
+
const passes = await evaluateAll(node.filter, rows, context)
|
|
184
210
|
filteredRows = rows.filter((_, i) => passes[i])
|
|
185
211
|
}
|
|
186
212
|
|
|
@@ -191,9 +217,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
191
217
|
return filteredRows.length
|
|
192
218
|
}
|
|
193
219
|
|
|
194
|
-
const values = await
|
|
195
|
-
evaluateExpr({ node: argNode, row, context })
|
|
196
|
-
))
|
|
220
|
+
const values = await evaluateAll(argNode, filteredRows, context)
|
|
197
221
|
if (node.distinct) {
|
|
198
222
|
const seen = new Set()
|
|
199
223
|
for (const v of values) {
|
|
@@ -209,9 +233,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
209
233
|
}
|
|
210
234
|
|
|
211
235
|
if (funcName === 'COUNTIF') {
|
|
212
|
-
const values = await
|
|
213
|
-
evaluateExpr({ node: argNode, row, context })
|
|
214
|
-
))
|
|
236
|
+
const values = await evaluateAll(argNode, filteredRows, context)
|
|
215
237
|
let count = 0
|
|
216
238
|
for (const v of values) {
|
|
217
239
|
if (v) count++
|
|
@@ -220,9 +242,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
|
|
223
|
-
const rawValues = await
|
|
224
|
-
evaluateExpr({ node: argNode, row, context })
|
|
225
|
-
))
|
|
245
|
+
const rawValues = await evaluateAll(argNode, filteredRows, context)
|
|
226
246
|
let sum = 0
|
|
227
247
|
let count = 0
|
|
228
248
|
/** @type {SqlPrimitive} */
|
|
@@ -247,9 +267,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
250
|
-
const rawValues = await
|
|
251
|
-
evaluateExpr({ node: argNode, row, context })
|
|
252
|
-
))
|
|
270
|
+
const rawValues = await evaluateAll(argNode, filteredRows, context)
|
|
253
271
|
let sum = 0
|
|
254
272
|
/** @type {number[]} */
|
|
255
273
|
const values = []
|
|
@@ -290,9 +308,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
290
308
|
...node,
|
|
291
309
|
})
|
|
292
310
|
}
|
|
293
|
-
const rawValues = await
|
|
294
|
-
evaluateExpr({ node: valueNode, row, context })
|
|
295
|
-
))
|
|
311
|
+
const rawValues = await evaluateAll(valueNode, filteredRows, context)
|
|
296
312
|
/** @type {number[]} */
|
|
297
313
|
const values = []
|
|
298
314
|
for (const raw of rawValues) {
|
|
@@ -311,12 +327,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
311
327
|
}
|
|
312
328
|
|
|
313
329
|
if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG') {
|
|
330
|
+
const allValues = await evaluateAll(argNode, filteredRows, context)
|
|
314
331
|
if (node.distinct) {
|
|
315
332
|
/** @type {SqlPrimitive[]} */
|
|
316
333
|
const values = []
|
|
317
334
|
const seen = new Set()
|
|
318
|
-
for (const
|
|
319
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
335
|
+
for (const v of allValues) {
|
|
320
336
|
const key = keyify(v)
|
|
321
337
|
if (!seen.has(key)) {
|
|
322
338
|
seen.add(key)
|
|
@@ -325,9 +341,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
325
341
|
}
|
|
326
342
|
return values
|
|
327
343
|
} else {
|
|
328
|
-
return
|
|
329
|
-
evaluateExpr({ node: argNode, row, context })
|
|
330
|
-
))
|
|
344
|
+
return allValues
|
|
331
345
|
}
|
|
332
346
|
}
|
|
333
347
|
|
|
@@ -336,10 +350,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
336
350
|
const separator = String(await evaluateExpr({ node: separatorNode, row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
|
|
337
351
|
/** @type {string[]} */
|
|
338
352
|
const values = []
|
|
353
|
+
const allValues = await evaluateAll(argNode, filteredRows, context)
|
|
339
354
|
if (node.distinct) {
|
|
340
355
|
const seen = new Set()
|
|
341
|
-
for (const
|
|
342
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
356
|
+
for (const v of allValues) {
|
|
343
357
|
if (v == null) continue
|
|
344
358
|
const str = String(v)
|
|
345
359
|
const key = keyify(str)
|
|
@@ -349,8 +363,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
349
363
|
}
|
|
350
364
|
}
|
|
351
365
|
} else {
|
|
352
|
-
for (const
|
|
353
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
366
|
+
for (const v of allValues) {
|
|
354
367
|
if (v != null) values.push(String(v))
|
|
355
368
|
}
|
|
356
369
|
}
|
|
@@ -516,7 +529,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
516
529
|
return arr.length
|
|
517
530
|
}
|
|
518
531
|
|
|
519
|
-
if (funcName === 'ARRAY_LENGTH' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
|
|
532
|
+
if (funcName === 'ARRAY_LENGTH' || funcName === 'LIST_LENGTH' || funcName === 'LEN' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
|
|
520
533
|
const arr = args[0]
|
|
521
534
|
if (!Array.isArray(arr)) return null
|
|
522
535
|
if (funcName === 'ARRAY_LENGTH' && args.length === 2) {
|
|
@@ -539,19 +552,31 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
539
552
|
return arr.length
|
|
540
553
|
}
|
|
541
554
|
|
|
542
|
-
if (funcName === 'ARRAY_POSITION') {
|
|
555
|
+
if (funcName === 'ARRAY_POSITION' || funcName === 'LIST_POSITION') {
|
|
543
556
|
const [arr, target] = args
|
|
544
557
|
if (!Array.isArray(arr)) return null
|
|
545
558
|
const index = arr.indexOf(target)
|
|
546
559
|
return index === -1 ? null : index + 1
|
|
547
560
|
}
|
|
548
561
|
|
|
549
|
-
if (funcName === 'ARRAY_CONTAINS') {
|
|
562
|
+
if (funcName === 'ARRAY_CONTAINS' || funcName === 'LIST_CONTAINS') {
|
|
550
563
|
const [arr, target] = args
|
|
551
564
|
if (!Array.isArray(arr)) return null
|
|
552
565
|
return arr.includes(target)
|
|
553
566
|
}
|
|
554
567
|
|
|
568
|
+
if (funcName === 'ARRAY_APPEND' || funcName === 'LIST_APPEND') {
|
|
569
|
+
const [arr, element] = args
|
|
570
|
+
if (!Array.isArray(arr)) return null
|
|
571
|
+
return [...arr, element]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (funcName === 'ARRAY_CONCAT' || funcName === 'LIST_CONCAT') {
|
|
575
|
+
const [a, b] = args
|
|
576
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return null
|
|
577
|
+
return [...a, ...b]
|
|
578
|
+
}
|
|
579
|
+
|
|
555
580
|
if (funcName === 'ARRAY_SORT') {
|
|
556
581
|
const arr = args[0]
|
|
557
582
|
if (!Array.isArray(arr)) return null
|
|
@@ -667,7 +692,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
667
692
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
668
693
|
for (const valueNode of node.values) {
|
|
669
694
|
const val = await evaluateExpr({ node: valueNode, row, rowIndex, rows, context })
|
|
670
|
-
if (exprVal
|
|
695
|
+
if (sqlEquals(exprVal, val)) return true
|
|
671
696
|
}
|
|
672
697
|
return false
|
|
673
698
|
}
|
|
@@ -675,9 +700,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
675
700
|
if (node.type === 'in') {
|
|
676
701
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
677
702
|
const subResult = executeStatement({ query: node.subquery, context })
|
|
703
|
+
let innerCount = 0
|
|
678
704
|
for await (const resRow of subResult.rows()) {
|
|
705
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
706
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
707
|
+
context.signal?.throwIfAborted()
|
|
708
|
+
}
|
|
679
709
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
680
|
-
if (exprVal
|
|
710
|
+
if (sqlEquals(exprVal, value)) return true
|
|
681
711
|
}
|
|
682
712
|
return false
|
|
683
713
|
}
|
|
@@ -703,7 +733,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
703
733
|
for (const whenClause of node.whenClauses) {
|
|
704
734
|
const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
|
|
705
735
|
// compare caseValue with condition or evaluate as boolean
|
|
706
|
-
if (caseValue !== undefined ? caseValue
|
|
736
|
+
if (caseValue !== undefined ? sqlEquals(caseValue, whenValue) : whenValue) {
|
|
707
737
|
return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
|
|
708
738
|
}
|
|
709
739
|
}
|
package/src/parse/primary.js
CHANGED
|
@@ -40,7 +40,7 @@ export function parsePrimary(state) {
|
|
|
40
40
|
return expr
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Array literal: [elem, elem, ...]
|
|
43
|
+
// Array literal: [elem, elem, ...] elements must be literals
|
|
44
44
|
if (match(state, 'bracket', '[')) {
|
|
45
45
|
/** @type {SqlPrimitive[]} */
|
|
46
46
|
const values = []
|
|
@@ -158,7 +158,7 @@ export function parsePrimary(state) {
|
|
|
158
158
|
prefix = name
|
|
159
159
|
name = expect(state, 'identifier').value
|
|
160
160
|
} else if (match(state, 'bracket', '[')) {
|
|
161
|
-
// table['column']
|
|
161
|
+
// table['column'] string subscript is equivalent to dot access
|
|
162
162
|
const fieldTok = current(state)
|
|
163
163
|
if (fieldTok.type !== 'string') {
|
|
164
164
|
throw parseError(state, 'string literal')
|
|
@@ -184,9 +184,17 @@ export const FUNCTION_SIGNATURES = {
|
|
|
184
184
|
|
|
185
185
|
// Array functions
|
|
186
186
|
ARRAY_LENGTH: { min: 1, max: 2, signature: 'array[, dimension]' },
|
|
187
|
+
LIST_LENGTH: { min: 1, max: 1, signature: 'array' },
|
|
188
|
+
LEN: { min: 1, max: 1, signature: 'array' },
|
|
187
189
|
ARRAY_POSITION: { min: 2, max: 2, signature: 'array, element' },
|
|
190
|
+
LIST_POSITION: { min: 2, max: 2, signature: 'array, element' },
|
|
188
191
|
ARRAY_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
|
|
192
|
+
LIST_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
|
|
189
193
|
ARRAY_SORT: { min: 1, max: 1, signature: 'array' },
|
|
194
|
+
ARRAY_APPEND: { min: 2, max: 2, signature: 'array, element' },
|
|
195
|
+
LIST_APPEND: { min: 2, max: 2, signature: 'array, element' },
|
|
196
|
+
ARRAY_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
|
|
197
|
+
LIST_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
|
|
190
198
|
CARDINALITY: { min: 1, max: 1, signature: 'array' },
|
|
191
199
|
SIZE: { min: 1, max: 1, signature: 'array' },
|
|
192
200
|
|