squirreling 0.11.2 → 0.11.3

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.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -244,7 +244,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
244
244
  if (spec.funcName === 'COUNT' && spec.distinct) {
245
245
  const seen = new Set()
246
246
  for await (const chunk of values) {
247
- if (signal?.aborted) return null
247
+ if (signal?.aborted) return
248
248
  for (let i = 0; i < chunk.length; i++) {
249
249
  const v = chunk[i]
250
250
  if (v == null) continue
@@ -257,7 +257,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
257
257
  if (spec.funcName === 'COUNT') {
258
258
  let count = 0
259
259
  for await (const chunk of values) {
260
- if (signal?.aborted) return null
260
+ if (signal?.aborted) return
261
261
  for (let i = 0; i < chunk.length; i++) {
262
262
  if (chunk[i] != null) count++
263
263
  }
@@ -274,7 +274,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
274
274
  let max = null
275
275
 
276
276
  for await (const chunk of values) {
277
- if (signal?.aborted) return null
277
+ if (signal?.aborted) return
278
278
  for (let i = 0; i < chunk.length; i++) {
279
279
  const v = chunk[i]
280
280
  if (v == null) continue
@@ -98,6 +98,29 @@ async function* executeScan(plan, context) {
98
98
  const table = validateTable({ ...plan, tables })
99
99
  validateScan({ ...plan, tables })
100
100
 
101
+ // Fast path: single column scan without WHERE
102
+ if (table.scanColumn && plan.hints.columns?.length === 1 && !plan.hints.where) {
103
+ const column = plan.hints.columns[0]
104
+ const chunks = table.scanColumn({
105
+ column,
106
+ limit: plan.hints.limit,
107
+ offset: plan.hints.offset,
108
+ signal,
109
+ })
110
+ const columns = [column]
111
+ for await (const chunk of chunks) {
112
+ if (signal?.aborted) return
113
+ for (let i = 0; i < chunk.length; i++) {
114
+ const value = chunk[i]
115
+ yield {
116
+ columns,
117
+ cells: { [column]: () => Promise.resolve(value) },
118
+ }
119
+ }
120
+ }
121
+ return
122
+ }
123
+
101
124
  // do the scan
102
125
  const { rows, appliedWhere, appliedLimitOffset } = table.scan({ ...plan.hints, signal })
103
126
 
@@ -169,19 +169,25 @@ function parseIntersectOperations(state) {
169
169
  */
170
170
  function parseSelect(state) {
171
171
  const { positionStart } = current(state)
172
- expect(state, 'keyword', 'SELECT')
173
-
174
- const distinct = match(state, 'keyword', 'DISTINCT')
175
-
176
- const columns = parseSelectList(state)
172
+ /** @type {SelectColumn[]} */
173
+ let columns
174
+ let distinct = false
177
175
 
178
- expect(state, 'keyword', 'FROM')
176
+ // Support duckdb-style shorthand "FROM table"
177
+ if (match(state, 'keyword', 'FROM')) {
178
+ columns = [{ type: 'star' }]
179
+ } else {
180
+ expect(state, 'keyword', 'SELECT')
181
+ distinct = match(state, 'keyword', 'DISTINCT')
182
+ columns = parseSelectList(state)
183
+ expect(state, 'keyword', 'FROM')
184
+ }
179
185
 
180
186
  // Check if it's a subquery or table name
181
187
  /** @type {FromTable | FromSubquery} */
182
188
  let from
183
- const tok = current(state)
184
- if (tok.type === 'paren' && tok.value === '(') {
189
+ const fromTok = current(state)
190
+ if (fromTok.type === 'paren' && fromTok.value === '(') {
185
191
  // Subquery: SELECT * FROM (SELECT ...) AS alias
186
192
  expect(state, 'paren', '(')
187
193
  const query = parseStatement(state)
@@ -191,7 +197,7 @@ function parseSelect(state) {
191
197
  type: 'subquery',
192
198
  query,
193
199
  alias,
194
- positionStart: tok.positionStart,
200
+ positionStart: fromTok.positionStart,
195
201
  positionEnd: state.lastPos,
196
202
  }
197
203
  } else {
@@ -200,9 +206,9 @@ function parseSelect(state) {
200
206
  const alias = parseTableAlias(state)
201
207
  from = {
202
208
  type: 'table',
203
- table: tok.value,
209
+ table: fromTok.value,
204
210
  alias,
205
- positionStart: tok.positionStart,
211
+ positionStart: fromTok.positionStart,
206
212
  positionEnd: state.lastPos,
207
213
  }
208
214
  }
@@ -84,7 +84,7 @@ export function extractColumns({ select, parentColumns }) {
84
84
  collectColumnsFromExpr(item.expr, identifiers, selectAliases)
85
85
  }
86
86
  for (const expr of select.groupBy) {
87
- collectColumnsFromExpr(expr, identifiers)
87
+ collectColumnsFromExpr(expr, identifiers, selectAliases)
88
88
  }
89
89
  collectColumnsFromExpr(select.having, identifiers, selectAliases)
90
90
  for (const join of select.joins) {
package/src/plan/plan.js CHANGED
@@ -113,15 +113,25 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
113
113
  // Source alias for FROM clause
114
114
  const sourceAlias = fromAlias(select.from)
115
115
 
116
- // Validate qualified references
116
+ // Validate qualified references and resolve aliases
117
117
  const scopeTables = Object.fromEntries([sourceAlias, ...select.joins.map(j => j.alias ?? j.table)].map(a => [a, true]))
118
- for (const col of select.columns) {
118
+ /** @type {Map<string, ExprNode>} */
119
+ const aliases = new Map()
120
+ const columns = select.columns.map(col => {
119
121
  if (col.type === 'derived') {
120
122
  validateTableRefs(col.expr, scopeTables)
121
- } else if (col.table && !(col.table in scopeTables)) {
123
+ const expr = resolveAliases(col.expr, aliases)
124
+ if (col.alias) {
125
+ aliases.set(col.alias, expr)
126
+ }
127
+ return { ...col, expr }
128
+ }
129
+ // Validate qualified references
130
+ if (col.table && !(col.table in scopeTables)) {
122
131
  throw new TableNotFoundError({ table: col.table, tables: scopeTables })
123
132
  }
124
- }
133
+ return col
134
+ })
125
135
 
126
136
  // Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
127
137
  // included so they are only applied to fresh scans, not CTE/subquery plans)
@@ -158,11 +168,15 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
158
168
  // Aggregation path: GROUP BY or scalar aggregate
159
169
  // HAVING is integrated into aggregate nodes for access to group context
160
170
  if (select.groupBy.length) {
161
- plan = { type: 'HashAggregate', groupBy: select.groupBy, columns: select.columns, having: select.having, child: plan }
171
+ // Resolve SELECT aliases in GROUP BY expressions at plan time
172
+ const groupBy = aliases.size > 0
173
+ ? select.groupBy.map(expr => resolveAliases(expr, aliases))
174
+ : select.groupBy
175
+ plan = { type: 'HashAggregate', groupBy, columns, having: select.having, child: plan }
162
176
  } else if (!select.having && !select.where && plan.type === 'Scan' && isOwnScan && isAllCountStar(select.columns)) {
163
177
  plan = { type: 'Count', table: plan.table, columns: select.columns }
164
178
  } else {
165
- plan = { type: 'ScalarAggregate', columns: select.columns, having: select.having, child: plan }
179
+ plan = { type: 'ScalarAggregate', columns, having: select.having, child: plan }
166
180
  }
167
181
 
168
182
  // ORDER BY (after aggregation)
@@ -185,13 +199,6 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
185
199
  // ORDER BY (before projection so it can access all columns)
186
200
  // Resolve SELECT aliases in ORDER BY expressions at plan time
187
201
  if (select.orderBy.length) {
188
- /** @type {Map<string, ExprNode>} */
189
- const aliases = new Map()
190
- for (const col of select.columns) {
191
- if (col.type === 'derived' && col.alias) {
192
- aliases.set(col.alias, col.expr)
193
- }
194
- }
195
202
  const orderBy = aliases.size > 0
196
203
  ? select.orderBy.map(term => ({ ...term, expr: resolveAliases(term.expr, aliases) }))
197
204
  : select.orderBy
@@ -205,17 +212,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
205
212
  // Fast path for SELECT * without joins
206
213
  const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star' && !select.joins.length
207
214
  if (!isPassthrough) {
208
- // Resolve earlier SELECT aliases in later column expressions
209
- /** @type {Map<string, ExprNode>} */
210
- const colAliases = new Map()
211
- let projectColumns = select.columns.map(col => {
212
- if (col.type !== 'derived') return col
213
- const expr = resolveAliases(col.expr, colAliases)
214
- if (col.alias) {
215
- colAliases.set(col.alias, expr)
216
- }
217
- return { ...col, expr }
218
- })
215
+ let projectColumns = columns
219
216
  // When parent only needs specific columns, drop unneeded projections
220
217
  if (parentColumns) {
221
218
  projectColumns = projectColumns.filter(col =>
package/src/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExprNode, SelectStatement, SqlPrimitive, Statement } from './ast.js'
1
+ import type { ExprNode, SqlPrimitive, Statement } from './ast.js'
2
2
 
3
3
  export * from './ast.js'
4
4
  export { ParserState, Token, TokenType } from './parse/types.js'