squirreling 0.11.2 → 0.11.4
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/ast.d.ts +1 -0
- package/src/execute/aggregates.js +4 -4
- package/src/execute/execute.js +24 -1
- package/src/execute/join.js +13 -27
- package/src/expression/alias.js +0 -5
- package/src/expression/evaluate.js +14 -11
- package/src/parse/parse.js +17 -11
- package/src/parse/primary.js +5 -1
- package/src/plan/columns.js +14 -16
- package/src/plan/plan.js +35 -32
- package/src/types.d.ts +1 -1
- package/src/validation/tables.js +3 -5
package/package.json
CHANGED
package/src/ast.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ function projectAggregateColumns(selectColumns, group, context) {
|
|
|
30
30
|
for (const key of firstRow.columns) {
|
|
31
31
|
if (prefix && !key.startsWith(prefix)) continue
|
|
32
32
|
const dotIndex = key.indexOf('.')
|
|
33
|
-
const outputKey = dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
33
|
+
const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
34
34
|
columns.push(outputKey)
|
|
35
35
|
cells[outputKey] = firstRow.cells[key]
|
|
36
36
|
}
|
|
@@ -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
|
|
|
@@ -267,7 +290,7 @@ async function* executeProject(plan, context) {
|
|
|
267
290
|
if (prefix && !key.startsWith(prefix)) continue
|
|
268
291
|
// Strip table prefix for output column names
|
|
269
292
|
const dotIndex = key.indexOf('.')
|
|
270
|
-
const outputKey = dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
293
|
+
const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
271
294
|
columns.push(outputKey)
|
|
272
295
|
cells[outputKey] = row.cells[key]
|
|
273
296
|
}
|
package/src/execute/join.js
CHANGED
|
@@ -26,18 +26,18 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
26
26
|
rightRows.push(row)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
30
30
|
|
|
31
31
|
/** @type {string[] | undefined} */
|
|
32
|
-
let
|
|
32
|
+
let leftCols = undefined
|
|
33
33
|
/** @type {Set<AsyncRow> | undefined} */
|
|
34
34
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
35
35
|
|
|
36
36
|
for await (const leftRow of executePlan({ plan: plan.left, context })) {
|
|
37
37
|
if (context.signal?.aborted) break
|
|
38
38
|
|
|
39
|
-
if (!
|
|
40
|
-
|
|
39
|
+
if (!leftCols) {
|
|
40
|
+
leftCols = leftRow.columns
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
let hasMatch = false
|
|
@@ -58,7 +58,7 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (!hasMatch && (plan.joinType === 'LEFT' || plan.joinType === 'FULL')) {
|
|
61
|
-
const nullRight = createNullRow(
|
|
61
|
+
const nullRight = createNullRow(rightCols)
|
|
62
62
|
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -67,7 +67,7 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
67
67
|
if (matchedRightRows) {
|
|
68
68
|
for (const rightRow of rightRows) {
|
|
69
69
|
if (!matchedRightRows.has(rightRow)) {
|
|
70
|
-
const nullLeft = createNullRow(
|
|
70
|
+
const nullLeft = createNullRow(leftCols ?? [])
|
|
71
71
|
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -104,13 +104,11 @@ export async function* executePositionalJoin(plan, context) {
|
|
|
104
104
|
const maxLen = Math.max(leftRows.length, rightRows.length)
|
|
105
105
|
const leftCols = leftRows[0]?.columns ?? []
|
|
106
106
|
const rightCols = rightRows[0]?.columns ?? []
|
|
107
|
-
const leftPrefixedCols = prefixColumns(leftCols, leftTable)
|
|
108
|
-
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
109
107
|
|
|
110
108
|
for (let i = 0; i < maxLen; i++) {
|
|
111
109
|
if (signal?.aborted) return
|
|
112
|
-
const leftRow = leftRows[i] ?? createNullRow(
|
|
113
|
-
const rightRow = rightRows[i] ?? createNullRow(
|
|
110
|
+
const leftRow = leftRows[i] ?? createNullRow(leftCols)
|
|
111
|
+
const rightRow = rightRows[i] ?? createNullRow(rightCols)
|
|
114
112
|
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
115
113
|
}
|
|
116
114
|
}
|
|
@@ -154,10 +152,9 @@ export async function* executeHashJoin(plan, context) {
|
|
|
154
152
|
|
|
155
153
|
// Get column info for NULL row generation
|
|
156
154
|
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
157
|
-
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
158
155
|
|
|
159
156
|
/** @type {string[] | undefined} */
|
|
160
|
-
let
|
|
157
|
+
let leftCols
|
|
161
158
|
/** @type {Set<AsyncRow> | undefined} */
|
|
162
159
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
163
160
|
|
|
@@ -165,8 +162,8 @@ export async function* executeHashJoin(plan, context) {
|
|
|
165
162
|
for await (const leftRow of executePlan({ plan: plan.left, context })) {
|
|
166
163
|
if (context.signal?.aborted) break
|
|
167
164
|
|
|
168
|
-
if (!
|
|
169
|
-
|
|
165
|
+
if (!leftCols) {
|
|
166
|
+
leftCols = leftRow.columns
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
const keyValue = await evaluateExpr({
|
|
@@ -183,7 +180,7 @@ export async function* executeHashJoin(plan, context) {
|
|
|
183
180
|
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
184
181
|
}
|
|
185
182
|
} else if (plan.joinType === 'LEFT' || plan.joinType === 'FULL') {
|
|
186
|
-
const nullRight = createNullRow(
|
|
183
|
+
const nullRight = createNullRow(rightCols)
|
|
187
184
|
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
188
185
|
}
|
|
189
186
|
}
|
|
@@ -192,7 +189,7 @@ export async function* executeHashJoin(plan, context) {
|
|
|
192
189
|
if (matchedRightRows) {
|
|
193
190
|
for (const rightRow of rightRows) {
|
|
194
191
|
if (!matchedRightRows.has(rightRow)) {
|
|
195
|
-
const nullLeft = createNullRow(
|
|
192
|
+
const nullLeft = createNullRow(leftCols ?? [])
|
|
196
193
|
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
197
194
|
}
|
|
198
195
|
}
|
|
@@ -244,14 +241,3 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
244
241
|
|
|
245
242
|
return { columns, cells }
|
|
246
243
|
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Prefixes column names with table alias, keeping already-prefixed columns as-is
|
|
250
|
-
*
|
|
251
|
-
* @param {string[]} cols
|
|
252
|
-
* @param {string} table
|
|
253
|
-
* @returns {string[]}
|
|
254
|
-
*/
|
|
255
|
-
function prefixColumns(cols, table) {
|
|
256
|
-
return cols.map(col => col.includes('.') ? col : `${table}.${col}`)
|
|
257
|
-
}
|
package/src/expression/alias.js
CHANGED
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export function derivedAlias(expr) {
|
|
12
12
|
if (expr.type === 'identifier') {
|
|
13
|
-
// For qualified names like 'users.name', use just the column part as alias
|
|
14
|
-
const dotIndex = expr.name.indexOf('.')
|
|
15
|
-
if (dotIndex >= 0) {
|
|
16
|
-
return expr.name.substring(dotIndex + 1)
|
|
17
|
-
}
|
|
18
13
|
return expr.name
|
|
19
14
|
}
|
|
20
15
|
if (expr.type === 'literal') {
|
|
@@ -33,18 +33,21 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (node.type === 'identifier') {
|
|
36
|
-
// Try
|
|
37
|
-
if (node.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return row.cells[colName]()
|
|
36
|
+
// Try qualified name first (e.g. 'users.id')
|
|
37
|
+
if (node.prefix) {
|
|
38
|
+
const qualified = node.prefix + '.' + node.name
|
|
39
|
+
if (qualified in row.cells) {
|
|
40
|
+
return row.cells[qualified]()
|
|
41
|
+
}
|
|
42
|
+
// Fall back to just the column part
|
|
43
|
+
if (node.name in row.cells) {
|
|
44
|
+
return row.cells[node.name]()
|
|
46
45
|
}
|
|
47
46
|
} else {
|
|
47
|
+
// Try exact match first
|
|
48
|
+
if (node.name in row.cells) {
|
|
49
|
+
return row.cells[node.name]()
|
|
50
|
+
}
|
|
48
51
|
// For unqualified names, search for a matching prefixed column (e.g. 'id' to 'a.id')
|
|
49
52
|
const suffix = '.' + node.name
|
|
50
53
|
const match = row.columns.find(col => col.endsWith(suffix))
|
|
@@ -54,7 +57,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
54
57
|
}
|
|
55
58
|
// Unknown identifier
|
|
56
59
|
throw new ColumnNotFoundError({
|
|
57
|
-
missingColumn: node.name,
|
|
60
|
+
missingColumn: node.prefix ? node.prefix + '.' + node.name : node.name,
|
|
58
61
|
availableColumns: row.columns,
|
|
59
62
|
rowIndex,
|
|
60
63
|
...node,
|
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/parse/primary.js
CHANGED
|
@@ -112,15 +112,19 @@ export function parsePrimary(state) {
|
|
|
112
112
|
|
|
113
113
|
// Table identifier
|
|
114
114
|
let name = consume(state).value
|
|
115
|
+
/** @type {string | undefined} */
|
|
116
|
+
let prefix
|
|
115
117
|
|
|
116
118
|
// table.column
|
|
117
119
|
if (match(state, 'dot')) {
|
|
118
|
-
|
|
120
|
+
prefix = name
|
|
121
|
+
name = expect(state, 'identifier').value
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
return {
|
|
122
125
|
type: 'identifier',
|
|
123
126
|
name,
|
|
127
|
+
prefix,
|
|
124
128
|
positionStart,
|
|
125
129
|
positionEnd: state.lastPos,
|
|
126
130
|
}
|
package/src/plan/columns.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { derivedAlias } from '../expression/alias.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @import { AsyncDataSource, ExprNode, FromSubquery, FromTable, SelectStatement, Statement } from '../types.js'
|
|
4
|
+
* @import { AsyncDataSource, ExprNode, FromSubquery, FromTable, IdentifierNode, SelectStatement, Statement } from '../types.js'
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -18,7 +18,7 @@ export function fromAlias(from) {
|
|
|
18
18
|
*
|
|
19
19
|
* @param {object} options
|
|
20
20
|
* @param {SelectStatement} options.select
|
|
21
|
-
* @param {
|
|
21
|
+
* @param {IdentifierNode[]} [options.parentColumns] - columns needed by the parent query
|
|
22
22
|
* @returns {Map<string, string[] | undefined>}
|
|
23
23
|
*/
|
|
24
24
|
export function extractColumns({ select, parentColumns }) {
|
|
@@ -54,7 +54,8 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
54
54
|
// directly. For non-star queries, parent names may be aliases and are
|
|
55
55
|
// handled below by filtering derived columns and collecting from expressions.
|
|
56
56
|
const hasStar = select.columns.some(col => col.type === 'star' && !col.table)
|
|
57
|
-
|
|
57
|
+
/** @type {IdentifierNode[]} */
|
|
58
|
+
const identifiers = hasStar && parentColumns ? [...parentColumns] : []
|
|
58
59
|
|
|
59
60
|
// Collect ORDER BY identifiers, excluding SELECT aliases (their underlying
|
|
60
61
|
// columns are already collected from select.columns expressions above)
|
|
@@ -69,7 +70,7 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
69
70
|
// When parentColumns is set, skip columns the parent doesn't need
|
|
70
71
|
if (parentColumns) {
|
|
71
72
|
const outputName = col.alias ?? derivedAlias(col.expr)
|
|
72
|
-
if (!parentColumns.
|
|
73
|
+
if (!parentColumns.some(id => id.name === outputName)) continue
|
|
73
74
|
}
|
|
74
75
|
// Exclude earlier SELECT aliases so they aren't treated as source columns
|
|
75
76
|
collectColumnsFromExpr(col.expr, identifiers, selectAliases)
|
|
@@ -84,7 +85,7 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
84
85
|
collectColumnsFromExpr(item.expr, identifiers, selectAliases)
|
|
85
86
|
}
|
|
86
87
|
for (const expr of select.groupBy) {
|
|
87
|
-
collectColumnsFromExpr(expr, identifiers)
|
|
88
|
+
collectColumnsFromExpr(expr, identifiers, selectAliases)
|
|
88
89
|
}
|
|
89
90
|
collectColumnsFromExpr(select.having, identifiers, selectAliases)
|
|
90
91
|
for (const join of select.joins) {
|
|
@@ -92,14 +93,11 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// Partition identifiers by table prefix
|
|
95
|
-
for (const name of identifiers) {
|
|
96
|
-
|
|
97
|
-
if (dotIndex >= 0) {
|
|
96
|
+
for (const { prefix, name } of identifiers) {
|
|
97
|
+
if (prefix) {
|
|
98
98
|
// Qualified: add to matching table only
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const set = perTable.get(tablePrefix)
|
|
102
|
-
if (set) set.add(columnName)
|
|
99
|
+
const set = perTable.get(prefix)
|
|
100
|
+
if (set) set.add(name)
|
|
103
101
|
} else if (aliases.length > 1) {
|
|
104
102
|
// Unqualified in a JOIN: can't disambiguate, request all columns from all tables
|
|
105
103
|
for (const alias of aliases) {
|
|
@@ -122,17 +120,17 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
/**
|
|
125
|
-
* Recursively collects
|
|
123
|
+
* Recursively collects identifier nodes from an expression
|
|
126
124
|
*
|
|
127
125
|
* @param {ExprNode} expr
|
|
128
|
-
* @param {
|
|
126
|
+
* @param {IdentifierNode[]} columns
|
|
129
127
|
* @param {Set<string>} [aliases] - aliases to exclude from columns
|
|
130
128
|
*/
|
|
131
129
|
function collectColumnsFromExpr(expr, columns, aliases) {
|
|
132
130
|
if (!expr) return
|
|
133
131
|
if (expr.type === 'identifier') {
|
|
134
|
-
if (!aliases?.has(expr.name)) {
|
|
135
|
-
columns.
|
|
132
|
+
if (expr.prefix || !aliases?.has(expr.name)) {
|
|
133
|
+
columns.push(expr)
|
|
136
134
|
}
|
|
137
135
|
} else if (expr.type === 'binary') {
|
|
138
136
|
collectColumnsFromExpr(expr.left, columns, aliases)
|
package/src/plan/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ import { validateScan, validateTableRefs } from '../validation/tables.js'
|
|
|
6
6
|
import { extractColumns, fromAlias, inferStatementColumns } from './columns.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
|
|
9
|
+
* @import { AsyncDataSource, ExprNode, DerivedColumn, IdentifierNode, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
|
|
10
10
|
* @import { QueryPlan } from './types.d.ts'
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ export function planSql({ query, functions, tables }) {
|
|
|
31
31
|
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
32
32
|
* @param {Map<string, string[]>} [options.cteColumns]
|
|
33
33
|
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
34
|
-
* @param {
|
|
34
|
+
* @param {IdentifierNode[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
|
|
35
35
|
* @returns {QueryPlan}
|
|
36
36
|
*/
|
|
37
37
|
function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumns }) {
|
|
@@ -99,7 +99,7 @@ function planSetOperation({ compound, ctePlans, cteColumns, tables }) {
|
|
|
99
99
|
* @param {Map<string, QueryPlan>} [options.ctePlans]
|
|
100
100
|
* @param {Map<string, string[]>} [options.cteColumns]
|
|
101
101
|
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
102
|
-
* @param {
|
|
102
|
+
* @param {IdentifierNode[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
|
|
103
103
|
* @returns {QueryPlan}
|
|
104
104
|
*/
|
|
105
105
|
function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
@@ -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,21 +212,11 @@ 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 =>
|
|
222
|
-
col.type === 'star' || parentColumns.
|
|
219
|
+
col.type === 'star' || parentColumns.some(id => id.name === (col.alias ?? derivedAlias(col.expr)))
|
|
223
220
|
)
|
|
224
221
|
}
|
|
225
222
|
plan = { type: 'Project', columns: projectColumns, child: plan }
|
|
@@ -255,7 +252,13 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables }) {
|
|
|
255
252
|
validateScan({ ...select.from, hints, tables })
|
|
256
253
|
return { type: 'Scan', table: select.from.table, hints }
|
|
257
254
|
} else {
|
|
258
|
-
const subPlan = planStatement({
|
|
255
|
+
const subPlan = planStatement({
|
|
256
|
+
stmt: select.from.query,
|
|
257
|
+
ctePlans,
|
|
258
|
+
cteColumns,
|
|
259
|
+
tables,
|
|
260
|
+
parentColumns: hints.columns?.map(name => ({ type: 'identifier', name, positionStart: 0, positionEnd: 0 })),
|
|
261
|
+
})
|
|
259
262
|
// Validate that requested columns exist in subquery output
|
|
260
263
|
const availableColumns = inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
|
|
261
264
|
if (hints.columns && availableColumns.length) {
|
|
@@ -345,7 +348,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
345
348
|
function resolveAliases(node, aliases) {
|
|
346
349
|
if (!node || !aliases.size) return node
|
|
347
350
|
if (node.type === 'identifier') {
|
|
348
|
-
return aliases.get(node.name) ?? node
|
|
351
|
+
return node.prefix ? node : aliases.get(node.name) ?? node
|
|
349
352
|
}
|
|
350
353
|
if (node.type === 'unary') {
|
|
351
354
|
return { ...node, argument: resolveAliases(node.argument, aliases) }
|
|
@@ -397,8 +400,8 @@ function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
|
|
|
397
400
|
if (left.type !== 'identifier' || right.type !== 'identifier') return
|
|
398
401
|
|
|
399
402
|
// Check if keys are in swapped order (right table ref on left side)
|
|
400
|
-
const leftRefsRight = left.
|
|
401
|
-
const rightRefsLeft = right.
|
|
403
|
+
const leftRefsRight = left.prefix === rightTable
|
|
404
|
+
const rightRefsLeft = right.prefix === leftTable
|
|
402
405
|
|
|
403
406
|
if (leftRefsRight && rightRefsLeft) {
|
|
404
407
|
return { leftKey: right, rightKey: left }
|
package/src/types.d.ts
CHANGED
package/src/validation/tables.js
CHANGED
|
@@ -53,11 +53,9 @@ export function validateScan({ table, hints, tables, positionStart, positionEnd
|
|
|
53
53
|
export function validateTableRefs(expr, tables) {
|
|
54
54
|
if (!expr) return
|
|
55
55
|
if (expr.type === 'identifier') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!(table in tables)) {
|
|
60
|
-
throw new TableNotFoundError({ table, tables, positionStart: expr.positionStart, positionEnd: expr.positionStart + dotIndex })
|
|
56
|
+
if (expr.prefix) {
|
|
57
|
+
if (!(expr.prefix in tables)) {
|
|
58
|
+
throw new TableNotFoundError({ table: expr.prefix, tables, positionStart: expr.positionStart, positionEnd: expr.positionStart + expr.prefix.length })
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
return
|