squirreling 0.16.6 → 0.16.8
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 +6 -6
- package/src/backend/batch.js +1 -0
- package/src/execute/aggregates.js +3 -0
- package/src/execute/batches.js +5 -1
- package/src/execute/execute.js +19 -4
- package/src/execute/sort.js +9 -1
- package/src/execute/streamingAggregate.js +19 -0
- package/src/expression/batch.js +7 -0
- package/src/index.d.ts +1 -0
- package/src/plan/types.d.ts +3 -0
- package/src/types.d.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.8",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@eslint/js": "10.0.1",
|
|
43
|
-
"@types/node": "26.
|
|
44
|
-
"@vitest/coverage-v8": "5.0.
|
|
45
|
-
"eslint": "10.
|
|
46
|
-
"eslint-plugin-jsdoc": "
|
|
43
|
+
"@types/node": "26.6.3",
|
|
44
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
45
|
+
"eslint": "10.11.0",
|
|
46
|
+
"eslint-plugin-jsdoc": "65.0.0",
|
|
47
47
|
"globals": "17.12.0",
|
|
48
48
|
"typescript": "7.0.2",
|
|
49
|
-
"vitest": "5.0.
|
|
49
|
+
"vitest": "5.0.2"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/backend/batch.js
CHANGED
|
@@ -125,6 +125,7 @@ export function selectVector(vector, selection) {
|
|
|
125
125
|
throw new Error(`Cannot select ${selection.length} rows from vector length ${vector.length}`)
|
|
126
126
|
}
|
|
127
127
|
if (selection.type === 'all') return vector
|
|
128
|
+
if (vector.type === 'constant') return { ...vector, length: selectedRowCount(selection) }
|
|
128
129
|
if (vector.type === 'selected') {
|
|
129
130
|
const composed = composeSelections(vector.selection, selection)
|
|
130
131
|
return {
|
|
@@ -309,6 +309,9 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
309
309
|
specs.push(spec)
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// Prepared batches can count selected rows without decoding a physical column.
|
|
313
|
+
if (table.prepareScan && specs.every(spec => spec.funcName === 'COUNT' && spec.star)) return
|
|
314
|
+
|
|
312
315
|
const physicalColumns = new Set(specs.map(spec => spec.column))
|
|
313
316
|
const hasPushdown = where || limit !== undefined || offset !== undefined
|
|
314
317
|
if (hasPushdown) {
|
package/src/execute/batches.js
CHANGED
|
@@ -238,12 +238,16 @@ function projectedBatchColumn(batch, columnIndex) {
|
|
|
238
238
|
/**
|
|
239
239
|
* @param {ColumnVector} predicate
|
|
240
240
|
* @param {number} rowCount
|
|
241
|
-
* @returns {{ selection:
|
|
241
|
+
* @returns {{ selection: RowSelection, selectedCount: number }}
|
|
242
242
|
*/
|
|
243
243
|
function predicateSelection(predicate, rowCount) {
|
|
244
244
|
if (predicate.length !== rowCount) {
|
|
245
245
|
throw new Error(`Predicate returned ${predicate.length} rows, expected ${rowCount}`)
|
|
246
246
|
}
|
|
247
|
+
if (predicate.type === 'constant') {
|
|
248
|
+
const selectedCount = predicate.value ? rowCount : 0
|
|
249
|
+
return { selection: { type: 'range', start: 0, end: selectedCount, length: rowCount }, selectedCount }
|
|
250
|
+
}
|
|
247
251
|
const indices = new Uint32Array(rowCount)
|
|
248
252
|
let selectedCount = 0
|
|
249
253
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
package/src/execute/execute.js
CHANGED
|
@@ -21,7 +21,7 @@ import { yieldToEventLoop } from './yield.js'
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* @import { BatchProjection, CompiledBatchExpression } from '../internalTypes.js'
|
|
24
|
-
* @import { AsyncBatch, AsyncCells, AsyncDataSource, AsyncRow, ColumnDemand, ColumnVector, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, PreparedScan, QueryResults, RelationSchema, ScanRequest, SelectColumn, SqlPrimitive, Statement } from '../types.js'
|
|
24
|
+
* @import { AsyncBatch, AsyncCells, AsyncDataSource, AsyncRow, ColumnDemand, ColumnVector, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, NumericArray, PreparedScan, QueryResults, RelationSchema, ScanColumnResults, ScanRequest, ScanTopK, SelectColumn, SqlPrimitive, Statement } from '../types.js'
|
|
25
25
|
* @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode, TableFunctionNode } from '../plan/types.js'
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -277,7 +277,7 @@ export function selectColumnNames(selectColumns, childColumns) {
|
|
|
277
277
|
/**
|
|
278
278
|
* @param {ScanNode} plan
|
|
279
279
|
* @param {ExecuteContext} context
|
|
280
|
-
* @param {
|
|
280
|
+
* @param {ScanColumnResults} [existingColumnResult]
|
|
281
281
|
* @returns {QueryResults}
|
|
282
282
|
*/
|
|
283
283
|
export function executeScan(plan, context, existingColumnResult) {
|
|
@@ -539,12 +539,27 @@ function scanRequest(plan, schema, context) {
|
|
|
539
539
|
mode: predicate ? 'required' : 'deferred',
|
|
540
540
|
}
|
|
541
541
|
})
|
|
542
|
-
|
|
542
|
+
/** @type {ScanRequest} */
|
|
543
|
+
const request = {
|
|
543
544
|
columns,
|
|
544
545
|
filter: plan.hints.where,
|
|
545
546
|
limit: plan.hints.limit,
|
|
546
547
|
offset: plan.hints.offset,
|
|
547
548
|
}
|
|
549
|
+
if (plan.topK) {
|
|
550
|
+
/** @type {ScanTopK['orderBy'][number][]} */
|
|
551
|
+
const orderBy = []
|
|
552
|
+
for (const term of plan.topK.orderBy) {
|
|
553
|
+
const { expr } = term
|
|
554
|
+
if (expr.type !== 'identifier' ||
|
|
555
|
+
expr.prefix && expr.prefix !== (plan.alias ?? plan.table)) return request
|
|
556
|
+
const field = schema.fields.find(field => field.name === expr.name)
|
|
557
|
+
if (!field) return request
|
|
558
|
+
orderBy.push({ field: field.id, direction: term.direction, nulls: term.nulls ?? 'FIRST' })
|
|
559
|
+
}
|
|
560
|
+
if (orderBy.length) request.topK = { orderBy, limit: plan.topK.limit }
|
|
561
|
+
}
|
|
562
|
+
return request
|
|
548
563
|
}
|
|
549
564
|
|
|
550
565
|
/**
|
|
@@ -1016,7 +1031,7 @@ function vectorFromChunk(chunk) {
|
|
|
1016
1031
|
|
|
1017
1032
|
/**
|
|
1018
1033
|
* @param {ArrayLike<SqlPrimitive>} values
|
|
1019
|
-
* @returns {values is
|
|
1034
|
+
* @returns {values is NumericArray}
|
|
1020
1035
|
*/
|
|
1021
1036
|
function isNumericArray(values) {
|
|
1022
1037
|
return values instanceof Int8Array
|
package/src/execute/sort.js
CHANGED
|
@@ -128,8 +128,16 @@ export async function sortEntriesByTerms({ entries, orderBy, context, cacheValue
|
|
|
128
128
|
* @returns {QueryResults}
|
|
129
129
|
*/
|
|
130
130
|
export function executeSort(plan, context) {
|
|
131
|
-
const child = executePlan({ plan: plan.child, context })
|
|
132
131
|
const { topK } = plan
|
|
132
|
+
let childPlan = plan.child
|
|
133
|
+
// Only a directly adjacent scan can prune candidates for this sort. Never
|
|
134
|
+
// cross filters, joins, aggregates or windows, or change an existing scan
|
|
135
|
+
// slice. Copy the node: CTE plans can be shared by other consumers.
|
|
136
|
+
if (Number.isSafeInteger(topK) && topK > 0 && childPlan.type === 'Scan' &&
|
|
137
|
+
childPlan.hints.limit === undefined && !childPlan.hints.offset) {
|
|
138
|
+
childPlan = { ...childPlan, topK: { orderBy: plan.orderBy, limit: topK } }
|
|
139
|
+
}
|
|
140
|
+
const child = executePlan({ plan: childPlan, context })
|
|
133
141
|
// With a LIMIT bound pushed into the sort, keep at most this many buffered
|
|
134
142
|
// rows: periodically sort and discard everything past topK, so memory is
|
|
135
143
|
// bounded by the limit instead of the input size.
|
|
@@ -523,6 +523,25 @@ async function accumulateBatch({ batch, inputs, specs, groups, context, rowOffse
|
|
|
523
523
|
evaluateBatchInputs(inputs.args, batch, context, rowOffset),
|
|
524
524
|
])
|
|
525
525
|
const rowCount = selectedRowCount(batch.selection)
|
|
526
|
+
context.signal?.throwIfAborted()
|
|
527
|
+
if (rowCount > 0 && keys.every(vector => vector?.type === 'constant') &&
|
|
528
|
+
specs.every(spec => spec.star && spec.funcName === 'COUNT' && !spec.node.distinct && !spec.node.filter)) {
|
|
529
|
+
const keyValues = keys.map(vector => valueAt(/** @type {ColumnVector} */ vector, 0))
|
|
530
|
+
const key = keyValues.length === 0
|
|
531
|
+
? true
|
|
532
|
+
: keyValues.length === 1 ? keyify(keyValues[0]) : keyify(...keyValues)
|
|
533
|
+
let group = groups.get(key)
|
|
534
|
+
if (!group) {
|
|
535
|
+
group = {
|
|
536
|
+
firstRow: undefined,
|
|
537
|
+
keyValues,
|
|
538
|
+
accumulators: specs.map(spec => newAccumulator(spec.funcName, spec.node.distinct)),
|
|
539
|
+
}
|
|
540
|
+
groups.set(key, group)
|
|
541
|
+
}
|
|
542
|
+
for (const accumulator of group.accumulators) accumulator.count += rowCount
|
|
543
|
+
return
|
|
544
|
+
}
|
|
526
545
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
527
546
|
if (rowIndex > 0 && rowIndex % CHUNK_SIZE === 0) {
|
|
528
547
|
await yieldToEventLoop()
|
package/src/expression/batch.js
CHANGED
|
@@ -626,6 +626,13 @@ function resolveIdentifier(identifier, columns) {
|
|
|
626
626
|
*/
|
|
627
627
|
function evaluateKernel(kernel, vectors, selection, signal, rowOffset = 0, rowOrdinals) {
|
|
628
628
|
const length = selectedRowCount(selection)
|
|
629
|
+
// Supported kernels are deterministic; streamRowIndex only labels errors.
|
|
630
|
+
// Empty selections must not evaluate expressions (including invalid casts).
|
|
631
|
+
if (length > 0 && vectors.every(vector => vector.type === 'constant')) {
|
|
632
|
+
signal?.throwIfAborted()
|
|
633
|
+
const ordinal = rowOrdinals ? Number(valueAt(rowOrdinals, 0)) : 0
|
|
634
|
+
return { type: 'constant', value: kernel(vectors, 0, rowOffset + ordinal), length }
|
|
635
|
+
}
|
|
629
636
|
if (signal && length > YIELD_INTERVAL) {
|
|
630
637
|
return evaluateKernelAsync(kernel, vectors, length, signal, rowOffset, rowOrdinals)
|
|
631
638
|
}
|
package/src/index.d.ts
CHANGED
package/src/plan/types.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface ScanNode {
|
|
|
25
25
|
table: string
|
|
26
26
|
alias?: string
|
|
27
27
|
hints: ScanOptions
|
|
28
|
+
// Execution-local hint from an immediately enclosing bounded Sort. Kept
|
|
29
|
+
// separate from scan LIMIT/OFFSET, which select an unordered input slice.
|
|
30
|
+
topK?: { orderBy: OrderByItem[], limit: number }
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// Source for FROM-less SELECT like `SELECT 1`. Yields exactly one empty row.
|
package/src/types.d.ts
CHANGED
|
@@ -126,8 +126,25 @@ export interface ColumnDemand {
|
|
|
126
126
|
mode: 'required' | 'deferred'
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Optional candidate-pruning hint for an ordered slice. A source may ignore
|
|
131
|
+
* it, or omit rows proven unable to enter the first `limit` rows after the
|
|
132
|
+
* scan's filter. Preserve all boundary ties and the relative input order of
|
|
133
|
+
* retained rows. Return an unordered superset of the winners, not an arbitrary
|
|
134
|
+
* `limit` rows. The engine retains the final sort and slice.
|
|
135
|
+
*/
|
|
136
|
+
export interface ScanTopK {
|
|
137
|
+
orderBy: readonly {
|
|
138
|
+
field: number
|
|
139
|
+
direction: 'ASC' | 'DESC'
|
|
140
|
+
nulls: 'FIRST' | 'LAST'
|
|
141
|
+
}[]
|
|
142
|
+
limit: number // Includes the outer OFFSET.
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
export interface ScanRequest {
|
|
130
146
|
columns: readonly ColumnDemand[]
|
|
147
|
+
topK?: ScanTopK
|
|
131
148
|
filter?: ExprNode
|
|
132
149
|
limit?: number
|
|
133
150
|
offset?: number
|