@quereus/quereus 0.6.2 → 0.6.3

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.
@@ -1,319 +1,319 @@
1
- import type * as AST from '../../parser/ast.js';
2
- import type { PlanningContext } from '../planning-context.js';
3
- import { UpdateNode, type UpdateAssignment } from '../nodes/update-node.js';
4
- import { DmlExecutorNode } from '../nodes/dml-executor-node.js';
5
- import { buildTableReference } from './table.js';
6
- import { buildExpression } from './expression.js';
7
- import { PlanNode, type RelationalPlanNode, type ScalarPlanNode, type Attribute, type RowDescriptor } from '../nodes/plan-node.js';
8
- import { FilterNode } from '../nodes/filter.js';
9
- import { QuereusError } from '../../common/errors.js';
10
- import { StatusCode } from '../../common/types.js';
11
- import { RegisteredScope } from '../scopes/registered.js';
12
- import { ColumnReferenceNode } from '../nodes/reference.js';
13
- import { SinkNode } from '../nodes/sink-node.js';
14
- import { ConstraintCheckNode } from '../nodes/constraint-check-node.js';
15
- import { RowOpFlag } from '../../schema/table.js';
16
- import { ReturningNode } from '../nodes/returning-node.js';
17
- import { buildOldNewRowDescriptors } from '../../util/row-descriptor.js';
18
- import { buildConstraintChecks } from './constraint-builder.js';
19
-
20
- export function buildUpdateStmt(
21
- ctx: PlanningContext,
22
- stmt: AST.UpdateStmt,
23
- ): PlanNode {
24
- const tableRetrieve = buildTableReference({ type: 'table', table: stmt.table }, ctx);
25
- const tableReference = tableRetrieve.tableRef; // Extract the actual TableReferenceNode
26
-
27
- // Process mutation context assignments if present
28
- const mutationContextValues = new Map<string, ScalarPlanNode>();
29
- const contextAttributes: Attribute[] = [];
30
-
31
- if (stmt.contextValues && tableReference.tableSchema.mutationContext) {
32
- // Create context attributes
33
- tableReference.tableSchema.mutationContext.forEach((contextVar) => {
34
- contextAttributes.push({
35
- id: PlanNode.nextAttrId(),
36
- name: contextVar.name,
37
- type: {
38
- typeClass: 'scalar' as const,
39
- logicalType: contextVar.logicalType,
40
- nullable: !contextVar.notNull,
41
- isReadOnly: true
42
- },
43
- sourceRelation: `context.${tableReference.tableSchema.name}`
44
- });
45
- });
46
-
47
- // Build context value expressions (evaluated in the base scope, before table scope)
48
- stmt.contextValues.forEach((assignment) => {
49
- const valueExpr = buildExpression(ctx, assignment.value) as ScalarPlanNode;
50
- mutationContextValues.set(assignment.name, valueExpr);
51
- });
52
- }
53
-
54
- // Plan the source of rows to update. This is typically the table itself, potentially filtered.
55
- let sourceNode: RelationalPlanNode = buildTableReference({ type: 'table', table: stmt.table }, ctx);
56
-
57
- // Create a new scope with the table columns registered for column resolution
58
- const tableScope = new RegisteredScope(ctx.scope);
59
- const sourceAttributes = sourceNode.getAttributes();
60
- sourceNode.getType().columns.forEach((c, i) => {
61
- const attr = sourceAttributes[i];
62
- tableScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
63
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
64
- });
65
-
66
- // Create a new planning context with the updated scope for WHERE clause resolution
67
- const updateCtx = { ...ctx, scope: tableScope };
68
-
69
- if (stmt.where) {
70
- const filterExpression = buildExpression(updateCtx, stmt.where);
71
- sourceNode = new FilterNode(updateCtx.scope, sourceNode, filterExpression);
72
- }
73
-
74
- const assignments: UpdateAssignment[] = stmt.assignments.map(assign => {
75
- // TODO: Validate assign.column against tableReference.tableSchema
76
- const targetColumn: AST.ColumnExpr = { type: 'column', name: assign.column, table: stmt.table.name, schema: stmt.table.schema };
77
- return {
78
- targetColumn, // Keep as AST for now, emitter can resolve index
79
- value: buildExpression(updateCtx, assign.value),
80
- };
81
- });
82
-
83
- // Create OLD/NEW attributes for UPDATE (used for both RETURNING and non-RETURNING paths)
84
- const oldAttributes = tableReference.tableSchema.columns.map((col) => ({
85
- id: PlanNode.nextAttrId(),
86
- name: col.name,
87
- type: {
88
- typeClass: 'scalar' as const,
89
- logicalType: col.logicalType,
90
- nullable: !col.notNull,
91
- isReadOnly: false
92
- },
93
- sourceRelation: `OLD.${tableReference.tableSchema.name}`
94
- }));
95
-
96
- const newAttributes = tableReference.tableSchema.columns.map((col) => ({
97
- id: PlanNode.nextAttrId(),
98
- name: col.name,
99
- type: {
100
- typeClass: 'scalar' as const,
101
- logicalType: col.logicalType,
102
- nullable: !col.notNull,
103
- isReadOnly: false
104
- },
105
- sourceRelation: `NEW.${tableReference.tableSchema.name}`
106
- }));
107
-
108
- const { oldRowDescriptor, newRowDescriptor, flatRowDescriptor } = buildOldNewRowDescriptors(oldAttributes, newAttributes);
109
-
110
- // Build context descriptor if we have context attributes
111
- const contextDescriptor: RowDescriptor = contextAttributes.length > 0 ? [] : undefined as any;
112
- if (contextDescriptor) {
113
- contextAttributes.forEach((attr, index) => {
114
- contextDescriptor[attr.id] = index;
115
- });
116
- }
117
-
118
- // Build constraint checks at plan time
119
- const constraintChecks = buildConstraintChecks(
120
- updateCtx,
121
- tableReference.tableSchema,
122
- RowOpFlag.UPDATE,
123
- oldAttributes,
124
- newAttributes,
125
- flatRowDescriptor,
126
- contextAttributes
127
- );
128
-
129
- if (stmt.returning && stmt.returning.length > 0) {
130
- // For RETURNING, create coordinated attribute IDs like we do for INSERT
131
- const returningScope = new RegisteredScope(updateCtx.scope);
132
-
133
- // Create consistent attribute IDs for all table columns (both NEW and OLD)
134
- const newColumnAttributeIds: number[] = [];
135
- const oldColumnAttributeIds: number[] = [];
136
- newAttributes.forEach((attr, columnIndex) => {
137
- newColumnAttributeIds[columnIndex] = attr.id;
138
- });
139
- oldAttributes.forEach((attr, columnIndex) => {
140
- oldColumnAttributeIds[columnIndex] = attr.id;
141
- });
142
-
143
- tableReference.tableSchema.columns.forEach((tableColumn, columnIndex) => {
144
- const newAttributeId = newAttributes[columnIndex].id;
145
- const oldAttributeId = oldAttributes[columnIndex].id;
146
-
147
- // Register the unqualified column name in the RETURNING scope (defaults to NEW values)
148
- returningScope.registerSymbol(tableColumn.name.toLowerCase(), (exp, s) => {
149
- return new ColumnReferenceNode(
150
- s,
151
- exp as AST.ColumnExpr,
152
- {
153
- typeClass: 'scalar',
154
- logicalType: tableColumn.logicalType,
155
- nullable: !tableColumn.notNull,
156
- isReadOnly: false
157
- },
158
- newAttributeId,
159
- columnIndex
160
- );
161
- });
162
-
163
- // Also register the table-qualified form (table.column) - defaults to NEW values
164
- const tblQualified = `${tableReference.tableSchema.name.toLowerCase()}.${tableColumn.name.toLowerCase()}`;
165
- returningScope.registerSymbol(tblQualified, (exp, s) =>
166
- new ColumnReferenceNode(
167
- s,
168
- exp as AST.ColumnExpr,
169
- {
170
- typeClass: 'scalar',
171
- logicalType: tableColumn.logicalType,
172
- nullable: !tableColumn.notNull,
173
- isReadOnly: false
174
- },
175
- newAttributeId,
176
- columnIndex
177
- )
178
- );
179
-
180
- // Register NEW.column for UPDATE RETURNING (updated values)
181
- returningScope.registerSymbol(`new.${tableColumn.name.toLowerCase()}`, (exp, s) =>
182
- new ColumnReferenceNode(
183
- s,
184
- exp as AST.ColumnExpr,
185
- {
186
- typeClass: 'scalar',
187
- logicalType: tableColumn.logicalType,
188
- nullable: !tableColumn.notNull,
189
- isReadOnly: false
190
- },
191
- newAttributeId,
192
- columnIndex
193
- )
194
- );
195
-
196
- // Register OLD.column for UPDATE RETURNING (original values)
197
- returningScope.registerSymbol(`old.${tableColumn.name.toLowerCase()}`, (exp, s) =>
198
- new ColumnReferenceNode(
199
- s,
200
- exp as AST.ColumnExpr,
201
- {
202
- typeClass: 'scalar',
203
- logicalType: tableColumn.logicalType,
204
- nullable: !tableColumn.notNull,
205
- isReadOnly: false
206
- },
207
- oldAttributeId,
208
- columnIndex
209
- )
210
- );
211
- });
212
-
213
- const returningProjections = stmt.returning.map(rc => {
214
- // TODO: Support RETURNING *
215
- if (rc.type === 'all') throw new QuereusError('RETURNING * not yet supported', StatusCode.UNSUPPORTED);
216
-
217
- // Infer alias from column name if not explicitly provided
218
- let alias = rc.alias;
219
- if (!alias && rc.expr.type === 'column') {
220
- // For qualified column references like NEW.id or OLD.id, normalize to lowercase
221
- if (rc.expr.table) {
222
- alias = `${rc.expr.table.toLowerCase()}.${rc.expr.name.toLowerCase()}`;
223
- } else {
224
- alias = rc.expr.name.toLowerCase();
225
- }
226
- }
227
-
228
- const columnIndex = tableReference.tableSchema.columns.findIndex(col => col.name.toLowerCase() === (rc.expr.type === 'column' ? rc.expr.name.toLowerCase() : ''));
229
- const projAttributeId = rc.expr.type === 'column' && columnIndex !== -1 ? newColumnAttributeIds[columnIndex] : undefined;
230
-
231
- return {
232
- node: buildExpression({ ...updateCtx, scope: returningScope }, rc.expr) as ScalarPlanNode,
233
- alias: alias,
234
- attributeId: projAttributeId
235
- };
236
- });
237
-
238
- // Create UpdateNode with both row descriptors for RETURNING coordination
239
- const updateNodeWithDescriptor = new UpdateNode(
240
- updateCtx.scope,
241
- tableReference,
242
- assignments,
243
- sourceNode,
244
- stmt.onConflict,
245
- oldRowDescriptor,
246
- newRowDescriptor,
247
- flatRowDescriptor,
248
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
249
- contextAttributes.length > 0 ? contextAttributes : undefined,
250
- contextDescriptor
251
- );
252
-
253
- // For returning, we still need to execute the update before projecting
254
- // Always inject ConstraintCheckNode for UPDATE operations (provides required metadata)
255
- const constraintCheckNode = new ConstraintCheckNode(
256
- updateCtx.scope,
257
- updateNodeWithDescriptor,
258
- tableReference,
259
- RowOpFlag.UPDATE,
260
- oldRowDescriptor,
261
- newRowDescriptor,
262
- flatRowDescriptor,
263
- constraintChecks,
264
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
265
- contextAttributes.length > 0 ? contextAttributes : undefined,
266
- contextDescriptor
267
- );
268
-
269
- const updateExecutorNode = new DmlExecutorNode(
270
- updateCtx.scope,
271
- constraintCheckNode,
272
- tableReference,
273
- 'update'
274
- );
275
-
276
- // Return the RETURNING results from the executed update
277
- return new ReturningNode(updateCtx.scope, updateExecutorNode, returningProjections);
278
- }
279
-
280
- // Step 1: Create UpdateNode that produces updated rows (but doesn't execute them)
281
- // Create newRowDescriptor and oldRowDescriptor for constraint checking with NEW/OLD references
282
- const updateNode = new UpdateNode(
283
- updateCtx.scope,
284
- tableReference,
285
- assignments,
286
- sourceNode,
287
- stmt.onConflict,
288
- oldRowDescriptor,
289
- newRowDescriptor,
290
- flatRowDescriptor,
291
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
292
- contextAttributes.length > 0 ? contextAttributes : undefined,
293
- contextDescriptor
294
- );
295
-
296
- // Step 2: inject constraint checking AFTER update row generation
297
- const constraintCheckNode = new ConstraintCheckNode(
298
- updateCtx.scope,
299
- updateNode,
300
- tableReference,
301
- RowOpFlag.UPDATE,
302
- oldRowDescriptor,
303
- newRowDescriptor,
304
- flatRowDescriptor,
305
- constraintChecks,
306
- mutationContextValues.size > 0 ? mutationContextValues : undefined,
307
- contextAttributes.length > 0 ? contextAttributes : undefined,
308
- contextDescriptor
309
- );
310
-
311
- const updateExecutorNode = new DmlExecutorNode(
312
- updateCtx.scope,
313
- constraintCheckNode,
314
- tableReference,
315
- 'update'
316
- );
317
-
318
- return new SinkNode(updateCtx.scope, updateExecutorNode, 'update');
319
- }
1
+ import type * as AST from '../../parser/ast.js';
2
+ import type { PlanningContext } from '../planning-context.js';
3
+ import { UpdateNode, type UpdateAssignment } from '../nodes/update-node.js';
4
+ import { DmlExecutorNode } from '../nodes/dml-executor-node.js';
5
+ import { buildTableReference } from './table.js';
6
+ import { buildExpression } from './expression.js';
7
+ import { PlanNode, type RelationalPlanNode, type ScalarPlanNode, type Attribute, type RowDescriptor } from '../nodes/plan-node.js';
8
+ import { FilterNode } from '../nodes/filter.js';
9
+ import { QuereusError } from '../../common/errors.js';
10
+ import { StatusCode } from '../../common/types.js';
11
+ import { RegisteredScope } from '../scopes/registered.js';
12
+ import { ColumnReferenceNode } from '../nodes/reference.js';
13
+ import { SinkNode } from '../nodes/sink-node.js';
14
+ import { ConstraintCheckNode } from '../nodes/constraint-check-node.js';
15
+ import { RowOpFlag } from '../../schema/table.js';
16
+ import { ReturningNode } from '../nodes/returning-node.js';
17
+ import { buildOldNewRowDescriptors } from '../../util/row-descriptor.js';
18
+ import { buildConstraintChecks } from './constraint-builder.js';
19
+
20
+ export function buildUpdateStmt(
21
+ ctx: PlanningContext,
22
+ stmt: AST.UpdateStmt,
23
+ ): PlanNode {
24
+ const tableRetrieve = buildTableReference({ type: 'table', table: stmt.table }, ctx);
25
+ const tableReference = tableRetrieve.tableRef; // Extract the actual TableReferenceNode
26
+
27
+ // Process mutation context assignments if present
28
+ const mutationContextValues = new Map<string, ScalarPlanNode>();
29
+ const contextAttributes: Attribute[] = [];
30
+
31
+ if (stmt.contextValues && tableReference.tableSchema.mutationContext) {
32
+ // Create context attributes
33
+ tableReference.tableSchema.mutationContext.forEach((contextVar) => {
34
+ contextAttributes.push({
35
+ id: PlanNode.nextAttrId(),
36
+ name: contextVar.name,
37
+ type: {
38
+ typeClass: 'scalar' as const,
39
+ logicalType: contextVar.logicalType,
40
+ nullable: !contextVar.notNull,
41
+ isReadOnly: true
42
+ },
43
+ sourceRelation: `context.${tableReference.tableSchema.name}`
44
+ });
45
+ });
46
+
47
+ // Build context value expressions (evaluated in the base scope, before table scope)
48
+ stmt.contextValues.forEach((assignment) => {
49
+ const valueExpr = buildExpression(ctx, assignment.value) as ScalarPlanNode;
50
+ mutationContextValues.set(assignment.name, valueExpr);
51
+ });
52
+ }
53
+
54
+ // Plan the source of rows to update. This is typically the table itself, potentially filtered.
55
+ let sourceNode: RelationalPlanNode = buildTableReference({ type: 'table', table: stmt.table }, ctx);
56
+
57
+ // Create a new scope with the table columns registered for column resolution
58
+ const tableScope = new RegisteredScope(ctx.scope);
59
+ const sourceAttributes = sourceNode.getAttributes();
60
+ sourceNode.getType().columns.forEach((c, i) => {
61
+ const attr = sourceAttributes[i];
62
+ tableScope.registerSymbol(c.name.toLowerCase(), (exp, s) =>
63
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, c.type, attr.id, i));
64
+ });
65
+
66
+ // Create a new planning context with the updated scope for WHERE clause resolution
67
+ const updateCtx = { ...ctx, scope: tableScope };
68
+
69
+ if (stmt.where) {
70
+ const filterExpression = buildExpression(updateCtx, stmt.where);
71
+ sourceNode = new FilterNode(updateCtx.scope, sourceNode, filterExpression);
72
+ }
73
+
74
+ const assignments: UpdateAssignment[] = stmt.assignments.map(assign => {
75
+ // TODO: Validate assign.column against tableReference.tableSchema
76
+ const targetColumn: AST.ColumnExpr = { type: 'column', name: assign.column, table: stmt.table.name, schema: stmt.table.schema };
77
+ return {
78
+ targetColumn, // Keep as AST for now, emitter can resolve index
79
+ value: buildExpression(updateCtx, assign.value),
80
+ };
81
+ });
82
+
83
+ // Create OLD/NEW attributes for UPDATE (used for both RETURNING and non-RETURNING paths)
84
+ const oldAttributes = tableReference.tableSchema.columns.map((col) => ({
85
+ id: PlanNode.nextAttrId(),
86
+ name: col.name,
87
+ type: {
88
+ typeClass: 'scalar' as const,
89
+ logicalType: col.logicalType,
90
+ nullable: !col.notNull,
91
+ isReadOnly: false
92
+ },
93
+ sourceRelation: `OLD.${tableReference.tableSchema.name}`
94
+ }));
95
+
96
+ const newAttributes = tableReference.tableSchema.columns.map((col) => ({
97
+ id: PlanNode.nextAttrId(),
98
+ name: col.name,
99
+ type: {
100
+ typeClass: 'scalar' as const,
101
+ logicalType: col.logicalType,
102
+ nullable: !col.notNull,
103
+ isReadOnly: false
104
+ },
105
+ sourceRelation: `NEW.${tableReference.tableSchema.name}`
106
+ }));
107
+
108
+ const { oldRowDescriptor, newRowDescriptor, flatRowDescriptor } = buildOldNewRowDescriptors(oldAttributes, newAttributes);
109
+
110
+ // Build context descriptor if we have context attributes
111
+ const contextDescriptor: RowDescriptor = contextAttributes.length > 0 ? [] : undefined as any;
112
+ if (contextDescriptor) {
113
+ contextAttributes.forEach((attr, index) => {
114
+ contextDescriptor[attr.id] = index;
115
+ });
116
+ }
117
+
118
+ // Build constraint checks at plan time
119
+ const constraintChecks = buildConstraintChecks(
120
+ updateCtx,
121
+ tableReference.tableSchema,
122
+ RowOpFlag.UPDATE,
123
+ oldAttributes,
124
+ newAttributes,
125
+ flatRowDescriptor,
126
+ contextAttributes
127
+ );
128
+
129
+ if (stmt.returning && stmt.returning.length > 0) {
130
+ // For RETURNING, create coordinated attribute IDs like we do for INSERT
131
+ const returningScope = new RegisteredScope(updateCtx.scope);
132
+
133
+ // Create consistent attribute IDs for all table columns (both NEW and OLD)
134
+ const newColumnAttributeIds: number[] = [];
135
+ const oldColumnAttributeIds: number[] = [];
136
+ newAttributes.forEach((attr, columnIndex) => {
137
+ newColumnAttributeIds[columnIndex] = attr.id;
138
+ });
139
+ oldAttributes.forEach((attr, columnIndex) => {
140
+ oldColumnAttributeIds[columnIndex] = attr.id;
141
+ });
142
+
143
+ tableReference.tableSchema.columns.forEach((tableColumn, columnIndex) => {
144
+ const newAttributeId = newAttributes[columnIndex].id;
145
+ const oldAttributeId = oldAttributes[columnIndex].id;
146
+
147
+ // Register the unqualified column name in the RETURNING scope (defaults to NEW values)
148
+ returningScope.registerSymbol(tableColumn.name.toLowerCase(), (exp, s) => {
149
+ return new ColumnReferenceNode(
150
+ s,
151
+ exp as AST.ColumnExpr,
152
+ {
153
+ typeClass: 'scalar',
154
+ logicalType: tableColumn.logicalType,
155
+ nullable: !tableColumn.notNull,
156
+ isReadOnly: false
157
+ },
158
+ newAttributeId,
159
+ columnIndex
160
+ );
161
+ });
162
+
163
+ // Also register the table-qualified form (table.column) - defaults to NEW values
164
+ const tblQualified = `${tableReference.tableSchema.name.toLowerCase()}.${tableColumn.name.toLowerCase()}`;
165
+ returningScope.registerSymbol(tblQualified, (exp, s) =>
166
+ new ColumnReferenceNode(
167
+ s,
168
+ exp as AST.ColumnExpr,
169
+ {
170
+ typeClass: 'scalar',
171
+ logicalType: tableColumn.logicalType,
172
+ nullable: !tableColumn.notNull,
173
+ isReadOnly: false
174
+ },
175
+ newAttributeId,
176
+ columnIndex
177
+ )
178
+ );
179
+
180
+ // Register NEW.column for UPDATE RETURNING (updated values)
181
+ returningScope.registerSymbol(`new.${tableColumn.name.toLowerCase()}`, (exp, s) =>
182
+ new ColumnReferenceNode(
183
+ s,
184
+ exp as AST.ColumnExpr,
185
+ {
186
+ typeClass: 'scalar',
187
+ logicalType: tableColumn.logicalType,
188
+ nullable: !tableColumn.notNull,
189
+ isReadOnly: false
190
+ },
191
+ newAttributeId,
192
+ columnIndex
193
+ )
194
+ );
195
+
196
+ // Register OLD.column for UPDATE RETURNING (original values)
197
+ returningScope.registerSymbol(`old.${tableColumn.name.toLowerCase()}`, (exp, s) =>
198
+ new ColumnReferenceNode(
199
+ s,
200
+ exp as AST.ColumnExpr,
201
+ {
202
+ typeClass: 'scalar',
203
+ logicalType: tableColumn.logicalType,
204
+ nullable: !tableColumn.notNull,
205
+ isReadOnly: false
206
+ },
207
+ oldAttributeId,
208
+ columnIndex
209
+ )
210
+ );
211
+ });
212
+
213
+ const returningProjections = stmt.returning.map(rc => {
214
+ // TODO: Support RETURNING *
215
+ if (rc.type === 'all') throw new QuereusError('RETURNING * not yet supported', StatusCode.UNSUPPORTED);
216
+
217
+ // Infer alias from column name if not explicitly provided
218
+ let alias = rc.alias;
219
+ if (!alias && rc.expr.type === 'column') {
220
+ // For qualified column references like NEW.id or OLD.id, normalize to lowercase
221
+ if (rc.expr.table) {
222
+ alias = `${rc.expr.table.toLowerCase()}.${rc.expr.name.toLowerCase()}`;
223
+ } else {
224
+ alias = rc.expr.name.toLowerCase();
225
+ }
226
+ }
227
+
228
+ const columnIndex = tableReference.tableSchema.columns.findIndex(col => col.name.toLowerCase() === (rc.expr.type === 'column' ? rc.expr.name.toLowerCase() : ''));
229
+ const projAttributeId = rc.expr.type === 'column' && columnIndex !== -1 ? newColumnAttributeIds[columnIndex] : undefined;
230
+
231
+ return {
232
+ node: buildExpression({ ...updateCtx, scope: returningScope }, rc.expr) as ScalarPlanNode,
233
+ alias: alias,
234
+ attributeId: projAttributeId
235
+ };
236
+ });
237
+
238
+ // Create UpdateNode with both row descriptors for RETURNING coordination
239
+ const updateNodeWithDescriptor = new UpdateNode(
240
+ updateCtx.scope,
241
+ tableReference,
242
+ assignments,
243
+ sourceNode,
244
+ stmt.onConflict,
245
+ oldRowDescriptor,
246
+ newRowDescriptor,
247
+ flatRowDescriptor,
248
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
249
+ contextAttributes.length > 0 ? contextAttributes : undefined,
250
+ contextDescriptor
251
+ );
252
+
253
+ // For returning, we still need to execute the update before projecting
254
+ // Always inject ConstraintCheckNode for UPDATE operations (provides required metadata)
255
+ const constraintCheckNode = new ConstraintCheckNode(
256
+ updateCtx.scope,
257
+ updateNodeWithDescriptor,
258
+ tableReference,
259
+ RowOpFlag.UPDATE,
260
+ oldRowDescriptor,
261
+ newRowDescriptor,
262
+ flatRowDescriptor,
263
+ constraintChecks,
264
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
265
+ contextAttributes.length > 0 ? contextAttributes : undefined,
266
+ contextDescriptor
267
+ );
268
+
269
+ const updateExecutorNode = new DmlExecutorNode(
270
+ updateCtx.scope,
271
+ constraintCheckNode,
272
+ tableReference,
273
+ 'update'
274
+ );
275
+
276
+ // Return the RETURNING results from the executed update
277
+ return new ReturningNode(updateCtx.scope, updateExecutorNode, returningProjections);
278
+ }
279
+
280
+ // Step 1: Create UpdateNode that produces updated rows (but doesn't execute them)
281
+ // Create newRowDescriptor and oldRowDescriptor for constraint checking with NEW/OLD references
282
+ const updateNode = new UpdateNode(
283
+ updateCtx.scope,
284
+ tableReference,
285
+ assignments,
286
+ sourceNode,
287
+ stmt.onConflict,
288
+ oldRowDescriptor,
289
+ newRowDescriptor,
290
+ flatRowDescriptor,
291
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
292
+ contextAttributes.length > 0 ? contextAttributes : undefined,
293
+ contextDescriptor
294
+ );
295
+
296
+ // Step 2: inject constraint checking AFTER update row generation
297
+ const constraintCheckNode = new ConstraintCheckNode(
298
+ updateCtx.scope,
299
+ updateNode,
300
+ tableReference,
301
+ RowOpFlag.UPDATE,
302
+ oldRowDescriptor,
303
+ newRowDescriptor,
304
+ flatRowDescriptor,
305
+ constraintChecks,
306
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
307
+ contextAttributes.length > 0 ? contextAttributes : undefined,
308
+ contextDescriptor
309
+ );
310
+
311
+ const updateExecutorNode = new DmlExecutorNode(
312
+ updateCtx.scope,
313
+ constraintCheckNode,
314
+ tableReference,
315
+ 'update'
316
+ );
317
+
318
+ return new SinkNode(updateCtx.scope, updateExecutorNode, 'update');
319
+ }
@@ -195,7 +195,7 @@ export function emitExplainSchema(plan: PlanNode, _ctx: EmissionContext): Instru
195
195
  }
196
196
 
197
197
  // Compute hash
198
- const hash = await computeShortSchemaHash(declaredSchema);
198
+ const hash = computeShortSchemaHash(declaredSchema);
199
199
 
200
200
  // Return hash with version if specified
201
201
  const result = explainStmt.version