@quereus/quereus 0.6.4 → 0.6.6

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 (97) hide show
  1. package/README.md +19 -1
  2. package/dist/src/common/logger.d.ts +59 -0
  3. package/dist/src/common/logger.d.ts.map +1 -1
  4. package/dist/src/common/logger.js +68 -0
  5. package/dist/src/common/logger.js.map +1 -1
  6. package/dist/src/func/builtins/datetime.d.ts.map +1 -1
  7. package/dist/src/func/builtins/datetime.js +10 -5
  8. package/dist/src/func/builtins/datetime.js.map +1 -1
  9. package/dist/src/index.d.ts +2 -1
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +3 -1
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
  14. package/dist/src/planner/building/constraint-builder.js +4 -0
  15. package/dist/src/planner/building/constraint-builder.js.map +1 -1
  16. package/dist/src/planner/building/delete.d.ts.map +1 -1
  17. package/dist/src/planner/building/delete.js +2 -1
  18. package/dist/src/planner/building/delete.js.map +1 -1
  19. package/dist/src/planner/building/insert.d.ts.map +1 -1
  20. package/dist/src/planner/building/insert.js +4 -1
  21. package/dist/src/planner/building/insert.js.map +1 -1
  22. package/dist/src/planner/building/update.d.ts.map +1 -1
  23. package/dist/src/planner/building/update.js +4 -2
  24. package/dist/src/planner/building/update.js.map +1 -1
  25. package/dist/src/planner/nodes/dml-executor-node.d.ts +8 -2
  26. package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
  27. package/dist/src/planner/nodes/dml-executor-node.js +11 -2
  28. package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
  29. package/dist/src/planner/validation/determinism-validator.d.ts +47 -0
  30. package/dist/src/planner/validation/determinism-validator.d.ts.map +1 -0
  31. package/dist/src/planner/validation/determinism-validator.js +63 -0
  32. package/dist/src/planner/validation/determinism-validator.js.map +1 -0
  33. package/dist/src/runtime/async-util.d.ts.map +1 -1
  34. package/dist/src/runtime/async-util.js +4 -3
  35. package/dist/src/runtime/async-util.js.map +1 -1
  36. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  37. package/dist/src/runtime/emit/add-constraint.js +3 -0
  38. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  39. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  40. package/dist/src/runtime/emit/dml-executor.js +84 -8
  41. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  42. package/dist/src/runtime/emitters.d.ts.map +1 -1
  43. package/dist/src/runtime/emitters.js +2 -1
  44. package/dist/src/runtime/emitters.js.map +1 -1
  45. package/dist/src/runtime/types.d.ts +1 -0
  46. package/dist/src/runtime/types.d.ts.map +1 -1
  47. package/dist/src/runtime/types.js.map +1 -1
  48. package/dist/src/runtime/utils.d.ts +14 -0
  49. package/dist/src/runtime/utils.d.ts.map +1 -1
  50. package/dist/src/runtime/utils.js +40 -1
  51. package/dist/src/runtime/utils.js.map +1 -1
  52. package/dist/src/schema/manager.d.ts.map +1 -1
  53. package/dist/src/schema/manager.js +50 -0
  54. package/dist/src/schema/manager.js.map +1 -1
  55. package/dist/src/util/ast-stringify.d.ts.map +1 -1
  56. package/dist/src/util/ast-stringify.js +14 -1
  57. package/dist/src/util/ast-stringify.js.map +1 -1
  58. package/dist/src/util/mutation-statement.d.ts +16 -0
  59. package/dist/src/util/mutation-statement.d.ts.map +1 -0
  60. package/dist/src/util/mutation-statement.js +92 -0
  61. package/dist/src/util/mutation-statement.js.map +1 -0
  62. package/dist/src/util/sql-literal.d.ts +11 -0
  63. package/dist/src/util/sql-literal.d.ts.map +1 -0
  64. package/dist/src/util/sql-literal.js +18 -0
  65. package/dist/src/util/sql-literal.js.map +1 -0
  66. package/dist/src/vtab/memory/table.d.ts +1 -2
  67. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  68. package/dist/src/vtab/memory/table.js +3 -3
  69. package/dist/src/vtab/memory/table.js.map +1 -1
  70. package/dist/src/vtab/table.d.ts +23 -5
  71. package/dist/src/vtab/table.d.ts.map +1 -1
  72. package/dist/src/vtab/table.js +6 -0
  73. package/dist/src/vtab/table.js.map +1 -1
  74. package/package.json +1 -1
  75. package/src/common/logger.ts +75 -1
  76. package/src/func/builtins/datetime.ts +10 -5
  77. package/src/index.ts +4 -1
  78. package/src/planner/building/constraint-builder.ts +178 -173
  79. package/src/planner/building/delete.ts +5 -1
  80. package/src/planner/building/insert.ts +8 -1
  81. package/src/planner/building/update.ts +10 -2
  82. package/src/planner/nodes/dml-executor-node.ts +8 -2
  83. package/src/planner/validation/determinism-validator.ts +104 -0
  84. package/src/runtime/async-util.ts +4 -3
  85. package/src/runtime/emit/add-constraint.ts +3 -0
  86. package/src/runtime/emit/dml-executor.ts +105 -9
  87. package/src/runtime/emitters.ts +2 -1
  88. package/src/runtime/types.ts +3 -0
  89. package/src/runtime/utils.ts +41 -1
  90. package/src/schema/manager.ts +800 -742
  91. package/src/util/ast-stringify.ts +24 -1
  92. package/src/util/hash.ts +90 -90
  93. package/src/util/mutation-statement.ts +129 -0
  94. package/src/util/plugin-helper.ts +110 -110
  95. package/src/util/sql-literal.ts +22 -0
  96. package/src/vtab/memory/table.ts +3 -8
  97. package/src/vtab/table.ts +25 -10
