squirreling 0.16.13 → 0.16.14
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/package.json +2 -2
- package/src/execute/aggregates.js +12 -0
- package/src/execute/dynamicFilter.js +131 -0
- package/src/execute/execute.js +32 -1
- package/src/execute/join.js +20 -7
- package/src/execute/semiJoin.js +45 -40
- package/src/types.d.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.14",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/node": "26.6.5",
|
|
44
44
|
"@vitest/coverage-v8": "5.0.5",
|
|
45
45
|
"eslint": "10.13.0",
|
|
46
|
-
"eslint-plugin-jsdoc": "65.4.
|
|
46
|
+
"eslint-plugin-jsdoc": "65.4.1",
|
|
47
47
|
"globals": "17.13.0",
|
|
48
48
|
"typescript": "7.0.2",
|
|
49
49
|
"vitest": "5.0.5"
|
|
@@ -193,6 +193,18 @@ export function executeHashAggregate(plan, context) {
|
|
|
193
193
|
* @returns {QueryResults}
|
|
194
194
|
*/
|
|
195
195
|
export function executeScalarAggregate(plan, context) {
|
|
196
|
+
if (context.deferScans && plan.child.type === 'Scan') {
|
|
197
|
+
const child = executeScan(plan.child, context)
|
|
198
|
+
return {
|
|
199
|
+
columns: selectColumnNames(plan.columns, child.columns),
|
|
200
|
+
numRows: plan.having ? undefined : 1,
|
|
201
|
+
maxRows: 1,
|
|
202
|
+
async *rows() {
|
|
203
|
+
context.signal?.throwIfAborted()
|
|
204
|
+
yield* executeScalarAggregate(plan, { ...context, deferScans: false }).rows()
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
}
|
|
196
208
|
// Fast path: use scanColumn when available
|
|
197
209
|
const columnScan = tryColumnScanAggregate(plan, context)
|
|
198
210
|
if (columnScan?.rows) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ExprNode, SqlPrimitive } from '../types.js'
|
|
3
|
+
* @import { QueryPlan } from '../plan/types.js'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Bound filter memory and match the session-index pruning limit.
|
|
7
|
+
const MAX_DYNAMIC_FILTER_KEYS = 256
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bounded, per-column key sets. Once a column exceeds the cap, release it and
|
|
11
|
+
* stop collecting that column. Unsupported values disable that column too.
|
|
12
|
+
*
|
|
13
|
+
* @param {number} count
|
|
14
|
+
* @param {boolean} [hash] - preserve the hash join's serialized key equivalences
|
|
15
|
+
* @returns {{ add(values: SqlPrimitive[]): void, apply(plan: QueryPlan, keys: ExprNode[]): QueryPlan }}
|
|
16
|
+
*/
|
|
17
|
+
export function dynamicFilter(count, hash = false) {
|
|
18
|
+
/** @type {(Set<SqlPrimitive> | undefined)[]} */
|
|
19
|
+
const sets = Array.from({ length: count }, () => new Set())
|
|
20
|
+
let active = true
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* @param {SqlPrimitive[]} values
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
add(values) {
|
|
27
|
+
if (!active) return
|
|
28
|
+
// Hash tuple serialization uses delimiters and serializes objects. Do
|
|
29
|
+
// not prune when those encodings can collide across column boundaries.
|
|
30
|
+
if (hash && values.some(value => typeof value === 'object' && value !== null ||
|
|
31
|
+
typeof value === 'string' && (value.includes('|') || ['[', '{', '"'].includes(value[0]) || value === 'null'))) {
|
|
32
|
+
sets.fill(undefined)
|
|
33
|
+
active = false
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < sets.length; i++) {
|
|
37
|
+
const set = sets[i]
|
|
38
|
+
const value = values[i]
|
|
39
|
+
if (!set || value == null) continue
|
|
40
|
+
if (!['string', 'number', 'bigint', 'boolean'].includes(typeof value) ||
|
|
41
|
+
typeof value === 'number' && !Number.isFinite(value)) {
|
|
42
|
+
sets[i] = undefined
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
set.add(value)
|
|
46
|
+
if (hash && count > 1) {
|
|
47
|
+
if (typeof value === 'boolean') set.add(String(value))
|
|
48
|
+
if (value === 'true' || value === 'false') set.add(value === 'true')
|
|
49
|
+
}
|
|
50
|
+
if (set.size > MAX_DYNAMIC_FILTER_KEYS) sets[i] = undefined
|
|
51
|
+
}
|
|
52
|
+
active = sets.some(Boolean)
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* @param {QueryPlan} plan
|
|
56
|
+
* @param {ExprNode[]} keys
|
|
57
|
+
* @returns {QueryPlan}
|
|
58
|
+
*/
|
|
59
|
+
apply(plan, keys) {
|
|
60
|
+
for (let i = 0; i < keys.length; i++) {
|
|
61
|
+
const set = sets[i]
|
|
62
|
+
if (set?.size) plan = pushFilter(plan, keys[i], [...set])
|
|
63
|
+
}
|
|
64
|
+
return plan
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Only traverse operators that preserve the meaning of a physical column.
|
|
71
|
+
* Limits, aggregates, windows and projections are deliberate barriers.
|
|
72
|
+
*
|
|
73
|
+
* @param {QueryPlan} plan
|
|
74
|
+
* @param {ExprNode} key
|
|
75
|
+
* @param {SqlPrimitive[]} values
|
|
76
|
+
* @returns {QueryPlan}
|
|
77
|
+
*/
|
|
78
|
+
function pushFilter(plan, key, values) {
|
|
79
|
+
if (key?.type !== 'identifier') return plan
|
|
80
|
+
if (plan.type === 'Scan') {
|
|
81
|
+
if (key.prefix && key.prefix !== (plan.alias ?? plan.table) ||
|
|
82
|
+
plan.hints.limit !== undefined || plan.hints.offset || plan.topK) return plan
|
|
83
|
+
const position = { positionStart: key.positionStart, positionEnd: key.positionEnd }
|
|
84
|
+
/** @type {ExprNode} */
|
|
85
|
+
const filter = {
|
|
86
|
+
type: 'in valuelist', expr: key, ...position,
|
|
87
|
+
values: values.map(value => ({ type: 'literal', value, ...position })),
|
|
88
|
+
}
|
|
89
|
+
/** @type {ExprNode} */
|
|
90
|
+
const where = plan.hints.where
|
|
91
|
+
? { type: 'binary', op: 'AND', left: plan.hints.where, right: filter, ...position }
|
|
92
|
+
: filter
|
|
93
|
+
return { ...plan, hints: { ...plan.hints, where } }
|
|
94
|
+
}
|
|
95
|
+
if (plan.type === 'Filter' || plan.type === 'Sort' && !plan.topK || plan.type === 'Distinct') {
|
|
96
|
+
return { ...plan, child: pushFilter(plan.child, key, values) }
|
|
97
|
+
}
|
|
98
|
+
if (plan.type === 'HashJoin' && key.prefix) {
|
|
99
|
+
// Restrict only preserved inputs of outer joins; restricting an optional
|
|
100
|
+
// input could turn a formerly matched row into a new null-extended row.
|
|
101
|
+
const left = plan.joinType === 'INNER' || plan.joinType === 'LEFT'
|
|
102
|
+
? pushFilter(plan.left, key, values) : plan.left
|
|
103
|
+
const right = plan.joinType === 'INNER' || plan.joinType === 'RIGHT'
|
|
104
|
+
? pushFilter(plan.right, key, values) : plan.right
|
|
105
|
+
return { ...plan, left, right }
|
|
106
|
+
}
|
|
107
|
+
return plan
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Selectivity heuristic when exact filtered cardinalities are unavailable.
|
|
112
|
+
* Count conjuncts so a date plus a LIKE wins over a lone part-type predicate.
|
|
113
|
+
*
|
|
114
|
+
* @param {QueryPlan} plan
|
|
115
|
+
* @returns {number}
|
|
116
|
+
*/
|
|
117
|
+
export function filterScore(plan) {
|
|
118
|
+
if (plan.type === 'Scan') return conjuncts(plan.hints.where)
|
|
119
|
+
if (plan.type === 'Filter') return conjuncts(plan.condition) + filterScore(plan.child)
|
|
120
|
+
if (plan.type === 'HashJoin' && plan.joinType === 'INNER') return Math.max(filterScore(plan.left), filterScore(plan.right))
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {ExprNode} expr
|
|
126
|
+
* @returns {number}
|
|
127
|
+
*/
|
|
128
|
+
function conjuncts(expr) {
|
|
129
|
+
return !expr ? 0 : expr.type === 'binary' && expr.op === 'AND'
|
|
130
|
+
? conjuncts(expr.left) + conjuncts(expr.right) : 1
|
|
131
|
+
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { selectedRowCount } from '../backend/batch.js'
|
|
2
|
-
import { batchesToRows } from '../backend/batchAdapters.js'
|
|
2
|
+
import { batchesToRows, rowsToBatches } from '../backend/batchAdapters.js'
|
|
3
3
|
import { dataSourceColumns, memorySource } from '../backend/dataSource.js'
|
|
4
4
|
import { derivedAlias } from '../expression/alias.js'
|
|
5
5
|
import { compileBatchExpression } from '../expression/batch.js'
|
|
@@ -318,6 +318,37 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
318
318
|
const { tables, signal } = context
|
|
319
319
|
const table = validateTable({ ...plan, tables })
|
|
320
320
|
validateScan({ ...plan, tables })
|
|
321
|
+
if (context.deferScans && !existingColumnResult) {
|
|
322
|
+
const size = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
323
|
+
const columns = plan.hints.columns ?? dataSourceColumns(table)
|
|
324
|
+
/** @type {QueryResults} */
|
|
325
|
+
const deferred = {
|
|
326
|
+
columns,
|
|
327
|
+
numRows: plan.hints.where ? undefined : size,
|
|
328
|
+
maxRows: size,
|
|
329
|
+
async *rows() {
|
|
330
|
+
context.signal?.throwIfAborted()
|
|
331
|
+
yield* executeScan(plan, { ...context, deferScans: false }).rows()
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
// Preserve native batch execution above a deferred input. Legacy fallback
|
|
335
|
+
// remains at the source boundary if a source declines its column path.
|
|
336
|
+
if (table.prepareScan && table.schema || table.scanColumn && plan.hints.columns?.length === 1) {
|
|
337
|
+
deferred.batches = async function* deferredBatches() {
|
|
338
|
+
context.signal?.throwIfAborted()
|
|
339
|
+
const result = executeScan(plan, { ...context, deferScans: false })
|
|
340
|
+
if (result.batches) {
|
|
341
|
+
// Prepared sources may return extra fields or a different order.
|
|
342
|
+
// Align vectors with the metadata exposed before opening the scan.
|
|
343
|
+
const indexes = columns.map(column => result.columns.indexOf(column))
|
|
344
|
+
for await (const batch of result.batches()) {
|
|
345
|
+
yield { ...batch, columns: indexes.map(index => batch.columns[index]) }
|
|
346
|
+
}
|
|
347
|
+
} else yield* rowsToBatches(result.rows(), columns, { signal })
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return deferred
|
|
351
|
+
}
|
|
321
352
|
const { where, residual } = scanFilter(plan, dataSourceColumns(table))
|
|
322
353
|
const hints = { ...plan.hints, where }
|
|
323
354
|
if (residual) {
|
package/src/execute/join.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
2
|
import { keyify, maxBounds } from './utils.js'
|
|
3
3
|
import { executePlan } from './execute.js'
|
|
4
|
+
import { dynamicFilter, filterScore } from './dynamicFilter.js'
|
|
4
5
|
import { yieldToEventLoop } from './yield.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -222,14 +223,18 @@ export function executePositionalJoin(plan, context) {
|
|
|
222
223
|
* @returns {QueryResults}
|
|
223
224
|
*/
|
|
224
225
|
export function executeHashJoin(plan, context) {
|
|
225
|
-
const left = executePlan({ plan: plan.left, context })
|
|
226
|
-
const right = executePlan({ plan: plan.right, context })
|
|
227
|
-
//
|
|
228
|
-
//
|
|
226
|
+
const left = executePlan({ plan: plan.left, context: { ...context, deferScans: true } })
|
|
227
|
+
const right = executePlan({ plan: plan.right, context: { ...context, deferScans: true } })
|
|
228
|
+
// Prefer exact sizes, then predicate selectivity, then upper bounds.
|
|
229
|
+
// A filtered scan's table size is only an upper bound, not its cardinality.
|
|
229
230
|
// Swapping reorders output rows, which SQL leaves unspecified.
|
|
230
231
|
const leftSize = left.numRows ?? left.maxRows
|
|
231
232
|
const rightSize = right.numRows ?? right.maxRows
|
|
232
|
-
const swap =
|
|
233
|
+
const swap = left.numRows !== undefined && right.numRows !== undefined
|
|
234
|
+
? leftSize < rightSize
|
|
235
|
+
: filterScore(plan.left) !== filterScore(plan.right)
|
|
236
|
+
? filterScore(plan.left) > filterScore(plan.right)
|
|
237
|
+
: leftSize !== undefined && rightSize !== undefined && leftSize < rightSize
|
|
233
238
|
return {
|
|
234
239
|
columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
|
|
235
240
|
async *rows() {
|
|
@@ -237,7 +242,7 @@ export function executeHashJoin(plan, context) {
|
|
|
237
242
|
const rightTable = plan.rightAlias
|
|
238
243
|
const { residual } = plan
|
|
239
244
|
const build = swap ? left : right
|
|
240
|
-
|
|
245
|
+
let probe = swap ? right : left
|
|
241
246
|
const buildKeys = swap ? plan.leftKeys : plan.rightKeys
|
|
242
247
|
const probeKeys = swap ? plan.rightKeys : plan.leftKeys
|
|
243
248
|
// Which sides must also emit their unmatched rows
|
|
@@ -260,6 +265,7 @@ export function executeHashJoin(plan, context) {
|
|
|
260
265
|
// otherwise rows with NULL join keys are released immediately.
|
|
261
266
|
/** @type {Map<string | number | bigint | boolean, AsyncRow[]>} */
|
|
262
267
|
const hashMap = new Map()
|
|
268
|
+
const filters = dynamicFilter(buildKeys.length, true)
|
|
263
269
|
/** @type {AsyncRow[] | undefined} */
|
|
264
270
|
const buildRows = buildOuter ? [] : undefined
|
|
265
271
|
/** @type {string[]} */
|
|
@@ -281,6 +287,7 @@ export function executeHashJoin(plan, context) {
|
|
|
281
287
|
// SQL semantics: NULL never equals anything, so a row with any NULL
|
|
282
288
|
// join key is excluded from the hash table.
|
|
283
289
|
if (keyValues.some(v => v == null)) continue
|
|
290
|
+
if (!probeOuter) filters.add(keyValues)
|
|
284
291
|
const key = keyify(...keyValues)
|
|
285
292
|
let bucket = hashMap.get(key)
|
|
286
293
|
if (!bucket) {
|
|
@@ -295,8 +302,14 @@ export function executeHashJoin(plan, context) {
|
|
|
295
302
|
/** @type {Set<AsyncRow> | undefined} */
|
|
296
303
|
const matchedBuildRows = buildOuter ? new Set() : undefined
|
|
297
304
|
|
|
305
|
+
if (!probeOuter && !hashMap.size && !buildOuter) return
|
|
306
|
+
if (!probeOuter && hashMap.size) {
|
|
307
|
+
const filtered = filters.apply(swap ? plan.right : plan.left, probeKeys)
|
|
308
|
+
probe = executePlan({ plan: filtered, context: { ...context, deferScans: true } })
|
|
309
|
+
}
|
|
310
|
+
|
|
298
311
|
// Probe phase: stream the other side
|
|
299
|
-
for await (const probeRow of probe.rows()) {
|
|
312
|
+
for await (const probeRow of !probeOuter && !hashMap.size ? [] : probe.rows()) {
|
|
300
313
|
context.signal?.throwIfAborted()
|
|
301
314
|
|
|
302
315
|
if (!probeCols) {
|
package/src/execute/semiJoin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
2
|
import { executePlan } from './execute.js'
|
|
3
|
+
import { dynamicFilter } from './dynamicFilter.js'
|
|
3
4
|
import { sqlEquals } from './utils.js'
|
|
4
5
|
import { yieldToEventLoop } from './yield.js'
|
|
5
6
|
|
|
@@ -19,7 +20,7 @@ import { yieldToEventLoop } from './yield.js'
|
|
|
19
20
|
*/
|
|
20
21
|
export function executeSemiJoin(plan, context) {
|
|
21
22
|
if (plan.keys) return executeExistsJoin(plan, context)
|
|
22
|
-
const left = executePlan({ plan: plan.left, context })
|
|
23
|
+
const left = executePlan({ plan: plan.left, context: { ...context, deferScans: true } })
|
|
23
24
|
return {
|
|
24
25
|
columns: left.columns,
|
|
25
26
|
maxRows: left.maxRows,
|
|
@@ -28,29 +29,30 @@ export function executeSemiJoin(plan, context) {
|
|
|
28
29
|
const values = new Map()
|
|
29
30
|
let hasNull = false
|
|
30
31
|
let count = 0
|
|
31
|
-
let built = false
|
|
32
32
|
let rowIndex = 0
|
|
33
|
-
|
|
33
|
+
const filters = dynamicFilter(1)
|
|
34
|
+
for await (const inner of executePlan({ plan: plan.right, context }).rows()) {
|
|
35
|
+
context.signal?.throwIfAborted()
|
|
36
|
+
if (++count % 4000 === 0) await yieldToEventLoop()
|
|
37
|
+
const value = await inner.cells[inner.columns[0]]()
|
|
38
|
+
if (value == null) hasNull = true
|
|
39
|
+
else {
|
|
40
|
+
const type = typeof value
|
|
41
|
+
let group = values.get(type)
|
|
42
|
+
if (!group) values.set(type, group = new Set())
|
|
43
|
+
group.add(value)
|
|
44
|
+
if (!plan.not) filters.add([value])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!plan.not && !values.size || plan.not && hasNull) return
|
|
48
|
+
const probe = executePlan({
|
|
49
|
+
plan: plan.not ? plan.left : filters.apply(plan.left, [plan.expr]),
|
|
50
|
+
context: { ...context, deferScans: true },
|
|
51
|
+
})
|
|
52
|
+
for await (const row of probe.rows()) {
|
|
34
53
|
context.signal?.throwIfAborted()
|
|
35
54
|
if (++rowIndex % 4000 === 0) await yieldToEventLoop()
|
|
36
55
|
const value = await evaluateExpr({ node: plan.expr, row, rowIndex, context })
|
|
37
|
-
if (!built) {
|
|
38
|
-
for await (const inner of executePlan({ plan: plan.right, context }).rows()) {
|
|
39
|
-
if (++count % 4000 === 0) {
|
|
40
|
-
await yieldToEventLoop()
|
|
41
|
-
context.signal?.throwIfAborted()
|
|
42
|
-
}
|
|
43
|
-
const value = await inner.cells[inner.columns[0]]()
|
|
44
|
-
if (value == null) hasNull = true
|
|
45
|
-
else {
|
|
46
|
-
const type = typeof value
|
|
47
|
-
let group = values.get(type)
|
|
48
|
-
if (!group) values.set(type, group = new Set())
|
|
49
|
-
group.add(value)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
built = true
|
|
53
|
-
}
|
|
54
56
|
const matched = count === 0 ? false : value == null ? null : contains(values, value) ? true : hasNull ? null : false
|
|
55
57
|
if (matched !== null && matched !== plan.not) yield row
|
|
56
58
|
}
|
|
@@ -87,36 +89,39 @@ function contains(values, value) {
|
|
|
87
89
|
* @returns {QueryResults}
|
|
88
90
|
*/
|
|
89
91
|
function executeExistsJoin(plan, context) {
|
|
90
|
-
const left = executePlan({ plan: plan.left, context })
|
|
92
|
+
const left = executePlan({ plan: plan.left, context: { ...context, deferScans: true } })
|
|
91
93
|
return {
|
|
92
94
|
columns: left.columns,
|
|
93
95
|
maxRows: left.maxRows,
|
|
94
96
|
async *rows() {
|
|
95
97
|
/** @type {KeyTrie} */
|
|
96
98
|
const root = { groups: new Map() }
|
|
97
|
-
let built = false
|
|
98
99
|
let count = 0
|
|
99
|
-
|
|
100
|
+
const filters = dynamicFilter(plan.keys?.length ?? 0)
|
|
101
|
+
for await (const inner of executePlan({ plan: plan.right, context }).rows()) {
|
|
100
102
|
context.signal?.throwIfAborted()
|
|
101
103
|
if (++count % 4000 === 0) await yieldToEventLoop()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let child = group.get(value)
|
|
114
|
-
if (!child) group.set(value, child = { groups: new Map() })
|
|
115
|
-
node = child
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
built = true
|
|
104
|
+
const values = await Promise.all(inner.columns.map(col => inner.cells[col]()))
|
|
105
|
+
if (values.some(value => value == null)) continue
|
|
106
|
+
if (!plan.not) filters.add(values)
|
|
107
|
+
let node = root
|
|
108
|
+
for (const value of values) {
|
|
109
|
+
const type = typeof value
|
|
110
|
+
let group = node.groups.get(type)
|
|
111
|
+
if (!group) node.groups.set(type, group = new Map())
|
|
112
|
+
let child = group.get(value)
|
|
113
|
+
if (!child) group.set(value, child = { groups: new Map() })
|
|
114
|
+
node = child
|
|
119
115
|
}
|
|
116
|
+
}
|
|
117
|
+
if (!plan.not && !root.groups.size) return
|
|
118
|
+
const probe = executePlan({
|
|
119
|
+
plan: plan.not ? plan.left : filters.apply(plan.left, plan.keys ?? []),
|
|
120
|
+
context: { ...context, deferScans: true },
|
|
121
|
+
})
|
|
122
|
+
for await (const row of probe.rows()) {
|
|
123
|
+
context.signal?.throwIfAborted()
|
|
124
|
+
if (++count % 4000 === 0) await yieldToEventLoop()
|
|
120
125
|
const values = await Promise.all((plan.keys ?? []).map(node => evaluateExpr({ node, row, context })))
|
|
121
126
|
const matched = !values.some(value => value == null) && matchesKeys(root, values, 0)
|
|
122
127
|
if (matched !== plan.not) yield row
|
package/src/types.d.ts
CHANGED
|
@@ -199,6 +199,8 @@ export interface PlanSqlOptions {
|
|
|
199
199
|
|
|
200
200
|
// executePlan(plan, context)
|
|
201
201
|
export interface ExecuteContext {
|
|
202
|
+
// Delay opening join input scans until their rows are consumed.
|
|
203
|
+
deferScans?: boolean
|
|
202
204
|
tables: Record<string, AsyncDataSource>
|
|
203
205
|
functions?: Record<string, UserDefinedFunction>
|
|
204
206
|
signal?: AbortSignal
|