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 +1 -1
- package/src/execute/aggregates.js +3 -3
- package/src/execute/execute.js +23 -0
- package/src/parse/parse.js +17 -11
- package/src/plan/columns.js +1 -1
- package/src/plan/plan.js +21 -24
- package/src/types.d.ts +1 -1
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
package/src/execute/execute.js
CHANGED
|
@@ -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
|
|
package/src/parse/parse.js
CHANGED
|
@@ -169,19 +169,25 @@ function parseIntersectOperations(state) {
|
|
|
169
169
|
*/
|
|
170
170
|
function parseSelect(state) {
|
|
171
171
|
const { positionStart } = current(state)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const columns = parseSelectList(state)
|
|
172
|
+
/** @type {SelectColumn[]} */
|
|
173
|
+
let columns
|
|
174
|
+
let distinct = false
|
|
177
175
|
|
|
178
|
-
|
|
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
|
|
184
|
-
if (
|
|
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:
|
|
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:
|
|
209
|
+
table: fromTok.value,
|
|
204
210
|
alias,
|
|
205
|
-
positionStart:
|
|
211
|
+
positionStart: fromTok.positionStart,
|
|
206
212
|
positionEnd: state.lastPos,
|
|
207
213
|
}
|
|
208
214
|
}
|
package/src/plan/columns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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