@quereus/quereus 0.6.12 → 0.7.1

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.
Files changed (53) hide show
  1. package/dist/src/parser/lexer.d.ts +6 -0
  2. package/dist/src/parser/lexer.d.ts.map +1 -1
  3. package/dist/src/parser/lexer.js +33 -1
  4. package/dist/src/parser/lexer.js.map +1 -1
  5. package/dist/src/parser/parser.d.ts.map +1 -1
  6. package/dist/src/parser/parser.js +28 -24
  7. package/dist/src/parser/parser.js.map +1 -1
  8. package/dist/src/planner/building/select-aggregates.d.ts +6 -1
  9. package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
  10. package/dist/src/planner/building/select-aggregates.js +23 -4
  11. package/dist/src/planner/building/select-aggregates.js.map +1 -1
  12. package/dist/src/planner/building/select-modifiers.js +7 -2
  13. package/dist/src/planner/building/select-modifiers.js.map +1 -1
  14. package/dist/src/planner/building/select.d.ts.map +1 -1
  15. package/dist/src/planner/building/select.js +2 -2
  16. package/dist/src/planner/building/select.js.map +1 -1
  17. package/dist/src/planner/building/update.d.ts.map +1 -1
  18. package/dist/src/planner/building/update.js +8 -4
  19. package/dist/src/planner/building/update.js.map +1 -1
  20. package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
  21. package/dist/src/planner/nodes/join-node.js +6 -1
  22. package/dist/src/planner/nodes/join-node.js.map +1 -1
  23. package/dist/src/planner/rules/access/rule-select-access-path.js +15 -2
  24. package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
  25. package/dist/src/schema/manager.d.ts +30 -0
  26. package/dist/src/schema/manager.d.ts.map +1 -1
  27. package/dist/src/schema/manager.js +205 -0
  28. package/dist/src/schema/manager.js.map +1 -1
  29. package/dist/src/vtab/best-access-plan.d.ts +2 -0
  30. package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
  31. package/dist/src/vtab/best-access-plan.js.map +1 -1
  32. package/dist/src/vtab/memory/layer/scan-plan.js +2 -2
  33. package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
  34. package/dist/src/vtab/memory/module.d.ts +1 -1
  35. package/dist/src/vtab/memory/module.d.ts.map +1 -1
  36. package/dist/src/vtab/memory/module.js +2 -1
  37. package/dist/src/vtab/memory/module.js.map +1 -1
  38. package/dist/src/vtab/module.d.ts +2 -1
  39. package/dist/src/vtab/module.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/parser/lexer.ts +806 -771
  42. package/src/parser/parser.ts +3352 -3347
  43. package/src/planner/building/select-aggregates.ts +30 -5
  44. package/src/planner/building/select-modifiers.ts +8 -2
  45. package/src/planner/building/select.ts +567 -560
  46. package/src/planner/building/update.ts +9 -5
  47. package/src/planner/nodes/join-node.ts +6 -1
  48. package/src/planner/rules/access/rule-select-access-path.ts +399 -384
  49. package/src/schema/manager.ts +235 -1
  50. package/src/vtab/best-access-plan.ts +2 -0
  51. package/src/vtab/memory/layer/scan-plan.ts +2 -2
  52. package/src/vtab/memory/module.ts +2 -1
  53. package/src/vtab/module.ts +162 -160
