@quereus/quereus 0.6.2 → 0.6.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/src/index.d.ts +4 -6
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js +2 -4
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/runtime/emit/schema-declarative.js +1 -1
  7. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  8. package/dist/src/schema/schema-hasher.d.ts +3 -3
  9. package/dist/src/schema/schema-hasher.d.ts.map +1 -1
  10. package/dist/src/schema/schema-hasher.js +9 -27
  11. package/dist/src/schema/schema-hasher.js.map +1 -1
  12. package/dist/src/util/hash.d.ts +19 -0
  13. package/dist/src/util/hash.d.ts.map +1 -0
  14. package/dist/src/util/hash.js +76 -0
  15. package/dist/src/util/hash.js.map +1 -0
  16. package/dist/src/util/plugin-helper.d.ts +45 -0
  17. package/dist/src/util/plugin-helper.d.ts.map +1 -0
  18. package/dist/src/util/plugin-helper.js +85 -0
  19. package/dist/src/util/plugin-helper.js.map +1 -0
  20. package/package.json +4 -2
  21. package/src/index.ts +10 -16
  22. package/src/planner/building/delete.ts +214 -214
  23. package/src/planner/building/insert.ts +428 -428
  24. package/src/planner/building/update.ts +319 -319
  25. package/src/runtime/emit/schema-declarative.ts +1 -1
  26. package/src/schema/schema-hasher.ts +9 -27
  27. package/src/util/ast-stringify.ts +864 -864
  28. package/src/util/hash.ts +90 -0
  29. package/src/util/plugin-helper.ts +110 -0
  30. package/src/vtab/memory/table.ts +256 -256
  31. package/src/vtab/table.ts +162 -162
  32. package/dist/src/config/loader.d.ts +0 -41
  33. package/dist/src/config/loader.d.ts.map +0 -1
  34. package/dist/src/config/loader.js +0 -102
  35. package/dist/src/config/loader.js.map +0 -1
  36. package/dist/src/planner/nodes/physical-access-nodes.d.ts +0 -83
  37. package/dist/src/planner/nodes/physical-access-nodes.d.ts.map +0 -1
  38. package/dist/src/planner/nodes/physical-access-nodes.js +0 -226
  39. package/dist/src/planner/nodes/physical-access-nodes.js.map +0 -1
  40. package/dist/src/planner/nodes/scan.d.ts +0 -27
  41. package/dist/src/planner/nodes/scan.d.ts.map +0 -1
  42. package/dist/src/planner/nodes/scan.js +0 -78
  43. package/dist/src/planner/nodes/scan.js.map +0 -1
  44. package/dist/src/planner/nodes/update-executor-node.d.ts +0 -24
  45. package/dist/src/planner/nodes/update-executor-node.d.ts.map +0 -1
  46. package/dist/src/planner/nodes/update-executor-node.js +0 -57
  47. package/dist/src/planner/nodes/update-executor-node.js.map +0 -1
  48. package/dist/src/planner/physical-utils.d.ts +0 -36
  49. package/dist/src/planner/physical-utils.d.ts.map +0 -1
  50. package/dist/src/planner/physical-utils.js +0 -122
  51. package/dist/src/planner/physical-utils.js.map +0 -1
  52. package/dist/src/planner/rules/physical/rule-filter-optimization.d.ts +0 -11
  53. package/dist/src/planner/rules/physical/rule-filter-optimization.d.ts.map +0 -1
  54. package/dist/src/planner/rules/physical/rule-filter-optimization.js +0 -49
  55. package/dist/src/planner/rules/physical/rule-filter-optimization.js.map +0 -1
  56. package/dist/src/planner/rules/physical/rule-mark-physical.d.ts +0 -11
  57. package/dist/src/planner/rules/physical/rule-mark-physical.d.ts.map +0 -1
  58. package/dist/src/planner/rules/physical/rule-mark-physical.js +0 -29
  59. package/dist/src/planner/rules/physical/rule-mark-physical.js.map +0 -1
  60. package/dist/src/planner/rules/physical/rule-project-optimization.d.ts +0 -11
  61. package/dist/src/planner/rules/physical/rule-project-optimization.d.ts.map +0 -1
  62. package/dist/src/planner/rules/physical/rule-project-optimization.js +0 -44
  63. package/dist/src/planner/rules/physical/rule-project-optimization.js.map +0 -1
  64. package/dist/src/planner/rules/physical/rule-sort-optimization.d.ts +0 -11
  65. package/dist/src/planner/rules/physical/rule-sort-optimization.d.ts.map +0 -1
  66. package/dist/src/planner/rules/physical/rule-sort-optimization.js +0 -53
  67. package/dist/src/planner/rules/physical/rule-sort-optimization.js.map +0 -1
  68. package/dist/src/planner/rules/rewrite/rule-constant-folding.d.ts +0 -11
  69. package/dist/src/planner/rules/rewrite/rule-constant-folding.d.ts.map +0 -1
  70. package/dist/src/planner/rules/rewrite/rule-constant-folding.js +0 -59
  71. package/dist/src/planner/rules/rewrite/rule-constant-folding.js.map +0 -1
  72. package/dist/src/planner/util/deferred-constraint.d.ts +0 -14
  73. package/dist/src/planner/util/deferred-constraint.d.ts.map +0 -1
  74. package/dist/src/planner/util/deferred-constraint.js +0 -85
  75. package/dist/src/planner/util/deferred-constraint.js.map +0 -1
  76. package/dist/src/runtime/emit/table-reference.d.ts +0 -5
  77. package/dist/src/runtime/emit/table-reference.d.ts.map +0 -1
  78. package/dist/src/runtime/emit/table-reference.js +0 -67
  79. package/dist/src/runtime/emit/table-reference.js.map +0 -1
  80. package/dist/src/runtime/emit/update-executor.d.ts +0 -5
  81. package/dist/src/runtime/emit/update-executor.d.ts.map +0 -1
  82. package/dist/src/runtime/emit/update-executor.js +0 -54
  83. package/dist/src/runtime/emit/update-executor.js.map +0 -1
  84. package/dist/src/util/plugin-loader.d.ts +0 -52
  85. package/dist/src/util/plugin-loader.d.ts.map +0 -1
  86. package/dist/src/util/plugin-loader.js +0 -307
  87. package/dist/src/util/plugin-loader.js.map +0 -1
  88. package/src/config/loader.ts +0 -140
  89. package/src/util/plugin-loader.ts +0 -387