@@ -1,173 +1,178 @@
1
- import type { PlanningContext } from '../planning-context.js';
2
- import type { TableSchema, RowConstraintSchema } from '../../schema/table.js';
3
- import type { RowOpFlag } from '../../schema/table.js';
4
- import type { Attribute, RowDescriptor } from '../nodes/plan-node.js';
5
- import type { ConstraintCheck } from '../nodes/constraint-check-node.js';
6
- import { RegisteredScope } from '../scopes/registered.js';
7
- import { buildExpression } from './expression.js';
8
- import { PlanNodeType } from '../nodes/plan-node-type.js';
9
- import { ColumnReferenceNode } from '../nodes/reference.js';
10
- import type { ScalarPlanNode } from '../nodes/plan-node.js';
11
- import * as AST from '../../parser/ast.js';
12
-
13
- /**
14
- * Determines if a constraint should be checked for the given operation
15
- */
16
- function shouldCheckConstraint(constraint: RowConstraintSchema, operation: RowOpFlag): boolean {
17
- // Check if the current operation is in the constraint's operations bitmask
18
- return (constraint.operations & operation) !== 0;
19
- }
20
-
21
- /**
22
- * Builds constraint check expressions at plan time.
23
- * This allows the optimizer to see and optimize constraint expressions.
24
- */
25
- export function buildConstraintChecks(
26
- ctx: PlanningContext,
27
- tableSchema: TableSchema,
28
- operation: RowOpFlag,
29
- oldAttributes: Attribute[],
30
- newAttributes: Attribute[],
31
- _flatRowDescriptor: RowDescriptor,
32
- contextAttributes: Attribute[] = []
33
- ): ConstraintCheck[] {
34
- // Build attribute ID mappings for column registration
35
- const newAttrIdByCol: Record<string, number> = {};
36
- const oldAttrIdByCol: Record<string, number> = {};
37
-
38
- newAttributes.forEach((attr, columnIndex) => {
39
- if (columnIndex < tableSchema.columns.length) {
40
- const column = tableSchema.columns[columnIndex];
41
- newAttrIdByCol[column.name.toLowerCase()] = attr.id;
42
- }
43
- });
44
-
45
- oldAttributes.forEach((attr, columnIndex) => {
46
- if (columnIndex < tableSchema.columns.length) {
47
- const column = tableSchema.columns[columnIndex];
48
- oldAttrIdByCol[column.name.toLowerCase()] = attr.id;
49
- }
50
- });
51
-
52
- // Filter constraints by operation
53
- const applicableConstraints = tableSchema.checkConstraints
54
- .filter(constraint => shouldCheckConstraint(constraint, operation));
55
-
56
- // Build expression nodes for each constraint
57
- return applicableConstraints.map(constraint => {
58
- // Create scope with OLD/NEW column access for constraint evaluation
59
- const constraintScope = new RegisteredScope(ctx.scope);
60
-
61
- // Register mutation context variables FIRST (so they shadow column names if conflicts exist)
62
- contextAttributes.forEach((attr, contextVarIndex) => {
63
- if (contextVarIndex < (tableSchema.mutationContext?.length || 0)) {
64
- const contextVar = tableSchema.mutationContext![contextVarIndex];
65
- const varNameLower = contextVar.name.toLowerCase();
66
-
67
- // Register both unqualified and qualified names
68
- constraintScope.subscribeFactory(varNameLower, (exp, s) =>
69
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, contextVarIndex)
70
- );
71
- constraintScope.subscribeFactory(`context.${varNameLower}`, (exp, s) =>
72
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, contextVarIndex)
73
- );
74
- }
75
- });
76
-
77
- // Register column symbols (similar to current emitConstraintCheck logic)
78
- tableSchema.columns.forEach((tableColumn, tableColIndex) => {
79
- const colNameLower = tableColumn.name.toLowerCase();
80
-
81
- // Register NEW.col and unqualified col (defaults to NEW for INSERT/UPDATE, OLD for DELETE)
82
- const newAttrId = newAttrIdByCol[colNameLower];
83
- if (newAttrId !== undefined) {
84
- const newColumnType = {
85
- typeClass: 'scalar' as const,
86
- logicalType: tableColumn.logicalType,
87
- nullable: !tableColumn.notNull,
88
- isReadOnly: false
89
- };
90
-
91
- // NEW.column
92
- constraintScope.registerSymbol(`new.${colNameLower}`, (exp, s) =>
93
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, newColumnType, newAttrId, tableColIndex));
94
-
95
- // For INSERT/UPDATE, unqualified column defaults to NEW
96
- if (operation === 1 || operation === 2) { // INSERT or UPDATE
97
- constraintScope.registerSymbol(colNameLower, (exp, s) =>
98
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, newColumnType, newAttrId, tableColIndex));
99
- }
100
- }
101
-
102
- // Register OLD.col
103
- const oldAttrId = oldAttrIdByCol[colNameLower];
104
- if (oldAttrId !== undefined) {
105
- const oldColumnType = {
106
- typeClass: 'scalar' as const,
107
- logicalType: tableColumn.logicalType,
108
- nullable: true, // OLD values can be NULL (especially for INSERT)
109
- isReadOnly: false
110
- };
111
-
112
- // OLD.column
113
- constraintScope.registerSymbol(`old.${colNameLower}`, (exp, s) =>
114
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, oldColumnType, oldAttrId, tableColIndex));
115
-
116
- // For DELETE, unqualified column defaults to OLD
117
- if (operation === 4) { // DELETE
118
- constraintScope.registerSymbol(colNameLower, (exp, s) =>
119
- new ColumnReferenceNode(s, exp as AST.ColumnExpr, oldColumnType, oldAttrId, tableColIndex));
120
- }
121
- }
122
- });
123
-
124
- // Build the constraint expression using the specialized scope
125
- // Temporarily set the current schema to match the table's schema
126
- // This ensures unqualified table references in CHECK constraints resolve correctly
127
- const originalCurrentSchema = ctx.schemaManager.getCurrentSchemaName();
128
- const needsSchemaSwitch = tableSchema.schemaName !== originalCurrentSchema;
129
-
130
- if (needsSchemaSwitch) {
131
- ctx.schemaManager.setCurrentSchema(tableSchema.schemaName);
132
- }
133
-
134
- try {
135
- const expression = buildExpression(
136
- { ...ctx, scope: constraintScope },
137
- constraint.expr
138
- ) as ScalarPlanNode;
139
-
140
- // Heuristic: auto-defer if the expression contains a subquery
141
- // or references a different relation via attribute bindings (NEW/OLD already localized).
142
- const needsDeferred = containsSubquery(expression);
143
-
144
- return {
145
- constraint,
146
- expression,
147
- deferrable: needsDeferred,
148
- initiallyDeferred: needsDeferred,
149
- containsSubquery: needsDeferred
150
- } satisfies ConstraintCheck;
151
- } finally {
152
- // Restore original schema context
153
- if (needsSchemaSwitch) {
154
- ctx.schemaManager.setCurrentSchema(originalCurrentSchema);
155
- }
156
- }
157
- });
158
- }
159
-
160
- function containsSubquery(expr: ScalarPlanNode): boolean {
161
- const stack: ScalarPlanNode[] = [expr];
162
- while (stack.length) {
163
- const n = stack.pop()!;
164
- if (n.nodeType === PlanNodeType.ScalarSubquery || n.nodeType === PlanNodeType.Exists) {
165
- return true;
166
- }
167
- for (const c of n.getChildren()) {
168
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
- stack.push(c as any);
170
- }
171
- }
172
- return false;
173
- }
1
+ import type { PlanningContext } from '../planning-context.js';
2
+ import type { TableSchema, RowConstraintSchema } from '../../schema/table.js';
3
+ import type { RowOpFlag } from '../../schema/table.js';
4
+ import type { Attribute, RowDescriptor } from '../nodes/plan-node.js';
5
+ import type { ConstraintCheck } from '../nodes/constraint-check-node.js';
6
+ import { RegisteredScope } from '../scopes/registered.js';
7
+ import { buildExpression } from './expression.js';
8
+ import { PlanNodeType } from '../nodes/plan-node-type.js';
9
+ import { ColumnReferenceNode } from '../nodes/reference.js';
10
+ import type { ScalarPlanNode } from '../nodes/plan-node.js';
11
+ import * as AST from '../../parser/ast.js';
12
+ import { validateDeterministicConstraint } from '../validation/determinism-validator.js';
13
+
14
+ /**
15
+ * Determines if a constraint should be checked for the given operation
16
+ */
17
+ function shouldCheckConstraint(constraint: RowConstraintSchema, operation: RowOpFlag): boolean {
18
+ // Check if the current operation is in the constraint's operations bitmask
19
+ return (constraint.operations & operation) !== 0;
20
+ }
21
+
22
+ /**
23
+ * Builds constraint check expressions at plan time.
24
+ * This allows the optimizer to see and optimize constraint expressions.
25
+ */
26
+ export function buildConstraintChecks(
27
+ ctx: PlanningContext,
28
+ tableSchema: TableSchema,
29
+ operation: RowOpFlag,
30
+ oldAttributes: Attribute[],
31
+ newAttributes: Attribute[],
32
+ _flatRowDescriptor: RowDescriptor,
33
+ contextAttributes: Attribute[] = []
34
+ ): ConstraintCheck[] {
35
+ // Build attribute ID mappings for column registration
36
+ const newAttrIdByCol: Record<string, number> = {};
37
+ const oldAttrIdByCol: Record<string, number> = {};
38
+
39
+ newAttributes.forEach((attr, columnIndex) => {
40
+ if (columnIndex < tableSchema.columns.length) {
41
+ const column = tableSchema.columns[columnIndex];
42
+ newAttrIdByCol[column.name.toLowerCase()] = attr.id;
43
+ }
44
+ });
45
+
46
+ oldAttributes.forEach((attr, columnIndex) => {
47
+ if (columnIndex < tableSchema.columns.length) {
48
+ const column = tableSchema.columns[columnIndex];
49
+ oldAttrIdByCol[column.name.toLowerCase()] = attr.id;
50
+ }
51
+ });
52
+
53
+ // Filter constraints by operation
54
+ const applicableConstraints = tableSchema.checkConstraints
55
+ .filter(constraint => shouldCheckConstraint(constraint, operation));
56
+
57
+ // Build expression nodes for each constraint
58
+ return applicableConstraints.map(constraint => {
59
+ // Create scope with OLD/NEW column access for constraint evaluation
60
+ const constraintScope = new RegisteredScope(ctx.scope);
61
+
62
+ // Register mutation context variables FIRST (so they shadow column names if conflicts exist)
63
+ contextAttributes.forEach((attr, contextVarIndex) => {
64
+ if (contextVarIndex < (tableSchema.mutationContext?.length || 0)) {
65
+ const contextVar = tableSchema.mutationContext![contextVarIndex];
66
+ const varNameLower = contextVar.name.toLowerCase();
67
+
68
+ // Register both unqualified and qualified names
69
+ constraintScope.subscribeFactory(varNameLower, (exp, s) =>
70
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, contextVarIndex)
71
+ );
72
+ constraintScope.subscribeFactory(`context.${varNameLower}`, (exp, s) =>
73
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, attr.type, attr.id, contextVarIndex)
74
+ );
75
+ }
76
+ });
77
+
78
+ // Register column symbols (similar to current emitConstraintCheck logic)
79
+ tableSchema.columns.forEach((tableColumn, tableColIndex) => {
80
+ const colNameLower = tableColumn.name.toLowerCase();
81
+
82
+ // Register NEW.col and unqualified col (defaults to NEW for INSERT/UPDATE, OLD for DELETE)
83
+ const newAttrId = newAttrIdByCol[colNameLower];
84
+ if (newAttrId !== undefined) {
85
+ const newColumnType = {
86
+ typeClass: 'scalar' as const,
87
+ logicalType: tableColumn.logicalType,
88
+ nullable: !tableColumn.notNull,
89
+ isReadOnly: false
90
+ };
91
+
92
+ // NEW.column
93
+ constraintScope.registerSymbol(`new.${colNameLower}`, (exp, s) =>
94
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, newColumnType, newAttrId, tableColIndex));
95
+
96
+ // For INSERT/UPDATE, unqualified column defaults to NEW
97
+ if (operation === 1 || operation === 2) { // INSERT or UPDATE
98
+ constraintScope.registerSymbol(colNameLower, (exp, s) =>
99
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, newColumnType, newAttrId, tableColIndex));
100
+ }
101
+ }
102
+
103
+ // Register OLD.col
104
+ const oldAttrId = oldAttrIdByCol[colNameLower];
105
+ if (oldAttrId !== undefined) {
106
+ const oldColumnType = {
107
+ typeClass: 'scalar' as const,
108
+ logicalType: tableColumn.logicalType,
109
+ nullable: true, // OLD values can be NULL (especially for INSERT)
110
+ isReadOnly: false
111
+ };
112
+
113
+ // OLD.column
114
+ constraintScope.registerSymbol(`old.${colNameLower}`, (exp, s) =>
115
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, oldColumnType, oldAttrId, tableColIndex));
116
+
117
+ // For DELETE, unqualified column defaults to OLD
118
+ if (operation === 4) { // DELETE
119
+ constraintScope.registerSymbol(colNameLower, (exp, s) =>
120
+ new ColumnReferenceNode(s, exp as AST.ColumnExpr, oldColumnType, oldAttrId, tableColIndex));
121
+ }
122
+ }
123
+ });
124
+
125
+ // Build the constraint expression using the specialized scope
126
+ // Temporarily set the current schema to match the table's schema
127
+ // This ensures unqualified table references in CHECK constraints resolve correctly
128
+ const originalCurrentSchema = ctx.schemaManager.getCurrentSchemaName();
129
+ const needsSchemaSwitch = tableSchema.schemaName !== originalCurrentSchema;
130
+
131
+ if (needsSchemaSwitch) {
132
+ ctx.schemaManager.setCurrentSchema(tableSchema.schemaName);
133
+ }
134
+
135
+ try {
136
+ const expression = buildExpression(
137
+ { ...ctx, scope: constraintScope },
138
+ constraint.expr
139
+ ) as ScalarPlanNode;
140
+
141
+ // Validate that the constraint expression is deterministic
142
+ const constraintName = constraint.name ?? `_check_${tableSchema.name}`;
143
+ validateDeterministicConstraint(expression, constraintName, tableSchema.name);
144
+
145
+ // Heuristic: auto-defer if the expression contains a subquery
146
+ // or references a different relation via attribute bindings (NEW/OLD already localized).
147
+ const needsDeferred = containsSubquery(expression);
148
+
149
+ return {
150
+ constraint,
151
+ expression,
152
+ deferrable: needsDeferred,
153
+ initiallyDeferred: needsDeferred,
154
+ containsSubquery: needsDeferred
155
+ } satisfies ConstraintCheck;
156
+ } finally {
157
+ // Restore original schema context
158
+ if (needsSchemaSwitch) {
159
+ ctx.schemaManager.setCurrentSchema(originalCurrentSchema);
160
+ }
161
+ }
162
+ });
163
+ }
164
+
165
+ function containsSubquery(expr: ScalarPlanNode): boolean {
166
+ const stack: ScalarPlanNode[] = [expr];
167
+ while (stack.length) {
168
+ const n = stack.pop()!;
169
+ if (n.nodeType === PlanNodeType.ScalarSubquery || n.nodeType === PlanNodeType.Exists) {
170
+ return true;
171
+ }
172
+ for (const c of n.getChildren()) {
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ stack.push(c as any);
175
+ }
176
+ }
177
+ return false;
178
+ }
@@ -148,7 +148,11 @@ export function buildDeleteStmt(
148
148
  deleteCtx.scope,
149
149
  deleteNode,
150
150
  tableReference,
151
- 'delete'
151
+ 'delete',
152
+ undefined, // onConflict not used for DELETE
153
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
154
+ contextAttributes.length > 0 ? contextAttributes : undefined,
155
+ contextDescriptor
152
156
  );
