squirreling 0.16.0 → 0.16.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -40,10 +40,10 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "26.2.0",
43
- "@vitest/coverage-v8": "4.1.10",
43
+ "@vitest/coverage-v8": "4.1.11",
44
44
  "eslint": "9.39.4",
45
45
  "eslint-plugin-jsdoc": "64.2.1",
46
46
  "typescript": "7.0.2",
47
- "vitest": "4.1.10"
47
+ "vitest": "4.1.11"
48
48
  }
49
49
  }
@@ -17,6 +17,35 @@ export function selectedRowCount(selection) {
17
17
  return selection.indices.length
18
18
  }
19
19
 
20
+ /**
21
+ * Recognizes promises and thenables without relying on realm identity.
22
+ *
23
+ * @template T
24
+ * @param {T | PromiseLike<T>} value
25
+ * @returns {value is PromiseLike<T>}
26
+ */
27
+ export function isPromiseLike(value) {
28
+ if (value === null || typeof value !== 'object' && typeof value !== 'function') return false
29
+ return typeof Reflect.get(value, 'then') === 'function'
30
+ }
31
+
32
+ /**
33
+ * Avoids a promise boundary when every column is already loaded synchronously.
34
+ *
35
+ * @param {ColumnResult[]} results
36
+ * @returns {ColumnVector[] | Promise<ColumnVector[]>}
37
+ */
38
+ export function resolveColumnResults(results) {
39
+ if (results.some(isPromiseLike)) return Promise.all(results)
40
+ /** @type {ColumnVector[]} */
41
+ const vectors = []
42
+ for (const result of results) {
43
+ if (isPromiseLike(result)) throw new Error('Unexpected asynchronous column result')
44
+ vectors.push(result)
45
+ }
46
+ return vectors
47
+ }
48
+
20
49
  /**
21
50
  * Creates base-aligned ordinal values for the rows in a selection.
22
51
  *
@@ -158,8 +187,8 @@ export function readBatchColumn({ batch, columnIndex, selection = batch.selectio
158
187
  : undefined,
159
188
  })
160
189
  const validated = validateColumnResult(result, selectedRowCount(selection))
161
- if (validated instanceof Promise) {
162
- const settled = validated.then(function cacheResolved(vector) {
190
+ if (isPromiseLike(validated)) {
191
+ const settled = Promise.resolve(validated).then(function cacheResolved(vector) {
163
192
  cache.pending.delete(signal)
164
193
  cache.resolved ??= vector
165
194
  return vector
@@ -215,8 +244,8 @@ function selectionIndexAt(selection, index) {
215
244
  * @returns {ColumnResult}
216
245
  */
