@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.
- package/README.md +1 -1
- package/dist/src/index.d.ts +4 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +1 -1
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/schema/schema-hasher.d.ts +3 -3
- package/dist/src/schema/schema-hasher.d.ts.map +1 -1
- package/dist/src/schema/schema-hasher.js +9 -27
- package/dist/src/schema/schema-hasher.js.map +1 -1
- package/dist/src/util/hash.d.ts +19 -0
- package/dist/src/util/hash.d.ts.map +1 -0
- package/dist/src/util/hash.js +76 -0
- package/dist/src/util/hash.js.map +1 -0
- package/dist/src/util/plugin-helper.d.ts +45 -0
- package/dist/src/util/plugin-helper.d.ts.map +1 -0
- package/dist/src/util/plugin-helper.js +85 -0
- package/dist/src/util/plugin-helper.js.map +1 -0
- package/package.json +4 -2
- package/src/index.ts +10 -16
- package/src/planner/building/delete.ts +214 -214
- package/src/planner/building/insert.ts +428 -428
- package/src/planner/building/update.ts +319 -319
- package/src/runtime/emit/schema-declarative.ts +1 -1
- package/src/schema/schema-hasher.ts +9 -27
- package/src/util/ast-stringify.ts +864 -864
- package/src/util/hash.ts +90 -0
- package/src/util/plugin-helper.ts +110 -0
- package/src/vtab/memory/table.ts +256 -256
- package/src/vtab/table.ts +162 -162
- package/dist/src/config/loader.d.ts +0 -41
- package/dist/src/config/loader.d.ts.map +0 -1
- package/dist/src/config/loader.js +0 -102
- package/dist/src/config/loader.js.map +0 -1
- package/dist/src/planner/nodes/physical-access-nodes.d.ts +0 -83
- package/dist/src/planner/nodes/physical-access-nodes.d.ts.map +0 -1
- package/dist/src/planner/nodes/physical-access-nodes.js +0 -226
- package/dist/src/planner/nodes/physical-access-nodes.js.map +0 -1
- package/dist/src/planner/nodes/scan.d.ts +0 -27
- package/dist/src/planner/nodes/scan.d.ts.map +0 -1
- package/dist/src/planner/nodes/scan.js +0 -78
- package/dist/src/planner/nodes/scan.js.map +0 -1
- package/dist/src/planner/nodes/update-executor-node.d.ts +0 -24
- package/dist/src/planner/nodes/update-executor-node.d.ts.map +0 -1
- package/dist/src/planner/nodes/update-executor-node.js +0 -57
- package/dist/src/planner/nodes/update-executor-node.js.map +0 -1
- package/dist/src/planner/physical-utils.d.ts +0 -36
- package/dist/src/planner/physical-utils.d.ts.map +0 -1
- package/dist/src/planner/physical-utils.js +0 -122
- package/dist/src/planner/physical-utils.js.map +0 -1
- package/dist/src/planner/rules/physical/rule-filter-optimization.d.ts +0 -11
- package/dist/src/planner/rules/physical/rule-filter-optimization.d.ts.map +0 -1
- package/dist/src/planner/rules/physical/rule-filter-optimization.js +0 -49
- package/dist/src/planner/rules/physical/rule-filter-optimization.js.map +0 -1
- package/dist/src/planner/rules/physical/rule-mark-physical.d.ts +0 -11
- package/dist/src/planner/rules/physical/rule-mark-physical.d.ts.map +0 -1
- package/dist/src/planner/rules/physical/rule-mark-physical.js +0 -29
- package/dist/src/planner/rules/physical/rule-mark-physical.js.map +0 -1
- package/dist/src/planner/rules/physical/rule-project-optimization.d.ts +0 -11
- package/dist/src/planner/rules/physical/rule-project-optimization.d.ts.map +0 -1
- package/dist/src/planner/rules/physical/rule-project-optimization.js +0 -44
- package/dist/src/planner/rules/physical/rule-project-optimization.js.map +0 -1
- package/dist/src/planner/rules/physical/rule-sort-optimization.d.ts +0 -11
- package/dist/src/planner/rules/physical/rule-sort-optimization.d.ts.map +0 -1
- package/dist/src/planner/rules/physical/rule-sort-optimization.js +0 -53
- package/dist/src/planner/rules/physical/rule-sort-optimization.js.map +0 -1
- package/dist/src/planner/rules/rewrite/rule-constant-folding.d.ts +0 -11
- package/dist/src/planner/rules/rewrite/rule-constant-folding.d.ts.map +0 -1
- package/dist/src/planner/rules/rewrite/rule-constant-folding.js +0 -59
- package/dist/src/planner/rules/rewrite/rule-constant-folding.js.map +0 -1
- package/dist/src/planner/util/deferred-constraint.d.ts +0 -14
- package/dist/src/planner/util/deferred-constraint.d.ts.map +0 -1
- package/dist/src/planner/util/deferred-constraint.js +0 -85
- package/dist/src/planner/util/deferred-constraint.js.map +0 -1
- package/dist/src/runtime/emit/table-reference.d.ts +0 -5
- package/dist/src/runtime/emit/table-reference.d.ts.map +0 -1
- package/dist/src/runtime/emit/table-reference.js +0 -67
- package/dist/src/runtime/emit/table-reference.js.map +0 -1
- package/dist/src/runtime/emit/update-executor.d.ts +0 -5
- package/dist/src/runtime/emit/update-executor.d.ts.map +0 -1
- package/dist/src/runtime/emit/update-executor.js +0 -54
- package/dist/src/runtime/emit/update-executor.js.map +0 -1
- package/dist/src/util/plugin-loader.d.ts +0 -52
- package/dist/src/util/plugin-loader.d.ts.map +0 -1
- package/dist/src/util/plugin-loader.js +0 -307
- package/dist/src/util/plugin-loader.js.map +0 -1
- package/src/config/loader.ts +0 -140
- package/src/util/plugin-loader.ts +0 -387
|
@@ -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 =
|
|
198
|
+
const hash = computeShortSchemaHash(declaredSchema);
|
|
199
199
|
|
|
200
200
|
// Return hash with version if specified
|
|
201
201
|
const result = explainStmt.version
|