153
157
 
154
158
  const resultNode: RelationalPlanNode = dmlExecutorNode;
@@ -22,6 +22,7 @@ import { ProjectNode, type Projection } from '../nodes/project-node.js';
22
22
  import { buildOldNewRowDescriptors } from '../../util/row-descriptor.js';
23
23
  import { DmlExecutorNode } from '../nodes/dml-executor-node.js';
24
24
  import { buildConstraintChecks } from './constraint-builder.js';
25
+ import { validateDeterministicDefault } from '../validation/determinism-validator.js';
25
26
 
26
27
  /**
27
28
  * Creates a uniform row expansion projection that maps any relational source
@@ -103,6 +104,9 @@ function createRowExpansionProjection(
103
104
  if (typeof tableColumn.defaultValue === 'object' && tableColumn.defaultValue !== null && 'type' in tableColumn.defaultValue) {
104
105
  // It's an AST.Expression - build it into a plan node with context scope
105
106
  defaultNode = buildExpression(defaultCtx, tableColumn.defaultValue as AST.Expression) as ScalarPlanNode;
107
+
108
+ // Validate that the default expression is deterministic
109
+ validateDeterministicDefault(defaultNode, tableColumn.name, tableSchema.name);
106
110
  } else {
107
111
  // Literal default value
108
112
  defaultNode = buildExpression(defaultCtx, { type: 'literal', value: tableColumn.defaultValue }) as ScalarPlanNode;
@@ -358,7 +362,10 @@ export function buildInsertStmt(
358
362
  constraintCheckNode,
359
363
  tableReference,
360
364
  'insert',
361
- stmt.onConflict
365
+ stmt.onConflict,
366
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
367
+ contextAttributes.length > 0 ? contextAttributes : undefined,
368
+ contextDescriptor
362
369
  );
363
370
 
364
371
  const resultNode: RelationalPlanNode = dmlExecutorNode;
@@ -270,7 +270,11 @@ export function buildUpdateStmt(
270
270
  updateCtx.scope,
271
271
  constraintCheckNode,
272
272
  tableReference,
273
- 'update'
273
+ 'update',
274
+ undefined, // onConflict not used for UPDATE
275
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
276
+ contextAttributes.length > 0 ? contextAttributes : undefined,
277
+ contextDescriptor
274
278
  );
275
279
 
276
280
  // Return the RETURNING results from the executed update
@@ -312,7 +316,11 @@ export function buildUpdateStmt(
312
316
  updateCtx.scope,
313
317
  constraintCheckNode,
314
318
  tableReference,
315
- 'update'
319
+ 'update',
320
+ undefined, // onConflict not used for UPDATE
321
+ mutationContextValues.size > 0 ? mutationContextValues : undefined,
322
+ contextAttributes.length > 0 ? contextAttributes : undefined,
323
+ contextDescriptor
316
324
  );
317
325
 
318
326
  return new SinkNode(updateCtx.scope, updateExecutorNode, 'update');
@@ -1,5 +1,5 @@
1
1
  import type { Scope } from '../scopes/scope.js';
2
- import { PlanNode, type RelationalPlanNode, type Attribute, type PhysicalProperties, isRelationalNode } from './plan-node.js';
2
+ import { PlanNode, type RelationalPlanNode, type Attribute, type PhysicalProperties, type ScalarPlanNode, type RowDescriptor, isRelationalNode } from './plan-node.js';
3
3
  import { PlanNodeType } from './plan-node-type.js';
4
4
  import type { TableReferenceNode } from './reference.js';
5
5
  import type { RelationType } from '../../common/datatype.js';
@@ -20,6 +20,9 @@ export class DmlExecutorNode extends PlanNode implements RelationalPlanNode {
20
20
  public readonly table: TableReferenceNode,
21
21
  public readonly operation: RowOp,
22
22
  public readonly onConflict?: ConflictResolution, // Used for INSERT operations
23
+ public readonly mutationContextValues?: Map<string, ScalarPlanNode>, // Mutation context value expressions
24
+ public readonly contextAttributes?: Attribute[], // Mutation context attributes
25
+ public readonly contextDescriptor?: RowDescriptor, // Mutation context row descriptor
23
26
  ) {
24
27
  super(scope);
25
28
  }
@@ -63,7 +66,10 @@ export class DmlExecutorNode extends PlanNode implements RelationalPlanNode {
63
66
  newSource as RelationalPlanNode,
64
67
  this.table,
65
68
  this.operation,
66
- this.onConflict
69
+ this.onConflict,
70
+ this.mutationContextValues,
71
+ this.contextAttributes,
72
+ this.contextDescriptor
67
73
  );
68
74
  }
69
75
 
@@ -0,0 +1,104 @@
1
+ import { QuereusError } from '../../common/errors.js';
2
+ import { StatusCode } from '../../common/types.js';
3
+ import type { ScalarPlanNode } from '../nodes/plan-node.js';
4
+ import { createLogger } from '../../common/logger.js';
5
+
6
+ const log = createLogger('planner:validation:determinism');
7
+
8
+ /**
9
+ * Result of determinism validation. If valid, `error` is undefined.
10
+ * If invalid, contains the information needed to construct an error message.
11
+ */
12
+ export interface DeterminismValidationResult {
13
+ /** True if the expression is deterministic */
14
+ valid: boolean;
15
+ /** String representation of the offending expression (if invalid) */
16
+ expression?: string;
17
+ }
18
+
19
+ /**
20
+ * Checks if an expression is deterministic (suitable for constraints and defaults).
21
+ * Returns a result object instead of throwing, allowing the caller to decide how to handle.
22
+ *
23
+ * @param expr The expression plan node to check
24
+ * @returns Validation result indicating if deterministic
25
+ */
26
+ export function checkDeterministic(expr: ScalarPlanNode): DeterminismValidationResult {
27
+ const physical = expr.physical;
28
+
29
+ if (physical.deterministic === false) {
30
+ log('Non-deterministic expression detected: %s', expr.toString());
31
+ return {
32
+ valid: false,
33
+ expression: expr.toString()
34
+ };
35
+ }
36
+
37
+ return { valid: true };
38
+ }
39
+
40
+ /**
41
+ * Validates that an expression is deterministic (suitable for constraints and defaults).
42
+ * Non-deterministic expressions must be passed via mutation context instead.
43
+ *
44
+ * @param expr The expression plan node to validate
45
+ * @param context Description of where the expression is used (e.g., "DEFAULT for column 'created_at'")
46
+ * @throws QuereusError if the expression is non-deterministic
47
+ */
48
+ export function validateDeterministicExpression(
49
+ expr: ScalarPlanNode,
50
+ context: string
51
+ ): void {
52
+ log('Validating determinism for: %s', context);
53
+
54
+ const result = checkDeterministic(expr);
55
+
56
+ if (!result.valid) {
57
+ throw new QuereusError(
58
+ `Non-deterministic expression not allowed in ${context}. ` +
59
+ `Expression: ${result.expression}. ` +
60
+ `Use mutation context to pass non-deterministic values (e.g., WITH CONTEXT (timestamp = datetime('now'))).`,
61
+ StatusCode.ERROR
62
+ );
63
+ }
64
+
65
+ log('Expression is deterministic: %s', expr.toString());
66
+ }
67
+
68
+ /**
69
+ * Validates that a CHECK constraint expression is deterministic.
70
+ *
71
+ * @param expr The constraint expression plan node
72
+ * @param constraintName The name of the constraint (for error messages)
73
+ * @param tableName The name of the table (for error messages)
74
+ * @throws QuereusError if the expression is non-deterministic
75
+ */
76
+ export function validateDeterministicConstraint(
77
+ expr: ScalarPlanNode,
78
+ constraintName: string,
79
+ tableName: string
80
+ ): void {
81
+ validateDeterministicExpression(
82
+ expr,
83
+ `CHECK constraint '${constraintName}' on table '${tableName}'`
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Validates that a DEFAULT expression is deterministic.
89
+ *
90
+ * @param expr The default value expression plan node
91
+ * @param columnName The name of the column (for error messages)
92
+ * @param tableName The name of the table (for error messages)
93
+ * @throws QuereusError if the expression is non-deterministic
94
+ */
95
+ export function validateDeterministicDefault(
96
+ expr: ScalarPlanNode,
97
+ columnName: string,
98
+ tableName: string
99
+ ): void {
100
+ validateDeterministicExpression(
101
+ expr,
102
+ `DEFAULT for column '${columnName}' in table '${tableName}'`
103
+ );
104
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { MaybePromise, Row } from '../common/types.js';
7
7
  import { createLogger } from '../common/logger.js';
8
+ import { getAsyncIterator } from './utils.js';
8
9
 
9
10
  const log = createLogger('runtime:async-util');
10
11
 
@@ -52,7 +53,7 @@ export function tee<T>(src: AsyncIterable<T>): [AsyncIterable<T>, AsyncIterable<
52
53
  }
53
54
 
54
55
  if (!srcIterator) {
55
- srcIterator = src[Symbol.asyncIterator]();
56
+ srcIterator = getAsyncIterator(src);
56
57
  }
57
58
 
58
59
  while (buffer.length <= targetIndex && !srcDone) {
@@ -124,7 +125,7 @@ export async function* buffered<T>(
124
125
  maxBuffer: number
125
126
  ): AsyncIterable<T> {
126
127
  const buffer: T[] = [];
127
- const srcIterator = src[Symbol.asyncIterator]();
128
+ const srcIterator = getAsyncIterator(src);
128
129
  let srcDone = false;
129
130
  let fillPromise: Promise<void> | null = null;
130
131
 
@@ -217,7 +218,7 @@ export async function collect<T>(src: AsyncIterable<T>): Promise<T[]> {
217
218
  * Items are yielded as soon as they become available from any source
218
219
  */
219
220
  export async function* merge<T>(...sources: AsyncIterable<T>[]): AsyncIterable<T> {
220
- const iterators = sources.map(src => src[Symbol.asyncIterator]());
221
+ const iterators = sources.map(src => getAsyncIterator(src));
221
222
  const pending = new Map<number, Promise<IteratorResult<T>>>();
222
223
 
223
224
  // Start initial reads
@@ -31,6 +31,9 @@ export function emitAddConstraint(plan: AddConstraintNode, _ctx: EmissionContext
31
31
  }
32
32
 
33
33
  // Create the constraint schema object
34
+ // Note: We don't validate determinism here because constraints may reference NEW/OLD
35
+ // which require special scoping. Determinism is validated at INSERT/UPDATE plan time
36
+ // in constraint-builder.ts when the constraint is actually checked.
34
37
  const constraintSchema: RowConstraintSchema = {
35
38
  name: constraint.name || `check_${tableSchema.checkConstraints.length}`,
36
39
  expr: constraint.expr,