squirreling 0.12.13 → 0.12.14
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 +2 -2
- package/src/expression/evaluate.js +17 -7
- package/src/plan/columns.js +32 -0
- package/src/plan/plan.js +1 -1
- package/src/validation/tables.js +22 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.14",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "25.6.0",
|
|
43
43
|
"@vitest/coverage-v8": "4.1.5",
|
|
44
|
-
"eslint": "9.39.
|
|
44
|
+
"eslint": "9.39.4",
|
|
45
45
|
"eslint-plugin-jsdoc": "62.9.0",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
47
|
"vitest": "4.1.5"
|
|
@@ -62,6 +62,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
62
62
|
if (context.outerRow && context.outerAliases?.has(node.prefix) && node.name in context.outerRow.cells) {
|
|
63
63
|
return context.outerRow.cells[node.name]()
|
|
64
64
|
}
|
|
65
|
+
// Standalone `FROM UNNEST(...) AS alias` row has a single bare column;
|
|
66
|
+
// `alias.field` should struct-access that column's element.
|
|
67
|
+
if (context.scope?.includes(node.prefix) && row.columns.length === 1) {
|
|
68
|
+
const value = await row.cells[row.columns[0]]()
|
|
69
|
+
if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, node.name)) {
|
|
70
|
+
return value[node.name]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
// Fall back to just the column part
|
|
66
74
|
if (node.name in row.cells) {
|
|
67
75
|
return row.cells[node.name]()
|
|
@@ -650,13 +658,15 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
650
658
|
}
|
|
651
659
|
|
|
652
660
|
// EXISTS and NOT EXISTS with subqueries
|
|
653
|
-
if (node.type === 'exists') {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
661
|
+
if (node.type === 'exists' || node.type === 'not exists') {
|
|
662
|
+
const outerScope = context.scope
|
|
663
|
+
const subContext = outerScope
|
|
664
|
+
? { ...context, outerRow: row, outerAliases: new Set(outerScope) }
|
|
665
|
+
: context
|
|
666
|
+
const gen = executeStatement({ query: node.subquery, context: subContext, outerScope }).rows()
|
|
667
|
+
const results = await gen.next()
|
|
668
|
+
gen.return(undefined)
|
|
669
|
+
return node.type === 'exists' ? results.done === false : results.done === true
|
|
660
670
|
}
|
|
661
671
|
|
|
662
672
|
// CASE expressions
|
package/src/plan/columns.js
CHANGED
|
@@ -268,6 +268,11 @@ function collectColumnsFromExpr(expr, columns, aliases) {
|
|
|
268
268
|
for (const id of inner) {
|
|
269
269
|
if (id.prefix) columns.push(id)
|
|
270
270
|
}
|
|
271
|
+
// FROM-function args (e.g. UNNEST in the subquery's FROM) are evaluated
|
|
272
|
+
// against the outer scope — the table function is itself the FROM, so
|
|
273
|
+
// any identifier inside its args must be correlated. Push them even if
|
|
274
|
+
// unprefixed so the outer scan reads the columns they reference.
|
|
275
|
+
collectFromFunctionArgs(sub, columns)
|
|
271
276
|
}
|
|
272
277
|
}
|
|
273
278
|
// No columns: count(*), literal, interval
|
|
@@ -309,6 +314,33 @@ function collectColumnsFromStatement(stmt, columns) {
|
|
|
309
314
|
for (const item of stmt.orderBy) collectColumnsFromExpr(item.expr, columns)
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Walks a subquery statement and collects identifiers from FROM-function args
|
|
319
|
+
* (e.g. UNNEST(col) in FROM). These identifiers are necessarily correlated
|
|
320
|
+
* outer references because the table function is the only source.
|
|
321
|
+
*
|
|
322
|
+
* @param {Statement} stmt
|
|
323
|
+
* @param {IdentifierNode[]} columns
|
|
324
|
+
*/
|
|
325
|
+
function collectFromFunctionArgs(stmt, columns) {
|
|
326
|
+
if (stmt.type === 'compound') {
|
|
327
|
+
collectFromFunctionArgs(stmt.left, columns)
|
|
328
|
+
collectFromFunctionArgs(stmt.right, columns)
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (stmt.type === 'with') {
|
|
332
|
+
collectFromFunctionArgs(stmt.query, columns)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
if (stmt.from?.type === 'function') {
|
|
336
|
+
for (const arg of stmt.from.args) {
|
|
337
|
+
collectColumnsFromExpr(arg, columns)
|
|
338
|
+
}
|
|
339
|
+
} else if (stmt.from?.type === 'subquery') {
|
|
340
|
+
collectFromFunctionArgs(stmt.from.query, columns)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
312
344
|
/**
|
|
313
345
|
* Infers output columns for set-operation validation.
|
|
314
346
|
*
|
package/src/plan/plan.js
CHANGED
|
@@ -343,7 +343,7 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope }) {
|
|
|
343
343
|
return { type: 'Scan', table: select.from.table, hints }
|
|
344
344
|
} else if (select.from.type === 'function') {
|
|
345
345
|
for (const arg of select.from.args) {
|
|
346
|
-
validateNoIdentifiers(arg, select.from.funcName)
|
|
346
|
+
validateNoIdentifiers(arg, select.from.funcName, outerScope)
|
|
347
347
|
}
|
|
348
348
|
return planTableFunction(select.from)
|
|
349
349
|
} else {
|
package/src/validation/tables.js
CHANGED
|
@@ -48,13 +48,22 @@ export function validateScan({ table, hints, tables, positionStart, positionEnd
|
|
|
48
48
|
/**
|
|
49
49
|
* Throws if the expression references any column identifier. Used for
|
|
50
50
|
* expressions that have no row scope (e.g. table function arguments in FROM).
|
|
51
|
+
* When `outerScope` is provided, identifiers that resolve to an outer query's
|
|
52
|
+
* alias (correlated reference) are allowed — they will be evaluated against
|
|
53
|
+
* the outer row at runtime.
|
|
51
54
|
*
|
|
52
55
|
* @param {ExprNode} expr
|
|
53
56
|
* @param {string} context - context for the error message (e.g. function name)
|
|
57
|
+
* @param {string[]} [outerScope] - aliases of outer queries
|
|
54
58
|
*/
|
|
55
|
-
export function validateNoIdentifiers(expr, context) {
|
|
59
|
+
export function validateNoIdentifiers(expr, context, outerScope) {
|
|
56
60
|
if (!expr) return
|
|
57
61
|
if (expr.type === 'identifier') {
|
|
62
|
+
if (outerScope?.length) {
|
|
63
|
+
// Correlated reference: prefix matches an outer alias, or unqualified
|
|
64
|
+
// (resolved against the outer row at runtime).
|
|
65
|
+
if (!expr.prefix || outerScope.includes(expr.prefix)) return
|
|
66
|
+
}
|
|
58
67
|
const name = expr.prefix ? `${expr.prefix}.${expr.name}` : expr.name
|
|
59
68
|
throw new ExecutionError({
|
|
60
69
|
message: `${context} argument cannot reference column "${name}" — use JOIN ${context}(...) to reference columns from another table`,
|
|
@@ -63,31 +72,31 @@ export function validateNoIdentifiers(expr, context) {
|
|
|
63
72
|
})
|
|
64
73
|
}
|
|
65
74
|
if (expr.type === 'binary') {
|
|
66
|
-
validateNoIdentifiers(expr.left, context)
|
|
67
|
-
validateNoIdentifiers(expr.right, context)
|
|
75
|
+
validateNoIdentifiers(expr.left, context, outerScope)
|
|
76
|
+
validateNoIdentifiers(expr.right, context, outerScope)
|
|
68
77
|
} else if (expr.type === 'unary') {
|
|
69
|
-
validateNoIdentifiers(expr.argument, context)
|
|
78
|
+
validateNoIdentifiers(expr.argument, context, outerScope)
|
|
70
79
|
} else if (expr.type === 'function') {
|
|
71
80
|
for (const arg of expr.args) {
|
|
72
|
-
validateNoIdentifiers(arg, context)
|
|
81
|
+
validateNoIdentifiers(arg, context, outerScope)
|
|
73
82
|
}
|
|
74
83
|
} else if (expr.type === 'cast') {
|
|
75
|
-
validateNoIdentifiers(expr.expr, context)
|
|
84
|
+
validateNoIdentifiers(expr.expr, context, outerScope)
|
|
76
85
|
} else if (expr.type === 'in valuelist') {
|
|
77
|
-
validateNoIdentifiers(expr.expr, context)
|
|
86
|
+
validateNoIdentifiers(expr.expr, context, outerScope)
|
|
78
87
|
for (const val of expr.values) {
|
|
79
|
-
validateNoIdentifiers(val, context)
|
|
88
|
+
validateNoIdentifiers(val, context, outerScope)
|
|
80
89
|
}
|
|
81
90
|
} else if (expr.type === 'in') {
|
|
82
91
|
// LHS is in our scope; subquery is self-contained and planned separately.
|
|
83
|
-
validateNoIdentifiers(expr.expr, context)
|
|
92
|
+
validateNoIdentifiers(expr.expr, context, outerScope)
|
|
84
93
|
} else if (expr.type === 'case') {
|
|
85
|
-
validateNoIdentifiers(expr.caseExpr, context)
|
|
94
|
+
validateNoIdentifiers(expr.caseExpr, context, outerScope)
|
|
86
95
|
for (const w of expr.whenClauses) {
|
|
87
|
-
validateNoIdentifiers(w.condition, context)
|
|
88
|
-
validateNoIdentifiers(w.result, context)
|
|
96
|
+
validateNoIdentifiers(w.condition, context, outerScope)
|
|
97
|
+
validateNoIdentifiers(w.result, context, outerScope)
|
|
89
98
|
}
|
|
90
|
-
validateNoIdentifiers(expr.elseResult, context)
|
|
99
|
+
validateNoIdentifiers(expr.elseResult, context, outerScope)
|
|
91
100
|
}
|
|
92
101
|
// subquery / exists / not exists are self-contained — their identifiers
|
|
93
102
|
// resolve to their own FROM and are validated when the subquery is planned.
|