@quereus/quereus 0.6.5 → 0.6.7
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/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/planner/validation/determinism-validator.d.ts +18 -0
- package/dist/src/planner/validation/determinism-validator.d.ts.map +1 -1
- package/dist/src/planner/validation/determinism-validator.js +21 -5
- package/dist/src/planner/validation/determinism-validator.js.map +1 -1
- package/dist/src/runtime/async-util.d.ts.map +1 -1
- package/dist/src/runtime/async-util.js +4 -3
- package/dist/src/runtime/async-util.js.map +1 -1
- package/dist/src/runtime/emitters.d.ts.map +1 -1
- package/dist/src/runtime/emitters.js +2 -1
- package/dist/src/runtime/emitters.js.map +1 -1
- package/dist/src/runtime/utils.d.ts +14 -0
- package/dist/src/runtime/utils.d.ts.map +1 -1
- package/dist/src/runtime/utils.js +40 -1
- package/dist/src/runtime/utils.js.map +1 -1
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +13 -4
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/util/mutation-statement.js +5 -0
- package/dist/src/util/mutation-statement.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/planner/building/constraint-builder.ts +178 -178
- package/src/planner/validation/determinism-validator.ts +104 -76
- package/src/runtime/async-util.ts +4 -3
- package/src/runtime/emitters.ts +2 -1
- package/src/runtime/utils.ts +41 -1
- package/src/schema/manager.ts +800 -787
- package/src/util/mutation-statement.ts +6 -0
|
@@ -1,178 +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
|
-
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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,76 +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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
): void {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/runtime/emitters.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { StatusCode, type OutputValue } from '../common/types.js';
|
|
|
7
7
|
import { createLogger } from '../common/logger.js';
|
|
8
8
|
import { Scheduler } from "./scheduler.js";
|
|
9
9
|
import type { EmissionContext } from "./emission-context.js";
|
|
10
|
+
import { isAsyncIterable } from "./utils.js";
|
|
10
11
|
|
|
11
12
|
const log = createLogger('emitters');
|
|
12
13
|
|
|
@@ -81,7 +82,7 @@ function instrumentRunForTracing(plan: PlanNode, originalRun: InstructionRun): I
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
// If the result is an async iterable, defer the pop until iteration completes
|
|
84
|
-
if (result
|
|
85
|
+
if (isAsyncIterable(result)) {
|
|
85
86
|
const iterable = result as AsyncIterable<unknown>;
|
|
86
87
|
// Wrap iterable to pop stack in a finally block once iteration ends
|
|
87
88
|
return (async function* () {
|
package/src/runtime/utils.ts
CHANGED
|
@@ -14,8 +14,48 @@ const log = createLogger('runtime:utils');
|
|
|
14
14
|
const errorLog = log.extend('error');
|
|
15
15
|
export const ctxLog = createLogger('runtime:context');
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Check if a value is an AsyncIterable.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: Hermes (React Native's JS engine) has a bug where AsyncGenerator objects
|
|
21
|
+
* don't have Symbol.asyncIterator as an own or inherited property, even though
|
|
22
|
+
* they are valid async iterables. We work around this by also checking for
|
|
23
|
+
* the presence of a .next() method (duck typing for async iterators).
|
|
24
|
+
*/
|
|
17
25
|
export function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {
|
|
18
|
-
|
|
26
|
+
if (typeof value !== 'object' || value === null) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Standard check: Symbol.asyncIterator
|
|
30
|
+
if (Symbol.asyncIterator in value) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Hermes workaround: AsyncGenerator has .next() but not Symbol.asyncIterator
|
|
34
|
+
const maybeAsyncGen = value as { next?: unknown; constructor?: { name?: string } };
|
|
35
|
+
if (typeof maybeAsyncGen.next === 'function' &&
|
|
36
|
+
maybeAsyncGen.constructor?.name === 'AsyncGenerator') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get an AsyncIterator from an AsyncIterable, handling Hermes's missing Symbol.asyncIterator.
|
|
44
|
+
*
|
|
45
|
+
* @throws TypeError if value is not a valid async iterable
|
|
46
|
+
*/
|
|
47
|
+
export function getAsyncIterator<T>(value: AsyncIterable<T>): AsyncIterator<T> {
|
|
48
|
+
// Standard path: use Symbol.asyncIterator
|
|
49
|
+
if (Symbol.asyncIterator in value) {
|
|
50
|
+
return value[Symbol.asyncIterator]();
|
|
51
|
+
}
|
|
52
|
+
// Hermes workaround: AsyncGenerator is its own iterator
|
|
53
|
+
const maybeAsyncGen = value as unknown as { next?: () => Promise<IteratorResult<T>>; constructor?: { name?: string } };
|
|
54
|
+
if (typeof maybeAsyncGen.next === 'function' &&
|
|
55
|
+
maybeAsyncGen.constructor?.name === 'AsyncGenerator') {
|
|
56
|
+
return maybeAsyncGen as AsyncIterator<T>;
|
|
57
|
+
}
|
|
58
|
+
throw new TypeError('Value is not async iterable');
|
|
19
59
|
}
|
|
20
60
|
|
|
21
61
|
export async function asyncIterableToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|