squirreling 0.16.11 → 0.16.12
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 +1 -1
- package/src/execute/aggregates.js +4 -1
- package/src/execute/execute.js +67 -32
- package/src/execute/scanFilter.js +81 -0
- package/src/types.d.ts +4 -1
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { finalizeAccumulator, newAccumulator, updateAccumulator } from './accumu
|
|
|
5
5
|
import { executePlan, executeScan, selectColumnNames } from './execute.js'
|
|
6
6
|
import { foldEvaluatedRows } from './fold.js'
|
|
7
7
|
import { normalizeScanColumnResult } from './scanColumn.js'
|
|
8
|
+
import { scanFilter } from './scanFilter.js'
|
|
8
9
|
import { sortEntriesByTerms } from './sort.js'
|
|
9
10
|
import { planStreamingAggregates, streamingHashAggregateRows, streamingScalarAggregateRows } from './streamingAggregate.js'
|
|
10
11
|
import { keyify } from './utils.js'
|
|
@@ -289,10 +290,12 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
289
290
|
// Child must be a direct table scan
|
|
290
291
|
if (plan.child.type !== 'Scan') return
|
|
291
292
|
const scanNode = plan.child
|
|
292
|
-
const { limit, offset
|
|
293
|
+
const { limit, offset } = scanNode.hints
|
|
293
294
|
|
|
294
295
|
const table = tables[scanNode.table]
|
|
295
296
|
if (!table?.scanColumn) return
|
|
297
|
+
const { where, residual } = scanFilter(scanNode, dataSourceColumns(table))
|
|
298
|
+
if (residual) return
|
|
296
299
|
|
|
297
300
|
// COUNT(*) needs a physical column whose filtered chunk lengths can be
|
|
298
301
|
// counted. Prefer a predicate/projection column, then any table column.
|
package/src/execute/execute.js
CHANGED
|
@@ -14,6 +14,7 @@ import { distinctBatches, filterBatches, limitBatches, projectExpressionBatches
|
|
|
14
14
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
15
15
|
import { referencesRowScope } from './rowScope.js'
|
|
16
16
|
import { normalizeScanColumnResult } from './scanColumn.js'
|
|
17
|
+
import { scanFilter } from './scanFilter.js'
|
|
17
18
|
import { executeSort } from './sort.js'
|
|
18
19
|
import { executeSemiJoin } from './semiJoin.js'
|
|
19
20
|
import { addBounds, minBounds, stableRowKey } from './utils.js'
|
|
@@ -317,35 +318,51 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
317
318
|
const { tables, signal } = context
|
|
318
319
|
const table = validateTable({ ...plan, tables })
|
|
319
320
|
validateScan({ ...plan, tables })
|
|
320
|
-
const
|
|
321
|
+
const { where, residual } = scanFilter(plan, dataSourceColumns(table))
|
|
322
|
+
const hints = { ...plan.hints, where }
|
|
323
|
+
if (residual) {
|
|
324
|
+
// Keep range and Top-K outside the scan until the engine residual runs.
|
|
325
|
+
delete hints.limit
|
|
326
|
+
delete hints.offset
|
|
327
|
+
}
|
|
328
|
+
const hasLimitOffset = hints.limit !== undefined || hints.offset // 0 offset is noop
|
|
321
329
|
const scanContext = { ...context, scope: [plan.alias ?? plan.table] }
|
|
322
330
|
|
|
323
331
|
if (!existingColumnResult && table.prepareScan && table.schema) {
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
// Derive column demands from the original predicate, including fields
|
|
333
|
+
// needed by engine residuals, but send only the source-local predicate.
|
|
334
|
+
const request = scanRequest(plan, table.schema, scanContext)
|
|
335
|
+
request.filter = where
|
|
336
|
+
if (residual) {
|
|
337
|
+
delete request.limit
|
|
338
|
+
delete request.offset
|
|
339
|
+
delete request.topK
|
|
340
|
+
}
|
|
341
|
+
const prepared = table.prepareScan(request)
|
|
342
|
+
return finish(executePreparedScan({ plan: { ...plan, hints }, prepared, context: scanContext, table }))
|
|
326
343
|
}
|
|
327
344
|
|
|
328
345
|
// Fast path: single column scan. As with scan(), hints the source did not
|
|
329
346
|
// apply are handled by the engine over the returned column values.
|
|
330
|
-
const scanColumnOptions =
|
|
331
|
-
?
|
|
347
|
+
const scanColumnOptions = hints.columns?.length === 1
|
|
348
|
+
? hints.where
|
|
332
349
|
// Do not push a filtered range until WHERE is known to be applied: an
|
|
333
350
|
// older source may ignore WHERE but eagerly apply LIMIT/OFFSET.
|
|
334
|
-
? { column:
|
|
335
|
-
: { column:
|
|
351
|
+
? { column: hints.columns[0], where: hints.where, signal }
|
|
352
|
+
: { column: hints.columns[0], ...hints, signal }
|
|
336
353
|
: undefined
|
|
337
354
|
const columnResult = existingColumnResult ?? (table.scanColumn && scanColumnOptions
|
|
338
355
|
? normalizeScanColumnResult(table.scanColumn(scanColumnOptions), scanColumnOptions)
|
|
339
356
|
: undefined)
|
|
340
357
|
if (columnResult && scanColumnOptions) {
|
|
341
|
-
const column =
|
|
342
|
-
const scanRows = computeScanRows(table.numRows,
|
|
358
|
+
const column = hints.columns[0]
|
|
359
|
+
const scanRows = computeScanRows(table.numRows, hints.limit, hints.offset)
|
|
343
360
|
const columns = [column]
|
|
344
|
-
const appliedLimitOffset =
|
|
361
|
+
const appliedLimitOffset = hints.where
|
|
345
362
|
? false
|
|
346
363
|
: columnResult.appliedLimitOffset
|
|
347
|
-
const residualFilter =
|
|
348
|
-
? compileBatchExpression(
|
|
364
|
+
const residualFilter = hints.where && !columnResult.appliedWhere
|
|
365
|
+
? compileBatchExpression(hints.where, columns)
|
|
349
366
|
: undefined
|
|
350
367
|
|
|
351
368
|
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
@@ -353,28 +370,28 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
353
370
|
/** @type {AsyncIterable<AsyncBatch>} */
|
|
354
371
|
let batches = columnBatches(columnResult.chunks(), signal)
|
|
355
372
|
if (residualFilter) {
|
|
356
|
-
const targetRows =
|
|
373
|
+
const targetRows = hints.limit === undefined
|
|
357
374
|
? undefined
|
|
358
|
-
:
|
|
375
|
+
: hints.limit + (hints.offset ?? 0)
|
|
359
376
|
batches = filterBatches(batches, residualFilter, signal, targetRows)
|
|
360
377
|
}
|
|
361
378
|
if (!appliedLimitOffset && hasLimitOffset) {
|
|
362
|
-
batches = limitBatches(batches,
|
|
379
|
+
batches = limitBatches(batches, hints.limit, hints.offset, signal)
|
|
363
380
|
}
|
|
364
381
|
return batches
|
|
365
382
|
}
|
|
366
383
|
|
|
367
|
-
const canUseBatches = !
|
|
384
|
+
const canUseBatches = !hints.where || columnResult.appliedWhere || residualFilter !== undefined
|
|
368
385
|
if (canUseBatches) {
|
|
369
|
-
return batchResult({
|
|
386
|
+
return finish(batchResult({
|
|
370
387
|
columns,
|
|
371
|
-
numRows:
|
|
388
|
+
numRows: hints.where ? undefined : scanRows,
|
|
372
389
|
maxRows: scanRows,
|
|
373
390
|
batches: makeBatches,
|
|
374
391
|
signal,
|
|
375
|
-
})
|
|
392
|
+
}))
|
|
376
393
|
}
|
|
377
|
-
return {
|
|
394
|
+
return finish({
|
|
378
395
|
columns,
|
|
379
396
|
numRows: undefined,
|
|
380
397
|
maxRows: scanRows,
|
|
@@ -395,10 +412,10 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
395
412
|
}
|
|
396
413
|
})()
|
|
397
414
|
|
|
398
|
-
result = filterRows(result,
|
|
415
|
+
result = filterRows(result, hints.where, scanContext, hints.limit)
|
|
399
416
|
// Filtered LIMIT/OFFSET was intentionally not passed to scanColumn.
|
|
400
417
|
if (!appliedLimitOffset && hasLimitOffset) {
|
|
401
|
-
result = limitRows(result,
|
|
418
|
+
result = limitRows(result, hints.limit, hints.offset, signal)
|
|
402
419
|
}
|
|
403
420
|
|
|
404
421
|
yield* result
|
|
@@ -406,7 +423,7 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
406
423
|
// the abort so a truncated scan is not mistaken for a complete one
|
|
407
424
|
signal?.throwIfAborted()
|
|
408
425
|
},
|
|
409
|
-
}
|
|
426
|
+
})
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
if (!table.scan) {
|
|
@@ -414,30 +431,30 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
414
431
|
}
|
|
415
432
|
|
|
416
433
|
// do the scan
|
|
417
|
-
const scanResult = table.scan({ ...
|
|
434
|
+
const scanResult = table.scan({ ...hints, signal })
|
|
418
435
|
const { appliedWhere, appliedLimitOffset } = scanResult
|
|
419
436
|
|
|
420
437
|
// Applied limit/offset without applied where is invalid
|
|
421
|
-
if (!appliedWhere && appliedLimitOffset &&
|
|
438
|
+
if (!appliedWhere && appliedLimitOffset && hints.where && hasLimitOffset) {
|
|
422
439
|
throw new Error(`Data source "${plan.table}" applied limit/offset without applying where`)
|
|
423
440
|
}
|
|
424
441
|
|
|
425
|
-
const scanRows = computeScanRows(table.numRows,
|
|
426
|
-
return {
|
|
427
|
-
columns:
|
|
428
|
-
numRows: !
|
|
442
|
+
const scanRows = computeScanRows(table.numRows, hints.limit, hints.offset)
|
|
443
|
+
return finish({
|
|
444
|
+
columns: hints.columns ?? dataSourceColumns(table),
|
|
445
|
+
numRows: !hints.where ? scanRows : undefined,
|
|
429
446
|
maxRows: scanRows,
|
|
430
447
|
async *rows() {
|
|
431
448
|
let result = scanResult.rows()
|
|
432
449
|
|
|
433
450
|
// Apply WHERE if data source did not
|
|
434
|
-
if (!appliedWhere &&
|
|
435
|
-
result = filterRows(result,
|
|
451
|
+
if (!appliedWhere && hints.where) {
|
|
452
|
+
result = filterRows(result, hints.where, scanContext, hints.limit)
|
|
436
453
|
}
|
|
437
454
|
|
|
438
455
|
// Apply LIMIT/OFFSET if data source did not
|
|
439
456
|
if (!appliedLimitOffset && hasLimitOffset) {
|
|
440
|
-
result = limitRows(result,
|
|
457
|
+
result = limitRows(result, hints.limit, hints.offset, signal)
|
|
441
458
|
}
|
|
442
459
|
|
|
443
460
|
yield* result
|
|
@@ -446,6 +463,24 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
446
463
|
// the abort so a truncated scan is not mistaken for a complete one
|
|
447
464
|
signal?.throwIfAborted()
|
|
448
465
|
},
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Apply engine-only predicates and ranges after any source execution path.
|
|
470
|
+
* @param {QueryResults} source
|
|
471
|
+
* @returns {QueryResults}
|
|
472
|
+
*/
|
|
473
|
+
function finish(source) {
|
|
474
|
+
if (!residual) return source
|
|
475
|
+
const { limit, offset } = plan.hints
|
|
476
|
+
return {
|
|
477
|
+
columns: source.columns,
|
|
478
|
+
maxRows: computeScanRows(source.maxRows, limit, offset),
|
|
479
|
+
async *rows() {
|
|
480
|
+
const filtered = filterRows(source.rows(), residual, scanContext, limit)
|
|
481
|
+
yield* limitRows(filtered, limit, offset, signal)
|
|
482
|
+
},
|
|
483
|
+
}
|
|
449
484
|
}
|
|
450
485
|
}
|
|
451
486
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ExprNode } from '../types.js'
|
|
3
|
+
* @import { ScanNode } from '../plan/types.js'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Translate local conjuncts into physical column names. Keep subqueries and
|
|
8
|
+
* references outside this scan in the engine. Match scope names exactly, as
|
|
9
|
+
* expression evaluation does, including quoted identifiers and case.
|
|
10
|
+
*
|
|
11
|
+
* @param {ScanNode} plan
|
|
12
|
+
* @param {string[]} columns
|
|
13
|
+
* @returns {{ where?: ExprNode, residual?: ExprNode }}
|
|
14
|
+
*/
|
|
15
|
+
export function scanFilter(plan, columns) {
|
|
16
|
+
const scope = plan.alias ?? plan.table
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {ExprNode} node
|
|
20
|
+
* @returns {{ where?: ExprNode, residual?: ExprNode }}
|
|
21
|
+
*/
|
|
22
|
+
function split(node) {
|
|
23
|
+
if (!node) return {}
|
|
24
|
+
if (node.type === 'binary' && node.op === 'AND') {
|
|
25
|
+
const left = split(node.left)
|
|
26
|
+
const right = split(node.right)
|
|
27
|
+
return {
|
|
28
|
+
where: combine(left.where, right.where),
|
|
29
|
+
residual: combine(left.residual, right.residual),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let safe = true
|
|
33
|
+
/**
|
|
34
|
+
* @param {ExprNode} expr
|
|
35
|
+
* @returns {ExprNode}
|
|
36
|
+
*/
|
|
37
|
+
function rewrite(expr) {
|
|
38
|
+
if (!expr) return expr
|
|
39
|
+
if (expr.type === 'identifier') {
|
|
40
|
+
if (expr.prefix && expr.prefix !== scope || !columns.includes(expr.name) ||
|
|
41
|
+
expr.prefix && columns.includes(`${expr.prefix}.${expr.name}`)) {
|
|
42
|
+
safe = false
|
|
43
|
+
return expr
|
|
44
|
+
}
|
|
45
|
+
if (!expr.prefix) return expr
|
|
46
|
+
const bare = { ...expr }
|
|
47
|
+
delete bare.prefix
|
|
48
|
+
return bare
|
|
49
|
+
}
|
|
50
|
+
if (expr.type === 'literal' || expr.type === 'interval') return expr
|
|
51
|
+
if (expr.type === 'binary') return { ...expr, left: rewrite(expr.left), right: rewrite(expr.right) }
|
|
52
|
+
if (expr.type === 'unary') return { ...expr, argument: rewrite(expr.argument) }
|
|
53
|
+
if (expr.type === 'cast') return { ...expr, expr: rewrite(expr.expr) }
|
|
54
|
+
if (expr.type === 'function') return { ...expr, args: expr.args.map(rewrite), ...expr.filter ? { filter: rewrite(expr.filter) } : {} }
|
|
55
|
+
if (expr.type === 'in valuelist') return { ...expr, expr: rewrite(expr.expr), values: expr.values.map(rewrite) }
|
|
56
|
+
if (expr.type === 'subscript') return { ...expr, expr: rewrite(expr.expr), index: rewrite(expr.index) }
|
|
57
|
+
if (expr.type === 'case') return {
|
|
58
|
+
...expr,
|
|
59
|
+
caseExpr: rewrite(expr.caseExpr),
|
|
60
|
+
whenClauses: expr.whenClauses.map(w => ({ ...w, condition: rewrite(w.condition), result: rewrite(w.result) })),
|
|
61
|
+
elseResult: rewrite(expr.elseResult),
|
|
62
|
+
}
|
|
63
|
+
safe = false
|
|
64
|
+
return expr
|
|
65
|
+
}
|
|
66
|
+
const where = rewrite(node)
|
|
67
|
+
return safe ? { where } : { residual: node }
|
|
68
|
+
}
|
|
69
|
+
return split(plan.hints.where)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {ExprNode} left
|
|
74
|
+
* @param {ExprNode} right
|
|
75
|
+
* @returns {ExprNode}
|
|
76
|
+
*/
|
|
77
|
+
function combine(left, right) {
|
|
78
|
+
if (!left) return right
|
|
79
|
+
if (!right) return left
|
|
80
|
+
return { type: 'binary', op: 'AND', left, right, positionStart: left.positionStart, positionEnd: right.positionEnd }
|
|
81
|
+
}
|
package/src/types.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface ScanTopK {
|
|
|
145
145
|
export interface ScanRequest {
|
|
146
146
|
columns: readonly ColumnDemand[]
|
|
147
147
|
topK?: ScanTopK
|
|
148
|
+
/** Source-local predicate: identifiers use bare physical column names. */
|
|
148
149
|
filter?: ExprNode
|
|
149
150
|
limit?: number
|
|
150
151
|
offset?: number
|
|
@@ -271,7 +272,8 @@ export interface ScanResults {
|
|
|
271
272
|
*/
|
|
272
273
|
export interface ScanOptions {
|
|
273
274
|
columns?: string[] // columns needed (undefined means all columns)
|
|
274
|
-
|
|
275
|
+
/** Source-local predicate: identifiers use bare physical column names. */
|
|
276
|
+
where?: ExprNode
|
|
275
277
|
// important: only apply limit/offset if where is fully applied by the data source
|
|
276
278
|
// otherwise, the data source must return at least enough rows to ensure the engine
|
|
277
279
|
// can apply limit/offset correctly after filtering
|
|
@@ -285,6 +287,7 @@ export interface ScanOptions {
|
|
|
285
287
|
*/
|
|
286
288
|
export interface ScanColumnOptions {
|
|
287
289
|
column: string
|
|
290
|
+
/** Source-local predicate: identifiers use bare physical column names. */
|
|
288
291
|
where?: ExprNode
|
|
289
292
|
limit?: number
|
|
290
293
|
offset?: number
|