squirreling 0.11.5 → 0.12.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/README.md CHANGED
@@ -34,20 +34,23 @@ const users = [
34
34
  ]
35
35
 
36
36
  // Squirreling return types
37
+ interface QueryResults {
38
+ rows: () => AsyncGenerator<AsyncRow>
39
+ }
37
40
  interface AsyncRow {
38
41
  columns: string[]
39
42
  cells: Record<string, AsyncCell>
40
43
  }
41
44
  type AsyncCell = () => Promise<SqlPrimitive>
42
45
 
43
- // Returns an AsyncIterable of rows with async cell loading
44
- const asyncRows: AsyncIterable<AsyncRow> = executeSql({
46
+ // Returns a QueryResults object with streaming rows
47
+ const { rows } = executeSql({
45
48
  tables: { users },
46
49
  query: 'SELECT * FROM users',
47
50
  })
48
51
 
49
52
  // Process rows as they arrive (streaming)
50
- for await (const { cells } of asyncRows) {
53
+ for await (const { cells } of rows()) {
51
54
  console.log(`User id=${await cells.id()}, name=${await cells.name()}`)
52
55
  }
53
56
  ```
@@ -105,7 +108,7 @@ interface ScanOptions {
105
108
  }
106
109
 
107
110
  interface ScanResults {
108
- rows: AsyncIterable<AsyncRow> // async iterable of rows
111
+ rows(): AsyncIterable<AsyncRow> // async iterable of rows
109
112
  appliedWhere: boolean // WHERE filter applied at scan time?
110
113
  appliedLimitOffset: boolean // LIMIT and OFFSET applied at scan time?
111
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.11.5",
3
+ "version": "0.12.0",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { AsyncCells, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ScanColumnOptions, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  /**
@@ -55,12 +55,12 @@ export function memorySource({ data, columns }) {
55
55
  const start = !where ? offset ?? 0 : 0
56
56
  const end = !where && limit !== undefined ? start + limit : data.length
57
57
  return {
58
- rows: (async function* () {
58
+ async *rows() {
59
59
  for (let i = start; i < end && i < data.length; i++) {
60
60
  if (signal?.aborted) break
61
61
  yield asyncRow(data[i], scanColumns ?? columns)
62
62
  }
63
- })(),
63
+ },
64
64
  appliedWhere: false,
65
65
  appliedLimitOffset: !where,
66
66
  }
@@ -92,9 +92,9 @@ export function cachedDataSource(source) {
92
92
  const indexOffset = appliedLimitOffset && options.offset ? options.offset : 0
93
93
 
94
94
  return {
95
- rows: (async function* () {
95
+ async *rows() {
96
96
  let index = 0
97
- for await (const row of rows) {
97
+ for await (const row of rows()) {
98
98
  if (options.signal?.aborted) break
99
99
  const rowIndex = index + indexOffset
100
100
  /** @type {AsyncCells} */
@@ -115,10 +115,41 @@ export function cachedDataSource(source) {
115
115
  yield { columns: row.columns, cells }
116
116
  index++
117
117
  }
118
- })(),
118
+ },
119
119
  appliedWhere,
120
120
  appliedLimitOffset,
121
121
  }
122
122
  },
123
+ ...source.scanColumn && {
124
+ /**
125
+ * @param {ScanColumnOptions} options
126
+ * @returns {AsyncIterable<ArrayLike<SqlPrimitive>>}
127
+ */
128
+ scanColumn(options) {
129
+ const inner = source.scanColumn(options)
130
+ const indexOffset = options.offset ?? 0
131
+ return (async function* () {
132
+ let chunkStart = 0
133
+ for await (const chunk of inner) {
134
+ if (options.signal?.aborted) break
135
+ /** @type {SqlPrimitive[]} */
136
+ const cached = new Array(chunk.length)
137
+ for (let i = 0; i < chunk.length; i++) {
138
+ const cacheKey = `${chunkStart + i + indexOffset}:${options.column}`
139
+ const existing = cache.get(cacheKey)
140
+ if (existing) {
141
+ cached[i] = await existing
142
+ } else {
143
+ const value = chunk[i]
144
+ cache.set(cacheKey, Promise.resolve(value))
145
+ cached[i] = value
146
+ }
147
+ }
148
+ yield cached
149
+ chunkStart += chunk.length
150
+ }
151
+ })()
152
+ },
153
+ },
123
154
  }
124
155
  }
@@ -4,7 +4,7 @@ import { executePlan } from './execute.js'
4
4
  import { keyify } from './utils.js'
5
5
 
6
6
  /**
7
- * @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, SelectColumn, SqlPrimitive } from '../types.js'
7
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, QueryResults, SelectColumn, SqlPrimitive } from '../types.js'
8
8
  * @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
9
9
  */
10
10
 
@@ -55,52 +55,58 @@ function projectAggregateColumns(selectColumns, group, context) {
55
55
  *
56
56
  * @param {HashAggregateNode} plan
57
57
  * @param {ExecuteContext} context
58
- * @yields {AsyncRow}
58
+ * @returns {QueryResults}
59
59
  */
60
- export async function* executeHashAggregate(plan, context) {
61
- // Collect all rows
62
- /** @type {AsyncRow[]} */
63
- const allRows = []
64
- for await (const row of executePlan({ plan: plan.child, context })) {
65
- if (context.signal?.aborted) return
66
- allRows.push(row)
67
- }
60
+ export function executeHashAggregate(plan, context) {
61
+ const child = executePlan({ plan: plan.child, context })
62
+ return {
63
+ maxRows: child.maxRows,
64
+ async *rows () {
65
+ // Collect all rows
66
+ /** @type {AsyncRow[]} */
67
+ const allRows = []
68
+ for await (const row of child.rows()) {
69
+ if (context.signal?.aborted) return
70
+ allRows.push(row)
71
+ }
68
72
 
69
- // Group rows by GROUP BY keys
70
- /** @type {Map<any, AsyncRow[]>} */
71
- const groups = new Map()
73
+ // Group rows by GROUP BY keys
74
+ /** @type {Map<any, AsyncRow[]>} */
75
+ const groups = new Map()
72
76
 
73
- for (const row of allRows) {
74
- const key = keyify(...await Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context }))))
75
- let group = groups.get(key)
76
- if (!group) {
77
- group = []
78
- groups.set(key, group)
79
- }
80
- group.push(row)
81
- }
77
+ for (const row of allRows) {
78
+ const key = keyify(...await Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context }))))
79
+ let group = groups.get(key)
80
+ if (!group) {
81
+ group = []
82
+ groups.set(key, group)
83
+ }
84
+ group.push(row)
85
+ }
82
86
 
83
- // Yield one row per group
84
- for (const group of groups.values()) {
85
- const asyncRow = projectAggregateColumns(plan.columns, group, context)
87
+ // Yield one row per group
88
+ for (const group of groups.values()) {
89
+ const asyncRow = projectAggregateColumns(plan.columns, group, context)
86
90
 
87
- // Apply HAVING filter
88
- if (plan.having) {
89
- /** @type {AsyncRow} */
90
- const havingRow = {
91
- columns: [...group[0].columns, ...asyncRow.columns],
92
- cells: { ...group[0].cells, ...asyncRow.cells },
93
- }
94
- const passes = await evaluateExpr({
95
- node: plan.having,
96
- row: havingRow,
97
- rows: group,
98
- context,
99
- })
100
- if (!passes) continue
101
- }
91
+ // Apply HAVING filter
92
+ if (plan.having) {
93
+ /** @type {AsyncRow} */
94
+ const havingRow = {
95
+ columns: [...group[0].columns, ...asyncRow.columns],
96
+ cells: { ...group[0].cells, ...asyncRow.cells },
97
+ }
98
+ const passes = await evaluateExpr({
99
+ node: plan.having,
100
+ row: havingRow,
101
+ rows: group,
102
+ context,
103
+ })
104
+ if (!passes) continue
105
+ }
102
106
 
103
- yield asyncRow
107
+ yield asyncRow
108
+ }
109
+ },
104
110
  }
105
111
  }
106
112
 
@@ -109,43 +115,54 @@ export async function* executeHashAggregate(plan, context) {
109
115
  *
110
116
  * @param {ScalarAggregateNode} plan
111
117
  * @param {ExecuteContext} context
112
- * @yields {AsyncRow}
118
+ * @returns {QueryResults}
113
119
  */
114
- export async function* executeScalarAggregate(plan, context) {
120
+ export function executeScalarAggregate(plan, context) {
115
121
  // Fast path: use scanColumn when available
116
122
  const fast = tryColumnScanAggregate(plan, context)
117
123
  if (fast) {
118
- yield* fast
119
- return
124
+ return {
125
+ numRows: 1,
126
+ maxRows: 1,
127
+ rows: fast,
128
+ }
120
129
  }
121
130
 
122
- // Collect all rows into single group
123
- /** @type {AsyncRow[]} */
124
- const group = []
125
- for await (const row of executePlan({ plan: plan.child, context })) {
126
- if (context.signal?.aborted) return
127
- group.push(row)
128
- }
131
+ const child = executePlan({ plan: plan.child, context })
132
+ return {
133
+ numRows: plan.having ? undefined : 1,
134
+ maxRows: 1,
135
+ async *rows () {
136
+ // Collect all rows into single group
137
+ /** @type {AsyncRow[]} */
138
+ const group = []
139
+ for await (const row of child.rows()) {
140
+ if (context.signal?.aborted) return
141
+ group.push(row)
142
+ }
129
143
 
130
- const asyncRow = projectAggregateColumns(plan.columns, group, context)
144
+ const asyncRow = projectAggregateColumns(plan.columns, group, context)
131
145
 
132
- // Apply HAVING filter
133
- if (plan.having) {
134
- /** @type {AsyncRow} */
135
- const havingRow = {
136
- columns: [...group[0].columns, ...asyncRow.columns],
137
- cells: { ...group[0].cells, ...asyncRow.cells },
138
- }
139
- const passes = await evaluateExpr({
140
- node: plan.having,
141
- row: havingRow,
142
- rows: group,
143
- context,
144
- })
145
- if (!passes) return
146
- }
146
+ // Apply HAVING filter
147
+ if (plan.having) {
148
+ const baseRow = group[0] ?? { columns: [], cells: {} }
149
+ /** @type {AsyncRow} */
150
+ const havingRow = {
151
+ columns: [...baseRow.columns, ...asyncRow.columns],
152
+ cells: { ...baseRow.cells, ...asyncRow.cells },
153
+ }
154
+ const passes = await evaluateExpr({
155
+ node: plan.having,
156
+ row: havingRow,
157
+ rows: group,
158
+ context,
159
+ })
160
+ if (!passes) return
161
+ }
147
162
 
148
- yield asyncRow
163
+ yield asyncRow
164
+ },
165
+ }
149
166
  }
150
167
 
151
168
  /**
@@ -163,7 +180,7 @@ export async function* executeScalarAggregate(plan, context) {
163
180
  *
164
181
  * @param {ScalarAggregateNode} plan
165
182
  * @param {ExecuteContext} context
166
- * @returns {AsyncGenerator<AsyncRow> | undefined}
183
+ * @returns {(() => AsyncGenerator<AsyncRow>) | undefined}
167
184
  */
168
185
  function tryColumnScanAggregate(plan, { tables, signal }) {
169
186
  // No HAVING support in fast path
@@ -188,7 +205,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
188
205
  specs.push(spec)
189
206
  }
190
207
 
191
- return (async function* () {
208
+ return async function* () {
192
209
  /** @type {string[]} */
193
210
  const columns = []
194
211
  /** @type {AsyncCells} */
@@ -200,7 +217,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
200
217
  }
201
218
 
202
219
  yield { columns, cells }
203
- })()
220
+ }
204
221
  }
205
222
 
206
223
  /**