@@ -1,428 +1,428 @@
1
- import type * as AST from '../../parser/ast.js';
2
- import type { PlanningContext } from '../planning-context.js';
3
- import { InsertNode } from '../nodes/insert-node.js';
4
- import { buildTableReference } from './table.js';
5
- import { QuereusError } from '../../common/errors.js';
6
- import { StatusCode } from '../../common/types.js';
7
- import { buildSelectStmt } from './select.js';
8
- import { buildWithClause } from './with.js';
9
- import { ValuesNode } from '../nodes/values-node.js';
10
- import { PlanNode, type RelationalPlanNode, type ScalarPlanNode, type Attribute, type RowDescriptor } from '../nodes/plan-node.js';
11
- import { buildExpression } from './expression.js';
12
- import { checkColumnsAssignable, columnSchemaToDef } from '../type-utils.js';
13
- import type { ColumnDef } from '../../common/datatype.js';
14
- import type { CTEScopeNode } from '../nodes/cte-node.js';
15
- import { RegisteredScope } from '../scopes/registered.js';
16
- import { ColumnReferenceNode, TableReferenceNode } from '../nodes/reference.js';
17
- import { SinkNode } from '../nodes/sink-node.js';
18
- import { ConstraintCheckNode } from '../nodes/constraint-check-node.js';
19
- import { RowOpFlag } from '../../schema/table.js';
20
- import { ReturningNode } from '../nodes/returning-node.js';
21
- import { ProjectNode, type Projection } from '../nodes/project-node.js';
22
- import { buildOldNewRowDescriptors } from '../../util/row-descriptor.js';
23
- import { DmlExecutorNode } from '../nodes/dml-executor-node.js';
24
- import { buildConstraintChecks } from './constraint-builder.js';
25
-
26
- /**
27
- * Creates a uniform row expansion projection that maps any relational source
28
- * to the target table's column structure, filling in defaults for omitted columns.
29
- * This ensures INSERT works orthogonally with any relational source.
30
- */
31
- function createRowExpansionProjection(
32
- ctx: PlanningContext,
33
- sourceNode: RelationalPlanNode,
34
- targetColumns: ColumnDef[],
35
- tableReference: TableReferenceNode,
36
- contextScope?: RegisteredScope
37
- ): RelationalPlanNode {
38
- const tableSchema = tableReference.tableSchema;
39
-
40
- // If we're inserting into all columns in table order, no expansion needed
41
- if (targetColumns.length === tableSchema.columns.length) {
42
- const allColumnsMatch = targetColumns.every((tc, i) =>
43
- tc.name.toLowerCase() === tableSchema.columns[i].name.toLowerCase()
44
- );
45
- if (allColumnsMatch) {
46
- return sourceNode; // Source already matches table structure
47
- }
48
- }
49
-
50
- // Create projection expressions for each table column
51
- const projections: Projection[] = [];
52
- const sourceAttributes = sourceNode.getAttributes();
53
-
54
- // If we have a context scope, we need to also register source columns in it
55
- // so that defaults can reference them (e.g., DEFAULT base_price + markup)
56
- if (contextScope) {
57
- targetColumns.forEach((targetCol, index) => {
58
- if (index < sourceAttributes.length) {
59
- const sourceAttr = sourceAttributes[index];
60
- const colNameLower = targetCol.name.toLowerCase();
61
- contextScope.registerSymbol(colNameLower, (exp, s) =>
62
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, sourceAttr.type, sourceAttr.id, index)
63
- );
64
- }
65
- });
66
- }
67
-
68
- tableSchema.columns.forEach((tableColumn) => {
69
- // Find if this table column is in the target columns
70
- const targetColIndex = targetColumns.findIndex(tc =>
71
- tc.name.toLowerCase() === tableColumn.name.toLowerCase()
72
- );
73
-
74
- if (targetColIndex >= 0) {
75
- // This column is provided by the source - reference the source column
76
- if (targetColIndex < sourceAttributes.length) {
77
- const sourceAttr = sourceAttributes[targetColIndex];
78
- // Create a column reference to the source attribute
79
- const columnRef = new ColumnReferenceNode(
80
- ctx.scope,
81
- { type: 'column', name: sourceAttr.name } satisfies AST.ColumnExpr,
82
- sourceAttr.type,
83
- sourceAttr.id,
84
- targetColIndex
85
- );
86
- projections.push({
87
- node: columnRef,
88
- alias: tableColumn.name
89
- });
90
- } else {
91
- throw new QuereusError(
92
- `Source has fewer columns than expected for INSERT target columns`,
93
- StatusCode.ERROR
94
- );
95
- }
96
- } else {
97
- // This column is omitted - use default value or NULL
98
- let defaultNode: ScalarPlanNode;
99
- // Use context scope for default evaluation if available
100
- const defaultCtx = contextScope ? { ...ctx, scope: contextScope } : ctx;
101
- if (tableColumn.defaultValue !== undefined) {
102
- // Use default value
103
- if (typeof tableColumn.defaultValue === 'object' && tableColumn.defaultValue !== null && 'type' in tableColumn.defaultValue) {
104
- // It's an AST.Expression - build it into a plan node with context scope
105
- defaultNode = buildExpression(defaultCtx, tableColumn.defaultValue as AST.Expression) as ScalarPlanNode;
106
- } else {
107
- // Literal default value
108
- defaultNode = buildExpression(defaultCtx, { type: 'literal', value: tableColumn.defaultValue }) as ScalarPlanNode;
109
- }
110
- } else {
111
- // No default value - use NULL
112
- defaultNode = buildExpression(defaultCtx, { type: 'literal', value: null }) as ScalarPlanNode;
113
- }
114
- projections.push({
115
- node: defaultNode,
116
- alias: tableColumn.name
117
- });
118
- }
119
- });
120
-
121
- // Create projection node that expands source to table structure
122
- return new ProjectNode(ctx.scope, sourceNode, projections);
123
- }
124
-
125
- /**
126
- * Validates that RETURNING expressions use appropriate NEW/OLD qualifiers for the operation type
127
- */
128
- function validateReturningExpression(expr: AST.Expression, operationType: 'INSERT' | 'UPDATE' | 'DELETE'): void {
129
- function checkExpression(e: AST.Expression): void {
130
- if (e.type === 'column') {
131
- if (e.table?.toLowerCase() === 'old' && operationType === 'INSERT') {
132
- throw new QuereusError(
133
- 'OLD qualifier cannot be used in INSERT RETURNING clause',
134
- StatusCode.ERROR
135
- );
136
- }
137
- if (e.table?.toLowerCase() === 'new' && operationType === 'DELETE') {
138
- throw new QuereusError(
139
- 'NEW qualifier cannot be used in DELETE RETURNING clause',
140
- StatusCode.ERROR
141
- );
142
- }
143
- } else if (e.type === 'binary') {
144
- checkExpression(e.left);
145
- checkExpression(e.right);
146
- } else if (e.type === 'unary') {
147
- checkExpression(e.expr);
148
- } else if (e.type === 'function') {
149
- e.args.forEach(checkExpression);
150
- } else if (e.type === 'case') {
151
- if (e.baseExpr) checkExpression(e.baseExpr);
152
- e.whenThenClauses.forEach(clause => {
153
- checkExpression(clause.when);
154
- checkExpression(clause.then);
155
- });
156
- if (e.elseExpr) checkExpression(e.elseExpr);
157
- } else if (e.type === 'cast') {
158
- checkExpression(e.expr);
159
- } else if (e.type === 'collate') {
160
- checkExpression(e.expr);
161
- } else if (e.type === 'subquery') {
162
- // Subqueries in RETURNING are complex - for now, we'll skip validation
163
- // A full implementation would need to traverse the subquery's AST
164
- } else if (e.type === 'in') {
165
- checkExpression(e.expr);
166
- if (e.values) {
167
- e.values.forEach(checkExpression);
168
- }
169
- } else if (e.type === 'exists') {
170
- // EXISTS subqueries are complex - skip validation for now
171
- } else if (e.type === 'windowFunction') {
172
- checkExpression(e.function);
173
- }
174
- // Other expression types (literal, parameter) don't need validation
175
- }
176
-
177
- checkExpression(expr);
178
- }
179
-
180
- export function buildInsertStmt(
181
- ctx: PlanningContext,
182
- stmt: AST.InsertStmt,
183
- ): PlanNode {
184
- const tableRetrieve = buildTableReference({ type: 'table', table: stmt.table }, ctx);
185
- const tableReference = tableRetrieve.tableRef; // Extract the actual TableReferenceNode
186
-
187
- // Process mutation context assignments if present
188
- const mutationContextValues = new Map<string, ScalarPlanNode>();
189
- const contextAttributes: Attribute[] = [];
190
- let contextScope: RegisteredScope | undefined;
191
-
192
- if (stmt.contextValues && tableReference.tableSchema.mutationContext) {
193
- // Create context attributes
194
- tableReference.tableSchema.mutationContext.forEach((contextVar) => {
195
- contextAttributes.push({
196
- id: PlanNode.nextAttrId(),
197
- name: contextVar.name,
198
- type: {
199
- typeClass: 'scalar' as const,
200
- logicalType: contextVar.logicalType,
201
- nullable: !contextVar.notNull,
202
- isReadOnly: true
203
- },
204
- sourceRelation: `context.${tableReference.tableSchema.name}`
205
- });
206
- });
207
-
208
- // Create a new scope for mutation context
209
- contextScope = new RegisteredScope(ctx.scope);
210
-
211
- // Register mutation context variables in the scope (before evaluating expressions)
212
- contextAttributes.forEach((attr, index) => {
213
- const contextVar = tableReference.tableSchema.mutationContext![index];
214
- const varNameLower = contextVar.name.toLowerCase();
215
-
216
- // Register both unqualified and qualified names
217
- contextScope!.subscribeFactory(varNameLower, (exp, s) =>
218
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, index)
219
- );
220
- contextScope!.subscribeFactory(`context.${varNameLower}`, (exp, s) =>
221
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, index)
222
- );
223
- });
224
-
225
- // Build context value expressions using the context scope
226
- const contextWithScope = { ...ctx, scope: contextScope };
227
- stmt.contextValues.forEach((assignment) => {
228
- const valueExpr = buildExpression(contextWithScope, assignment.value) as ScalarPlanNode;
229
- mutationContextValues.set(assignment.name, valueExpr);
230
- });
231
- }
232
-
233
- let targetColumns: ColumnDef[] = [];
234
- if (stmt.columns && stmt.columns.length > 0) {
235
- // Explicit columns specified
236
- targetColumns = stmt.columns.map((colName, index) => columnSchemaToDef(colName, tableReference.tableSchema.columns[index]));
237
- } else {
238
- // No explicit columns - default to all table columns in order
239
- targetColumns = tableReference.tableSchema.columns.map(col => columnSchemaToDef(col.name, col));
240
- }
241
-
242
- let sourceNode: RelationalPlanNode;
243
-
244
- if (stmt.values) {
245
- // VALUES clause - build the VALUES node
246
- const rows = stmt.values.map(rowExprs =>
247
- rowExprs.map(expr => buildExpression(ctx, expr) as PlanNode as ScalarPlanNode)
248
- );
249
-
250
- // Check that there are the right number of columns in each row
251
- rows.forEach(row => {
252
- if (row.length !== targetColumns.length) {
253
- throw new QuereusError(`Column count mismatch in VALUES clause. Expected ${targetColumns.length} columns, got ${row.length}.`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
254
- }
255
- });
256
-
257
- // Create VALUES node with target column names
258
- const targetColumnNames = targetColumns.map(col => col.name);
259
- sourceNode = new ValuesNode(ctx.scope, rows, targetColumnNames);
260
-
261
- } else if (stmt.select) {
262
- // SELECT clause - build the SELECT statement
263
- let parentCtes: Map<string, CTEScopeNode> = new Map();
264
- if (stmt.withClause) {
265
- parentCtes = buildWithClause(ctx, stmt.withClause);
266
- }
267
- const selectPlan = buildSelectStmt(ctx, stmt.select, parentCtes);
268
- if (selectPlan.getType().typeClass !== 'relation') {
269
- throw new QuereusError('SELECT statement in INSERT did not produce a relational plan.', StatusCode.INTERNAL, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
270
- }
271
- sourceNode = selectPlan as RelationalPlanNode;
272
- checkColumnsAssignable(sourceNode.getType().columns, targetColumns, stmt);
273
-
274
- } else {
275
- throw new QuereusError('INSERT statement must have a VALUES clause or a SELECT query.', StatusCode.ERROR);
276
- }
277
-
278
- // ORTHOGONAL ROW EXPANSION: Apply uniform row expansion to map any source to table structure with defaults
279
- const expandedSourceNode = createRowExpansionProjection(ctx, sourceNode, targetColumns, tableReference, contextScope);
280
-
281
- // Update targetColumns to reflect all table columns since we've expanded the source
282
- const finalTargetColumns = tableReference.tableSchema.columns.map(col => columnSchemaToDef(col.name, col));
283
-
284
- // Create OLD/NEW attributes for INSERT (OLD = all NULL, NEW = actual values)
285
- const oldAttributes = tableReference.tableSchema.columns.map((col) => ({
286
- id: PlanNode.nextAttrId(),
287
- name: col.name,
288
- type: {
289
- typeClass: 'scalar' as const,
290
- logicalType: col.logicalType,
291
- nullable: true, // OLD values are always NULL for INSERT
292
- isReadOnly: false
293
- },
294
- sourceRelation: `OLD.${tableReference.tableSchema.name}`
295
- }));
296
-
297
- const newAttributes = tableReference.tableSchema.columns.map((col) => ({
298
- id: PlanNode.nextAttrId(),
299
- name: col.name,
300
- type: {
301
- typeClass: 'scalar' as const,
302
- logicalType: col.logicalType,
303
- nullable: !col.notNull,
304
- isReadOnly: false
305
- },
306
- sourceRelation: `NEW.${tableReference.tableSchema.name}`
307
- }));
308
-
309
- const { oldRowDescriptor, newRowDescriptor, flatRowDescriptor } = buildOldNewRowDescriptors(oldAttributes, newAttributes);
310
-
311
- // Build context descriptor if we have context attributes
312
- const contextDescriptor: RowDescriptor = contextAttributes.length > 0 ? [] : undefined as any;
313
- if (contextDescriptor) {
314
- contextAttributes.forEach((attr, index) => {
315
- contextDescriptor[attr.id] = index;
316
- });
317
- }
318
-
319
- // Build constraint checks at plan time
320
- const constraintChecks = buildConstraintChecks(
321
- ctx,
322
- tableReference.tableSchema,
323
- RowOpFlag.INSERT,
324
- oldAttributes,
325
- newAttributes,
326
- flatRowDescriptor,
327
- contextAttributes
328
- );
329
-
330
- const insertNode = new InsertNode(
331
- ctx.scope,
332
- tableReference,
333
- finalTargetColumns,
334
- expandedSourceNode,
335
- flatRowDescriptor,
336
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
337
- contextAttributes.length > 0 ? contextAttributes : undefined,
338
- contextDescriptor
339
- );
340
-
341
- const constraintCheckNode = new ConstraintCheckNode(
342
- ctx.scope,
343
- insertNode,
344
- tableReference,
345
- RowOpFlag.INSERT,
346
- oldRowDescriptor,
347
- newRowDescriptor,
348
- flatRowDescriptor,
349
- constraintChecks,
350
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
351
- contextAttributes.length > 0 ? contextAttributes : undefined,
352
- contextDescriptor
353
- );
354
-
355
- // Add DML executor node to perform the actual database insert operations
356
- const dmlExecutorNode = new DmlExecutorNode(
357
- ctx.scope,
358
- constraintCheckNode,
359
- tableReference,
360
- 'insert',
361
- stmt.onConflict
362
- );
363
-
364
- const resultNode: RelationalPlanNode = dmlExecutorNode;
365
-
366
- if (stmt.returning && stmt.returning.length > 0) {
367
- // Create returning scope with OLD/NEW attribute access
368
- const returningScope = new RegisteredScope(ctx.scope);
369
-
370
- // Register OLD.* symbols (always NULL for INSERT)
371
- oldAttributes.forEach((attr, columnIndex) => {
372
- const tableColumn = tableReference.tableSchema.columns[columnIndex];
373
- returningScope.registerSymbol(`old.${tableColumn.name.toLowerCase()}`, (exp, s) =>
374
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
375
- );
376
- });
377
-
378
- // Register NEW.* symbols and unqualified column names (default to NEW)
379
- newAttributes.forEach((attr, columnIndex) => {
380
- const tableColumn = tableReference.tableSchema.columns[columnIndex];
381
-
382
- // NEW.column
383
- returningScope.registerSymbol(`new.${tableColumn.name.toLowerCase()}`, (exp, s) =>
384
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
385
- );
386
-
387
- // Unqualified column (defaults to NEW)
388
- returningScope.registerSymbol(tableColumn.name.toLowerCase(), (exp, s) =>
389
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
390
- );
391
-
392
- // Table-qualified form (table.column -> NEW)
393
- const tblQualified = `${tableReference.tableSchema.name.toLowerCase()}.${tableColumn.name.toLowerCase()}`;
394
- returningScope.registerSymbol(tblQualified, (exp, s) =>
395
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
396
- );
397
- });
398
-
399
- // Build RETURNING projections in the OLD/NEW context
400
- const returningProjections = stmt.returning.map(rc => {
401
- // TODO: Support RETURNING *
402
- if (rc.type === 'all') throw new QuereusError('RETURNING * not yet supported', StatusCode.UNSUPPORTED);
403
-
404
- // Infer alias from column name if not explicitly provided
405
- let alias = rc.alias;
406
- if (!alias && rc.expr.type === 'column') {
407
- // For qualified column references like NEW.id, normalize to lowercase
408
- if (rc.expr.table) {
409
- alias = `${rc.expr.table.toLowerCase()}.${rc.expr.name.toLowerCase()}`;
410
- } else {
411
- alias = rc.expr.name.toLowerCase();
412
- }
413
- }
414
-
415
- // Validate that OLD references are not used in INSERT RETURNING
416
- validateReturningExpression(rc.expr, 'INSERT');
417
-
418
- return {
419
- node: buildExpression({ ...ctx, scope: returningScope }, rc.expr) as ScalarPlanNode,
420
- alias: alias
421
- };
422
- });
423
-
424
- return new ReturningNode(ctx.scope, dmlExecutorNode, returningProjections);
425
- }
426
-
427
- return new SinkNode(ctx.scope, resultNode, 'insert');
428
- }
1
+ import type * as AST from '../../parser/ast.js';
2
+ import type { PlanningContext } from '../planning-context.js';
3
+ import { InsertNode } from '../nodes/insert-node.js';
4
+ import { buildTableReference } from './table.js';
5
+ import { QuereusError } from '../../common/errors.js';
6
+ import { StatusCode } from '../../common/types.js';
7
+ import { buildSelectStmt } from './select.js';
8
+ import { buildWithClause } from './with.js';
9
+ import { ValuesNode } from '../nodes/values-node.js';
10
+ import { PlanNode, type RelationalPlanNode, type ScalarPlanNode, type Attribute, type RowDescriptor } from '../nodes/plan-node.js';
11
+ import { buildExpression } from './expression.js';
12
+ import { checkColumnsAssignable, columnSchemaToDef } from '../type-utils.js';
13
+ import type { ColumnDef } from '../../common/datatype.js';
14
+ import type { CTEScopeNode } from '../nodes/cte-node.js';
15
+ import { RegisteredScope } from '../scopes/registered.js';
16
+ import { ColumnReferenceNode, TableReferenceNode } from '../nodes/reference.js';
17
+ import { SinkNode } from '../nodes/sink-node.js';
18
+ import { ConstraintCheckNode } from '../nodes/constraint-check-node.js';
19
+ import { RowOpFlag } from '../../schema/table.js';
20
+ import { ReturningNode } from '../nodes/returning-node.js';
21
+ import { ProjectNode, type Projection } from '../nodes/project-node.js';
22
+ import { buildOldNewRowDescriptors } from '../../util/row-descriptor.js';
23
+ import { DmlExecutorNode } from '../nodes/dml-executor-node.js';
24
+ import { buildConstraintChecks } from './constraint-builder.js';
25
+
26
+ /**
27
+ * Creates a uniform row expansion projection that maps any relational source
28
+ * to the target table's column structure, filling in defaults for omitted columns.
29
+ * This ensures INSERT works orthogonally with any relational source.
30
+ */
31
+ function createRowExpansionProjection(
32
+ ctx: PlanningContext,
33
+ sourceNode: RelationalPlanNode,
34
+ targetColumns: ColumnDef[],
35
+ tableReference: TableReferenceNode,
36
+ contextScope?: RegisteredScope
37
+ ): RelationalPlanNode {
38
+ const tableSchema = tableReference.tableSchema;
39
+
40
+ // If we're inserting into all columns in table order, no expansion needed
41
+ if (targetColumns.length === tableSchema.columns.length) {
42
+ const allColumnsMatch = targetColumns.every((tc, i) =>
43
+ tc.name.toLowerCase() === tableSchema.columns[i].name.toLowerCase()
44
+ );
45
+ if (allColumnsMatch) {
46
+ return sourceNode; // Source already matches table structure
47
+ }
48
+ }
49
+
50
+ // Create projection expressions for each table column
51
+ const projections: Projection[] = [];
52
+ const sourceAttributes = sourceNode.getAttributes();
53
+
54
+ // If we have a context scope, we need to also register source columns in it
55
+ // so that defaults can reference them (e.g., DEFAULT base_price + markup)
56
+ if (contextScope) {
57
+ targetColumns.forEach((targetCol, index) => {
58
+ if (index < sourceAttributes.length) {
59
+ const sourceAttr = sourceAttributes[index];
60
+ const colNameLower = targetCol.name.toLowerCase();
61
+ contextScope.registerSymbol(colNameLower, (exp, s) =>
62
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, sourceAttr.type, sourceAttr.id, index)
63
+ );
64
+ }
65
+ });
66
+ }
67
+
68
+ tableSchema.columns.forEach((tableColumn) => {
69
+ // Find if this table column is in the target columns
70
+ const targetColIndex = targetColumns.findIndex(tc =>
71
+ tc.name.toLowerCase() === tableColumn.name.toLowerCase()
72
+ );
73
+
74
+ if (targetColIndex >= 0) {
75
+ // This column is provided by the source - reference the source column
76
+ if (targetColIndex < sourceAttributes.length) {
77
+ const sourceAttr = sourceAttributes[targetColIndex];
78
+ // Create a column reference to the source attribute
79
+ const columnRef = new ColumnReferenceNode(
80
+ ctx.scope,
81
+ { type: 'column', name: sourceAttr.name } satisfies AST.ColumnExpr,
82
+ sourceAttr.type,
83
+ sourceAttr.id,
84
+ targetColIndex
85
+ );
86
+ projections.push({
87
+ node: columnRef,
88
+ alias: tableColumn.name
89
+ });
90
+ } else {
91
+ throw new QuereusError(
92
+ `Source has fewer columns than expected for INSERT target columns`,
93
+ StatusCode.ERROR
94
+ );
95
+ }
96
+ } else {
97
+ // This column is omitted - use default value or NULL
98
+ let defaultNode: ScalarPlanNode;
99
+ // Use context scope for default evaluation if available
100
+ const defaultCtx = contextScope ? { ...ctx, scope: contextScope } : ctx;
101
+ if (tableColumn.defaultValue !== undefined) {
102
+ // Use default value
103
+ if (typeof tableColumn.defaultValue === 'object' && tableColumn.defaultValue !== null && 'type' in tableColumn.defaultValue) {
104
+ // It's an AST.Expression - build it into a plan node with context scope
105
+ defaultNode = buildExpression(defaultCtx, tableColumn.defaultValue as AST.Expression) as ScalarPlanNode;
106
+ } else {
107
+ // Literal default value
108
+ defaultNode = buildExpression(defaultCtx, { type: 'literal', value: tableColumn.defaultValue }) as ScalarPlanNode;
109
+ }
110
+ } else {
111
+ // No default value - use NULL
112
+ defaultNode = buildExpression(defaultCtx, { type: 'literal', value: null }) as ScalarPlanNode;
113
+ }
114
+ projections.push({
115
+ node: defaultNode,
116
+ alias: tableColumn.name
117
+ });
118
+ }
119
+ });
120
+
121
+ // Create projection node that expands source to table structure
122
+ return new ProjectNode(ctx.scope, sourceNode, projections);
123
+ }
124
+
125
+ /**
126
+ * Validates that RETURNING expressions use appropriate NEW/OLD qualifiers for the operation type
127
+ */
128
+ function validateReturningExpression(expr: AST.Expression, operationType: 'INSERT' | 'UPDATE' | 'DELETE'): void {
129
+ function checkExpression(e: AST.Expression): void {
130
+ if (e.type === 'column') {
131
+ if (e.table?.toLowerCase() === 'old' && operationType === 'INSERT') {
132
+ throw new QuereusError(
133
+ 'OLD qualifier cannot be used in INSERT RETURNING clause',
134
+ StatusCode.ERROR
135
+ );
136
+ }
137
+ if (e.table?.toLowerCase() === 'new' && operationType === 'DELETE') {
138
+ throw new QuereusError(
139
+ 'NEW qualifier cannot be used in DELETE RETURNING clause',
140
+ StatusCode.ERROR
141
+ );
142
+ }
143
+ } else if (e.type === 'binary') {
144
+ checkExpression(e.left);
145
+ checkExpression(e.right);
146
+ } else if (e.type === 'unary') {
147
+ checkExpression(e.expr);
148
+ } else if (e.type === 'function') {
149
+ e.args.forEach(checkExpression);
150
+ } else if (e.type === 'case') {
151
+ if (e.baseExpr) checkExpression(e.baseExpr);
152
+ e.whenThenClauses.forEach(clause => {
153
+ checkExpression(clause.when);
154
+ checkExpression(clause.then);
155
+ });
156
+ if (e.elseExpr) checkExpression(e.elseExpr);
157
+ } else if (e.type === 'cast') {
158
+ checkExpression(e.expr);
159
+ } else if (e.type === 'collate') {
160
+ checkExpression(e.expr);
161
+ } else if (e.type === 'subquery') {
162
+ // Subqueries in RETURNING are complex - for now, we'll skip validation
163
+ // A full implementation would need to traverse the subquery's AST
164
+ } else if (e.type === 'in') {
165
+ checkExpression(e.expr);
166
+ if (e.values) {
167
+ e.values.forEach(checkExpression);
168
+ }
169
+ } else if (e.type === 'exists') {
170
+ // EXISTS subqueries are complex - skip validation for now
171
+ } else if (e.type === 'windowFunction') {
172
+ checkExpression(e.function);
173
+ }
174
+ // Other expression types (literal, parameter) don't need validation
175
+ }
176
+
177
+ checkExpression(expr);
178
+ }
179
+
180
+ export function buildInsertStmt(
181
+ ctx: PlanningContext,
182
+ stmt: AST.InsertStmt,
183
+ ): PlanNode {
184
+ const tableRetrieve = buildTableReference({ type: 'table', table: stmt.table }, ctx);
185
+ const tableReference = tableRetrieve.tableRef; // Extract the actual TableReferenceNode
186
+
187
+ // Process mutation context assignments if present
188
+ const mutationContextValues = new Map<string, ScalarPlanNode>();
189
+ const contextAttributes: Attribute[] = [];
190
+ let contextScope: RegisteredScope | undefined;
191
+
192
+ if (stmt.contextValues && tableReference.tableSchema.mutationContext) {
193
+ // Create context attributes
194
+ tableReference.tableSchema.mutationContext.forEach((contextVar) => {
195
+ contextAttributes.push({
196
+ id: PlanNode.nextAttrId(),
197
+ name: contextVar.name,
198
+ type: {
199
+ typeClass: 'scalar' as const,
200
+ logicalType: contextVar.logicalType,
201
+ nullable: !contextVar.notNull,
202
+ isReadOnly: true
203
+ },
204
+ sourceRelation: `context.${tableReference.tableSchema.name}`
205
+ });
206
+ });
207
+
208
+ // Create a new scope for mutation context
209
+ contextScope = new RegisteredScope(ctx.scope);
210
+
211
+ // Register mutation context variables in the scope (before evaluating expressions)
212
+ contextAttributes.forEach((attr, index) => {
213
+ const contextVar = tableReference.tableSchema.mutationContext![index];
214
+ const varNameLower = contextVar.name.toLowerCase();
215
+
216
+ // Register both unqualified and qualified names
217
+ contextScope!.subscribeFactory(varNameLower, (exp, s) =>
218
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, index)
219
+ );
220
+ contextScope!.subscribeFactory(`context.${varNameLower}`, (exp, s) =>
221
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, index)
222
+ );
223
+ });
224
+
225
+ // Build context value expressions using the context scope
226
+ const contextWithScope = { ...ctx, scope: contextScope };
227
+ stmt.contextValues.forEach((assignment) => {
228
+ const valueExpr = buildExpression(contextWithScope, assignment.value) as ScalarPlanNode;
229
+ mutationContextValues.set(assignment.name, valueExpr);
230
+ });
231
+ }
232
+
233
+ let targetColumns: ColumnDef[] = [];
234
+ if (stmt.columns && stmt.columns.length > 0) {
235
+ // Explicit columns specified
236
+ targetColumns = stmt.columns.map((colName, index) => columnSchemaToDef(colName, tableReference.tableSchema.columns[index]));
237
+ } else {
238
+ // No explicit columns - default to all table columns in order
239
+ targetColumns = tableReference.tableSchema.columns.map(col => columnSchemaToDef(col.name, col));
240
+ }
241
+
242
+ let sourceNode: RelationalPlanNode;
243
+
244
+ if (stmt.values) {
245
+ // VALUES clause - build the VALUES node
246
+ const rows = stmt.values.map(rowExprs =>
247
+ rowExprs.map(expr => buildExpression(ctx, expr) as PlanNode as ScalarPlanNode)
248
+ );
249
+
250
+ // Check that there are the right number of columns in each row
251
+ rows.forEach(row => {
252
+ if (row.length !== targetColumns.length) {
253
+ throw new QuereusError(`Column count mismatch in VALUES clause. Expected ${targetColumns.length} columns, got ${row.length}.`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
254
+ }
255
+ });
256
+
257
+ // Create VALUES node with target column names
258
+ const targetColumnNames = targetColumns.map(col => col.name);
259
+ sourceNode = new ValuesNode(ctx.scope, rows, targetColumnNames);
260
+
261
+ } else if (stmt.select) {
262
+ // SELECT clause - build the SELECT statement
263
+ let parentCtes: Map<string, CTEScopeNode> = new Map();
264
+ if (stmt.withClause) {
265
+ parentCtes = buildWithClause(ctx, stmt.withClause);
266
+ }
267
+ const selectPlan = buildSelectStmt(ctx, stmt.select, parentCtes);
268
+ if (selectPlan.getType().typeClass !== 'relation') {
269
+ throw new QuereusError('SELECT statement in INSERT did not produce a relational plan.', StatusCode.INTERNAL, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
270
+ }
271
+ sourceNode = selectPlan as RelationalPlanNode;
272
+ checkColumnsAssignable(sourceNode.getType().columns, targetColumns, stmt);
273
+
274
+ } else {
275
+ throw new QuereusError('INSERT statement must have a VALUES clause or a SELECT query.', StatusCode.ERROR);
276
+ }
277
+
278
+ // ORTHOGONAL ROW EXPANSION: Apply uniform row expansion to map any source to table structure with defaults
279
+ const expandedSourceNode = createRowExpansionProjection(ctx, sourceNode, targetColumns, tableReference, contextScope);
280
+
281
+ // Update targetColumns to reflect all table columns since we've expanded the source
282
+ const finalTargetColumns = tableReference.tableSchema.columns.map(col => columnSchemaToDef(col.name, col));
283
+
284
+ // Create OLD/NEW attributes for INSERT (OLD = all NULL, NEW = actual values)
285
+ const oldAttributes = tableReference.tableSchema.columns.map((col) => ({
286
+ id: PlanNode.nextAttrId(),
287
+ name: col.name,
288
+ type: {
289
+ typeClass: 'scalar' as const,
290
+ logicalType: col.logicalType,
291
+ nullable: true, // OLD values are always NULL for INSERT
292
+ isReadOnly: false
293
+ },
294
+ sourceRelation: `OLD.${tableReference.tableSchema.name}`
295
+ }));
296
+
297
+ const newAttributes = tableReference.tableSchema.columns.map((col) => ({
298
+ id: PlanNode.nextAttrId(),
299
+ name: col.name,
300
+ type: {
301
+ typeClass: 'scalar' as const,
302
+ logicalType: col.logicalType,
303
+ nullable: !col.notNull,
304
+ isReadOnly: false
305
+ },
306
+ sourceRelation: `NEW.${tableReference.tableSchema.name}`
307
+ }));
308
+
309
+ const { oldRowDescriptor, newRowDescriptor, flatRowDescriptor } = buildOldNewRowDescriptors(oldAttributes, newAttributes);
310
+
311
+ // Build context descriptor if we have context attributes
312
+ const contextDescriptor: RowDescriptor = contextAttributes.length > 0 ? [] : undefined as any;
313
+ if (contextDescriptor) {
314
+ contextAttributes.forEach((attr, index) => {
315
+ contextDescriptor[attr.id] = index;
316
+ });
317
+ }
318
+
319
+ // Build constraint checks at plan time
320
+ const constraintChecks = buildConstraintChecks(
321
+ ctx,
322
+ tableReference.tableSchema,
323
+ RowOpFlag.INSERT,
324
+ oldAttributes,
325
+ newAttributes,
326
+ flatRowDescriptor,
327
+ contextAttributes
328
+ );
329
+
330
+ const insertNode = new InsertNode(
331
+ ctx.scope,
332
+ tableReference,
333
+ finalTargetColumns,
334
+ expandedSourceNode,
335
+ flatRowDescriptor,
336
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
337
+ contextAttributes.length > 0 ? contextAttributes : undefined,
338
+ contextDescriptor
339
+ );
340
+
341
+ const constraintCheckNode = new ConstraintCheckNode(
342
+ ctx.scope,
343
+ insertNode,
344
+ tableReference,
345
+ RowOpFlag.INSERT,
346
+ oldRowDescriptor,
347
+ newRowDescriptor,
348
+ flatRowDescriptor,
349
+ constraintChecks,
350
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
351
+ contextAttributes.length > 0 ? contextAttributes : undefined,
352
+ contextDescriptor
353
+ );
354
+
355
+ // Add DML executor node to perform the actual database insert operations
356
+ const dmlExecutorNode = new DmlExecutorNode(
357
+ ctx.scope,
358
+ constraintCheckNode,
359
+ tableReference,
360
+ 'insert',
361
+ stmt.onConflict
362
+ );
363
+
364
+ const resultNode: RelationalPlanNode = dmlExecutorNode;
365
+
366
+ if (stmt.returning && stmt.returning.length > 0) {
367
+ // Create returning scope with OLD/NEW attribute access
368
+ const returningScope = new RegisteredScope(ctx.scope);
369
+
370
+ // Register OLD.* symbols (always NULL for INSERT)
371
+ oldAttributes.forEach((attr, columnIndex) => {
372
+ const tableColumn = tableReference.tableSchema.columns[columnIndex];
373
+ returningScope.registerSymbol(`old.${tableColumn.name.toLowerCase()}`, (exp, s) =>
374
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
375
+ );
376
+ });
377
+
378
+ // Register NEW.* symbols and unqualified column names (default to NEW)
379
+ newAttributes.forEach((attr, columnIndex) => {
380
+ const tableColumn = tableReference.tableSchema.columns[columnIndex];
381
+
382
+ // NEW.column
383
+ returningScope.registerSymbol(`new.${tableColumn.name.toLowerCase()}`, (exp, s) =>
384
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
385
+ );
386
+
387
+ // Unqualified column (defaults to NEW)
388
+ returningScope.registerSymbol(tableColumn.name.toLowerCase(), (exp, s) =>
389
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
390
+ );
391
+
392
+ // Table-qualified form (table.column -> NEW)
393
+ const tblQualified = `${tableReference.tableSchema.name.toLowerCase()}.${tableColumn.name.toLowerCase()}`;
394
+ returningScope.registerSymbol(tblQualified, (exp, s) =>
395
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, columnIndex)
396
+ );
397
+ });
398
+
399
+ // Build RETURNING projections in the OLD/NEW context
400
+ const returningProjections = stmt.returning.map(rc => {
401
+ // TODO: Support RETURNING *
402
+ if (rc.type === 'all') throw new QuereusError('RETURNING * not yet supported', StatusCode.UNSUPPORTED);
403
+
404
+ // Infer alias from column name if not explicitly provided
405
+ let alias = rc.alias;
406
+ if (!alias && rc.expr.type === 'column') {
407
+ // For qualified column references like NEW.id, normalize to lowercase
408
+ if (rc.expr.table) {
409
+ alias = `${rc.expr.table.toLowerCase()}.${rc.expr.name.toLowerCase()}`;
410
+ } else {
411
+ alias = rc.expr.name.toLowerCase();
412
+ }
413
+ }
414
+
415
+ // Validate that OLD references are not used in INSERT RETURNING
416
+ validateReturningExpression(rc.expr, 'INSERT');
417
+
418
+ return {
419
+ node: buildExpression({ ...ctx, scope: returningScope }, rc.expr) as ScalarPlanNode,
420
+ alias: alias
421
+ };
422
+ });
423
+
424
+ return new ReturningNode(ctx.scope, dmlExecutorNode, returningProjections);
425
+ }
426
+
427
+ return new SinkNode(ctx.scope, resultNode, 'insert');
428
+ }