squirreling 0.15.2 → 0.16.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/package.json +2 -2
- package/src/backend/batch.js +236 -0
- package/src/backend/batchAdapters.js +178 -0
- package/src/backend/dataSource.js +17 -1
- package/src/execute/aggregates.js +2 -1
- package/src/execute/batchResults.js +28 -0
- package/src/execute/batches.js +274 -0
- package/src/execute/execute.js +440 -24
- package/src/execute/streamingAggregate.js +151 -1
- package/src/execute/utils.js +37 -0
- package/src/expression/batch.js +659 -0
- package/src/expression/binary.js +17 -4
- package/src/expression/evaluate.js +39 -109
- package/src/expression/scalar.js +105 -0
- package/src/index.d.ts +44 -1
- package/src/index.js +9 -0
- package/src/internalTypes.d.ts +28 -0
- package/src/plan/columns.js +105 -59
- package/src/plan/plan.js +12 -3
- package/src/plan/types.d.ts +3 -0
- package/src/types.d.ts +156 -4
- package/src/validation/tables.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@types/node": "26.2.0",
|
|
43
43
|
"@vitest/coverage-v8": "4.1.10",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "64.1
|
|
45
|
+
"eslint-plugin-jsdoc": "64.2.1",
|
|
46
46
|
"typescript": "7.0.2",
|
|
47
47
|
"vitest": "4.1.10"
|
|
48
48
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { AsyncBatch, ColumnResult, ColumnVector, ReadBatchColumnOptions, RowSelection, SqlPrimitive } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @type {WeakMap<AsyncBatch, Map<number, Map<RowSelection, { resolved?: ColumnVector, pending: Map<AbortSignal | undefined, Promise<ColumnVector>> }>>>} */
|
|
6
|
+
const batchCache = new WeakMap()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the number of selected rows.
|
|
10
|
+
*
|
|
11
|
+
* @param {RowSelection} selection
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
export function selectedRowCount(selection) {
|
|
15
|
+
if (selection.type === 'all') return selection.length
|
|
16
|
+
if (selection.type === 'range') return selection.end - selection.start
|
|
17
|
+
return selection.indices.length
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates base-aligned ordinal values for the rows in a selection.
|
|
22
|
+
*
|
|
23
|
+
* @param {RowSelection} selection
|
|
24
|
+
* @returns {ColumnVector}
|
|
25
|
+
*/
|
|
26
|
+
export function selectionOrdinals(selection) {
|
|
27
|
+
const values = new Uint32Array(selection.length)
|
|
28
|
+
const length = selectedRowCount(selection)
|
|
29
|
+
for (let ordinal = 0; ordinal < length; ordinal++) {
|
|
30
|
+
values[selectionIndexAt(selection, ordinal)] = ordinal
|
|
31
|
+
}
|
|
32
|
+
return { type: 'typed', values, length: selection.length }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Composes a selection over an already-selected domain with the selection
|
|
37
|
+
* that produced that domain.
|
|
38
|
+
*
|
|
39
|
+
* @param {RowSelection} outer - selection over the original base domain
|
|
40
|
+
* @param {RowSelection} inner - selection over the rows selected by `outer`
|
|
41
|
+
* @returns {RowSelection}
|
|
42
|
+
*/
|
|
43
|
+
export function composeSelections(outer, inner) {
|
|
44
|
+
const outerCount = selectedRowCount(outer)
|
|
45
|
+
if (inner.length !== outerCount) {
|
|
46
|
+
throw new Error(`Cannot compose selection of length ${inner.length} over ${outerCount} rows`)
|
|
47
|
+
}
|
|
48
|
+
if (inner.type === 'all') return outer
|
|
49
|
+
if (outer.type === 'all') return { ...inner, length: outer.length }
|
|
50
|
+
|
|
51
|
+
if (outer.type === 'range' && inner.type === 'range') {
|
|
52
|
+
return {
|
|
53
|
+
type: 'range',
|
|
54
|
+
start: outer.start + inner.start,
|
|
55
|
+
end: outer.start + inner.end,
|
|
56
|
+
length: outer.length,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const indices = new Uint32Array(selectedRowCount(inner))
|
|
61
|
+
for (let i = 0; i < indices.length; i++) {
|
|
62
|
+
indices[i] = selectionIndexAt(outer, selectionIndexAt(inner, i))
|
|
63
|
+
}
|
|
64
|
+
return { type: 'indices', indices, length: outer.length }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads one logical value from a vector.
|
|
69
|
+
*
|
|
70
|
+
* @param {ColumnVector} vector
|
|
71
|
+
* @param {number} index
|
|
72
|
+
* @returns {SqlPrimitive}
|
|
73
|
+
*/
|
|
74
|
+
export function valueAt(vector, index) {
|
|
75
|
+
if (!Number.isInteger(index) || index < 0 || index >= vector.length) {
|
|
76
|
+
throw new RangeError(`Column index ${index} is outside vector length ${vector.length}`)
|
|
77
|
+
}
|
|
78
|
+
if (vector.type === 'values') return vector.values[index]
|
|
79
|
+
if (vector.type === 'typed') {
|
|
80
|
+
if (vector.validity && vector.validity[index] === 0) return null
|
|
81
|
+
return vector.values[index]
|
|
82
|
+
}
|
|
83
|
+
if (vector.type === 'constant') return vector.value
|
|
84
|
+
return valueAt(vector.source, selectionIndexAt(vector.selection, index))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a zero-copy view of a vector through a row selection.
|
|
89
|
+
*
|
|
90
|
+
* @param {ColumnVector} vector
|
|
91
|
+
* @param {RowSelection} selection
|
|
92
|
+
* @returns {ColumnVector}
|
|
93
|
+
*/
|
|
94
|
+
export function selectVector(vector, selection) {
|
|
95
|
+
if (selection.length !== vector.length) {
|
|
96
|
+
throw new Error(`Cannot select ${selection.length} rows from vector length ${vector.length}`)
|
|
97
|
+
}
|
|
98
|
+
if (selection.type === 'all') return vector
|
|
99
|
+
if (vector.type === 'selected') {
|
|
100
|
+
const composed = composeSelections(vector.selection, selection)
|
|
101
|
+
return {
|
|
102
|
+
type: 'selected',
|
|
103
|
+
source: vector.source,
|
|
104
|
+
selection: composed,
|
|
105
|
+
length: selectedRowCount(composed),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: 'selected',
|
|
110
|
+
source: vector,
|
|
111
|
+
selection,
|
|
112
|
+
length: selectedRowCount(selection),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolves one batch column for an effective selection.
|
|
118
|
+
*
|
|
119
|
+
* @param {ReadBatchColumnOptions} options
|
|
120
|
+
* @returns {ColumnResult}
|
|
121
|
+
*/
|
|
122
|
+
export function readBatchColumn({ batch, columnIndex, selection = batch.selection, signal }) {
|
|
123
|
+
const column = batch.columns[columnIndex]
|
|
124
|
+
if (!column) throw new RangeError(`Column index ${columnIndex} is outside batch columns`)
|
|
125
|
+
if (selection.length !== batch.selection.length) {
|
|
126
|
+
throw new Error(`Selection length ${selection.length} does not match batch length ${batch.selection.length}`)
|
|
127
|
+
}
|
|
128
|
+
if (!('read' in column)) {
|
|
129
|
+
return selectVector(column, selection)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let batchResults = batchCache.get(batch)
|
|
133
|
+
if (!batchResults) {
|
|
134
|
+
batchResults = new Map()
|
|
135
|
+
batchCache.set(batch, batchResults)
|
|
136
|
+
}
|
|
137
|
+
let columnResults = batchResults.get(columnIndex)
|
|
138
|
+
if (!columnResults) {
|
|
139
|
+
columnResults = new Map()
|
|
140
|
+
batchResults.set(columnIndex, columnResults)
|
|
141
|
+
}
|
|
142
|
+
let cache = columnResults.get(selection)
|
|
143
|
+
if (!cache) {
|
|
144
|
+
cache = { pending: new Map() }
|
|
145
|
+
columnResults.set(selection, cache)
|
|
146
|
+
}
|
|
147
|
+
const pending = cache.pending.get(signal)
|
|
148
|
+
if (pending) return pending
|
|
149
|
+
if (cache.resolved) return cache.resolved
|
|
150
|
+
|
|
151
|
+
const result = column.read({
|
|
152
|
+
batch: column.input ?? batch,
|
|
153
|
+
selection,
|
|
154
|
+
signal,
|
|
155
|
+
rowOffset: column.rowOffset,
|
|
156
|
+
rowOrdinals: column.rowOrdinals
|
|
157
|
+
? selectVector(column.rowOrdinals, selection)
|
|
158
|
+
: undefined,
|
|
159
|
+
})
|
|
160
|
+
const validated = validateColumnResult(result, selectedRowCount(selection))
|
|
161
|
+
if (validated instanceof Promise) {
|
|
162
|
+
const settled = validated.then(function cacheResolved(vector) {
|
|
163
|
+
cache.pending.delete(signal)
|
|
164
|
+
cache.resolved ??= vector
|
|
165
|
+
return vector
|
|
166
|
+
}, function evictRejected(error) {
|
|
167
|
+
cache.pending.delete(signal)
|
|
168
|
+
throw error
|
|
169
|
+
})
|
|
170
|
+
cache.pending.set(signal, settled)
|
|
171
|
+
return settled
|
|
172
|
+
}
|
|
173
|
+
cache.resolved = validated
|
|
174
|
+
return validated
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Selects rows from a batch without reading or copying its columns.
|
|
179
|
+
*
|
|
180
|
+
* @param {AsyncBatch} batch
|
|
181
|
+
* @param {RowSelection} selection - selection over the batch's current rows
|
|
182
|
+
* @returns {AsyncBatch}
|
|
183
|
+
*/
|
|
184
|
+
export function selectBatch(batch, selection) {
|
|
185
|
+
const composed = composeSelections(batch.selection, selection)
|
|
186
|
+
return {
|
|
187
|
+
selection: composed,
|
|
188
|
+
columns: batch.columns,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns a base-domain row index for a logical selected-row index.
|
|
194
|
+
*
|
|
195
|
+
* @param {RowSelection} selection
|
|
196
|
+
* @param {number} index
|
|
197
|
+
* @returns {number}
|
|
198
|
+
*/
|
|
199
|
+
function selectionIndexAt(selection, index) {
|
|
200
|
+
const count = selectedRowCount(selection)
|
|
201
|
+
if (!Number.isInteger(index) || index < 0 || index >= count) {
|
|
202
|
+
throw new RangeError(`Selection index ${index} is outside selected length ${count}`)
|
|
203
|
+
}
|
|
204
|
+
if (selection.type === 'all') return index
|
|
205
|
+
if (selection.type === 'range') return selection.start + index
|
|
206
|
+
return selection.indices[index]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validates the resolved vector length without forcing synchronous column
|
|
211
|
+
* implementations through a promise.
|
|
212
|
+
*
|
|
213
|
+
* @param {ColumnResult} result
|
|
214
|
+
* @param {number} expectedLength
|
|
215
|
+
* @returns {ColumnResult}
|
|
216
|
+
*/
|
|
217
|
+
function validateColumnResult(result, expectedLength) {
|
|
218
|
+
if (result instanceof Promise) {
|
|
219
|
+
return result.then(function validateResolvedVector(vector) {
|
|
220
|
+
return validateVectorLength(vector, expectedLength)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return validateVectorLength(result, expectedLength)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {ColumnVector} vector
|
|
228
|
+
* @param {number} expectedLength
|
|
229
|
+
* @returns {ColumnVector}
|
|
230
|
+
*/
|
|
231
|
+
function validateVectorLength(vector, expectedLength) {
|
|
232
|
+
if (vector.length !== expectedLength) {
|
|
233
|
+
throw new Error(`Column returned ${vector.length} rows, expected ${expectedLength}`)
|
|
234
|
+
}
|
|
235
|
+
return vector
|
|
236
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readBatchColumn, selectVector, selectedRowCount, valueAt } from './batch.js'
|
|
2
|
+
import { yieldToEventLoop } from '../execute/yield.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import { AsyncBatch, AsyncCells, AsyncRow, ColumnResult, ColumnVector, RowsToBatchesOptions, SqlPrimitive } from '../types.js'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BATCH_ROWS = 1024
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Materializes a legacy row stream into aligned batches. This is a source or
|
|
12
|
+
* public compatibility boundary, not an operator implementation strategy.
|
|
13
|
+
*
|
|
14
|
+
* @param {AsyncIterable<AsyncRow>} rows
|
|
15
|
+
* @param {string[]} columnNames
|
|
16
|
+
* @param {RowsToBatchesOptions} [options]
|
|
17
|
+
* @yields {AsyncBatch}
|
|
18
|
+
*/
|
|
19
|
+
export async function* rowsToBatches(rows, columnNames, options) {
|
|
20
|
+
const batchRows = options?.batchRows ?? DEFAULT_BATCH_ROWS
|
|
21
|
+
if (!Number.isInteger(batchRows) || batchRows <= 0) {
|
|
22
|
+
throw new RangeError(`batchRows must be a positive integer, got ${batchRows}`)
|
|
23
|
+
}
|
|
24
|
+
let values = makeValueBuffers(columnNames.length)
|
|
25
|
+
let rowCount = 0
|
|
26
|
+
|
|
27
|
+
for await (const row of rows) {
|
|
28
|
+
options?.signal?.throwIfAborted()
|
|
29
|
+
for (let columnIndex = 0; columnIndex < columnNames.length; columnIndex++) {
|
|
30
|
+
const name = columnNames[columnIndex]
|
|
31
|
+
values[columnIndex].push(await readRowCell(row, name))
|
|
32
|
+
}
|
|
33
|
+
rowCount++
|
|
34
|
+
if (rowCount === batchRows) {
|
|
35
|
+
yield loadedBatch(values, rowCount)
|
|
36
|
+
values = makeValueBuffers(columnNames.length)
|
|
37
|
+
rowCount = 0
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (rowCount > 0) yield loadedBatch(values, rowCount)
|
|
42
|
+
options?.signal?.throwIfAborted()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Exposes batches through the legacy lazy-cell row interface. Column reads
|
|
47
|
+
* remain lazy and are memoized once per batch/selection by `readBatchColumn`.
|
|
48
|
+
*
|
|
49
|
+
* @param {AsyncIterable<AsyncBatch>} batches
|
|
50
|
+
* @param {string[]} columns
|
|
51
|
+
* @param {AbortSignal} [signal]
|
|
52
|
+
* @yields {AsyncRow}
|
|
53
|
+
*/
|
|
54
|
+
export async function* batchesToRows(batches, columns, signal) {
|
|
55
|
+
for await (const batch of batches) {
|
|
56
|
+
const loadedVectors = batch.columns.map(function loadedVector(column) {
|
|
57
|
+
if ('read' in column) return undefined
|
|
58
|
+
return selectVector(column, batch.selection)
|
|
59
|
+
})
|
|
60
|
+
const rowCount = selectedRowCount(batch.selection)
|
|
61
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
62
|
+
signal?.throwIfAborted()
|
|
63
|
+
/** @type {AsyncCells} */
|
|
64
|
+
const cells = {}
|
|
65
|
+
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
|
66
|
+
const currentColumn = columnIndex
|
|
67
|
+
const currentRow = rowIndex
|
|
68
|
+
const loadedVector = loadedVectors[columnIndex]
|
|
69
|
+
if (loadedVector) {
|
|
70
|
+
const value = valueAt(loadedVector, currentRow)
|
|
71
|
+
cells[columns[columnIndex]] = function readLoadedCell() {
|
|
72
|
+
return Promise.resolve(value)
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
cells[columns[columnIndex]] = async function readCell() {
|
|
76
|
+
const vector = await readBatchColumn({ batch, columnIndex: currentColumn, signal })
|
|
77
|
+
return valueAt(vector, currentRow)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
yield { columns, cells }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
signal?.throwIfAborted()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collects batches directly into the existing object-row result shape without
|
|
89
|
+
* constructing compatibility `AsyncRow` values.
|
|
90
|
+
*
|
|
91
|
+
* @param {AsyncIterable<AsyncBatch>} batches
|
|
92
|
+
* @param {string[]} names
|
|
93
|
+
* @param {AbortSignal} [signal]
|
|
94
|
+
* @returns {Promise<Record<string, SqlPrimitive>[]>}
|
|
95
|
+
*/
|
|
96
|
+
export async function collectBatches(batches, names, signal) {
|
|
97
|
+
/** @type {Record<string, SqlPrimitive>[]} */
|
|
98
|
+
const rows = []
|
|
99
|
+
for await (const batch of batches) {
|
|
100
|
+
const results = batch.columns.map(function readColumn(_column, columnIndex) {
|
|
101
|
+
return readBatchColumn({ batch, columnIndex, signal })
|
|
102
|
+
})
|
|
103
|
+
const vectors = await resolveColumns(results)
|
|
104
|
+
const rowCount = selectedRowCount(batch.selection)
|
|
105
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
106
|
+
if (signal && rowIndex % 4000 === 0) {
|
|
107
|
+
if (rowIndex > 0) await yieldToEventLoop()
|
|
108
|
+
signal.throwIfAborted()
|
|
109
|
+
}
|
|
110
|
+
/** @type {Record<string, SqlPrimitive>} */
|
|
111
|
+
const row = {}
|
|
112
|
+
for (let columnIndex = 0; columnIndex < names.length; columnIndex++) {
|
|
113
|
+
row[names[columnIndex]] = valueAt(vectors[columnIndex], rowIndex)
|
|
114
|
+
}
|
|
115
|
+
rows.push(row)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
signal?.throwIfAborted()
|
|
119
|
+
return rows
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {AsyncRow} row
|
|
124
|
+
* @param {string} name
|
|
125
|
+
* @returns {Promise<SqlPrimitive>}
|
|
126
|
+
*/
|
|
127
|
+
async function readRowCell(row, name) {
|
|
128
|
+
if (row.resolved && Object.prototype.hasOwnProperty.call(row.resolved, name)) {
|
|
129
|
+
return row.resolved[name]
|
|
130
|
+
}
|
|
131
|
+
const cell = row.cells[name]
|
|
132
|
+
if (!cell) throw new Error(`Row does not contain column "${name}"`)
|
|
133
|
+
return await cell()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {number} count
|
|
138
|
+
* @returns {SqlPrimitive[][]}
|
|
139
|
+
*/
|
|
140
|
+
function makeValueBuffers(count) {
|
|
141
|
+
/** @type {SqlPrimitive[][]} */
|
|
142
|
+
const values = []
|
|
143
|
+
for (let i = 0; i < count; i++) values.push([])
|
|
144
|
+
return values
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {SqlPrimitive[][]} values
|
|
149
|
+
* @param {number} rowCount
|
|
150
|
+
* @returns {AsyncBatch}
|
|
151
|
+
*/
|
|
152
|
+
function loadedBatch(values, rowCount) {
|
|
153
|
+
return {
|
|
154
|
+
selection: { type: 'all', length: rowCount },
|
|
155
|
+
columns: values.map(function loadedColumn(columnValues) {
|
|
156
|
+
return { type: 'values', values: columnValues, length: rowCount }
|
|
157
|
+
}),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Avoids a promise boundary when every column is already loaded synchronously.
|
|
163
|
+
*
|
|
164
|
+
* @param {ColumnResult[]} results
|
|
165
|
+
* @returns {ColumnVector[] | Promise<ColumnVector[]>}
|
|
166
|
+
*/
|
|
167
|
+
function resolveColumns(results) {
|
|
168
|
+
if (results.some(function isPromise(result) { return result instanceof Promise })) {
|
|
169
|
+
return Promise.all(results)
|
|
170
|
+
}
|
|
171
|
+
/** @type {ColumnVector[]} */
|
|
172
|
+
const vectors = []
|
|
173
|
+
for (const result of results) {
|
|
174
|
+
if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
|
|
175
|
+
vectors.push(result)
|
|
176
|
+
}
|
|
177
|
+
return vectors
|
|
178
|
+
}
|
|
@@ -18,6 +18,19 @@ export function asyncRow(obj, columns) {
|
|
|
18
18
|
return { columns, cells, resolved: obj }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Returns a source's authoritative logical column names.
|
|
23
|
+
*
|
|
24
|
+
* @param {AsyncDataSource} source
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
export function dataSourceColumns(source) {
|
|
28
|
+
if (source.prepareScan && source.schema) {
|
|
29
|
+
return source.schema.fields.map(function fieldName(field) { return field.name })
|
|
30
|
+
}
|
|
31
|
+
return source.columns ?? []
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Creates an async memory-backed data source from an array of plain objects
|
|
23
36
|
*
|
|
@@ -78,6 +91,9 @@ export function memorySource({ data, columns }) {
|
|
|
78
91
|
* @returns {AsyncDataSource}
|
|
79
92
|
*/
|
|
80
93
|
export function cachedDataSource(source) {
|
|
94
|
+
if (source.prepareScan && source.schema) return source
|
|
95
|
+
const { scan } = source
|
|
96
|
+
if (!scan) return source
|
|
81
97
|
/** @type {WeakMap<object, Map<string, Promise<SqlPrimitive>>>} */
|
|
82
98
|
const cache = new WeakMap()
|
|
83
99
|
return {
|
|
@@ -85,7 +101,7 @@ export function cachedDataSource(source) {
|
|
|
85
101
|
scan(options) {
|
|
86
102
|
// Does re-run the scan, but cache avoids re-computing expensive async cells
|
|
87
103
|
// TODO: check cache first to avoid re-scanning when possible
|
|
88
|
-
const { rows, appliedWhere, appliedLimitOffset } =
|
|
104
|
+
const { rows, appliedWhere, appliedLimitOffset } = scan.call(source, options)
|
|
89
105
|
|
|
90
106
|
// Applied where clause changes which rows are returned so can't be cached
|
|
91
107
|
if (appliedWhere && options.where) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dataSourceColumns } from '../backend/dataSource.js'
|
|
1
2
|
import { derivedAlias } from '../expression/alias.js'
|
|
2
3
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
3
4
|
import { finalizeAccumulator, newAccumulator, updateAccumulator } from './accumulator.js'
|
|
@@ -299,7 +300,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
299
300
|
|
|
300
301
|
// COUNT(*) needs a physical column whose filtered chunk lengths can be
|
|
301
302
|
// counted. Prefer a predicate/projection column, then any table column.
|
|
302
|
-
const starColumn = scanNode.hints.columns?.[0] ?? table
|
|
303
|
+
const starColumn = scanNode.hints.columns?.[0] ?? dataSourceColumns(table)[0]
|
|
303
304
|
if (!starColumn) return
|
|
304
305
|
|
|
305
306
|
// All columns must be simple aggregates on plain identifiers
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { batchesToRows } from '../backend/batchAdapters.js'
|
|
2
|
+
import { bindQuerySignal } from './utils.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import { AsyncBatch, QueryResults } from '../types.js'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates query results backed by batches.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* @param {string[]} options.columns
|
|
13
|
+
* @param {number} [options.numRows]
|
|
14
|
+
* @param {number} [options.maxRows]
|
|
15
|
+
* @param {() => AsyncIterable<AsyncBatch>} options.batches
|
|
16
|
+
* @param {AbortSignal} [options.signal]
|
|
17
|
+
* @returns {QueryResults}
|
|
18
|
+
*/
|
|
19
|
+
export function batchResult({ batches: readBatches, signal, ...metadata }) {
|
|
20
|
+
const results = {
|
|
21
|
+
...metadata,
|
|
22
|
+
batches: readBatches,
|
|
23
|
+
rows() {
|
|
24
|
+
return batchesToRows(readBatches(), metadata.columns, signal)
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
return bindQuerySignal(results, signal)
|
|
28
|
+
}
|