squirreling 0.8.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -37,10 +37,10 @@
37
37
  "test": "vitest run"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "25.2.2",
40
+ "@types/node": "25.2.3",
41
41
  "@vitest/coverage-v8": "4.0.18",
42
42
  "eslint": "9.39.2",
43
- "eslint-plugin-jsdoc": "62.5.4",
43
+ "eslint-plugin-jsdoc": "62.5.5",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.18"
46
46
  }
@@ -3,8 +3,8 @@ import { defaultDerivedAlias, stringify } from './utils.js'
3
3
  import { executePlan } from './execute.js'
4
4
 
5
5
  /**
6
- * @import { AsyncCells, AsyncDataSource, AsyncRow, SelectColumn, UserDefinedFunction } from '../types.js'
7
- * @import { ExecuteContext, HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
6
+ * @import { AsyncCells, AsyncRow, ExecuteContext, SelectColumn } from '../types.js'
7
+ * @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
8
8
  */
9
9
 
10
10
  /**
@@ -12,12 +12,10 @@ import { executePlan } from './execute.js'
12
12
  *
13
13
  * @param {SelectColumn[]} selectColumns
14
14
  * @param {AsyncRow[]} group
15
- * @param {Record<string, AsyncDataSource>} tables
16
- * @param {Record<string, UserDefinedFunction>} [functions]
17
- * @param {AbortSignal} [signal]
15
+ * @param {ExecuteContext} context
18
16
  * @returns {AsyncRow}
19
17
  */
