squirreling 0.12.0 → 0.12.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/README.md CHANGED
@@ -35,7 +35,10 @@ const users = [
35
35
 
36
36
  // Squirreling return types
37
37
  interface QueryResults {
38
- rows: () => AsyncGenerator<AsyncRow>
38
+ columns: string[]
39
+ numRows?: number
40
+ maxRows?: number
41
+ rows(): AsyncGenerator<AsyncRow>
39
42
  }
40
43
  interface AsyncRow {
41
44
  columns: string[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -39,11 +39,11 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "25.5.0",
43
- "@vitest/coverage-v8": "4.1.2",
42
+ "@types/node": "25.5.2",
43
+ "@vitest/coverage-v8": "4.1.3",
44
44
  "eslint": "9.39.2",
45
45
  "eslint-plugin-jsdoc": "62.9.0",
46
46
  "typescript": "6.0.2",
47
- "vitest": "4.1.2"
47
+ "vitest": "4.1.3"
48
48
  }
49
49
  }
@@ -1,6 +1,6 @@
1
1
  import { derivedAlias } from '../expression/alias.js'
2
2
  import { evaluateExpr } from '../expression/evaluate.js'
3
- import { executePlan } from './execute.js'
3
+ import { executePlan, selectColumnNames } from './execute.js'
4
4
  import { keyify } from './utils.js'
5
5
 
6
6
  /**
@@ -60,6 +60,7 @@ function projectAggregateColumns(selectColumns, group, context) {
60
60
  export function executeHashAggregate(plan, context) {
61
61
  const child = executePlan({ plan: plan.child, context })
62
62
  return {
63
+ columns: selectColumnNames(plan.columns, child.columns),
63
64
  maxRows: child.maxRows,
64
65
  async *rows () {
65
66
  // Collect all rows
@@ -119,9 +120,11 @@ export function executeHashAggregate(plan, context) {
119
120
  */
120
121
  export function executeScalarAggregate(plan, context) {
121
122
  // Fast path: use scanColumn when available
123
+ const scalarColumns = selectColumnNames(plan.columns, [])
122
124
  const fast = tryColumnScanAggregate(plan, context)
123
125
  if (fast) {
124
126
  return {
127
+ columns: scalarColumns,
125
128
  numRows: 1,
126
129
  maxRows: 1,
127
130
  rows: fast,
@@ -130,6 +133,7 @@ export function executeScalarAggregate(plan, context) {
130
133
 
131
134
  const child = executePlan({ plan: plan.child, context })
132
135
  return {
136
+ columns: selectColumnNames(plan.columns, child.columns),
133
137
  numRows: plan.having ? undefined : 1,
134
138
  maxRows: 1,
135
139
  async *rows () {
@@ -10,7 +10,7 @@ import { executeSort } from './sort.js'
10
10
  import { addBounds, minBounds, stableRowKey } from './utils.js'
11
11
 
12
12
  /**
13
- * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, QueryResults, Statement } from '../types.js'
13
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, QueryResults, SelectColumn, Statement } from '../types.js'
14
14
  * @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode } from '../plan/types.js'
15
15
  */
16
16
 
@@ -88,7 +88,33 @@ export function executePlan({ plan, context }) {
88
88
  } else if (plan.type === 'SetOperation') {
89
89
  return executeSetOperation(plan, context)
90
90
  }
91
- return { async *rows () {} }
91
+ return { columns: [], async *rows () {} }
92
+ }
93
+
94
+ /**
95
+ * Derives output column names from SELECT columns and available child columns.
96
+ *
97
+ * @param {SelectColumn[]} selectColumns
98
+ * @param {string[]} childColumns
99
+ * @returns {string[]}
100
+ */
101
+ export function selectColumnNames(selectColumns, childColumns) {
102
+ /** @type {string[]} */
103
+ const result = []
104
+ for (const col of selectColumns) {
105
+ if (col.type === 'star') {
106
+ const prefix = col.table ? `${col.table}.` : undefined
107
+ for (const key of childColumns) {
108
+ if (prefix && !key.startsWith(prefix)) continue
109
+ const dotIndex = key.indexOf('.')
110
+ const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
111
+ result.push(outputKey)
112
+ }
113
+ } else {
114
+ result.push(col.alias ?? derivedAlias(col.expr))
115
+ }
116
+ }
117
+ return result
92
118
  }
93
119
 
94
120
  /**
@@ -113,6 +139,7 @@ function executeScan(plan, context) {
113
139
  })
114
140
  const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
115
141
  return {
142
+ columns: [column],
116
143
  numRows: scanRows,
117
144
  maxRows: scanRows,
118
145
  async *rows () {
@@ -142,6 +169,7 @@ function executeScan(plan, context) {
142
169
 
143
170
  const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
144
171
  return {
172
+ columns: plan.hints.columns ?? table.columns,
145
173
  numRows: !plan.hints.where ? scanRows : undefined,
146
174
  maxRows: scanRows,
147
175
  async *rows () {
@@ -174,6 +202,7 @@ function executeCount(plan, context) {
174
202
  const table = validateTable({ ...plan, tables })
175
203
 
176
204
  return {
205
+ columns: plan.columns.map(col => col.alias ?? derivedAlias(col.expr)),
177
206
  numRows: 1,
178
207
  maxRows: 1,
179
208
  async *rows () {
@@ -300,6 +329,7 @@ async function* limitRows(rows, limit, offset, signal) {
300
329
  function executeFilter(plan, context) {
301
330
  const child = executePlan({ plan: plan.child, context })
302
331
  return {
332
+ columns: child.columns,
303
333
  maxRows: child.maxRows,
304
334
  rows: () => filterRows(child.rows(), plan.condition, context),
305
335
  }
@@ -315,6 +345,7 @@ function executeFilter(plan, context) {
315
345
  function executeProject(plan, context) {
316
346
  const child = executePlan({ plan: plan.child, context })
317
347
  return {
348
+ columns: selectColumnNames(plan.columns, child.columns),
318
349
  numRows: child.numRows,
319
350
  maxRows: child.maxRows,
320
351
  async *rows () {
@@ -369,6 +400,7 @@ function executeProject(plan, context) {
369
400
  function executeDistinct(plan, context) {
370
401
  const child = executePlan({ plan: plan.child, context })
371
402
  return {
403
+ columns: child.columns,
372
404
  maxRows: child.maxRows,
373
405
  async *rows () {
374
406
  const { signal } = context
@@ -421,6 +453,7 @@ function executeDistinct(plan, context) {
421
453
  function executeLimit(plan, context) {
422
454
  const child = executePlan({ plan: plan.child, context })
423
455
  return {
456
+ columns: child.columns,
424
457
  numRows: computeScanRows(child.numRows, plan.limit, plan.offset),
425
458
  maxRows: computeScanRows(child.maxRows, plan.limit, plan.offset),
426
459
  rows: () => limitRows(child.rows(), plan.limit, plan.offset, context.signal),
@@ -442,6 +475,7 @@ function executeSetOperation(plan, context) {
442
475
  const left = executePlan({ plan: plan.left, context })
443
476
  const right = executePlan({ plan: plan.right, context })
444
477
  return {
478
+ columns: left.columns,
445
479
  numRows: addBounds(left.numRows, right.numRows),
446
480
  maxRows: addBounds(left.maxRows, right.maxRows),
447
481
  async *rows () {
@@ -454,6 +488,7 @@ function executeSetOperation(plan, context) {
454
488
  const left = executePlan({ plan: plan.left, context })
455
489
  const right = executePlan({ plan: plan.right, context })
456
490
  return {
491
+ columns: left.columns,
457
492
  maxRows: addBounds(left.maxRows, right.maxRows),
458
493
  async *rows () {
459
494
  // UNION: yield deduplicated rows from both sides
@@ -481,6 +516,7 @@ function executeSetOperation(plan, context) {
481
516
  const left = executePlan({ plan: plan.left, context })
482
517
  const right = executePlan({ plan: plan.right, context })
483
518
  return {
519
+ columns: left.columns,
484
520
  maxRows: minBounds(left.maxRows, right.maxRows),
485
521
  async *rows () {
486
522
  // Materialize right side keys
@@ -522,6 +558,7 @@ function executeSetOperation(plan, context) {
522
558
  const left = executePlan({ plan: plan.left, context })
523
559
  const right = executePlan({ plan: plan.right, context })
524
560
  return {
561
+ columns: left.columns,
525
562
  maxRows: left.maxRows,
526
563
  async *rows () {
527
564
  // Materialize right side keys
@@ -18,6 +18,7 @@ export function executeNestedLoopJoin(plan, context) {
18
18
  const left = executePlan({ plan: plan.left, context })
19
19
  const right = executePlan({ plan: plan.right, context })
20
20
  return {
21
+ columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
21
22
  async *rows () {
22
23
  const leftTable = plan.leftAlias
23
24
  const rightTable = plan.rightAlias
@@ -93,6 +94,7 @@ export function executePositionalJoin(plan, context) {
93
94
  const numRows = left.numRows !== undefined && right.numRows !== undefined
94
95
  ? Math.max(left.numRows, right.numRows) : undefined
95
96
  return {
97
+ columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
96
98
  numRows,
97
99
  maxRows: maxBounds(left.maxRows, right.maxRows),
98
100
  async *rows () {
@@ -140,6 +142,7 @@ export function executeHashJoin(plan, context) {
140
142
  const left = executePlan({ plan: plan.left, context })
141
143
  const right = executePlan({ plan: plan.right, context })
142
144
  return {
145
+ columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
143
146
  async *rows () {
144
147
  const leftTable = plan.leftAlias
145
148
  const rightTable = plan.rightAlias
@@ -233,6 +236,22 @@ function createNullRow(columns) {
233
236
  return { columns, cells }
234
237
  }
235
238
 
239
+ /**
240
+ * Merges column name arrays with table prefixes, matching mergeRows logic.
241
+ *
242
+ * @param {string[]} leftColumns
243
+ * @param {string[]} rightColumns
244
+ * @param {string} leftTable
245
+ * @param {string} rightTable
246
+ * @returns {string[]}
247
+ */
248
+ function mergeColumnNames(leftColumns, rightColumns, leftTable, rightTable) {
249
+ return [
250
+ ...leftColumns.map(c => c.includes('.') ? c : `${leftTable}.${c}`),
251
+ ...rightColumns.map(c => c.includes('.') ? c : `${rightTable}.${c}`),
252
+ ]
253
+ }
254
+
236
255
  /**
237
256
  * Merges two rows into one, prefixing columns with table names
238
257
  *
@@ -17,6 +17,7 @@ import { compareForTerm } from './utils.js'
17
17
  export function executeSort(plan, context) {
18
18
  const child = executePlan({ plan: plan.child, context })
19
19
  return {
20
+ columns: child.columns,
20
21
  numRows: child.numRows,
21
22
  maxRows: child.maxRows,
22
23
  async *rows () {
package/src/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export { QueryPlan } from './plan/types.js'
8
8
  * Result of executing a SQL query.
9
9
  */
10
10
  export interface QueryResults {
11
+ columns: string[]
11
12
  rows(): AsyncGenerator<AsyncRow>
12
13
  numRows?: number
13
14
  maxRows?: number
@@ -68,7 +69,6 @@ export interface AsyncDataSource {
68
69
  */
69
70
  export interface ScanResults {
70
71
  rows(): AsyncIterable<AsyncRow>
71
- numRows?: number // exact row count if known
72
72
  appliedWhere: boolean // WHERE filter applied at scan time?
73
73
  appliedLimitOffset: boolean // LIMIT and OFFSET applied at scan time?
74
74
  }