@@ -1,560 +1,567 @@
1
- import type * as AST from '../../parser/ast.js';
2
- import { PlanNode, type RelationalPlanNode, type ScalarPlanNode } from '../nodes/plan-node.js';
3
- import { QuereusError } from '../../common/errors.js';
4
- import { StatusCode } from '../../common/types.js';
5
- import type { PlanningContext } from '../planning-context.js';
6
- import { SingleRowNode } from '../nodes/single-row.js';
7
- import { buildTableReference } from './table.js';
8
- import { AliasedScope } from '../scopes/aliased.js';
9
- import { RegisteredScope } from '../scopes/registered.js';
10
- import type { Scope } from '../scopes/scope.js';
11
- import { MultiScope } from '../scopes/multi.js';
12
- import { ProjectNode, type Projection } from '../nodes/project-node.js';
13
- import { buildExpression } from './expression.js';
14
- import { FilterNode } from '../nodes/filter.js';
15
- import { buildTableFunctionCall } from './table-function.js';
16
- import { CTEReferenceNode } from '../nodes/cte-reference-node.js';
17
- import { InternalRecursiveCTERefNode as _InternalRecursiveCTERefNode } from '../nodes/internal-recursive-cte-ref-node.js';
18
- import type { CTEScopeNode, CTEPlanNode } from '../nodes/cte-node.js';
19
- import { JoinNode } from '../nodes/join-node.js';
20
- import { ColumnReferenceNode } from '../nodes/reference.js';
21
- import { TEXT_TYPE } from '../../types/builtin-types.js';
22
- import { ValuesNode } from '../nodes/values-node.js';
23
- import { createLogger } from '../../common/logger.js';
24
-
25
- // Import decomposed functionality
26
- import { buildWithContext } from './select-context.js';
27
- import { buildCompoundSelect } from './select-compound.js';
28
- import { analyzeSelectColumns, buildStarProjections } from './select-projections.js';
29
- import { buildAggregatePhase, buildFinalAggregateProjections } from './select-aggregates.js';
30
- import { buildWindowPhase } from './select-window.js';
31
- import { buildFinalProjections, applyDistinct, applyOrderBy, applyLimitOffset } from './select-modifiers.js';
32
- import { SortNode, type SortKey } from '../nodes/sort.js';
33
-
34
- import { buildInsertStmt } from './insert.js';
35
- import { buildUpdateStmt } from './update.js';
36
- import { buildDeleteStmt } from './delete.js';
37
- import { CapabilityDetectors } from '../framework/characteristics.js';
38
-
39
- const logger = createLogger('planner:cte');
40
-
41
- /**
42
- * Creates an initial logical query plan for a SELECT statement.
43
- *
44
- * For this initial version, it only supports simple "SELECT ... FROM one_table" queries,
45
- * effectively returning a TableReferenceNode for that table.
46
- *
47
- * @param stmt The AST.SelectStmt to plan.
48
- * @param ctx The parent planning context for this SELECT statement.
49
- * @param parentCTEs A map of parent CTEs for compound statements.
50
- * @returns A BatchNode representing the plan for the SELECT statement.
51
- * @throws {QuereusError} If the FROM clause is missing, empty, or contains more than one source.
52
- */
53
- export function buildSelectStmt(
54
- ctx: PlanningContext,
55
- stmt: AST.SelectStmt,
56
- parentCTEs: Map<string, CTEScopeNode> = new Map(),
57
- /**
58
- * Whether ProjectNodes inside this SELECT should forward all input columns that are not explicitly
59
- * listed in the projection list. This is desirable for top-level queries (helps ORDER BY, window
60
- * functions, etc.) but must be switched off for scalar/IN/EXISTS sub-queries which are required to
61
- * expose only their declared columns.
62
- */
63
- preserveInputColumns: boolean = true
64
- ): PlanNode {
65
-
66
- // Phase 0: Handle WITH clause if present
67
- const { contextWithCTEs, cteNodes } = buildWithContext(ctx, stmt, parentCTEs);
68
-
69
- // Handle compound set operations (UNION / INTERSECT / EXCEPT)
70
- if (stmt.compound) {
71
- return buildCompoundSelect(stmt, contextWithCTEs, cteNodes,
72
- (ctx, stmt, parentCTEs) => buildSelectStmt(ctx, stmt, parentCTEs) as RelationalPlanNode);
73
- }
74
-
75
- // Phase 1: Plan FROM clause and determine local input relations for the current select scope
76
- // Use the context that includes CTEs as well as the merged CTE map so that table references
77
- // inside the FROM clause can correctly resolve to CTE definitions created by the WITH clause.
78
- const fromTables = !stmt.from || stmt.from.length === 0
79
- ? [SingleRowNode.instance]
80
- : stmt.from.map(from => buildFrom(from, contextWithCTEs, cteNodes));
81
-
82
- // Multiple FROM sources (from joins) are not supported - maybe never will be
83
- if (fromTables.length > 1) {
84
- throw new QuereusError(
85
- 'SELECT with multiple FROM sources (joins) not supported.',
86
- StatusCode.UNSUPPORTED, undefined, stmt.from![1].loc?.start.line, stmt.from![1].loc?.start.column
87
- );
88
- }
89
-
90
- // Phase 2: Create the main scope for this SELECT statement
91
- const columnScopes = fromTables.map(ft => ctx.outputScopes.get(ft) || ft.scope).filter(Boolean);
92
- const selectScope = new MultiScope([...columnScopes, contextWithCTEs.scope]);
93
- let selectContext: PlanningContext = { ...contextWithCTEs, scope: selectScope };
94
-
95
- let input: RelationalPlanNode = fromTables[0];
96
-
97
- // Plan WHERE clause
98
- if (stmt.where) {
99
- const whereExpression = buildExpression(selectContext, stmt.where);
100
- input = new FilterNode(selectScope, input, whereExpression);
101
- }
102
-
103
- // Build projections based on the SELECT list
104
- const projections: Projection[] = [];
105
-
106
- // Analyze SELECT columns
107
- const {
108
- projections: columnProjections,
109
- aggregates,
110
- windowFunctions,
111
- hasAggregates,
112
- hasWindowFunctions
113
- } = analyzeSelectColumns(stmt.columns, selectContext);
114
-
115
- // Handle SELECT * separately
116
- for (const column of stmt.columns) {
117
- if (column.type === 'all') {
118
- const starProjections = buildStarProjections(column, input, selectScope);
119
- projections.push(...starProjections);
120
- }
121
- }
122
-
123
- // Add non-star projections
124
- projections.push(...columnProjections);
125
-
126
- // Process aggregates if present
127
- const aggregateResult = buildAggregatePhase(input, stmt, selectContext, aggregates, hasAggregates, projections);
128
- input = aggregateResult.output;
129
- let preAggregateSort = aggregateResult.preAggregateSort;
130
-
131
- // Update context if we have aggregates
132
- if (aggregateResult.aggregateScope) {
133
- selectContext = { ...selectContext, scope: aggregateResult.aggregateScope };
134
-
135
- // Build final projections if needed
136
- if (aggregateResult.needsFinalProjection) {
137
- const finalProjections = buildFinalAggregateProjections(stmt, selectContext, aggregateResult.aggregateScope);
138
- input = new ProjectNode(selectScope, input, finalProjections, undefined, undefined, preserveInputColumns);
139
- }
140
- }
141
-
142
- // Handle window functions if present
143
- if (hasWindowFunctions) {
144
- // Check if ORDER BY references columns not in SELECT before applying window functions
145
- let preWindowSort = false;
146
- if (stmt.orderBy) {
147
- const selectedColumns = new Set<string>();
148
- for (const column of stmt.columns) {
149
- if (column.type === 'column' && column.expr.type === 'column') {
150
- selectedColumns.add(column.expr.name.toLowerCase());
151
- }
152
- if (column.type === 'column' && column.alias) {
153
- selectedColumns.add(column.alias.toLowerCase());
154
- }
155
- }
156
-
157
- // Check if ORDER BY references columns not in SELECT
158
- for (const orderByClause of stmt.orderBy) {
159
- if (orderByClause.expr.type === 'column') {
160
- const orderColumn = orderByClause.expr.name.toLowerCase();
161
- if (!selectedColumns.has(orderColumn)) {
162
- // Apply ORDER BY before window projections
163
- const sortKeys: SortKey[] = stmt.orderBy.map(orderBy => ({
164
- expression: buildExpression(selectContext, orderBy.expr),
165
- direction: orderBy.direction,
166
- nulls: orderBy.nulls
167
- }));
168
- input = new SortNode(selectContext.scope, input, sortKeys);
169
- preWindowSort = true;
170
- break;
171
- }
172
- }
173
- }
174
- }
175
-
176
- input = buildWindowPhase(input, windowFunctions, selectContext, stmt);
177
-
178
- // Update context to include window output columns
179
- const windowOutputScope = new RegisteredScope(selectContext.scope);
180
- const windowAttributes = input.getAttributes();
181
- input.getType().columns.forEach((col, index) => {
182
- const attr = windowAttributes[index];
183
- windowOutputScope.registerSymbol(col.name.toLowerCase(), (exp, s) =>
184
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, col.type, attr.id, index));
185
- });
186
-
187
- // Create combined scope that includes both original columns and window output
188
- const combinedScope = new MultiScope([windowOutputScope, selectScope]);
189
- selectContext = { ...selectContext, scope: combinedScope };
190
-
191
- // Don't apply ORDER BY again if we already did it
192
- if (preWindowSort) {
193
- preAggregateSort = true;
194
- }
195
- }
196
-
197
- // Handle final projections for non-aggregate, non-window cases
198
- if (!hasAggregates && !hasWindowFunctions) {
199
- const finalResult = buildFinalProjections(input, projections, selectScope, stmt, selectContext, preserveInputColumns);
200
- input = finalResult.output;
201
- selectContext = finalResult.finalContext;
202
- preAggregateSort = finalResult.preAggregateSort;
203
-
204
- // Apply final modifiers with projection scope for column alias resolution
205
- input = applyDistinct(input, stmt, selectScope);
206
- input = applyOrderBy(input, stmt, selectContext, preAggregateSort, finalResult.projectionScope);
207
- input = applyLimitOffset(input, stmt, selectContext, finalResult.projectionScope);
208
- } else {
209
- // Apply final modifiers without projection scope for aggregate/window cases
210
- input = applyDistinct(input, stmt, selectScope);
211
- input = applyOrderBy(input, stmt, selectContext, preAggregateSort);
212
- input = applyLimitOffset(input, stmt, selectContext);
213
- }
214
-
215
- return input;
216
- }
217
-
218
- /**
219
- * Creates a plan for a VALUES statement.
220
- *
221
- * @param ctx The planning context
222
- * @param stmt The AST.ValuesStmt to plan
223
- * @returns A ValuesNode representing the VALUES clause
224
- */
225
- export function buildValuesStmt(
226
- ctx: PlanningContext,
227
- stmt: AST.ValuesStmt
228
- ): ValuesNode {
229
- // Build each row of values
230
- const rows: ScalarPlanNode[][] = stmt.values.map(rowValues =>
231
- rowValues.map(valueExpr => buildExpression(ctx, valueExpr))
232
- );
233
-
234
- // Create the VALUES node
235
- return new ValuesNode(ctx.scope, rows);
236
- }
237
-
238
- /**
239
- * Processes a FROM clause item into a relational plan node.
240
- *
241
- * Handles different types of FROM items:
242
- * - Table references - creates a TableReferenceNode
243
- * - Subqueries - plans the subquery
244
- * - Joins - builds the join structure
245
- * - Table functions - creates a table function call node
246
- *
247
- * For a simple table reference, this calls buildTableReference which
248
- * returns a TableReferenceNode for that table.
249
- *
250
- * @param fromClause The FROM clause AST node to process
251
- * @param ctx The planning context
252
- * @returns A relational plan node representing the FROM clause
253
- */
254
- export function buildFrom(fromClause: AST.FromClause, parentContext: PlanningContext, cteNodes: Map<string, CTEScopeNode> = new Map()): RelationalPlanNode {
255
- let fromTable: RelationalPlanNode;
256
- let columnScope: Scope;
257
-
258
- if (fromClause.type === 'table') {
259
- const tableName = fromClause.table.name.toLowerCase();
260
-
261
- // Check if this is a CTE reference
262
- if (cteNodes.has(tableName)) {
263
- const cteNode = cteNodes.get(tableName)!;
264
-
265
- // Check if this is an internal recursive CTE reference
266
- if (CapabilityDetectors.isRecursiveCTERef(cteNode)) {
267
- // For internal recursive references, use the node directly
268
- fromTable = cteNode;
269
-
270
- // Create scope for internal recursive CTE columns
271
- const internalScope = new RegisteredScope(parentContext.scope);
272
- const internalAttributes = cteNode.getAttributes();
273
- cteNode.getType().columns.forEach((c, i) => {
274
- const attr = internalAttributes[i];
275
- internalScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
276
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
277
- });
278
-
279
- if (fromClause.alias) {
280
- columnScope = new AliasedScope(internalScope, tableName, fromClause.alias.toLowerCase());
281
- } else {
282
- columnScope = new AliasedScope(internalScope, tableName, tableName);
283
- }
284
- } else {
285
- // Regular CTE reference - cache by CTE name + alias to ensure consistent attribute IDs
286
- const cacheKey = `${tableName}:${fromClause.alias || tableName}`;
287
-
288
- // Initialize cache if not exists
289
- if (!parentContext.cteReferenceCache) {
290
- parentContext.cteReferenceCache = new Map();
291
- }
292
-
293
- let cteRefNode: CTEReferenceNode;
294
- if (parentContext.cteReferenceCache.has(cacheKey)) {
295
- cteRefNode = parentContext.cteReferenceCache.get(cacheKey)!;
296
- const attrs = cteRefNode.getAttributes();
297
- logger(`Using cached CTE reference ${cacheKey}, attrs=[${attrs.map(a => a.id).join(',')}]`);
298
- } else {
299
- cteRefNode = new CTEReferenceNode(parentContext.scope, cteNode as CTEPlanNode, fromClause.alias);
300
- parentContext.cteReferenceCache.set(cacheKey, cteRefNode);
301
- const attrs = cteRefNode.getAttributes();
302
- logger(`Created new CTE reference ${cacheKey}, attrs=[${attrs.map(a => a.id).join(',')}]`);
303
- }
304
-
305
- // Create scope for CTE columns using attributes from the reference node
306
- // CRITICAL: Use a closure to capture the reference node's attributes
307
- // This ensures all column references use the same attribute IDs
308
- const cteScope = new RegisteredScope(parentContext.scope);
309
- const refAttrs = cteRefNode.getAttributes();
310
- cteRefNode.getType().columns.forEach((c, i) => {
311
- const attr = refAttrs[i];
312
- cteScope.registerSymbol(c.name.toLowerCase(), (exp, s) => {
313
- // Always use the cached reference node's attribute ID
314
- return new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i);
315
- });
316
- });
317
-
318
- if (fromClause.alias) {
319
- columnScope = new AliasedScope(cteScope, tableName, fromClause.alias.toLowerCase());
320
- } else {
321
- columnScope = new AliasedScope(cteScope, tableName, tableName);
322
- }
323
-
324
- // CRITICAL: Cache the reference node so later expression compilation uses the same attribute IDs
325
- // TODO: replace this monkey patching with a proper interface
326
- (columnScope as any).referenceNode = cteRefNode;
327
-
328
- fromTable = cteRefNode;
329
- }
330
- } else {
331
- // Check if this is a view
332
- const schemaName = fromClause.table.schema || parentContext.db.schemaManager.getCurrentSchemaName();
333
- const viewSchema = parentContext.db.schemaManager.getView(schemaName, fromClause.table.name);
334
-
335
- if (viewSchema) {
336
- // Build the view's SELECT statement
337
- let viewSelectNode = buildSelectStmt(parentContext, viewSchema.selectAst, cteNodes) as RelationalPlanNode;
338
-
339
- // If the view has explicit column names, wrap with a projection to rename columns
340
- if (viewSchema.columns && viewSchema.columns.length > 0) {
341
- const viewAttributes = viewSelectNode.getAttributes();
342
- const projections = viewSchema.columns.map((columnName, i) => {
343
- if (i >= viewAttributes.length) {
344
- throw new QuereusError(
345
- `View '${viewSchema.name}' has more explicit column names than SELECT columns`,
346
- StatusCode.ERROR
347
- );
348
- }
349
- const attr = viewAttributes[i];
350
- const columnRef = new ColumnReferenceNode(
351
- parentContext.scope,
352
- { type: 'column', name: attr.name } as AST.ColumnExpr,
353
- attr.type,
354
- attr.id,
355
- i
356
- );
357
- return {
358
- node: columnRef,
359
- alias: columnName
360
- };
361
- });
362
- fromTable = new ProjectNode(parentContext.scope, viewSelectNode, projections);
363
- } else {
364
- fromTable = viewSelectNode;
365
- }
366
-
367
- // Create scope for view columns
368
- const viewScope = new RegisteredScope(parentContext.scope);
369
- const viewAttributes = fromTable.getAttributes();
370
- fromTable.getType().columns.forEach((c, i) => {
371
- const attr = viewAttributes[i];
372
- viewScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
373
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
374
- });
375
-
376
- if (fromClause.alias) {
377
- columnScope = new AliasedScope(viewScope, fromClause.table.name.toLowerCase(), fromClause.alias.toLowerCase());
378
- } else {
379
- columnScope = new AliasedScope(viewScope, fromClause.table.name.toLowerCase(), fromClause.table.name.toLowerCase());
380
- }
381
- } else {
382
- // Regular table
383
- fromTable = buildTableReference(fromClause, parentContext);
384
-
385
- // Create scope for table columns
386
- const tableScope = new RegisteredScope(parentContext.scope);
387
- const tableAttributes = fromTable.getAttributes();
388
- fromTable.getType().columns.forEach((c, i) => {
389
- const attr = tableAttributes[i];
390
- tableScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
391
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
392
- });
393
-
394
- if (fromClause.alias) {
395
- columnScope = new AliasedScope(tableScope, fromClause.table.name.toLowerCase(), fromClause.alias.toLowerCase());
396
- } else {
397
- columnScope = new AliasedScope(tableScope, fromClause.table.name.toLowerCase(), fromClause.table.name.toLowerCase());
398
- }
399
- }
400
- }
401
-
402
- } else if (fromClause.type === 'functionSource') {
403
- fromTable = buildTableFunctionCall(fromClause, parentContext);
404
-
405
- // Create scope for function columns
406
- const functionScope = new RegisteredScope(parentContext.scope);
407
- const functionAttributes = fromTable.getAttributes();
408
- fromTable.getType().columns.forEach((c, i) => {
409
- const attr = functionAttributes[i];
410
- functionScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
411
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
412
- });
413
-
414
- if (fromClause.alias) {
415
- // Use the alias as the table name
416
- columnScope = new AliasedScope(functionScope, '', fromClause.alias.toLowerCase());
417
- } else {
418
- // Use the function name as the table name
419
- columnScope = new AliasedScope(functionScope, '', fromClause.name.name.toLowerCase());
420
- }
421
-
422
- } else if (fromClause.type === 'subquerySource') {
423
- // Build the subquery
424
- if (fromClause.subquery.type === 'select') {
425
- fromTable = buildSelectStmt(parentContext, fromClause.subquery, cteNodes) as RelationalPlanNode;
426
- } else if (fromClause.subquery.type === 'values') {
427
- fromTable = buildValuesStmt(parentContext, fromClause.subquery);
428
- } else {
429
- const exhaustiveCheck: never = fromClause.subquery;
430
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
431
- throw new QuereusError(`Unsupported subquery type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
432
- }
433
-
434
- // Create scope for subquery columns
435
- const subqueryScope = new RegisteredScope(parentContext.scope);
436
- const subqueryAttributes = fromTable.getAttributes();
437
-
438
- // Use provided column names or infer from subquery
439
- const columnNames = fromClause.columns || fromTable.getType().columns.map(c => c.name);
440
-
441
- columnNames.forEach((colName, i) => {
442
- if (i < subqueryAttributes.length) {
443
- const attr = subqueryAttributes[i];
444
- const columnType = fromTable.getType().columns[i]?.type || { typeClass: 'scalar', logicalType: TEXT_TYPE, nullable: true, isReadOnly: true };
445
- subqueryScope.registerSymbol(colName.toLowerCase(), (exp, s) =>
446
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, columnType, attr.id, i));
447
- }
448
- });
449
-
450
- columnScope = new AliasedScope(subqueryScope, '', fromClause.alias.toLowerCase());
451
-
452
- } else if (fromClause.type === 'mutatingSubquerySource') {
453
- // Build the mutating subquery (DML with RETURNING)
454
- let dmlNode: RelationalPlanNode;
455
-
456
- if (fromClause.stmt.type === 'insert') {
457
- // Build INSERT without SinkNode wrapper since we need the RETURNING results
458
- dmlNode = buildInsertStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
459
- } else if (fromClause.stmt.type === 'update') {
460
- // Build UPDATE without SinkNode wrapper since we need the RETURNING results
461
- dmlNode = buildUpdateStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
462
- } else if (fromClause.stmt.type === 'delete') {
463
- // Build DELETE without SinkNode wrapper since we need the RETURNING results
464
- dmlNode = buildDeleteStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
465
- } else {
466
- const exhaustiveCheck: never = fromClause.stmt;
467
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
468
- throw new QuereusError(`Unsupported mutating subquery type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
469
- }
470
-
471
- fromTable = dmlNode;
472
-
473
- // Create scope for mutating subquery columns
474
- const mutatingScope = new RegisteredScope(parentContext.scope);
475
- const mutatingAttributes = fromTable.getAttributes();
476
-
477
- // Use provided column names or infer from RETURNING clause
478
- const columnNames = fromClause.columns || fromTable.getType().columns.map(c => c.name);
479
-
480
- columnNames.forEach((colName, i) => {
481
- if (i < mutatingAttributes.length) {
482
- const attr = mutatingAttributes[i];
483
- const columnType = fromTable.getType().columns[i]?.type || { typeClass: 'scalar', logicalType: TEXT_TYPE, nullable: true, isReadOnly: false };
484
- mutatingScope.registerSymbol(colName.toLowerCase(), (exp, s) =>
485
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, columnType, attr.id, i));
486
- }
487
- });
488
-
489
- columnScope = new AliasedScope(mutatingScope, '', fromClause.alias.toLowerCase());
490
-
491
- } else if (fromClause.type === 'join') {
492
- // Handle JOIN clauses
493
- return buildJoin(fromClause, parentContext, cteNodes);
494
- } else {
495
- // Handle the case where fromClause.type is not recognized
496
- const exhaustiveCheck: never = fromClause;
497
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
498
- throw new QuereusError(`Unsupported FROM clause type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
499
- }
500
-
501
- parentContext.outputScopes.set(fromTable, columnScope);
502
- return fromTable;
503
- }
504
-
505
- /**
506
- * Builds a join plan node from an AST join clause
507
- */
508
- function buildJoin(joinClause: AST.JoinClause, parentContext: PlanningContext, cteNodes: Map<string, CTEScopeNode>): JoinNode {
509
- // Build left and right sides recursively
510
- const leftNode = buildFrom(joinClause.left, parentContext, cteNodes);
511
- const rightNode = buildFrom(joinClause.right, parentContext, cteNodes);
512
-
513
- // Create a combined scope for join expressions
514
- const leftScope = parentContext.outputScopes.get(leftNode);
515
- const rightScope = parentContext.outputScopes.get(rightNode);
516
- if (!leftScope || !rightScope) {
517
- // This should not happen if buildFrom correctly populates the scopes
518
- throw new QuereusError('Could not find output scope for join source', StatusCode.INTERNAL);
519
- }
520
-
521
- // Create a combined scope for the join that includes both left and right columns
522
- const combinedScope = new MultiScope([leftScope, rightScope]);
523
-
524
- // Create a new planning context with the combined scope for condition evaluation
525
- const joinContext: PlanningContext = {
526
- ...parentContext,
527
- scope: combinedScope
528
- };
529
-
530
- let condition: ScalarPlanNode | undefined;
531
- let usingColumns: string[] | undefined;
532
-
533
- // Handle ON condition
534
- if (joinClause.condition) {
535
- condition = buildExpression(joinContext, joinClause.condition);
536
- }
537
-
538
- // Handle USING columns
539
- if (joinClause.columns) {
540
- usingColumns = joinClause.columns;
541
- // Convert USING to ON condition: table1.col1 = table2.col1 AND table1.col2 = table2.col2 ...
542
- // For now, store the column names and let the emitter handle the condition
543
- // TODO: This could be improved by synthesizing the equality conditions here
544
- }
545
-
546
- const joinNode = new JoinNode(
547
- parentContext.scope,
548
- leftNode,
549
- rightNode,
550
- joinClause.joinType,
551
- condition,
552
- usingColumns
553
- );
554
-
555
- // Use the combined scope as the column scope for the join
556
- // This allows both qualified and unqualified column references to resolve properly
557
- parentContext.outputScopes.set(joinNode, combinedScope);
558
-
559
- return joinNode;
560
- }
1
+ import type * as AST from '../../parser/ast.js';
2
+ import { PlanNode, type RelationalPlanNode, type ScalarPlanNode } from '../nodes/plan-node.js';
3
+ import { QuereusError } from '../../common/errors.js';
4
+ import { StatusCode } from '../../common/types.js';
5
+ import type { PlanningContext } from '../planning-context.js';
6
+ import { SingleRowNode } from '../nodes/single-row.js';
7
+ import { buildTableReference } from './table.js';
8
+ import { AliasedScope } from '../scopes/aliased.js';
9
+ import { RegisteredScope } from '../scopes/registered.js';
10
+ import type { Scope } from '../scopes/scope.js';
11
+ import { MultiScope } from '../scopes/multi.js';
12
+ import { ProjectNode, type Projection } from '../nodes/project-node.js';
13
+ import { buildExpression } from './expression.js';
14
+ import { FilterNode } from '../nodes/filter.js';
15
+ import { buildTableFunctionCall } from './table-function.js';
16
+ import { CTEReferenceNode } from '../nodes/cte-reference-node.js';
17
+ import { InternalRecursiveCTERefNode as _InternalRecursiveCTERefNode } from '../nodes/internal-recursive-cte-ref-node.js';
18
+ import type { CTEScopeNode, CTEPlanNode } from '../nodes/cte-node.js';
19
+ import { JoinNode } from '../nodes/join-node.js';
20
+ import { ColumnReferenceNode } from '../nodes/reference.js';
21
+ import { TEXT_TYPE } from '../../types/builtin-types.js';
22
+ import { ValuesNode } from '../nodes/values-node.js';
23
+ import { createLogger } from '../../common/logger.js';
24
+
25
+ // Import decomposed functionality
26
+ import { buildWithContext } from './select-context.js';
27
+ import { buildCompoundSelect } from './select-compound.js';
28
+ import { analyzeSelectColumns, buildStarProjections } from './select-projections.js';
29
+ import { buildAggregatePhase, buildFinalAggregateProjections } from './select-aggregates.js';
30
+ import { buildWindowPhase } from './select-window.js';
31
+ import { buildFinalProjections, applyDistinct, applyOrderBy, applyLimitOffset } from './select-modifiers.js';
32
+ import { SortNode, type SortKey } from '../nodes/sort.js';
33
+
34
+ import { buildInsertStmt } from './insert.js';
35
+ import { buildUpdateStmt } from './update.js';
36
+ import { buildDeleteStmt } from './delete.js';
37
+ import { CapabilityDetectors } from '../framework/characteristics.js';
38
+
39
+ const logger = createLogger('planner:cte');
40
+
41
+ /**
42
+ * Creates an initial logical query plan for a SELECT statement.
43
+ *
44
+ * For this initial version, it only supports simple "SELECT ... FROM one_table" queries,
45
+ * effectively returning a TableReferenceNode for that table.
46
+ *
47
+ * @param stmt The AST.SelectStmt to plan.
48
+ * @param ctx The parent planning context for this SELECT statement.
49
+ * @param parentCTEs A map of parent CTEs for compound statements.
50
+ * @returns A BatchNode representing the plan for the SELECT statement.
51
+ * @throws {QuereusError} If the FROM clause is missing, empty, or contains more than one source.
52
+ */
53
+ export function buildSelectStmt(
54
+ ctx: PlanningContext,
55
+ stmt: AST.SelectStmt,
56
+ parentCTEs: Map<string, CTEScopeNode> = new Map(),
57
+ /**
58
+ * Whether ProjectNodes inside this SELECT should forward all input columns that are not explicitly
59
+ * listed in the projection list. This is desirable for top-level queries (helps ORDER BY, window
60
+ * functions, etc.) but must be switched off for scalar/IN/EXISTS sub-queries which are required to
61
+ * expose only their declared columns.
62
+ */
63
+ preserveInputColumns: boolean = true
64
+ ): PlanNode {
65
+
66
+ // Phase 0: Handle WITH clause if present
67
+ const { contextWithCTEs, cteNodes } = buildWithContext(ctx, stmt, parentCTEs);
68
+
69
+ // Handle compound set operations (UNION / INTERSECT / EXCEPT)
70
+ if (stmt.compound) {
71
+ return buildCompoundSelect(stmt, contextWithCTEs, cteNodes,
72
+ (ctx, stmt, parentCTEs) => buildSelectStmt(ctx, stmt, parentCTEs) as RelationalPlanNode);
73
+ }
74
+
75
+ // Phase 1: Plan FROM clause and determine local input relations for the current select scope
76
+ // Use the context that includes CTEs as well as the merged CTE map so that table references
77
+ // inside the FROM clause can correctly resolve to CTE definitions created by the WITH clause.
78
+ const fromTables = !stmt.from || stmt.from.length === 0
79
+ ? [SingleRowNode.instance]
80
+ : stmt.from.map(from => buildFrom(from, contextWithCTEs, cteNodes));
81
+
82
+ // Multiple FROM sources (from joins) are not supported - maybe never will be
83
+ if (fromTables.length > 1) {
84
+ throw new QuereusError(
85
+ 'SELECT with multiple FROM sources (joins) not supported.',
86
+ StatusCode.UNSUPPORTED, undefined, stmt.from![1].loc?.start.line, stmt.from![1].loc?.start.column
87
+ );
88
+ }
89
+
90
+ // Phase 2: Create the main scope for this SELECT statement
91
+ const columnScopes = fromTables.map(ft => ctx.outputScopes.get(ft) || ft.scope).filter(Boolean);
92
+ const selectScope = new MultiScope([...columnScopes, contextWithCTEs.scope]);
93
+ let selectContext: PlanningContext = { ...contextWithCTEs, scope: selectScope };
94
+
95
+ let input: RelationalPlanNode = fromTables[0];
96
+
97
+ // Plan WHERE clause
98
+ if (stmt.where) {
99
+ const whereExpression = buildExpression(selectContext, stmt.where);
100
+ input = new FilterNode(selectScope, input, whereExpression);
101
+ }
102
+
103
+ // Build projections based on the SELECT list
104
+ const projections: Projection[] = [];
105
+
106
+ // Analyze SELECT columns
107
+ const {
108
+ projections: columnProjections,
109
+ aggregates,
110
+ windowFunctions,
111
+ hasAggregates,
112
+ hasWindowFunctions
113
+ } = analyzeSelectColumns(stmt.columns, selectContext);
114
+
115
+ // Handle SELECT * separately
116
+ for (const column of stmt.columns) {
117
+ if (column.type === 'all') {
118
+ const starProjections = buildStarProjections(column, input, selectScope);
119
+ projections.push(...starProjections);
120
+ }
121
+ }
122
+
123
+ // Add non-star projections
124
+ projections.push(...columnProjections);
125
+
126
+ // Process aggregates if present
127
+ const aggregateResult = buildAggregatePhase(input, stmt, selectContext, aggregates, hasAggregates, projections);
128
+ input = aggregateResult.output;
129
+ let preAggregateSort = aggregateResult.preAggregateSort;
130
+
131
+ // Update context if we have aggregates
132
+ if (aggregateResult.aggregateScope) {
133
+ selectContext = { ...selectContext, scope: aggregateResult.aggregateScope };
134
+
135
+ // Build final projections if needed
136
+ if (aggregateResult.needsFinalProjection && aggregateResult.aggregateNode && aggregateResult.groupByExpressions) {
137
+ const finalProjections = buildFinalAggregateProjections(
138
+ stmt,
139
+ selectContext,
140
+ aggregateResult.aggregateScope,
141
+ aggregateResult.aggregateNode,
142
+ aggregates,
143
+ aggregateResult.groupByExpressions
144
+ );
145
+ input = new ProjectNode(selectScope, input, finalProjections, undefined, undefined, preserveInputColumns);
146
+ }
147
+ }
148
+
149
+ // Handle window functions if present
150
+ if (hasWindowFunctions) {
151
+ // Check if ORDER BY references columns not in SELECT before applying window functions
152
+ let preWindowSort = false;
153
+ if (stmt.orderBy) {
154
+ const selectedColumns = new Set<string>();
155
+ for (const column of stmt.columns) {
156
+ if (column.type === 'column' && column.expr.type === 'column') {
157
+ selectedColumns.add(column.expr.name.toLowerCase());
158
+ }
159
+ if (column.type === 'column' && column.alias) {
160
+ selectedColumns.add(column.alias.toLowerCase());
161
+ }
162
+ }
163
+
164
+ // Check if ORDER BY references columns not in SELECT
165
+ for (const orderByClause of stmt.orderBy) {
166
+ if (orderByClause.expr.type === 'column') {
167
+ const orderColumn = orderByClause.expr.name.toLowerCase();
168
+ if (!selectedColumns.has(orderColumn)) {
169
+ // Apply ORDER BY before window projections
170
+ const sortKeys: SortKey[] = stmt.orderBy.map(orderBy => ({
171
+ expression: buildExpression(selectContext, orderBy.expr),
172
+ direction: orderBy.direction,
173
+ nulls: orderBy.nulls
174
+ }));
175
+ input = new SortNode(selectContext.scope, input, sortKeys);
176
+ preWindowSort = true;
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ input = buildWindowPhase(input, windowFunctions, selectContext, stmt);
184
+
185
+ // Update context to include window output columns
186
+ const windowOutputScope = new RegisteredScope(selectContext.scope);
187
+ const windowAttributes = input.getAttributes();
188
+ input.getType().columns.forEach((col, index) => {
189
+ const attr = windowAttributes[index];
190
+ windowOutputScope.registerSymbol(col.name.toLowerCase(), (exp, s) =>
191
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, col.type, attr.id, index));
192
+ });
193
+
194
+ // Create combined scope that includes both original columns and window output
195
+ const combinedScope = new MultiScope([windowOutputScope, selectScope]);
196
+ selectContext = { ...selectContext, scope: combinedScope };
197
+
198
+ // Don't apply ORDER BY again if we already did it
199
+ if (preWindowSort) {
200
+ preAggregateSort = true;
201
+ }
202
+ }
203
+
204
+ // Handle final projections for non-aggregate, non-window cases
205
+ if (!hasAggregates && !hasWindowFunctions) {
206
+ const finalResult = buildFinalProjections(input, projections, selectScope, stmt, selectContext, preserveInputColumns);
207
+ input = finalResult.output;
208
+ selectContext = finalResult.finalContext;
209
+ preAggregateSort = finalResult.preAggregateSort;
210
+
211
+ // Apply final modifiers with projection scope for column alias resolution
212
+ input = applyDistinct(input, stmt, selectScope);
213
+ input = applyOrderBy(input, stmt, selectContext, preAggregateSort, finalResult.projectionScope);
214
+ input = applyLimitOffset(input, stmt, selectContext, finalResult.projectionScope);
215
+ } else {
216
+ // Apply final modifiers without projection scope for aggregate/window cases
217
+ input = applyDistinct(input, stmt, selectScope);
218
+ input = applyOrderBy(input, stmt, selectContext, preAggregateSort);
219
+ input = applyLimitOffset(input, stmt, selectContext);
220
+ }
221
+
222
+ return input;
223
+ }
224
+
225
+ /**
226
+ * Creates a plan for a VALUES statement.
227
+ *
228
+ * @param ctx The planning context
229
+ * @param stmt The AST.ValuesStmt to plan
230
+ * @returns A ValuesNode representing the VALUES clause
231
+ */
232
+ export function buildValuesStmt(
233
+ ctx: PlanningContext,
234
+ stmt: AST.ValuesStmt
235
+ ): ValuesNode {
236
+ // Build each row of values
237
+ const rows: ScalarPlanNode[][] = stmt.values.map(rowValues =>
238
+ rowValues.map(valueExpr => buildExpression(ctx, valueExpr))
239
+ );
240
+
241
+ // Create the VALUES node
242
+ return new ValuesNode(ctx.scope, rows);
243
+ }
244
+
245
+ /**
246
+ * Processes a FROM clause item into a relational plan node.
247
+ *
248
+ * Handles different types of FROM items:
249
+ * - Table references - creates a TableReferenceNode
250
+ * - Subqueries - plans the subquery
251
+ * - Joins - builds the join structure
252
+ * - Table functions - creates a table function call node
253
+ *
254
+ * For a simple table reference, this calls buildTableReference which
255
+ * returns a TableReferenceNode for that table.
256
+ *
257
+ * @param fromClause The FROM clause AST node to process
258
+ * @param ctx The planning context
259
+ * @returns A relational plan node representing the FROM clause
260
+ */
261
+ export function buildFrom(fromClause: AST.FromClause, parentContext: PlanningContext, cteNodes: Map<string, CTEScopeNode> = new Map()): RelationalPlanNode {
262
+ let fromTable: RelationalPlanNode;
263
+ let columnScope: Scope;
264
+
265
+ if (fromClause.type === 'table') {
266
+ const tableName = fromClause.table.name.toLowerCase();
267
+
268
+ // Check if this is a CTE reference
269
+ if (cteNodes.has(tableName)) {
270
+ const cteNode = cteNodes.get(tableName)!;
271
+
272
+ // Check if this is an internal recursive CTE reference
273
+ if (CapabilityDetectors.isRecursiveCTERef(cteNode)) {
274
+ // For internal recursive references, use the node directly
275
+ fromTable = cteNode;
276
+
277
+ // Create scope for internal recursive CTE columns
278
+ const internalScope = new RegisteredScope(parentContext.scope);
279
+ const internalAttributes = cteNode.getAttributes();
280
+ cteNode.getType().columns.forEach((c, i) => {
281
+ const attr = internalAttributes[i];
282
+ internalScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
283
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
284
+ });
285
+
286
+ if (fromClause.alias) {
287
+ columnScope = new AliasedScope(internalScope, tableName, fromClause.alias.toLowerCase());
288
+ } else {
289
+ columnScope = new AliasedScope(internalScope, tableName, tableName);
290
+ }
291
+ } else {
292
+ // Regular CTE reference - cache by CTE name + alias to ensure consistent attribute IDs
293
+ const cacheKey = `${tableName}:${fromClause.alias || tableName}`;
294
+
295
+ // Initialize cache if not exists
296
+ if (!parentContext.cteReferenceCache) {
297
+ parentContext.cteReferenceCache = new Map();
298
+ }
299
+
300
+ let cteRefNode: CTEReferenceNode;
301
+ if (parentContext.cteReferenceCache.has(cacheKey)) {
302
+ cteRefNode = parentContext.cteReferenceCache.get(cacheKey)!;
303
+ const attrs = cteRefNode.getAttributes();
304
+ logger(`Using cached CTE reference ${cacheKey}, attrs=[${attrs.map(a => a.id).join(',')}]`);
305
+ } else {
306
+ cteRefNode = new CTEReferenceNode(parentContext.scope, cteNode as CTEPlanNode, fromClause.alias);
307
+ parentContext.cteReferenceCache.set(cacheKey, cteRefNode);
308
+ const attrs = cteRefNode.getAttributes();
309
+ logger(`Created new CTE reference ${cacheKey}, attrs=[${attrs.map(a => a.id).join(',')}]`);
310
+ }
311
+
312
+ // Create scope for CTE columns using attributes from the reference node
313
+ // CRITICAL: Use a closure to capture the reference node's attributes
314
+ // This ensures all column references use the same attribute IDs
315
+ const cteScope = new RegisteredScope(parentContext.scope);
316
+ const refAttrs = cteRefNode.getAttributes();
317
+ cteRefNode.getType().columns.forEach((c, i) => {
318
+ const attr = refAttrs[i];
319
+ cteScope.registerSymbol(c.name.toLowerCase(), (exp, s) => {
320
+ // Always use the cached reference node's attribute ID
321
+ return new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i);
322
+ });
323
+ });
324
+
325
+ if (fromClause.alias) {
326
+ columnScope = new AliasedScope(cteScope, tableName, fromClause.alias.toLowerCase());
327
+ } else {
328
+ columnScope = new AliasedScope(cteScope, tableName, tableName);
329
+ }
330
+
331
+ // CRITICAL: Cache the reference node so later expression compilation uses the same attribute IDs
332
+ // TODO: replace this monkey patching with a proper interface
333
+ (columnScope as any).referenceNode = cteRefNode;
334
+
335
+ fromTable = cteRefNode;
336
+ }
337
+ } else {
338
+ // Check if this is a view
339
+ const schemaName = fromClause.table.schema || parentContext.db.schemaManager.getCurrentSchemaName();
340
+ const viewSchema = parentContext.db.schemaManager.getView(schemaName, fromClause.table.name);
341
+
342
+ if (viewSchema) {
343
+ // Build the view's SELECT statement
344
+ let viewSelectNode = buildSelectStmt(parentContext, viewSchema.selectAst, cteNodes) as RelationalPlanNode;
345
+
346
+ // If the view has explicit column names, wrap with a projection to rename columns
347
+ if (viewSchema.columns && viewSchema.columns.length > 0) {
348
+ const viewAttributes = viewSelectNode.getAttributes();
349
+ const projections = viewSchema.columns.map((columnName, i) => {
350
+ if (i >= viewAttributes.length) {
351
+ throw new QuereusError(
352
+ `View '${viewSchema.name}' has more explicit column names than SELECT columns`,
353
+ StatusCode.ERROR
354
+ );
355
+ }
356
+ const attr = viewAttributes[i];
357
+ const columnRef = new ColumnReferenceNode(
358
+ parentContext.scope,
359
+ { type: 'column', name: attr.name } as AST.ColumnExpr,
360
+ attr.type,
361
+ attr.id,
362
+ i
363
+ );
364
+ return {
365
+ node: columnRef,
366
+ alias: columnName
367
+ };
368
+ });
369
+ fromTable = new ProjectNode(parentContext.scope, viewSelectNode, projections);
370
+ } else {
371
+ fromTable = viewSelectNode;
372
+ }
373
+
374
+ // Create scope for view columns
375
+ const viewScope = new RegisteredScope(parentContext.scope);
376
+ const viewAttributes = fromTable.getAttributes();
377
+ fromTable.getType().columns.forEach((c, i) => {
378
+ const attr = viewAttributes[i];
379
+ viewScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
380
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
381
+ });
382
+
383
+ if (fromClause.alias) {
384
+ columnScope = new AliasedScope(viewScope, fromClause.table.name.toLowerCase(), fromClause.alias.toLowerCase());
385
+ } else {
386
+ columnScope = new AliasedScope(viewScope, fromClause.table.name.toLowerCase(), fromClause.table.name.toLowerCase());
387
+ }
388
+ } else {
389
+ // Regular table
390
+ fromTable = buildTableReference(fromClause, parentContext);
391
+
392
+ // Create scope for table columns
393
+ const tableScope = new RegisteredScope(parentContext.scope);
394
+ const tableAttributes = fromTable.getAttributes();
395
+ fromTable.getType().columns.forEach((c, i) => {
396
+ const attr = tableAttributes[i];
397
+ tableScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
398
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
399
+ });
400
+
401
+ if (fromClause.alias) {
402
+ columnScope = new AliasedScope(tableScope, fromClause.table.name.toLowerCase(), fromClause.alias.toLowerCase());
403
+ } else {
404
+ columnScope = new AliasedScope(tableScope, fromClause.table.name.toLowerCase(), fromClause.table.name.toLowerCase());
405
+ }
406
+ }
407
+ }
408
+
409
+ } else if (fromClause.type === 'functionSource') {
410
+ fromTable = buildTableFunctionCall(fromClause, parentContext);
411
+
412
+ // Create scope for function columns
413
+ const functionScope = new RegisteredScope(parentContext.scope);
414
+ const functionAttributes = fromTable.getAttributes();
415
+ fromTable.getType().columns.forEach((c, i) => {
416
+ const attr = functionAttributes[i];
417
+ functionScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
418
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
419
+ });
420
+
421
+ if (fromClause.alias) {
422
+ // Use the alias as the table name
423
+ columnScope = new AliasedScope(functionScope, '', fromClause.alias.toLowerCase());
424
+ } else {
425
+ // Use the function name as the table name
426
+ columnScope = new AliasedScope(functionScope, '', fromClause.name.name.toLowerCase());
427
+ }
428
+
429
+ } else if (fromClause.type === 'subquerySource') {
430
+ // Build the subquery
431
+ if (fromClause.subquery.type === 'select') {
432
+ fromTable = buildSelectStmt(parentContext, fromClause.subquery, cteNodes) as RelationalPlanNode;
433
+ } else if (fromClause.subquery.type === 'values') {
434
+ fromTable = buildValuesStmt(parentContext, fromClause.subquery);
435
+ } else {
436
+ const exhaustiveCheck: never = fromClause.subquery;
437
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
438
+ throw new QuereusError(`Unsupported subquery type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
439
+ }
440
+
441
+ // Create scope for subquery columns
442
+ const subqueryScope = new RegisteredScope(parentContext.scope);
443
+ const subqueryAttributes = fromTable.getAttributes();
444
+
445
+ // Use provided column names or infer from subquery
446
+ const columnNames = fromClause.columns || fromTable.getType().columns.map(c => c.name);
447
+
448
+ columnNames.forEach((colName, i) => {
449
+ if (i < subqueryAttributes.length) {
450
+ const attr = subqueryAttributes[i];
451
+ const columnType = fromTable.getType().columns[i]?.type || { typeClass: 'scalar', logicalType: TEXT_TYPE, nullable: true, isReadOnly: true };
452
+ subqueryScope.registerSymbol(colName.toLowerCase(), (exp, s) =>
453
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, columnType, attr.id, i));
454
+ }
455
+ });
456
+
457
+ columnScope = new AliasedScope(subqueryScope, '', fromClause.alias.toLowerCase());
458
+
459
+ } else if (fromClause.type === 'mutatingSubquerySource') {
460
+ // Build the mutating subquery (DML with RETURNING)
461
+ let dmlNode: RelationalPlanNode;
462
+
463
+ if (fromClause.stmt.type === 'insert') {
464
+ // Build INSERT without SinkNode wrapper since we need the RETURNING results
465
+ dmlNode = buildInsertStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
466
+ } else if (fromClause.stmt.type === 'update') {
467
+ // Build UPDATE without SinkNode wrapper since we need the RETURNING results
468
+ dmlNode = buildUpdateStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
469
+ } else if (fromClause.stmt.type === 'delete') {
470
+ // Build DELETE without SinkNode wrapper since we need the RETURNING results
471
+ dmlNode = buildDeleteStmt(parentContext, fromClause.stmt) as RelationalPlanNode;
472
+ } else {
473
+ const exhaustiveCheck: never = fromClause.stmt;
474
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
+ throw new QuereusError(`Unsupported mutating subquery type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
476
+ }
477
+
478
+ fromTable = dmlNode;
479
+
480
+ // Create scope for mutating subquery columns
481
+ const mutatingScope = new RegisteredScope(parentContext.scope);
482
+ const mutatingAttributes = fromTable.getAttributes();
483
+
484
+ // Use provided column names or infer from RETURNING clause
485
+ const columnNames = fromClause.columns || fromTable.getType().columns.map(c => c.name);
486
+
487
+ columnNames.forEach((colName, i) => {
488
+ if (i < mutatingAttributes.length) {
489
+ const attr = mutatingAttributes[i];
490
+ const columnType = fromTable.getType().columns[i]?.type || { typeClass: 'scalar', logicalType: TEXT_TYPE, nullable: true, isReadOnly: false };
491
+ mutatingScope.registerSymbol(colName.toLowerCase(), (exp, s) =>
492
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, columnType, attr.id, i));
493
+ }
494
+ });
495
+
496
+ columnScope = new AliasedScope(mutatingScope, '', fromClause.alias.toLowerCase());
497
+
498
+ } else if (fromClause.type === 'join') {
499
+ // Handle JOIN clauses
500
+ return buildJoin(fromClause, parentContext, cteNodes);
501
+ } else {
502
+ // Handle the case where fromClause.type is not recognized
503
+ const exhaustiveCheck: never = fromClause;
504
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
505
+ throw new QuereusError(`Unsupported FROM clause type: ${(exhaustiveCheck as any).type}`, StatusCode.INTERNAL);
506
+ }
507
+
508
+ parentContext.outputScopes.set(fromTable, columnScope);
509
+ return fromTable;
510
+ }
511
+
512
+ /**
513
+ * Builds a join plan node from an AST join clause
514
+ */
515
+ function buildJoin(joinClause: AST.JoinClause, parentContext: PlanningContext, cteNodes: Map<string, CTEScopeNode>): JoinNode {
516
+ // Build left and right sides recursively
517
+ const leftNode = buildFrom(joinClause.left, parentContext, cteNodes);
518
+ const rightNode = buildFrom(joinClause.right, parentContext, cteNodes);
519
+
520
+ // Create a combined scope for join expressions
521
+ const leftScope = parentContext.outputScopes.get(leftNode);
522
+ const rightScope = parentContext.outputScopes.get(rightNode);
523
+ if (!leftScope || !rightScope) {
524
+ // This should not happen if buildFrom correctly populates the scopes
525
+ throw new QuereusError('Could not find output scope for join source', StatusCode.INTERNAL);
526
+ }
527
+
528
+ // Create a combined scope for the join that includes both left and right columns
529
+ const combinedScope = new MultiScope([leftScope, rightScope]);
530
+
531
+ // Create a new planning context with the combined scope for condition evaluation
532
+ const joinContext: PlanningContext = {
533
+ ...parentContext,
534
+ scope: combinedScope
535
+ };
536
+
537
+ let condition: ScalarPlanNode | undefined;
538
+ let usingColumns: string[] | undefined;
539
+
540
+ // Handle ON condition
541
+ if (joinClause.condition) {
542
+ condition = buildExpression(joinContext, joinClause.condition);
543
+ }
544
+
545
+ // Handle USING columns
546
+ if (joinClause.columns) {
547
+ usingColumns = joinClause.columns;
548
+ // Convert USING to ON condition: table1.col1 = table2.col1 AND table1.col2 = table2.col2 ...
549
+ // For now, store the column names and let the emitter handle the condition
550
+ // TODO: This could be improved by synthesizing the equality conditions here
551
+ }
552
+
553
+ const joinNode = new JoinNode(
554
+ parentContext.scope,
555
+ leftNode,
556
+ rightNode,
557
+ joinClause.joinType,
558
+ condition,
559
+ usingColumns
560
+ );
561
+
562
+ // Use the combined scope as the column scope for the join
563
+ // This allows both qualified and unqualified column references to resolve properly
564
+ parentContext.outputScopes.set(joinNode, combinedScope);
565
+
566
+ return joinNode;
567
+ }