217
246
  function validateColumnResult(result, expectedLength) {
218
- if (result instanceof Promise) {
219
- return result.then(function validateResolvedVector(vector) {
247
+ if (isPromiseLike(result)) {
248
+ return Promise.resolve(result).then(function validateResolvedVector(vector) {
220
249
  return validateVectorLength(vector, expectedLength)
221
250
  })
222
251
  }
@@ -1,8 +1,8 @@
1
- import { readBatchColumn, selectVector, selectedRowCount, valueAt } from './batch.js'
1
+ import { readBatchColumn, resolveColumnResults, selectVector, selectedRowCount, valueAt } from './batch.js'
2
2
  import { yieldToEventLoop } from '../execute/yield.js'
3
3
 
4
4
  /**
5
- * @import { AsyncBatch, AsyncCells, AsyncRow, ColumnResult, ColumnVector, RowsToBatchesOptions, SqlPrimitive } from '../types.js'
5
+ * @import { AsyncBatch, AsyncCells, AsyncRow, RowsToBatchesOptions, SqlPrimitive } from '../types.js'
6
6
  */
7
7
 
8
8
  const DEFAULT_BATCH_ROWS = 1024
@@ -21,24 +21,19 @@ export async function* rowsToBatches(rows, columnNames, options) {
21
21
  if (!Number.isInteger(batchRows) || batchRows <= 0) {
22
22
  throw new RangeError(`batchRows must be a positive integer, got ${batchRows}`)
23
23
  }
24
- let values = makeValueBuffers(columnNames.length)
25
- let rowCount = 0
24
+ /** @type {AsyncRow[]} */
25
+ let bufferedRows = []
26
26
 
27
27
  for await (const row of rows) {
28
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
29
+ bufferedRows.push(row)
30
+ if (bufferedRows.length === batchRows) {
31
+ yield await materializeRows(bufferedRows, columnNames)
32
+ bufferedRows = []
38
33
  }
39
34
  }
40
35
 
41
- if (rowCount > 0) yield loadedBatch(values, rowCount)
36
+ if (bufferedRows.length > 0) yield await materializeRows(bufferedRows, columnNames)
42
37
  options?.signal?.throwIfAborted()
43
38
  }
44
39
 
@@ -100,7 +95,7 @@ export async function collectBatches(batches, names, signal) {
100
95
  const results = batch.columns.map(function readColumn(_column, columnIndex) {
101
96
  return readBatchColumn({ batch, columnIndex, signal })
102
97
  })
103
- const vectors = await resolveColumns(results)
98
+ const vectors = await resolveColumnResults(results)
104
99
  const rowCount = selectedRowCount(batch.selection)
105
100
  for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
106
101
  if (signal && rowIndex % 4000 === 0) {
@@ -133,6 +128,27 @@ async function readRowCell(row, name) {
133
128
  return await cell()
134
129
  }
135
130
 
131
+ /**
132
+ * @param {AsyncRow[]} rows
133
+ * @param {string[]} columnNames
134
+ * @returns {Promise<AsyncBatch>}
135
+ */
136
+ async function materializeRows(rows, columnNames) {
137
+ const values = makeValueBuffers(columnNames.length)
138
+ /** @type {Promise<void>[]} */
139
+ const pendingReads = []
140
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
141
+ for (let columnIndex = 0; columnIndex < columnNames.length; columnIndex++) {
142
+ const name = columnNames[columnIndex]
143
+ pendingReads.push(readRowCell(rows[rowIndex], name).then(function storeValue(value) {
144
+ values[columnIndex][rowIndex] = value
145
+ }))
146
+ }
147
+ }
148
+ await Promise.all(pendingReads)
149
+ return loadedBatch(values, rows.length)
150
+ }
151
+
136
152
  /**
137
153
  * @param {number} count
138
154
  * @returns {SqlPrimitive[][]}
@@ -157,22 +173,3 @@ function loadedBatch(values, rowCount) {
157
173
  }),
158
174
  }
159
175
  }
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
- }
@@ -1,10 +1,10 @@
1
- import { readBatchColumn, selectBatch, selectedRowCount, selectionOrdinals, valueAt } from '../backend/batch.js'
1
+ import { isPromiseLike, readBatchColumn, resolveColumnResults, selectBatch, selectedRowCount, selectionOrdinals, valueAt } from '../backend/batch.js'
2
2
  import { keyify } from './utils.js'
3
3
  import { yieldToEventLoop } from './yield.js'
4
4
 
5
5
  /**
6
6
  * @import { BatchProjection, CompiledBatchExpression } from '../internalTypes.js'
7
- * @import { AsyncBatch, BatchColumn, ColumnResult, ColumnVector, ReadColumn, RowSelection, SqlPrimitive } from '../types.js'
7
+ * @import { AsyncBatch, BatchColumn, ColumnVector, ReadColumn, RowSelection, SqlPrimitive } from '../types.js'
8
8
  */
9
9
 
10
10
  const INITIAL_FILTER_WINDOW_ROWS = 256
@@ -95,7 +95,7 @@ export async function* filterBatches(batches, expression, signal, targetRows) {
95
95
  signal,
96
96
  rowOffset: rowOffset + start,
97
97
  })
98
- const predicate = result instanceof Promise ? await result : result
98
+ const predicate = isPromiseLike(result) ? await result : result
99
99
  const { selection, selectedCount } = predicateSelection(predicate, windowRowCount)
100
100
  start = end
101
101
  matchedRows += selectedCount
@@ -138,8 +138,8 @@ export async function* distinctBatches(batches, signal) {
138
138
  const results = batch.columns.map(function readColumn(_column, columnIndex) {
139
139
  return readBatchColumn({ batch, columnIndex, signal })
140
140
  })
141
- const resolved = resolveVectors(results)
142
- const vectors = resolved instanceof Promise ? await resolved : resolved
141
+ const resolved = resolveColumnResults(results)
142
+ const vectors = isPromiseLike(resolved) ? await resolved : resolved
143
143
  const rowCount = selectedRowCount(batch.selection)
144
144
  const indices = new Uint32Array(rowCount)
145
145
  /** @type {SqlPrimitive[]} */
@@ -255,20 +255,3 @@ function predicateSelection(predicate, rowCount) {
255
255
  selectedCount,
256
256
  }
257
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
- }
@@ -12,6 +12,7 @@ import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
12
12
  import { batchResult } from './batchResults.js'
13
13
  import { distinctBatches, filterBatches, limitBatches, projectExpressionBatches } from './batches.js'
14
14
  import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
15
+ import { referencesRowScope } from './rowScope.js'
15
16
  import { normalizeScanColumnResult } from './scanColumn.js'
16
17
  import { executeSort } from './sort.js'
17
18
  import { addBounds, minBounds, stableRowKey } from './utils.js'
@@ -720,26 +721,6 @@ function compileUnscopedBatchExpression(expression, columns, context) {
720
721
  : compileBatchExpression(expression, columns)
721
722
  }
722
723
 
723
- /**
724
- * Returns whether an expression reads a qualified identifier from row scope.
725
- *
726
- * @param {ExprNode} expression
727
- * @param {string[]} columns
728
- * @param {ExecuteContext} context
729
- * @returns {boolean}
730
- */
731
- function referencesRowScope(expression, columns, context) {
732
- /** @type {IdentifierNode[]} */
733
- const identifiers = []
734
- collectColumnsFromExpr(expression, identifiers)
735
- return identifiers.some(function scopedIdentifier(identifier) {
736
- return Boolean(identifier.prefix && (
737
- context.outerAliases?.has(identifier.prefix) ||
738
- context.scope?.includes(identifier.prefix) && columns.includes(identifier.prefix)
739
- ))
740
- })
741
- }
742
-
743
724
  /**
744
725
  * Executes a filter operation (WHERE clause)
745
726
  *
@@ -0,0 +1,27 @@
1
+ import { collectColumnsFromExpr } from '../plan/columns.js'
2
+
3
+ /**
4
+ * @import { ExecuteContext, ExprNode, IdentifierNode } from '../types.js'
5
+ */
6
+
7
+ /**
8
+ * Returns whether an expression reads a qualified identifier from row scope.
9
+ * A current-scope prefix is ambiguous only when it is also a child column and
10
+ * could therefore mean struct-field access.
11
+ *
12
+ * @param {ExprNode} expression
13
+ * @param {readonly string[]} columns
14
+ * @param {ExecuteContext} context
15
+ * @returns {boolean}
16
+ */
17
+ export function referencesRowScope(expression, columns, context) {
18
+ /** @type {IdentifierNode[]} */
19
+ const identifiers = []
20
+ collectColumnsFromExpr(expression, identifiers)
21
+ return identifiers.some(function scopedIdentifier(identifier) {
22
+ return Boolean(identifier.prefix && (
23
+ context.outerAliases?.has(identifier.prefix) ||
24
+ context.scope?.includes(identifier.prefix) && columns.includes(identifier.prefix)
25
+ ))
26
+ })
27
+ }
@@ -5,6 +5,7 @@ import { evaluateAll, evaluateExpr } from '../expression/evaluate.js'
5
5
  import { collectColumnsFromExpr } from '../plan/columns.js'
6
6
  import { isAggregateFunc } from '../validation/functions.js'
7
7
  import { finalizeAccumulator, newAccumulator, updateAccumulator } from './accumulator.js'
8
+ import { referencesRowScope } from './rowScope.js'
8
9
  import { sortEntriesByTerms } from './sort.js'
9
10
  import { keyify } from './utils.js'
10
11
  import { yieldToEventLoop } from './yield.js'
@@ -437,7 +438,7 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
437
438
  /** @type {CompiledBatchExpression[]} */
438
439
  const keys = []
439
440
  for (const expression of groupBy) {
440
- if (referencesRowScope(expression, context)) return undefined
441
+ if (referencesRowScope(expression, columns, context)) return undefined
441
442
  const key = compileBatchExpression(expression, columns)
442
443
  if (!key) return undefined
443
444
  keys.push(key)
@@ -449,8 +450,8 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
449
450
  const args = []
450
451
  for (const spec of specs) {
451
452
  if (spec.node.filter && !spec.star) return undefined
452
- if (spec.node.filter && referencesRowScope(spec.node.filter, context)) return undefined
453
- if (!spec.star && referencesRowScope(spec.node.args[0], context)) return undefined
453
+ if (spec.node.filter && referencesRowScope(spec.node.filter, columns, context)) return undefined
454
+ if (!spec.star && referencesRowScope(spec.node.args[0], columns, context)) return undefined
454
455
  const filter = spec.node.filter
455
456
  ? compileBatchExpression(spec.node.filter, columns)
456
457
  : undefined
@@ -468,25 +469,6 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
468
469
  }
469
470
  }
470
471
 
471
- /**
472
- * Returns whether an expression reads a qualified identifier whose table
473
- * scope the batch compiler cannot distinguish from struct-field access.
474
- *
475
- * @param {ExprNode} expression
476
- * @param {ExecuteContext} context
477
- * @returns {boolean}
478
- */
479
- function referencesRowScope(expression, context) {
480
- /** @type {IdentifierNode[]} */
481
- const identifiers = []
482
- collectColumnsFromExpr(expression, identifiers)
483
- return identifiers.some(function isScopedReference(identifier) {
484
- return Boolean(identifier.prefix && (
485
- context.scope?.includes(identifier.prefix) || context.outerAliases?.has(identifier.prefix)
486
- ))
487
- })
488
- }
489
-
490
472
  /**
491
473
  * Resolves one set of compiled expressions against a batch.
492
474
  *
@@ -1,4 +1,4 @@
1
- import { composeSelections, readBatchColumn, selectVector, selectedRowCount, valueAt } from '../backend/batch.js'
1
+ import { composeSelections, isPromiseLike, readBatchColumn, resolveColumnResults, selectVector, selectedRowCount, valueAt } from '../backend/batch.js'
2
2
  import { isPlainObject, sqlEquals } from '../execute/utils.js'
3
3
  import { yieldToEventLoop } from '../execute/yield.js'
4
4
  import { isStringFunc } from '../validation/functions.js'
@@ -126,8 +126,8 @@ function compileKernelEvaluator(node, columns) {
126
126
  const results = dependencies.map(function readDependency(columnIndex) {
127
127
  return readBatchColumn({ batch, columnIndex, selection, signal })
128
128
  })
129
- const vectors = resolveVectors(results)
130
- if (vectors instanceof Promise) {
129
+ const vectors = resolveColumnResults(results)
130
+ if (isPromiseLike(vectors)) {
131
131
  return vectors.then(function evaluateResolved(resolved) {
132
132
  signal?.throwIfAborted()
133
133
  return evaluateKernel(kernel, resolved, selection, signal, rowOffset, rowOrdinals)
@@ -640,20 +640,3 @@ function readsIdentifier(node) {
640
640
  if (node.type === 'function') return node.args.some(readsIdentifier)
641
641
  return false
642
642
  }
643
-
644
- /**
645
- * @param {ColumnResult[]} results
646
- * @returns {ColumnVector[] | Promise<ColumnVector[]>}
647
- */
648
- function resolveVectors(results) {
649
- if (results.some(function isPromise(result) { return result instanceof Promise })) {
650
- return Promise.all(results)
651
- }
652
- /** @type {ColumnVector[]} */
653
- const vectors = []
654
- for (const result of results) {
655
- if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
656
- vectors.push(result)
657
- }
658
- return vectors
659
- }