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.
@@ -0,0 +1,274 @@
1
+ import { readBatchColumn, selectBatch, selectedRowCount, selectionOrdinals, valueAt } from '../backend/batch.js'
2
+ import { keyify } from './utils.js'
3
+ import { yieldToEventLoop } from './yield.js'
4
+
5
+ /**
6
+ * @import { BatchProjection, CompiledBatchExpression } from '../internalTypes.js'
7
+ * @import { AsyncBatch, BatchColumn, ColumnResult, ColumnVector, ReadColumn, RowSelection, SqlPrimitive } from '../types.js'
8
+ */
9
+
10
+ const INITIAL_FILTER_WINDOW_ROWS = 256
11
+ const MAX_FILTER_WINDOW_ROWS = 16_384
12
+
13
+ /**
14
+ * Applies LIMIT/OFFSET by composing batch selections and stops consuming the
15
+ * source as soon as the requested range is complete.
16
+ *
17
+ * @param {AsyncIterable<AsyncBatch>} batches
18
+ * @param {number} [limit]
19
+ * @param {number} [offset]
20
+ * @param {AbortSignal} [signal]
21
+ * @yields {AsyncBatch}
22
+ */
23
+ export async function* limitBatches(batches, limit = Infinity, offset = 0, signal) {
24
+ if (limit <= 0) return
25
+ let skipped = 0
26
+ let yielded = 0
27
+ for await (const batch of batches) {
28
+ signal?.throwIfAborted()
29
+ const rowCount = selectedRowCount(batch.selection)
30
+ if (skipped + rowCount <= offset) {
31
+ skipped += rowCount
32
+ continue
33
+ }
34
+
35
+ const start = Math.max(0, offset - skipped)
36
+ const remaining = limit - yielded
37
+ const end = Math.min(rowCount, start + remaining)
38
+ if (end > start) {
39
+ if (start === 0 && end === rowCount) {
40
+ yield batch
41
+ } else {
42
+ yield selectBatch(batch, {
43
+ type: 'range',
44
+ start,
45
+ end,
46
+ length: rowCount,
47
+ })
48
+ }
49
+ yielded += end - start
50
+ }
51
+ skipped += rowCount
52
+ if (yielded >= limit) return
53
+ }
54
+ signal?.throwIfAborted()
55
+ }
56
+
57
+ /**
58
+ * Evaluates a predicate in adaptive windows when a downstream limit can stop
59
+ * early, then composes selections without reading or copying payload columns.
60
+ *
61
+ * @param {AsyncIterable<AsyncBatch>} batches
62
+ * @param {CompiledBatchExpression} expression
63
+ * @param {AbortSignal} [signal]
64
+ * @param {number} [targetRows] - Matches needed by a downstream limit.
65
+ * @yields {AsyncBatch}
66
+ */
67
+ export async function* filterBatches(batches, expression, signal, targetRows) {
68
+ let rowOffset = 0
69
+ let matchedRows = 0
70
+ let windowRows = targetRows === undefined
71
+ ? Infinity
72
+ : Math.min(MAX_FILTER_WINDOW_ROWS, Math.max(INITIAL_FILTER_WINDOW_ROWS, targetRows))
73
+ for await (const batch of batches) {
74
+ signal?.throwIfAborted()
75
+ const rowCount = selectedRowCount(batch.selection)
76
+ let start = 0
77
+ while (start < rowCount) {
78
+ const remainingRows = rowCount - start
79
+ const requestedRows = targetRows !== undefined && matchedRows < targetRows
80
+ ? windowRows
81
+ : remainingRows
82
+ const end = Math.min(rowCount, start + requestedRows)
83
+ const windowBatch = start === 0 && end === rowCount
84
+ ? batch
85
+ : selectBatch(batch, {
86
+ type: 'range',
87
+ start,
88
+ end,
89
+ length: rowCount,
90
+ })
91
+ const windowRowCount = end - start
92
+ const result = expression.evaluate({
93
+ batch: windowBatch,
94
+ selection: windowBatch.selection,
95
+ signal,
96
+ rowOffset: rowOffset + start,
97
+ })
98
+ const predicate = result instanceof Promise ? await result : result
99
+ const { selection, selectedCount } = predicateSelection(predicate, windowRowCount)
100
+ start = end
101
+ matchedRows += selectedCount
102
+
103
+ if (targetRows !== undefined && matchedRows < targetRows) {
104
+ if (selectedCount === 0) {
105
+ windowRows = Math.min(MAX_FILTER_WINDOW_ROWS, windowRows * 2)
106
+ } else {
107
+ const remainingMatches = targetRows - matchedRows
108
+ const estimatedRows = Math.ceil(remainingMatches * windowRowCount / selectedCount * 1.25)
109
+ windowRows = Math.min(MAX_FILTER_WINDOW_ROWS, Math.max(INITIAL_FILTER_WINDOW_ROWS, estimatedRows))
110
+ }
111
+ }
112
+
113
+ if (selectedCount === 0) continue
114
+ if (selectedCount === windowRowCount) {
115
+ yield windowBatch
116
+ } else {
117
+ yield selectBatch(windowBatch, selection)
118
+ }
119
+ }
120
+ rowOffset += rowCount
121
+ }
122
+ signal?.throwIfAborted()
123
+ }
124
+
125
+ /**
126
+ * Removes duplicate rows while retaining zero-copy selections over the input
127
+ * batches. DISTINCT requires every output column as both a key and payload, so
128
+ * resolving all batch columns here does not introduce eager payload work.
129
+ *
130
+ * @param {AsyncIterable<AsyncBatch>} batches
131
+ * @param {AbortSignal} [signal]
132
+ * @yields {AsyncBatch}
133
+ */
134
+ export async function* distinctBatches(batches, signal) {
135
+ const seen = new Set()
136
+ for await (const batch of batches) {
137
+ signal?.throwIfAborted()
138
+ const results = batch.columns.map(function readColumn(_column, columnIndex) {
139
+ return readBatchColumn({ batch, columnIndex, signal })
140
+ })
141
+ const resolved = resolveVectors(results)
142
+ const vectors = resolved instanceof Promise ? await resolved : resolved
143
+ const rowCount = selectedRowCount(batch.selection)
144
+ const indices = new Uint32Array(rowCount)
145
+ /** @type {SqlPrimitive[]} */
146
+ const row = new Array(vectors.length)
147
+ let selectedCount = 0
148
+
149
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
150
+ if (signal && rowIndex > 0 && rowIndex % 4000 === 0) {
151
+ await yieldToEventLoop()
152
+ signal.throwIfAborted()
153
+ }
154
+ for (let columnIndex = 0; columnIndex < vectors.length; columnIndex++) {
155
+ row[columnIndex] = valueAt(vectors[columnIndex], rowIndex)
156
+ }
157
+ const key = keyify(...row)
158
+ if (seen.has(key)) continue
159
+ seen.add(key)
160
+ indices[selectedCount++] = rowIndex
161
+ }
162
+
163
+ if (selectedCount === 0) continue
164
+ if (selectedCount === rowCount) {
165
+ yield batch
166
+ } else {
167
+ yield selectBatch(batch, {
168
+ type: 'indices',
169
+ indices: indices.subarray(0, selectedCount),
170
+ length: rowCount,
171
+ })
172
+ }
173
+ }
174
+ signal?.throwIfAborted()
175
+ }
176
+
177
+ /**
178
+ * Projects batches into direct, constant, or lazily computed columns. A
179
+ * computed column retains the input batch that owns its dependency indices,
180
+ * so output columns remain aligned without hidden dependency columns.
181
+ *
182
+ * @param {AsyncIterable<AsyncBatch>} batches
183
+ * @param {readonly BatchProjection[]} projections
184
+ * @yields {AsyncBatch}
185
+ */
186
+ export async function* projectExpressionBatches(batches, projections) {
187
+ let rowOffset = 0
188
+ for await (const batch of batches) {
189
+ const currentRowOffset = rowOffset
190
+ rowOffset += selectedRowCount(batch.selection)
191
+ /** @type {ColumnVector | undefined} */
192
+ let rowOrdinals
193
+ yield {
194
+ selection: batch.selection,
195
+ columns: projections.map(function projectColumn(projection) {
196
+ /** @type {BatchColumn} */
197
+ let column
198
+ if (projection.type === 'column') {
199
+ column = projectedBatchColumn(batch, projection.columnIndex)
200
+ } else if (projection.type === 'constant') {
201
+ column = {
202
+ type: 'constant',
203
+ value: projection.value,
204
+ length: batch.selection.length,
205
+ }
206
+ } else {
207
+ rowOrdinals ??= selectionOrdinals(batch.selection)
208
+ column = {
209
+ read: projection.expression.evaluate,
210
+ input: batch,
211
+ rowOffset: currentRowOffset,
212
+ rowOrdinals,
213
+ }
214
+ }
215
+ return column
216
+ }),
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Preserves the source batch's deferred-read cache through projection.
223
+ *
224
+ * @param {AsyncBatch} batch
225
+ * @param {number} columnIndex
226
+ * @returns {BatchColumn}
227
+ */
228
+ function projectedBatchColumn(batch, columnIndex) {
229
+ const column = batch.columns[columnIndex]
230
+ if (!('read' in column)) return column
231
+ /** @type {ReadColumn} */
232
+ function readProjectedColumn({ selection, signal }) {
233
+ return readBatchColumn({ batch, columnIndex, selection, signal })
234
+ }
235
+ return { read: readProjectedColumn }
236
+ }
237
+
238
+ /**
239
+ * @param {ColumnVector} predicate
240
+ * @param {number} rowCount
241
+ * @returns {{ selection: Extract<RowSelection, { type: 'indices' }>, selectedCount: number }}
242
+ */
243
+ function predicateSelection(predicate, rowCount) {
244
+ if (predicate.length !== rowCount) {
245
+ throw new Error(`Predicate returned ${predicate.length} rows, expected ${rowCount}`)
246
+ }
247
+ const indices = new Uint32Array(rowCount)
248
+ let selectedCount = 0
249
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
250
+ if (!valueAt(predicate, rowIndex)) continue
251
+ indices[selectedCount++] = rowIndex
252
+ }
253
+ return {
254
+ selection: { type: 'indices', indices: indices.subarray(0, selectedCount), length: rowCount },
255
+ selectedCount,
256
+ }
257
+ }
258
+
259
+ /**
260
+ * @param {ColumnResult[]} results
261
+ * @returns {ColumnVector[] | Promise<ColumnVector[]>}
262
+ */
263
+ function resolveVectors(results) {
264
+ if (results.some(function isPromise(result) { return result instanceof Promise })) {
265
+ return Promise.all(results)
266
+ }
267
+ /** @type {ColumnVector[]} */
268
+ const vectors = []
269
+ for (const result of results) {
270
+ if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
271
+ vectors.push(result)
272
+ }
273
+ return vectors
274
+ }