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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.13",
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.2",
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 results = await executeStatement({ query: node.subquery, context }).rows().next()
655
- return results.done === false
656
- }
657
- if (node.type === 'not exists') {
658
- const results = await executeStatement({ query: node.subquery, context }).rows().next()
659
- return results.done === true
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
@@ -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 {
@@ -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.