squirreling 0.11.1 → 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 +9 -5
- package/src/execute/execute.js +33 -16
- package/src/execute/join.js +7 -18
- package/src/expression/evaluate.js +13 -5
- package/src/parse/parse.js +17 -11
- package/src/plan/columns.js +5 -4
- package/src/plan/plan.js +44 -63
- package/src/types.d.ts +1 -1
- package/src/validation/planErrors.js +3 -3
- package/src/validation/tables.js +89 -0
package/package.json
CHANGED
|
@@ -26,9 +26,13 @@ function projectAggregateColumns(selectColumns, group, context) {
|
|
|
26
26
|
if (col.type === 'star') {
|
|
27
27
|
const firstRow = group[0]
|
|
28
28
|
if (firstRow) {
|
|
29
|
+
const prefix = col.table ? `${col.table}.` : undefined
|
|
29
30
|
for (const key of firstRow.columns) {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if (prefix && !key.startsWith(prefix)) continue
|
|
32
|
+
const dotIndex = key.indexOf('.')
|
|
33
|
+
const outputKey = dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
34
|
+
columns.push(outputKey)
|
|
35
|
+
cells[outputKey] = firstRow.cells[key]
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
} else {
|
|
@@ -240,7 +244,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
|
|
|
240
244
|
if (spec.funcName === 'COUNT' && spec.distinct) {
|
|
241
245
|
const seen = new Set()
|
|
242
246
|
for await (const chunk of values) {
|
|
243
|
-
if (signal?.aborted) return
|
|
247
|
+
if (signal?.aborted) return
|
|
244
248
|
for (let i = 0; i < chunk.length; i++) {
|
|
245
249
|
const v = chunk[i]
|
|
246
250
|
if (v == null) continue
|
|
@@ -253,7 +257,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
|
|
|
253
257
|
if (spec.funcName === 'COUNT') {
|
|
254
258
|
let count = 0
|
|
255
259
|
for await (const chunk of values) {
|
|
256
|
-
if (signal?.aborted) return
|
|
260
|
+
if (signal?.aborted) return
|
|
257
261
|
for (let i = 0; i < chunk.length; i++) {
|
|
258
262
|
if (chunk[i] != null) count++
|
|
259
263
|
}
|
|
@@ -270,7 +274,7 @@ async function scanColumnAggregate({ table, spec, limit, offset, signal }) {
|
|
|
270
274
|
let max = null
|
|
271
275
|
|
|
272
276
|
for await (const chunk of values) {
|
|
273
|
-
if (signal?.aborted) return
|
|
277
|
+
if (signal?.aborted) return
|
|
274
278
|
for (let i = 0; i < chunk.length; i++) {
|
|
275
279
|
const v = chunk[i]
|
|
276
280
|
if (v == null) continue
|
package/src/execute/execute.js
CHANGED
|
@@ -3,7 +3,7 @@ import { derivedAlias } from '../expression/alias.js'
|
|
|
3
3
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
4
4
|
import { parseSql } from '../parse/parse.js'
|
|
5
5
|
import { planSql } from '../plan/plan.js'
|
|
6
|
-
import {
|
|
6
|
+
import { validateScan, validateTable } from '../validation/tables.js'
|
|
7
7
|
import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
8
8
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
9
9
|
import { executeSort } from './sort.js'
|
|
@@ -95,15 +95,30 @@ export async function* executePlan({ plan, context }) {
|
|
|
95
95
|
*/
|
|
96
96
|
async function* executeScan(plan, context) {
|
|
97
97
|
const { tables, signal } = context
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
const table = validateTable({ ...plan, tables })
|
|
99
|
+
validateScan({ ...plan, tables })
|
|
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
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
// do the scan
|
|
@@ -138,10 +153,7 @@ async function* executeScan(plan, context) {
|
|
|
138
153
|
* @yields {AsyncRow}
|
|
139
154
|
*/
|
|
140
155
|
async function* executeCount(plan, { tables, signal }) {
|
|
141
|
-
const table = tables
|
|
142
|
-
if (!table) {
|
|
143
|
-
throw new TableNotFoundError({ table: plan.table, tables })
|
|
144
|
-
}
|
|
156
|
+
const table = validateTable({ ...plan, tables })
|
|
145
157
|
|
|
146
158
|
// Use source numRows if available
|
|
147
159
|
let count = table.numRows
|
|
@@ -273,9 +285,14 @@ async function* executeProject(plan, context) {
|
|
|
273
285
|
|
|
274
286
|
for (const col of plan.columns) {
|
|
275
287
|
if (col.type === 'star') {
|
|
288
|
+
const prefix = col.table ? `${col.table}.` : undefined
|
|
276
289
|
for (const key of row.columns) {
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
if (prefix && !key.startsWith(prefix)) continue
|
|
291
|
+
// Strip table prefix for output column names
|
|
292
|
+
const dotIndex = key.indexOf('.')
|
|
293
|
+
const outputKey = dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
294
|
+
columns.push(outputKey)
|
|
295
|
+
cells[outputKey] = row.cells[key]
|
|
279
296
|
}
|
|
280
297
|
} else {
|
|
281
298
|
const alias = col.alias ?? derivedAlias(col.expr)
|
package/src/execute/join.js
CHANGED
|
@@ -230,27 +230,16 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
230
230
|
|
|
231
231
|
// Add left table columns with prefix
|
|
232
232
|
for (const [key, cell] of Object.entries(leftRow.cells)) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
columns.push(alias)
|
|
237
|
-
cells[alias] = cell
|
|
238
|
-
}
|
|
239
|
-
// Also keep unqualified name for convenience
|
|
240
|
-
columns.push(key)
|
|
241
|
-
cells[key] = cell
|
|
233
|
+
const alias = key.includes('.') ? key : `${leftTable}.${key}`
|
|
234
|
+
columns.push(alias)
|
|
235
|
+
cells[alias] = cell
|
|
242
236
|
}
|
|
243
237
|
|
|
244
238
|
// Add right table columns with prefix
|
|
245
239
|
for (const [key, cell] of Object.entries(rightRow.cells)) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cells[alias] = cell
|
|
250
|
-
}
|
|
251
|
-
// Unqualified name (overwrites if same name exists in left table)
|
|
252
|
-
columns.push(key)
|
|
253
|
-
cells[key] = cell
|
|
240
|
+
const alias = key.includes('.') ? key : `${rightTable}.${key}`
|
|
241
|
+
columns.push(alias)
|
|
242
|
+
cells[alias] = cell
|
|
254
243
|
}
|
|
255
244
|
|
|
256
245
|
return { columns, cells }
|
|
@@ -264,5 +253,5 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
264
253
|
* @returns {string[]}
|
|
265
254
|
*/
|
|
266
255
|
function prefixColumns(cols, table) {
|
|
267
|
-
return cols.
|
|
256
|
+
return cols.map(col => col.includes('.') ? col : `${table}.${col}`)
|
|
268
257
|
}
|
|
@@ -37,16 +37,24 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
37
37
|
if (node.name in row.cells) {
|
|
38
38
|
return row.cells[node.name]()
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const dotIndex = node.name.indexOf('.')
|
|
41
|
+
if (dotIndex >= 0) {
|
|
42
|
+
// For qualified names like 'users.id', try just the column part
|
|
43
|
+
const colName = node.name.substring(dotIndex + 1)
|
|
44
|
+
if (colName in row.cells) {
|
|
44
45
|
return row.cells[colName]()
|
|
45
46
|
}
|
|
47
|
+
} else {
|
|
48
|
+
// For unqualified names, search for a matching prefixed column (e.g. 'id' to 'a.id')
|
|
49
|
+
const suffix = '.' + node.name
|
|
50
|
+
const match = row.columns.find(col => col.endsWith(suffix))
|
|
51
|
+
if (match) {
|
|
52
|
+
return row.cells[match]()
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
// Unknown identifier
|
|
48
56
|
throw new ColumnNotFoundError({
|
|
49
|
-
|
|
57
|
+
missingColumn: node.name,
|
|
50
58
|
availableColumns: row.columns,
|
|
51
59
|
rowIndex,
|
|
52
60
|
...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/plan/columns.js
CHANGED
|
@@ -71,7 +71,8 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
71
71
|
const outputName = col.alias ?? derivedAlias(col.expr)
|
|
72
72
|
if (!parentColumns.includes(outputName)) continue
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// Exclude earlier SELECT aliases so they aren't treated as source columns
|
|
75
|
+
collectColumnsFromExpr(col.expr, identifiers, selectAliases)
|
|
75
76
|
if (col.alias) {
|
|
76
77
|
selectAliases.add(col.alias)
|
|
77
78
|
}
|
|
@@ -83,7 +84,7 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
83
84
|
collectColumnsFromExpr(item.expr, identifiers, selectAliases)
|
|
84
85
|
}
|
|
85
86
|
for (const expr of select.groupBy) {
|
|
86
|
-
collectColumnsFromExpr(expr, identifiers)
|
|
87
|
+
collectColumnsFromExpr(expr, identifiers, selectAliases)
|
|
87
88
|
}
|
|
88
89
|
collectColumnsFromExpr(select.having, identifiers, selectAliases)
|
|
89
90
|
for (const join of select.joins) {
|
|
@@ -223,12 +224,12 @@ function inferSelectSourceColumns({ select, cteColumns, tables }) {
|
|
|
223
224
|
const result = []
|
|
224
225
|
const fromAlias = select.from.alias ?? select.from.table
|
|
225
226
|
for (const col of lookupTableColumns(select.from.table, cteColumns, tables)) {
|
|
226
|
-
result.push(`${fromAlias}.${col}
|
|
227
|
+
result.push(`${fromAlias}.${col}`)
|
|
227
228
|
}
|
|
228
229
|
for (const join of select.joins) {
|
|
229
230
|
const alias = join.alias ?? join.table
|
|
230
231
|
for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
|
|
231
|
-
result.push(`${alias}.${col}
|
|
232
|
+
result.push(`${alias}.${col}`)
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
return result
|
package/src/plan/plan.js
CHANGED
|
@@ -2,6 +2,7 @@ import { derivedAlias } from '../expression/alias.js'
|
|
|
2
2
|
import { parseSql } from '../parse/parse.js'
|
|
3
3
|
import { findAggregate } from '../validation/aggregates.js'
|
|
4
4
|
import { ColumnNotFoundError, TableNotFoundError } from '../validation/planErrors.js'
|
|
5
|
+
import { validateScan, validateTableRefs } from '../validation/tables.js'
|
|
5
6
|
import { extractColumns, fromAlias, inferStatementColumns } from './columns.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -112,6 +113,26 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
112
113
|
// Source alias for FROM clause
|
|
113
114
|
const sourceAlias = fromAlias(select.from)
|
|
114
115
|
|
|
116
|
+
// Validate qualified references and resolve aliases
|
|
117
|
+
const scopeTables = Object.fromEntries([sourceAlias, ...select.joins.map(j => j.alias ?? j.table)].map(a => [a, true]))
|
|
118
|
+
/** @type {Map<string, ExprNode>} */
|
|
119
|
+
const aliases = new Map()
|
|
120
|
+
const columns = select.columns.map(col => {
|
|
121
|
+
if (col.type === 'derived') {
|
|
122
|
+
validateTableRefs(col.expr, 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)) {
|
|
131
|
+
throw new TableNotFoundError({ table: col.table, tables: scopeTables })
|
|
132
|
+
}
|
|
133
|
+
return col
|
|
134
|
+
})
|
|
135
|
+
|
|
115
136
|
// Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
|
|
116
137
|
// included so they are only applied to fresh scans, not CTE/subquery plans)
|
|
117
138
|
/** @type {ScanOptions} */
|
|
@@ -147,11 +168,15 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
147
168
|
// Aggregation path: GROUP BY or scalar aggregate
|
|
148
169
|
// HAVING is integrated into aggregate nodes for access to group context
|
|
149
170
|
if (select.groupBy.length) {
|
|
150
|
-
|
|
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 }
|
|
151
176
|
} else if (!select.having && !select.where && plan.type === 'Scan' && isOwnScan && isAllCountStar(select.columns)) {
|
|
152
177
|
plan = { type: 'Count', table: plan.table, columns: select.columns }
|
|
153
178
|
} else {
|
|
154
|
-
plan = { type: 'ScalarAggregate', columns
|
|
179
|
+
plan = { type: 'ScalarAggregate', columns, having: select.having, child: plan }
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
// ORDER BY (after aggregation)
|
|
@@ -174,13 +199,6 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
174
199
|
// ORDER BY (before projection so it can access all columns)
|
|
175
200
|
// Resolve SELECT aliases in ORDER BY expressions at plan time
|
|
176
201
|
if (select.orderBy.length) {
|
|
177
|
-
/** @type {Map<string, ExprNode>} */
|
|
178
|
-
const aliases = new Map()
|
|
179
|
-
for (const col of select.columns) {
|
|
180
|
-
if (col.type === 'derived' && col.alias) {
|
|
181
|
-
aliases.set(col.alias, col.expr)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
202
|
const orderBy = aliases.size > 0
|
|
185
203
|
? select.orderBy.map(term => ({ ...term, expr: resolveAliases(term.expr, aliases) }))
|
|
186
204
|
: select.orderBy
|
|
@@ -191,13 +209,13 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
191
209
|
// However, for streaming distinct we need to project first
|
|
192
210
|
// So the order is: Sort -> Project -> Distinct -> Limit
|
|
193
211
|
|
|
194
|
-
// Fast path for SELECT *
|
|
195
|
-
const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star'
|
|
212
|
+
// Fast path for SELECT * without joins
|
|
213
|
+
const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star' && !select.joins.length
|
|
196
214
|
if (!isPassthrough) {
|
|
215
|
+
let projectColumns = columns
|
|
197
216
|
// When parent only needs specific columns, drop unneeded projections
|
|
198
|
-
let projectColumns = select.columns
|
|
199
217
|
if (parentColumns) {
|
|
200
|
-
projectColumns =
|
|
218
|
+
projectColumns = projectColumns.filter(col =>
|
|
201
219
|
col.type === 'star' || parentColumns.includes(col.alias ?? derivedAlias(col.expr))
|
|
202
220
|
)
|
|
203
221
|
}
|
|
@@ -240,11 +258,7 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables }) {
|
|
|
240
258
|
if (hints.columns && availableColumns.length) {
|
|
241
259
|
const missingColumn = hints.columns.find(col => !availableColumns.includes(col))
|
|
242
260
|
if (missingColumn) {
|
|
243
|
-
throw new ColumnNotFoundError({
|
|
244
|
-
columnName: missingColumn,
|
|
245
|
-
availableColumns,
|
|
246
|
-
...select.from,
|
|
247
|
-
})
|
|
261
|
+
throw new ColumnNotFoundError({ missingColumn, availableColumns, ...select.from })
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
264
|
return subPlan
|
|
@@ -321,50 +335,44 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
321
335
|
* Recursively replaces identifier nodes that match SELECT aliases
|
|
322
336
|
* with their aliased expressions.
|
|
323
337
|
*
|
|
324
|
-
* @param {ExprNode} node
|
|
338
|
+
* @param {ExprNode | undefined} node
|
|
325
339
|
* @param {Map<string, ExprNode>} aliases
|
|
326
340
|
* @returns {ExprNode}
|
|
327
341
|
*/
|
|
328
342
|
function resolveAliases(node, aliases) {
|
|
343
|
+
if (!node || !aliases.size) return node
|
|
329
344
|
if (node.type === 'identifier') {
|
|
330
|
-
|
|
331
|
-
if (resolved) return resolved
|
|
332
|
-
return node
|
|
345
|
+
return aliases.get(node.name) ?? node
|
|
333
346
|
}
|
|
334
347
|
if (node.type === 'unary') {
|
|
335
|
-
|
|
336
|
-
return argument === node.argument ? node : { ...node, argument }
|
|
348
|
+
return { ...node, argument: resolveAliases(node.argument, aliases) }
|
|
337
349
|
}
|
|
338
350
|
if (node.type === 'binary') {
|
|
339
351
|
const left = resolveAliases(node.left, aliases)
|
|
340
352
|
const right = resolveAliases(node.right, aliases)
|
|
341
|
-
return
|
|
353
|
+
return { ...node, left, right }
|
|
342
354
|
}
|
|
343
355
|
if (node.type === 'function') {
|
|
344
356
|
const args = node.args.map(arg => resolveAliases(arg, aliases))
|
|
345
|
-
|
|
346
|
-
return changed ? { ...node, args } : node
|
|
357
|
+
return { ...node, args }
|
|
347
358
|
}
|
|
348
359
|
if (node.type === 'cast') {
|
|
349
|
-
|
|
350
|
-
return expr === node.expr ? node : { ...node, expr }
|
|
360
|
+
return { ...node, expr: resolveAliases(node.expr, aliases) }
|
|
351
361
|
}
|
|
352
362
|
if (node.type === 'in valuelist') {
|
|
353
363
|
const expr = resolveAliases(node.expr, aliases)
|
|
354
364
|
const values = node.values.map(v => resolveAliases(v, aliases))
|
|
355
|
-
|
|
356
|
-
return changed ? { ...node, expr, values } : node
|
|
365
|
+
return { ...node, expr, values }
|
|
357
366
|
}
|
|
358
367
|
if (node.type === 'case') {
|
|
359
|
-
const caseExpr =
|
|
368
|
+
const caseExpr = resolveAliases(node.caseExpr, aliases)
|
|
360
369
|
const whenClauses = node.whenClauses.map(w => {
|
|
361
370
|
const condition = resolveAliases(w.condition, aliases)
|
|
362
371
|
const result = resolveAliases(w.result, aliases)
|
|
363
|
-
return
|
|
372
|
+
return { ...w, condition, result }
|
|
364
373
|
})
|
|
365
|
-
const elseResult =
|
|
366
|
-
|
|
367
|
-
return changed ? { ...node, caseExpr, whenClauses, elseResult } : node
|
|
374
|
+
const elseResult = resolveAliases(node.elseResult, aliases)
|
|
375
|
+
return { ...node, caseExpr, whenClauses, elseResult }
|
|
368
376
|
}
|
|
369
377
|
// literal, interval, subquery, in, exists: no identifiers to resolve
|
|
370
378
|
return node
|
|
@@ -396,33 +404,6 @@ function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
|
|
|
396
404
|
return { leftKey: left, rightKey: right }
|
|
397
405
|
}
|
|
398
406
|
|
|
399
|
-
/**
|
|
400
|
-
* Validates that a table exists and requested columns are available.
|
|
401
|
-
*
|
|
402
|
-
* @param {object} options
|
|
403
|
-
* @param {string} options.table
|
|
404
|
-
* @param {ScanOptions} options.hints
|
|
405
|
-
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
406
|
-
* @param {number} options.positionStart
|
|
407
|
-
* @param {number} options.positionEnd
|
|
408
|
-
*/
|
|
409
|
-
function validateScan({ table, hints, tables, positionStart, positionEnd }) {
|
|
410
|
-
if (!tables) return
|
|
411
|
-
const resolved = tables[table]
|
|
412
|
-
if (!resolved) {
|
|
413
|
-
throw new TableNotFoundError({ table, tables, positionStart, positionEnd })
|
|
414
|
-
}
|
|
415
|
-
const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
|
|
416
|
-
if (missingColumn) {
|
|
417
|
-
throw new ColumnNotFoundError({
|
|
418
|
-
columnName: missingColumn,
|
|
419
|
-
availableColumns: resolved.columns,
|
|
420
|
-
positionStart,
|
|
421
|
-
positionEnd,
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
407
|
/**
|
|
427
408
|
* Checks if every SELECT column is a plain COUNT(*).
|
|
428
409
|
*
|
package/src/types.d.ts
CHANGED
|
@@ -30,18 +30,18 @@ export class TableNotFoundError extends ExecutionError {
|
|
|
30
30
|
export class ColumnNotFoundError extends ExecutionError {
|
|
31
31
|
/**
|
|
32
32
|
* @param {Object} options
|
|
33
|
-
* @param {string} options.
|
|
33
|
+
* @param {string} options.missingColumn - The missing column name
|
|
34
34
|
* @param {string[]} options.availableColumns - List of available column names
|
|
35
35
|
* @param {number} options.positionStart
|
|
36
36
|
* @param {number} options.positionEnd
|
|
37
37
|
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
38
38
|
*/
|
|
39
|
-
constructor({
|
|
39
|
+
constructor({ missingColumn, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
40
40
|
const available = availableColumns.length > 0
|
|
41
41
|
? `. Available columns: ${availableColumns.join(', ')}`
|
|
42
42
|
: ''
|
|
43
43
|
super({
|
|
44
|
-
message: `Column "${
|
|
44
|
+
message: `Column "${missingColumn}" not found${available}`,
|
|
45
45
|
positionStart,
|
|
46
46
|
positionEnd,
|
|
47
47
|
rowIndex,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ColumnNotFoundError, TableNotFoundError } from './planErrors.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { AsyncDataSource, ExprNode, ScanOptions } from '../types.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {string} options.table - The name of the table to validate
|
|
10
|
+
* @param {Record<string, AsyncDataSource>} options.tables - Object mapping table names to data sources
|
|
11
|
+
* @param {number} [options.positionStart] - Optional start position for error reporting
|
|
12
|
+
* @param {number} [options.positionEnd] - Optional end position for error reporting
|
|
13
|
+
* @returns {AsyncDataSource}
|
|
14
|
+
*/
|
|
15
|
+
export function validateTable({ table, tables, positionStart, positionEnd } ) {
|
|
16
|
+
const resolved = tables[table]
|
|
17
|
+
if (!resolved) {
|
|
18
|
+
throw new TableNotFoundError({ table, tables, positionStart, positionEnd })
|
|
19
|
+
}
|
|
20
|
+
return resolved
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates that a table exists and requested columns are available.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} options
|
|
27
|
+
* @param {string} [options.table]
|
|
28
|
+
* @param {ScanOptions} options.hints
|
|
29
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
30
|
+
* @param {number} [options.positionStart]
|
|
31
|
+
* @param {number} [options.positionEnd]
|
|
32
|
+
*/
|
|
33
|
+
export function validateScan({ table, hints, tables, positionStart, positionEnd }) {
|
|
34
|
+
if (!tables) return
|
|
35
|
+
const resolved = validateTable({ table, tables, positionStart, positionEnd })
|
|
36
|
+
const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
|
|
37
|
+
if (missingColumn) {
|
|
38
|
+
throw new ColumnNotFoundError({
|
|
39
|
+
missingColumn,
|
|
40
|
+
availableColumns: resolved.columns,
|
|
41
|
+
positionStart,
|
|
42
|
+
positionEnd,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates that qualified identifiers reference known table aliases.
|
|
49
|
+
*
|
|
50
|
+
* @param {ExprNode} expr
|
|
51
|
+
* @param {Record<string, any>} tables
|
|
52
|
+
*/
|
|
53
|
+
export function validateTableRefs(expr, tables) {
|
|
54
|
+
if (!expr) return
|
|
55
|
+
if (expr.type === 'identifier') {
|
|
56
|
+
const dotIndex = expr.name.indexOf('.')
|
|
57
|
+
if (dotIndex >= 0) {
|
|
58
|
+
const table = expr.name.substring(0, dotIndex)
|
|
59
|
+
if (!(table in tables)) {
|
|
60
|
+
throw new TableNotFoundError({ table, tables, positionStart: expr.positionStart, positionEnd: expr.positionStart + dotIndex })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (expr.type === 'binary') {
|
|
66
|
+
validateTableRefs(expr.left, tables)
|
|
67
|
+
validateTableRefs(expr.right, tables)
|
|
68
|
+
} else if (expr.type === 'unary') {
|
|
69
|
+
validateTableRefs(expr.argument, tables)
|
|
70
|
+
} else if (expr.type === 'function') {
|
|
71
|
+
for (const arg of expr.args) {
|
|
72
|
+
validateTableRefs(arg, tables)
|
|
73
|
+
}
|
|
74
|
+
} else if (expr.type === 'cast') {
|
|
75
|
+
validateTableRefs(expr.expr, tables)
|
|
76
|
+
} else if (expr.type === 'in valuelist') {
|
|
77
|
+
validateTableRefs(expr.expr, tables)
|
|
78
|
+
for (const val of expr.values) {
|
|
79
|
+
validateTableRefs(val, tables)
|
|
80
|
+
}
|
|
81
|
+
} else if (expr.type === 'case') {
|
|
82
|
+
validateTableRefs(expr.caseExpr, tables)
|
|
83
|
+
for (const w of expr.whenClauses) {
|
|
84
|
+
validateTableRefs(w.condition, tables)
|
|
85
|
+
validateTableRefs(w.result, tables)
|
|
86
|
+
}
|
|
87
|
+
validateTableRefs(expr.elseResult, tables)
|
|
88
|
+
}
|
|
89
|
+
}
|