squirreling 0.11.4 → 0.12.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 +7 -4
- package/package.json +2 -2
- package/src/ast.d.ts +4 -4
- package/src/backend/dataSource.js +37 -6
- package/src/execute/aggregates.js +89 -72
- package/src/execute/execute.js +314 -224
- package/src/execute/join.js +174 -152
- package/src/execute/sort.js +71 -64
- package/src/execute/utils.js +40 -5
- package/src/expression/binary.js +1 -1
- package/src/expression/evaluate.js +8 -8
- package/src/expression/math.js +1 -1
- package/src/index.d.ts +5 -4
- package/src/parse/parse.js +9 -6
- package/src/parse/primary.js +12 -9
- package/src/parse/state.js +8 -1
- package/src/parse/tokenize.js +29 -27
- package/src/parse/types.d.ts +2 -2
- package/src/plan/columns.js +2 -2
- package/src/plan/plan.js +17 -3
- package/src/types.d.ts +13 -2
- package/src/validation/functions.js +3 -2
- package/src/validation/parseErrors.js +19 -2
- package/src/validation/tables.js +62 -10
- package/src/validation/planErrors.js +0 -50
package/src/execute/execute.js
CHANGED
|
@@ -7,10 +7,10 @@ import { validateScan, validateTable } from '../validation/tables.js'
|
|
|
7
7
|
import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
8
8
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
9
9
|
import { executeSort } from './sort.js'
|
|
10
|
-
import { stableRowKey } from './utils.js'
|
|
10
|
+
import { addBounds, minBounds, stableRowKey } from './utils.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, Statement } from '../types.js'
|
|
13
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, QueryResults, Statement } from '../types.js'
|
|
14
14
|
* @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode } from '../plan/types.js'
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -18,9 +18,9 @@ import { stableRowKey } from './utils.js'
|
|
|
18
18
|
* Executes a SQL SELECT query against tables
|
|
19
19
|
*
|
|
20
20
|
* @param {ExecuteSqlOptions} options
|
|
21
|
-
* @
|
|
21
|
+
* @returns {QueryResults}
|
|
22
22
|
*/
|
|
23
|
-
export
|
|
23
|
+
export function executeSql({ tables, query, functions, signal }) {
|
|
24
24
|
const parsed = typeof query === 'string' ? parseSql({ query, functions }) : query
|
|
25
25
|
|
|
26
26
|
// Normalize tables: convert arrays to AsyncDataSource
|
|
@@ -34,7 +34,9 @@ export async function* executeSql({ tables, query, functions, signal }) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const context = { tables: normalizedTables, functions, signal }
|
|
38
|
+
const plan = planSql({ query: parsed, functions, tables: normalizedTables })
|
|
39
|
+
return executePlan({ plan, context })
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -43,60 +45,62 @@ export async function* executeSql({ tables, query, functions, signal }) {
|
|
|
43
45
|
* @param {Object} options
|
|
44
46
|
* @param {Statement} options.query
|
|
45
47
|
* @param {ExecuteContext} options.context
|
|
46
|
-
* @
|
|
48
|
+
* @returns {QueryResults}
|
|
47
49
|
*/
|
|
48
|
-
export
|
|
50
|
+
export function executeStatement({ query, context }) {
|
|
49
51
|
const plan = planSql({ query, functions: context.functions, tables: context.tables })
|
|
50
|
-
|
|
52
|
+
return executePlan({ plan, context })
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
* Executes a query plan and
|
|
56
|
+
* Executes a query plan and returns query results with row count estimates
|
|
55
57
|
*
|
|
56
58
|
* @param {Object} options
|
|
57
59
|
* @param {QueryPlan} options.plan - the query plan to execute
|
|
58
60
|
* @param {ExecuteContext} options.context - execution context
|
|
59
|
-
* @returns {
|
|
61
|
+
* @returns {QueryResults}
|
|
60
62
|
*/
|
|
61
|
-
export
|
|
63
|
+
export function executePlan({ plan, context }) {
|
|
62
64
|
if (plan.type === 'Scan') {
|
|
63
|
-
|
|
65
|
+
return executeScan(plan, context)
|
|
64
66
|
} else if (plan.type === 'Count') {
|
|
65
|
-
|
|
67
|
+
return executeCount(plan, context)
|
|
66
68
|
} else if (plan.type === 'Filter') {
|
|
67
|
-
|
|
69
|
+
return executeFilter(plan, context)
|
|
68
70
|
} else if (plan.type === 'Project') {
|
|
69
|
-
|
|
71
|
+
return executeProject(plan, context)
|
|
70
72
|
} else if (plan.type === 'HashJoin') {
|
|
71
|
-
|
|
73
|
+
return executeHashJoin(plan, context)
|
|
72
74
|
} else if (plan.type === 'NestedLoopJoin') {
|
|
73
|
-
|
|
75
|
+
return executeNestedLoopJoin(plan, context)
|
|
74
76
|
} else if (plan.type === 'PositionalJoin') {
|
|
75
|
-
|
|
77
|
+
return executePositionalJoin(plan, context)
|
|
76
78
|
} else if (plan.type === 'HashAggregate') {
|
|
77
|
-
|
|
79
|
+
return executeHashAggregate(plan, context)
|
|
78
80
|
} else if (plan.type === 'ScalarAggregate') {
|
|
79
|
-
|
|
81
|
+
return executeScalarAggregate(plan, context)
|
|
80
82
|
} else if (plan.type === 'Sort') {
|
|
81
|
-
|
|
83
|
+
return executeSort(plan, context)
|
|
82
84
|
} else if (plan.type === 'Distinct') {
|
|
83
|
-
|
|
85
|
+
return executeDistinct(plan, context)
|
|
84
86
|
} else if (plan.type === 'Limit') {
|
|
85
|
-
|
|
87
|
+
return executeLimit(plan, context)
|
|
86
88
|
} else if (plan.type === 'SetOperation') {
|
|
87
|
-
|
|
89
|
+
return executeSetOperation(plan, context)
|
|
88
90
|
}
|
|
91
|
+
return { async *rows () {} }
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/**
|
|
92
95
|
* @param {ScanNode} plan
|
|
93
96
|
* @param {ExecuteContext} context
|
|
94
|
-
* @
|
|
97
|
+
* @returns {QueryResults}
|
|
95
98
|
*/
|
|
96
|
-
|
|
99
|
+
function executeScan(plan, context) {
|
|
97
100
|
const { tables, signal } = context
|
|
98
101
|
const table = validateTable({ ...plan, tables })
|
|
99
102
|
validateScan({ ...plan, tables })
|
|
103
|
+
const hasLimitOffset = plan.hints.limit !== undefined || plan.hints.offset // 0 offset is noop
|
|
100
104
|
|
|
101
105
|
// Fast path: single column scan without WHERE
|
|
102
106
|
if (table.scanColumn && plan.hints.columns?.length === 1 && !plan.hints.where) {
|
|
@@ -107,42 +111,55 @@ async function* executeScan(plan, context) {
|
|
|
107
111
|
offset: plan.hints.offset,
|
|
108
112
|
signal,
|
|
109
113
|
})
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
115
|
+
return {
|
|
116
|
+
numRows: scanRows,
|
|
117
|
+
maxRows: scanRows,
|
|
118
|
+
async *rows () {
|
|
119
|
+
const columns = [column]
|
|
120
|
+
for await (const chunk of chunks) {
|
|
121
|
+
if (signal?.aborted) return
|
|
122
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
123
|
+
const value = chunk[i]
|
|
124
|
+
yield {
|
|
125
|
+
columns,
|
|
126
|
+
cells: { [column]: () => Promise.resolve(value) },
|
|
127
|
+
}
|
|
128
|
+
}
|
|
118
129
|
}
|
|
119
|
-
}
|
|
130
|
+
},
|
|
120
131
|
}
|
|
121
|
-
return
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
// do the scan
|
|
125
|
-
const
|
|
135
|
+
const scanResult = table.scan({ ...plan.hints, signal })
|
|
136
|
+
const { appliedWhere, appliedLimitOffset } = scanResult
|
|
126
137
|
|
|
127
138
|
// Applied limit/offset without applied where is invalid
|
|
128
|
-
const hasLimitOffset = plan.hints.limit !== undefined || plan.hints.offset // 0 offset is noop
|
|
129
139
|
if (!appliedWhere && appliedLimitOffset && plan.hints.where && hasLimitOffset) {
|
|
130
140
|
throw new Error(`Data source "${plan.table}" applied limit/offset without applying where`)
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
|
|
143
|
+
const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
144
|
+
return {
|
|
145
|
+
numRows: !plan.hints.where ? scanRows : undefined,
|
|
146
|
+
maxRows: scanRows,
|
|
147
|
+
async *rows () {
|
|
148
|
+
let result = scanResult.rows()
|
|
134
149
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
// Apply WHERE if data source did not
|
|
151
|
+
if (!appliedWhere && plan.hints.where) {
|
|
152
|
+
result = filterRows(result, plan.hints.where, context, plan.hints.limit)
|
|
153
|
+
}
|
|
139
154
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
// Apply LIMIT/OFFSET if data source did not
|
|
156
|
+
if (!appliedLimitOffset && hasLimitOffset) {
|
|
157
|
+
result = limitRows(result, plan.hints.limit, plan.hints.offset, signal)
|
|
158
|
+
}
|
|
144
159
|
|
|
145
|
-
|
|
160
|
+
yield* result
|
|
161
|
+
},
|
|
162
|
+
}
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
/**
|
|
@@ -150,34 +167,55 @@ async function* executeScan(plan, context) {
|
|
|
150
167
|
*
|
|
151
168
|
* @param {CountNode} plan
|
|
152
169
|
* @param {ExecuteContext} context
|
|
153
|
-
* @
|
|
170
|
+
* @returns {QueryResults}
|
|
154
171
|
*/
|
|
155
|
-
|
|
172
|
+
function executeCount(plan, context) {
|
|
173
|
+
const { tables, signal } = context
|
|
156
174
|
const table = validateTable({ ...plan, tables })
|
|
157
175
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
return {
|
|
177
|
+
numRows: 1,
|
|
178
|
+
maxRows: 1,
|
|
179
|
+
async *rows () {
|
|
180
|
+
// Use source numRows if available
|
|
181
|
+
let count = table.numRows
|
|
182
|
+
if (count === undefined) {
|
|
183
|
+
// Fall back to counting rows via scan
|
|
184
|
+
count = 0
|
|
185
|
+
const { rows } = table.scan({ signal })
|
|
186
|
+
// eslint-disable-next-line no-unused-vars
|
|
187
|
+
for await (const _ of rows()) {
|
|
188
|
+
if (signal?.aborted) return
|
|
189
|
+
count++
|
|
190
|
+
}
|
|
191
|
+
}
|
|
170
192
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
/** @type {string[]} */
|
|
194
|
+
const columns = []
|
|
195
|
+
/** @type {AsyncCells} */
|
|
196
|
+
const cells = {}
|
|
197
|
+
for (const col of plan.columns) {
|
|
198
|
+
const alias = col.alias ?? derivedAlias(col.expr)
|
|
199
|
+
columns.push(alias)
|
|
200
|
+
cells[alias] = () => Promise.resolve(count)
|
|
201
|
+
}
|
|
202
|
+
yield { columns, cells }
|
|
203
|
+
},
|
|
179
204
|
}
|
|
180
|
-
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Computes numRows for a scan when the table provides numRows and there is no WHERE.
|
|
209
|
+
*
|
|
210
|
+
* @param {number | undefined} tableNumRows
|
|
211
|
+
* @param {number} [limit]
|
|
212
|
+
* @param {number} [offset]
|
|
213
|
+
* @returns {number | undefined}
|
|
214
|
+
*/
|
|
215
|
+
function computeScanRows(tableNumRows, limit, offset) {
|
|
216
|
+
if (tableNumRows === undefined) return undefined
|
|
217
|
+
const afterOffset = Math.max(0, tableNumRows - (offset ?? 0))
|
|
218
|
+
return limit !== undefined ? Math.min(limit, afterOffset) : afterOffset
|
|
181
219
|
}
|
|
182
220
|
|
|
183
221
|
/**
|
|
@@ -257,10 +295,14 @@ async function* limitRows(rows, limit, offset, signal) {
|
|
|
257
295
|
*
|
|
258
296
|
* @param {FilterNode} plan
|
|
259
297
|
* @param {ExecuteContext} context
|
|
260
|
-
* @
|
|
298
|
+
* @returns {QueryResults}
|
|
261
299
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
300
|
+
function executeFilter(plan, context) {
|
|
301
|
+
const child = executePlan({ plan: plan.child, context })
|
|
302
|
+
return {
|
|
303
|
+
maxRows: child.maxRows,
|
|
304
|
+
rows: () => filterRows(child.rows(), plan.condition, context),
|
|
305
|
+
}
|
|
264
306
|
}
|
|
265
307
|
|
|
266
308
|
/**
|
|
@@ -268,45 +310,52 @@ async function* executeFilter(plan, context) {
|
|
|
268
310
|
*
|
|
269
311
|
* @param {ProjectNode} plan
|
|
270
312
|
* @param {ExecuteContext} context
|
|
271
|
-
* @
|
|
313
|
+
* @returns {QueryResults}
|
|
272
314
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
function executeProject(plan, context) {
|
|
316
|
+
const child = executePlan({ plan: plan.child, context })
|
|
317
|
+
return {
|
|
318
|
+
numRows: child.numRows,
|
|
319
|
+
maxRows: child.maxRows,
|
|
320
|
+
async *rows () {
|
|
321
|
+
let rowIndex = 0
|
|
322
|
+
|
|
323
|
+
for await (const row of child.rows()) {
|
|
324
|
+
if (context.signal?.aborted) return
|
|
325
|
+
rowIndex++
|
|
326
|
+
const currentRowIndex = rowIndex
|
|
327
|
+
|
|
328
|
+
/** @type {string[]} */
|
|
329
|
+
const columns = []
|
|
330
|
+
/** @type {AsyncCells} */
|
|
331
|
+
const cells = {}
|
|
332
|
+
|
|
333
|
+
for (const col of plan.columns) {
|
|
334
|
+
if (col.type === 'star') {
|
|
335
|
+
const prefix = col.table ? `${col.table}.` : undefined
|
|
336
|
+
for (const key of row.columns) {
|
|
337
|
+
if (prefix && !key.startsWith(prefix)) continue
|
|
338
|
+
// Strip table prefix for output column names
|
|
339
|
+
const dotIndex = key.indexOf('.')
|
|
340
|
+
const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
341
|
+
columns.push(outputKey)
|
|
342
|
+
cells[outputKey] = row.cells[key]
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
const alias = col.alias ?? derivedAlias(col.expr)
|
|
346
|
+
columns.push(alias)
|
|
347
|
+
cells[alias] = () => evaluateExpr({
|
|
348
|
+
node: col.expr,
|
|
349
|
+
row,
|
|
350
|
+
rowIndex: currentRowIndex,
|
|
351
|
+
context,
|
|
352
|
+
})
|
|
353
|
+
}
|
|
296
354
|
}
|
|
297
|
-
} else {
|
|
298
|
-
const alias = col.alias ?? derivedAlias(col.expr)
|
|
299
|
-
columns.push(alias)
|
|
300
|
-
cells[alias] = () => evaluateExpr({
|
|
301
|
-
node: col.expr,
|
|
302
|
-
row,
|
|
303
|
-
rowIndex: currentRowIndex,
|
|
304
|
-
context,
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
355
|
|
|
309
|
-
|
|
356
|
+
yield { columns, cells }
|
|
357
|
+
}
|
|
358
|
+
},
|
|
310
359
|
}
|
|
311
360
|
}
|
|
312
361
|
|
|
@@ -315,44 +364,50 @@ async function* executeProject(plan, context) {
|
|
|
315
364
|
*
|
|
316
365
|
* @param {DistinctNode} plan
|
|
317
366
|
* @param {ExecuteContext} context
|
|
318
|
-
* @
|
|
367
|
+
* @returns {QueryResults}
|
|
319
368
|
*/
|
|
320
|
-
|
|
321
|
-
const {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
369
|
+
function executeDistinct(plan, context) {
|
|
370
|
+
const child = executePlan({ plan: plan.child, context })
|
|
371
|
+
return {
|
|
372
|
+
maxRows: child.maxRows,
|
|
373
|
+
async *rows () {
|
|
374
|
+
const { signal } = context
|
|
375
|
+
const MAX_CHUNK = 256
|
|
325
376
|
|
|
326
|
-
|
|
327
|
-
let buffer = []
|
|
377
|
+
const seen = new Set()
|
|
328
378
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
buffer.push(row)
|
|
379
|
+
/** @type {AsyncRow[]} */
|
|
380
|
+
let buffer = []
|
|
332
381
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
382
|
+
for await (const row of child.rows()) {
|
|
383
|
+
if (signal?.aborted) return
|
|
384
|
+
buffer.push(row)
|
|
385
|
+
|
|
386
|
+
if (buffer.length >= MAX_CHUNK) {
|
|
387
|
+
const keys = buffer.map(stableRowKey)
|
|
388
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
389
|
+
const key = await keys[i]
|
|
390
|
+
if (!seen.has(key)) {
|
|
391
|
+
seen.add(key)
|
|
392
|
+
yield buffer[i]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
buffer = []
|
|
340
396
|
}
|
|
341
397
|
}
|
|
342
|
-
buffer = []
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
398
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
399
|
+
// Flush remaining
|
|
400
|
+
if (buffer.length > 0) {
|
|
401
|
+
const keys = buffer.map(stableRowKey)
|
|
402
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
403
|
+
const key = await keys[i]
|
|
404
|
+
if (!seen.has(key)) {
|
|
405
|
+
seen.add(key)
|
|
406
|
+
yield buffer[i]
|
|
407
|
+
}
|
|
408
|
+
}
|
|
354
409
|
}
|
|
355
|
-
}
|
|
410
|
+
},
|
|
356
411
|
}
|
|
357
412
|
}
|
|
358
413
|
|
|
@@ -361,10 +416,15 @@ async function* executeDistinct(plan, context) {
|
|
|
361
416
|
*
|
|
362
417
|
* @param {LimitNode} plan
|
|
363
418
|
* @param {ExecuteContext} context
|
|
364
|
-
* @
|
|
419
|
+
* @returns {QueryResults}
|
|
365
420
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
421
|
+
function executeLimit(plan, context) {
|
|
422
|
+
const child = executePlan({ plan: plan.child, context })
|
|
423
|
+
return {
|
|
424
|
+
numRows: computeScanRows(child.numRows, plan.limit, plan.offset),
|
|
425
|
+
maxRows: computeScanRows(child.maxRows, plan.limit, plan.offset),
|
|
426
|
+
rows: () => limitRows(child.rows(), plan.limit, plan.offset, context.signal),
|
|
427
|
+
}
|
|
368
428
|
}
|
|
369
429
|
|
|
370
430
|
/**
|
|
@@ -372,102 +432,132 @@ async function* executeLimit(plan, context) {
|
|
|
372
432
|
*
|
|
373
433
|
* @param {SetOperationNode} plan
|
|
374
434
|
* @param {ExecuteContext} context
|
|
375
|
-
* @
|
|
435
|
+
* @returns {QueryResults}
|
|
376
436
|
*/
|
|
377
|
-
|
|
437
|
+
function executeSetOperation(plan, context) {
|
|
378
438
|
const { signal } = context
|
|
379
439
|
|
|
380
440
|
if (plan.operator === 'UNION') {
|
|
381
441
|
if (plan.all) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
seen.add(key)
|
|
393
|
-
yield row
|
|
394
|
-
}
|
|
442
|
+
const left = executePlan({ plan: plan.left, context })
|
|
443
|
+
const right = executePlan({ plan: plan.right, context })
|
|
444
|
+
return {
|
|
445
|
+
numRows: addBounds(left.numRows, right.numRows),
|
|
446
|
+
maxRows: addBounds(left.maxRows, right.maxRows),
|
|
447
|
+
async *rows () {
|
|
448
|
+
// UNION ALL: yield all rows from both sides
|
|
449
|
+
yield* left.rows()
|
|
450
|
+
yield* right.rows()
|
|
451
|
+
},
|
|
395
452
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
453
|
+
} else {
|
|
454
|
+
const left = executePlan({ plan: plan.left, context })
|
|
455
|
+
const right = executePlan({ plan: plan.right, context })
|
|
456
|
+
return {
|
|
457
|
+
maxRows: addBounds(left.maxRows, right.maxRows),
|
|
458
|
+
async *rows () {
|
|
459
|
+
// UNION: yield deduplicated rows from both sides
|
|
460
|
+
const seen = new Set()
|
|
461
|
+
for await (const row of left.rows()) {
|
|
462
|
+
if (signal?.aborted) return
|
|
463
|
+
const key = await stableRowKey(row)
|
|
464
|
+
if (!seen.has(key)) {
|
|
465
|
+
seen.add(key)
|
|
466
|
+
yield row
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
for await (const row of right.rows()) {
|
|
470
|
+
if (signal?.aborted) return
|
|
471
|
+
const key = await stableRowKey(row)
|
|
472
|
+
if (!seen.has(key)) {
|
|
473
|
+
seen.add(key)
|
|
474
|
+
yield row
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
},
|
|
403
478
|
}
|
|
404
479
|
}
|
|
405
480
|
} else if (plan.operator === 'INTERSECT') {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (signal?.aborted) return
|
|
419
|
-
const key = await stableRowKey(row)
|
|
420
|
-
const count = rightKeys.get(key)
|
|
421
|
-
if (count) {
|
|
422
|
-
rightKeys.set(key, count - 1)
|
|
423
|
-
yield row
|
|
481
|
+
const left = executePlan({ plan: plan.left, context })
|
|
482
|
+
const right = executePlan({ plan: plan.right, context })
|
|
483
|
+
return {
|
|
484
|
+
maxRows: minBounds(left.maxRows, right.maxRows),
|
|
485
|
+
async *rows () {
|
|
486
|
+
// Materialize right side keys
|
|
487
|
+
/** @type {Map<any, number>} */
|
|
488
|
+
const rightKeys = new Map()
|
|
489
|
+
for await (const row of right.rows()) {
|
|
490
|
+
if (signal?.aborted) return
|
|
491
|
+
const key = await stableRowKey(row)
|
|
492
|
+
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
424
493
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
494
|
+
|
|
495
|
+
if (plan.all) {
|
|
496
|
+
// INTERSECT ALL: yield each left row that matches, consuming right counts
|
|
497
|
+
for await (const row of left.rows()) {
|
|
498
|
+
if (signal?.aborted) return
|
|
499
|
+
const key = await stableRowKey(row)
|
|
500
|
+
const count = rightKeys.get(key)
|
|
501
|
+
if (count) {
|
|
502
|
+
rightKeys.set(key, count - 1)
|
|
503
|
+
yield row
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
// INTERSECT: yield deduplicated rows present in both
|
|
508
|
+
const seen = new Set()
|
|
509
|
+
for await (const row of left.rows()) {
|
|
510
|
+
if (signal?.aborted) return
|
|
511
|
+
const key = await stableRowKey(row)
|
|
512
|
+
if (rightKeys.has(key) && !seen.has(key)) {
|
|
513
|
+
seen.add(key)
|
|
514
|
+
yield row
|
|
515
|
+
}
|
|
516
|
+
}
|
|
435
517
|
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
} else if (plan.operator === 'EXCEPT') {
|
|
439
|
-
// Materialize right side keys
|
|
440
|
-
/** @type {Map<any, number>} */
|
|
441
|
-
const rightKeys = new Map()
|
|
442
|
-
for await (const row of executePlan({ plan: plan.right, context })) {
|
|
443
|
-
if (signal?.aborted) return
|
|
444
|
-
const key = await stableRowKey(row)
|
|
445
|
-
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
518
|
+
},
|
|
446
519
|
}
|
|
520
|
+
} else {
|
|
521
|
+
// EXCEPT
|
|
522
|
+
const left = executePlan({ plan: plan.left, context })
|
|
523
|
+
const right = executePlan({ plan: plan.right, context })
|
|
524
|
+
return {
|
|
525
|
+
maxRows: left.maxRows,
|
|
526
|
+
async *rows () {
|
|
527
|
+
// Materialize right side keys
|
|
528
|
+
/** @type {Map<any, number>} */
|
|
529
|
+
const rightKeys = new Map()
|
|
530
|
+
for await (const row of right.rows()) {
|
|
531
|
+
if (signal?.aborted) return
|
|
532
|
+
const key = await stableRowKey(row)
|
|
533
|
+
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
534
|
+
}
|
|
447
535
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
536
|
+
if (plan.all) {
|
|
537
|
+
// EXCEPT ALL: yield left rows, consuming right counts
|
|
538
|
+
for await (const row of left.rows()) {
|
|
539
|
+
if (signal?.aborted) return
|
|
540
|
+
const key = await stableRowKey(row)
|
|
541
|
+
const count = rightKeys.get(key)
|
|
542
|
+
if (count) {
|
|
543
|
+
rightKeys.set(key, count - 1)
|
|
544
|
+
} else {
|
|
545
|
+
yield row
|
|
546
|
+
}
|
|
547
|
+
}
|
|
456
548
|
} else {
|
|
457
|
-
yield
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
seen.add(key)
|
|
468
|
-
yield row
|
|
549
|
+
// EXCEPT: yield deduplicated left rows not in right
|
|
550
|
+
const seen = new Set()
|
|
551
|
+
for await (const row of left.rows()) {
|
|
552
|
+
if (signal?.aborted) return
|
|
553
|
+
const key = await stableRowKey(row)
|
|
554
|
+
if (!rightKeys.has(key) && !seen.has(key)) {
|
|
555
|
+
seen.add(key)
|
|
556
|
+
yield row
|
|
557
|
+
}
|
|
558
|
+
}
|
|
469
559
|
}
|
|
470
|
-
}
|
|
560
|
+
},
|
|
471
561
|
}
|
|
472
562
|
}
|
|
473
563
|
}
|