20
- function projectAggregateColumns(selectColumns, group, tables, functions, signal) {
18
+ function projectAggregateColumns(selectColumns, group, context) {
21
19
  /** @type {string[]} */
22
20
  const columns = []
23
21
  /** @type {AsyncCells} */
@@ -38,10 +36,8 @@ function projectAggregateColumns(selectColumns, group, tables, functions, signal
38
36
  cells[alias] = () => evaluateExpr({
39
37
  node: col.expr,
40
38
  row: group[0] ?? { columns: [], cells: {} },
41
- tables,
42
- functions,
43
39
  rows: group,
44
- signal,
40
+ context,
45
41
  })
46
42
  }
47
43
  }
@@ -57,13 +53,11 @@ function projectAggregateColumns(selectColumns, group, tables, functions, signal
57
53
  * @yields {AsyncRow}
58
54
  */
59
55
  export async function* executeHashAggregate(plan, context) {
60
- const { tables, functions, signal } = context
61
-
62
56
  // Collect all rows
63
57
  /** @type {AsyncRow[]} */
64
58
  const allRows = []
65
- for await (const row of executePlan(plan.child, context)) {
66
- if (signal?.aborted) return
59
+ for await (const row of executePlan({ plan: plan.child, context })) {
60
+ if (context.signal?.aborted) return
67
61
  allRows.push(row)
68
62
  }
69
63
 
@@ -77,7 +71,7 @@ export async function* executeHashAggregate(plan, context) {
77
71
  /** @type {string[]} */
78
72
  const keyParts = []
79
73
  for (const expr of plan.groupBy) {
80
- const v = await evaluateExpr({ node: expr, row, tables, functions, signal })
74
+ const v = await evaluateExpr({ node: expr, row, context })
81
75
  keyParts.push(stringify(v))
82
76
  }
83
77
  const key = keyParts.join('|')
@@ -92,18 +86,16 @@ export async function* executeHashAggregate(plan, context) {
92
86
 
93
87
  // Yield one row per group
94
88
  for (const group of groups) {
95
- const asyncRow = projectAggregateColumns(plan.columns, group, tables, functions, signal)
89
+ const asyncRow = projectAggregateColumns(plan.columns, group, context)
96
90
 
97
91
  // Apply HAVING filter
98
92
  if (plan.having) {
99
- const context = { ...group[0], ...asyncRow }
93
+ const havingRow = { ...group[0], ...asyncRow }
100
94
  const passes = await evaluateExpr({
101
95
  node: plan.having,
102
- row: context,
96
+ row: havingRow,
103
97
  rows: group,
104
- tables,
105
- functions,
106
- signal,
98
+ context,
107
99
  })
108
100
  if (!passes) continue
109
101
  }
@@ -120,28 +112,24 @@ export async function* executeHashAggregate(plan, context) {
120
112
  * @yields {AsyncRow}
121
113
  */
122
114
  export async function* executeScalarAggregate(plan, context) {
123
- const { tables, functions, signal } = context
124
-
125
115
  // Collect all rows into single group
126
116
  /** @type {AsyncRow[]} */
127
117
  const group = []
128
- for await (const row of executePlan(plan.child, context)) {
129
- if (signal?.aborted) return
118
+ for await (const row of executePlan({ plan: plan.child, context })) {
119
+ if (context.signal?.aborted) return
130
120
  group.push(row)
131
121
  }
132
122
 
133
- const asyncRow = projectAggregateColumns(plan.columns, group, tables, functions, signal)
123
+ const asyncRow = projectAggregateColumns(plan.columns, group, context)
134
124
 
135
125
  // Apply HAVING filter
136
126
  if (plan.having) {
137
- const context = { ...group[0], ...asyncRow }
127
+ const havingRow = { ...group[0], ...asyncRow }
138
128
  const passes = await evaluateExpr({
139
129
  node: plan.having,
140
- row: context,
130
+ row: havingRow,
141
131
  rows: group,
142
- tables,
143
- functions,
144
- signal,
132
+ context,
145
133
  })
146
134
  if (!passes) return
147
135
  }
@@ -2,35 +2,26 @@ import { memorySource } from '../backend/dataSource.js'
2
2
  import { tableNotFoundError } from '../executionErrors.js'
3
3
  import { evaluateExpr } from '../expression/evaluate.js'
4
4
  import { parseSql } from '../parse/parse.js'
5
- import { missingClauseError } from '../parseErrors.js'
6
- import { queryPlan } from '../plan/plan.js'
5
+ import { planSql } from '../plan/plan.js'
7
6
  import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
8
7
  import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
9
8
  import { executeSort } from './sort.js'
10
9
  import { defaultDerivedAlias, stableRowKey } from './utils.js'
11
10
 
12
11
  /**
13
- * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, SelectStatement, UserDefinedFunction } from '../types.js'
14
- * @import { DistinctNode, ExecuteContext, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode } from '../plan/types.js'
12
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, SelectStatement } from '../types.js'
13
+ * @import { DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode } from '../plan/types.js'
15
14
  */
16
15
 
17
16
  /**
18
- * Executes a SQL SELECT query against named data sources
17
+ * Executes a SQL SELECT query against tables
19
18
  *
20
- * @param {ExecuteSqlOptions} options - the execution options
21
- * @yields {AsyncRow} async generator yielding result rows
19
+ * @param {ExecuteSqlOptions} options
20
+ * @yields {AsyncRow}
22
21
  */
23
22
  export async function* executeSql({ tables, query, functions, signal }) {
24
23
  const select = typeof query === 'string' ? parseSql({ query, functions }) : query
25
24
 
26
- // Check for unsupported operations
27
- if (!select.from) {
28
- throw missingClauseError({
29
- missing: 'FROM clause',
30
- context: 'SELECT statement',
31
- })
32
- }
33
-
34
25
  // Normalize tables: convert arrays to AsyncDataSource
35
26
  /** @type {Record<string, AsyncDataSource>} */
36
27
  const normalizedTables = {}
@@ -42,7 +33,7 @@ export async function* executeSql({ tables, query, functions, signal }) {
42
33
  }
43
34
  }
44
35
 
45
- yield* executeSelect({ select, tables: normalizedTables, functions, signal })
36
+ yield* executeSelect({ select, context: { tables: normalizedTables, functions, signal } })
46
37
  }
47
38
 
48
39
  /**
@@ -50,24 +41,23 @@ export async function* executeSql({ tables, query, functions, signal }) {
50
41
  *
51
42
  * @param {Object} options
52
43
  * @param {SelectStatement} options.select
53
- * @param {Record<string, AsyncDataSource>} options.tables
54
- * @param {Record<string, UserDefinedFunction>} [options.functions]
55
- * @param {AbortSignal} [options.signal]
44
+ * @param {ExecuteContext} options.context
56
45
  * @yields {AsyncRow}
57
46
  */
58
- export async function* executeSelect({ select, tables, functions, signal }) {
59
- const plan = queryPlan(select)
60
- yield* executePlan(plan, { tables, functions, signal })
47
+ export async function* executeSelect({ select, context }) {
48
+ const plan = planSql({ query: select, functions: context.functions })
49
+ yield* executePlan({ plan, context })
61
50
  }
62
51
 
63
52
  /**
64
53
  * Executes a query plan and yields result rows
65
54
  *
66
- * @param {QueryPlan} plan - the query plan to execute
67
- * @param {ExecuteContext} context - execution context
55
+ * @param {Object} options
56
+ * @param {QueryPlan} options.plan - the query plan to execute
57
+ * @param {ExecuteContext} options.context - execution context
68
58
  * @returns {AsyncGenerator<AsyncRow>}
69
59
  */
70
- export async function* executePlan(plan, context) {
60
+ export async function* executePlan({ plan, context }) {
71
61
  if (plan.type === 'Scan') {
72
62
  yield* executeScan(plan, context)
73
63
  } else if (plan.type === 'Filter') {
@@ -147,7 +137,7 @@ async function* filterRows(rows, condition, context) {
147
137
  for await (const row of rows) {
148
138
  if (context.signal?.aborted) return
149
139
  rowIndex++
150
- const pass = await evaluateExpr({ node: condition, row, rowIndex, ...context })
140
+ const pass = await evaluateExpr({ node: condition, row, rowIndex, context })
151
141
  if (pass) yield row
152
142
  }
153
143
  }
@@ -187,7 +177,7 @@ async function* limitRows(rows, limit, offset, signal) {
187
177
  * @yields {AsyncRow}
188
178
  */
189
179
  async function* executeFilter(plan, context) {
190
- yield* filterRows(executePlan(plan.child, context), plan.condition, context)
180
+ yield* filterRows(executePlan({ plan: plan.child, context }), plan.condition, context)
191
181
  }
192
182
 
193
183
  /**
@@ -198,11 +188,10 @@ async function* executeFilter(plan, context) {
198
188
  * @yields {AsyncRow}
199
189
  */
200
190
  async function* executeProject(plan, context) {
201
- const { tables, functions, signal } = context
202
191
  let rowIndex = 0
203
192
 
204
- for await (const row of executePlan(plan.child, context)) {
205
- if (signal?.aborted) return
193
+ for await (const row of executePlan({ plan: plan.child, context })) {
194
+ if (context.signal?.aborted) return
206
195
  rowIndex++
207
196
  const currentRowIndex = rowIndex
208
197
 
@@ -223,10 +212,8 @@ async function* executeProject(plan, context) {
223
212
  cells[alias] = () => evaluateExpr({
224
213
  node: col.expr,
225
214
  row,
226
- tables,
227
- functions,
228
215
  rowIndex: currentRowIndex,
229
- signal,
216
+ context,
230
217
  })
231
218
  }
232
219
  }
@@ -248,7 +235,7 @@ async function* executeDistinct(plan, context) {
248
235
  /** @type {Set<string>} */
249
236
  const seen = new Set()
250
237
 
251
- for await (const row of executePlan(plan.child, context)) {
238
+ for await (const row of executePlan({ plan: plan.child, context })) {
252
239
  if (signal?.aborted) return
253
240
 
254
241
  const key = await stableRowKey(row.cells)
@@ -267,5 +254,5 @@ async function* executeDistinct(plan, context) {
267
254
  * @yields {AsyncRow}
268
255
  */
269
256
  async function* executeLimit(plan, context) {
270
- yield* limitRows(executePlan(plan.child, context), plan.limit, plan.offset, context.signal)
257
+ yield* limitRows(executePlan({ plan: plan.child, context }), plan.limit, plan.offset, context.signal)
271
258
  }
@@ -1,11 +1,10 @@
1
1
  import { evaluateExpr } from '../expression/evaluate.js'
2
- import { missingClauseError } from '../parseErrors.js'
3
2
  import { stringify } from './utils.js'
4
3
  import { executePlan } from './execute.js'
5
4
 
6
5
  /**
7
- * @import { AsyncCells, AsyncRow } from '../types.js'
8
- * @import { ExecuteContext, HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
6
+ * @import { AsyncCells, AsyncRow, ExecuteContext } from '../types.js'
7
+ * @import { HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
9
8
  */
10
9
 
11
10
  /**
@@ -16,22 +15,14 @@ import { executePlan } from './execute.js'
16
15
  * @yields {AsyncRow}
17
16
  */
18
17
  export async function* executeNestedLoopJoin(plan, context) {
19
- const { tables, functions, signal } = context
20
18
  const leftTable = plan.leftAlias
21
19
  const rightTable = plan.rightAlias
22
20
 
23
- if (!plan.condition) {
24
- throw missingClauseError({
25
- missing: 'ON condition',
26
- context: 'JOIN',
27
- })
28
- }
29
-
30
21
  // Buffer right rows
31
22
  /** @type {AsyncRow[]} */
32
23
  const rightRows = []
33
- for await (const row of executePlan(plan.right, context)) {
34
- if (signal?.aborted) return
24
+ for await (const row of executePlan({ plan: plan.right, context })) {
25
+ if (context.signal?.aborted) return
35
26
  rightRows.push(row)
36
27
  }
37
28
 
@@ -43,8 +34,8 @@ export async function* executeNestedLoopJoin(plan, context) {
43
34
  /** @type {Set<AsyncRow> | null} */
44
35
  const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
45
36
 
46
- for await (const leftRow of executePlan(plan.left, context)) {
47
- if (signal?.aborted) break
37
+ for await (const leftRow of executePlan({ plan: plan.left, context })) {
38
+ if (context.signal?.aborted) break
48
39
 
49
40
  if (!leftPrefixedCols) {
50
41
  leftPrefixedCols = prefixColumns(leftRow.columns, leftTable)
@@ -57,9 +48,7 @@ export async function* executeNestedLoopJoin(plan, context) {
57
48
  const matches = await evaluateExpr({
58
49
  node: plan.condition,
59
50
  row: tempMerged,
60
- tables,
61
- functions,
62
- signal,
51
+ context,
63
52
  })
64
53
 
65
54
  if (matches) {
@@ -101,14 +90,14 @@ export async function* executePositionalJoin(plan, context) {
101
90
  // Buffer both sides (required for positional join)
102
91
  /** @type {AsyncRow[]} */
103
92
  const leftRows = []
104
- for await (const row of executePlan(plan.left, context)) {
93
+ for await (const row of executePlan({ plan: plan.left, context })) {
105
94
  if (signal?.aborted) return
106
95
  leftRows.push(row)
107
96
  }
108
97
 
109
98
  /** @type {AsyncRow[]} */
110
99
  const rightRows = []
111
- for await (const row of executePlan(plan.right, context)) {
100
+ for await (const row of executePlan({ plan: plan.right, context })) {
112
101
  if (signal?.aborted) return
113
102
  rightRows.push(row)
114
103
  }
@@ -135,15 +124,14 @@ export async function* executePositionalJoin(plan, context) {
135
124
  * @yields {AsyncRow}
136
125
  */
137
126
  export async function* executeHashJoin(plan, context) {
138
- const { tables, functions, signal } = context
139
127
  const leftTable = plan.leftAlias
140
128
  const rightTable = plan.rightAlias
141
129
 
142
130
  // Buffer right rows and build hash map
143
131
  /** @type {AsyncRow[]} */
144
132
  const rightRows = []
145
- for await (const row of executePlan(plan.right, context)) {
146
- if (signal?.aborted) return
133
+ for await (const row of executePlan({ plan: plan.right, context })) {
134
+ if (context.signal?.aborted) return
147
135
  rightRows.push(row)
148
136
  }
149
137
 
@@ -153,9 +141,7 @@ export async function* executeHashJoin(plan, context) {
153
141
  const keyValue = await evaluateExpr({
154
142
  node: plan.rightKey,
155
143
  row: rightRow,
156
- tables,
157
- functions,
158
- signal,
144
+ context,
159
145
  })
160
146
  if (keyValue == null) continue
161
147
  const keyStr = stringify(keyValue)
@@ -177,8 +163,8 @@ export async function* executeHashJoin(plan, context) {
177
163
  const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
178
164
 
179
165
  // Probe phase: stream left rows
180
- for await (const leftRow of executePlan(plan.left, context)) {
181
- if (signal?.aborted) break
166
+ for await (const leftRow of executePlan({ plan: plan.left, context })) {
167
+ if (context.signal?.aborted) break
182
168
 
183
169
  if (!leftPrefixedCols) {
184
170
  leftPrefixedCols = prefixColumns(leftRow.columns, leftTable)
@@ -187,9 +173,7 @@ export async function* executeHashJoin(plan, context) {
187
173
  const keyValue = await evaluateExpr({
188
174
  node: plan.leftKey,
189
175
  row: leftRow,
190
- tables,
191
- functions,
192
- signal,
176
+ context,
193
177
  })
194
178
  const keyStr = stringify(keyValue)
195
179
  const matchingRightRows = hashMap.get(keyStr)
@@ -3,8 +3,8 @@ import { executePlan } from './execute.js'
3
3
  import { compareForTerm } from './utils.js'
4
4
 
5
5
  /**
6
- * @import { AsyncRow, SqlPrimitive } from '../types.js'
7
- * @import { ExecuteContext, SortNode } from '../plan/types.js'
6
+ * @import { AsyncRow, ExecuteContext, SqlPrimitive } from '../types.js'
7
+ * @import { SortNode } from '../plan/types.js'
8
8
  */
9
9
 
10
10
  /**
@@ -15,13 +15,11 @@ import { compareForTerm } from './utils.js'
15
15
  * @yields {AsyncRow}
16
16
  */
17
17
  export async function* executeSort(plan, context) {
18
- const { tables, functions, signal } = context
19
-
20
18
  // Buffer all rows
21
19
  /** @type {AsyncRow[]} */
22
20
  const rows = []
23
- for await (const row of executePlan(plan.child, context)) {
24
- if (signal?.aborted) return
21
+ for await (const row of executePlan({ plan: plan.child, context })) {
22
+ if (context.signal?.aborted) return
25
23
  rows.push(row)
26
24
  }
27
25
 
@@ -51,10 +49,7 @@ export async function* executeSort(plan, context) {
51
49
  evaluatedValues[idx][orderByIdx] = await evaluateExpr({
52
50
  node: term.expr,
53
51
  row: rows[idx],
54
- tables,
55
- functions,
56
- aliases: plan.aliases,
57
- signal,
52
+ context,
58
53
  })
59
54
  }
60
55
  }