squirreling 0.11.3 → 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 +1 -1
- package/src/execute/execute.js +1 -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/primary.js +5 -1
- package/src/plan/columns.js +13 -15
- package/src/plan/plan.js +14 -8
- 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
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -290,7 +290,7 @@ async function* executeProject(plan, context) {
|
|
|
290
290
|
if (prefix && !key.startsWith(prefix)) continue
|
|
291
291
|
// Strip table prefix for output column names
|
|
292
292
|
const dotIndex = key.indexOf('.')
|
|
293
|
-
const outputKey = dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
293
|
+
const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
|
|
294
294
|
columns.push(outputKey)
|
|
295
295
|
cells[outputKey] = row.cells[key]
|
|
296
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/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)
|
|
@@ -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 }) {
|
|
@@ -216,7 +216,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
216
216
|
// When parent only needs specific columns, drop unneeded projections
|
|
217
217
|
if (parentColumns) {
|
|
218
218
|
projectColumns = projectColumns.filter(col =>
|
|
219
|
-
col.type === 'star' || parentColumns.
|
|
219
|
+
col.type === 'star' || parentColumns.some(id => id.name === (col.alias ?? derivedAlias(col.expr)))
|
|
220
220
|
)
|
|
221
221
|
}
|
|
222
222
|
plan = { type: 'Project', columns: projectColumns, child: plan }
|
|
@@ -252,7 +252,13 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables }) {
|
|
|
252
252
|
validateScan({ ...select.from, hints, tables })
|
|
253
253
|
return { type: 'Scan', table: select.from.table, hints }
|
|
254
254
|
} else {
|
|
255
|
-
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
|
+
})
|
|
256
262
|
// Validate that requested columns exist in subquery output
|
|
257
263
|
const availableColumns = inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
|
|
258
264
|
if (hints.columns && availableColumns.length) {
|
|
@@ -342,7 +348,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
342
348
|
function resolveAliases(node, aliases) {
|
|
343
349
|
if (!node || !aliases.size) return node
|
|
344
350
|
if (node.type === 'identifier') {
|
|
345
|
-
return aliases.get(node.name) ?? node
|
|
351
|
+
return node.prefix ? node : aliases.get(node.name) ?? node
|
|
346
352
|
}
|
|
347
353
|
if (node.type === 'unary') {
|
|
348
354
|
return { ...node, argument: resolveAliases(node.argument, aliases) }
|
|
@@ -394,8 +400,8 @@ function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
|
|
|
394
400
|
if (left.type !== 'identifier' || right.type !== 'identifier') return
|
|
395
401
|
|
|
396
402
|
// Check if keys are in swapped order (right table ref on left side)
|
|
397
|
-
const leftRefsRight = left.
|
|
398
|
-
const rightRefsLeft = right.
|
|
403
|
+
const leftRefsRight = left.prefix === rightTable
|
|
404
|
+
const rightRefsLeft = right.prefix === leftTable
|
|
399
405
|
|
|
400
406
|
if (leftRefsRight && rightRefsLeft) {
|
|
401
407
|
return { leftKey: right, rightKey: left }
|
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
|