@quereus/quereus 0.6.4 → 0.6.5
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 +19 -1
- package/dist/src/common/logger.d.ts +59 -0
- package/dist/src/common/logger.d.ts.map +1 -1
- package/dist/src/common/logger.js +68 -0
- package/dist/src/common/logger.js.map +1 -1
- package/dist/src/func/builtins/datetime.d.ts.map +1 -1
- package/dist/src/func/builtins/datetime.js +10 -5
- package/dist/src/func/builtins/datetime.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
- package/dist/src/planner/building/constraint-builder.js +4 -0
- package/dist/src/planner/building/constraint-builder.js.map +1 -1
- package/dist/src/planner/building/delete.d.ts.map +1 -1
- package/dist/src/planner/building/delete.js +2 -1
- package/dist/src/planner/building/delete.js.map +1 -1
- package/dist/src/planner/building/insert.d.ts.map +1 -1
- package/dist/src/planner/building/insert.js +4 -1
- package/dist/src/planner/building/insert.js.map +1 -1
- package/dist/src/planner/building/update.d.ts.map +1 -1
- package/dist/src/planner/building/update.js +4 -2
- package/dist/src/planner/building/update.js.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts +8 -2
- package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.js +11 -2
- package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
- package/dist/src/planner/validation/determinism-validator.d.ts +29 -0
- package/dist/src/planner/validation/determinism-validator.d.ts.map +1 -0
- package/dist/src/planner/validation/determinism-validator.js +47 -0
- package/dist/src/planner/validation/determinism-validator.js.map +1 -0
- package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +3 -0
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +84 -8
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/types.d.ts +1 -0
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +41 -0
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/util/ast-stringify.d.ts.map +1 -1
- package/dist/src/util/ast-stringify.js +14 -1
- package/dist/src/util/ast-stringify.js.map +1 -1
- package/dist/src/util/mutation-statement.d.ts +16 -0
- package/dist/src/util/mutation-statement.d.ts.map +1 -0
- package/dist/src/util/mutation-statement.js +92 -0
- package/dist/src/util/mutation-statement.js.map +1 -0
- package/dist/src/util/sql-literal.d.ts +11 -0
- package/dist/src/util/sql-literal.d.ts.map +1 -0
- package/dist/src/util/sql-literal.js +18 -0
- package/dist/src/util/sql-literal.js.map +1 -0
- package/dist/src/vtab/memory/table.d.ts +1 -2
- package/dist/src/vtab/memory/table.d.ts.map +1 -1
- package/dist/src/vtab/memory/table.js +3 -3
- package/dist/src/vtab/memory/table.js.map +1 -1
- package/dist/src/vtab/table.d.ts +23 -5
- package/dist/src/vtab/table.d.ts.map +1 -1
- package/dist/src/vtab/table.js +6 -0
- package/dist/src/vtab/table.js.map +1 -1
- package/package.json +1 -1
- package/src/common/logger.ts +75 -1
- package/src/func/builtins/datetime.ts +10 -5
- package/src/index.ts +3 -0
- package/src/planner/building/constraint-builder.ts +5 -0
- package/src/planner/building/delete.ts +5 -1
- package/src/planner/building/insert.ts +8 -1
- package/src/planner/building/update.ts +10 -2
- package/src/planner/nodes/dml-executor-node.ts +8 -2
- package/src/planner/validation/determinism-validator.ts +76 -0
- package/src/runtime/emit/add-constraint.ts +3 -0
- package/src/runtime/emit/dml-executor.ts +105 -9
- package/src/runtime/types.ts +3 -0
- package/src/schema/manager.ts +45 -0
- package/src/util/ast-stringify.ts +24 -1
- package/src/util/hash.ts +90 -90
- package/src/util/mutation-statement.ts +129 -0
- package/src/util/plugin-helper.ts +110 -110
- package/src/util/sql-literal.ts +22 -0
- package/src/vtab/memory/table.ts +3 -8
- package/src/vtab/table.ts +25 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.js","sourceRoot":"","sources":["../../../src/vtab/table.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../../../src/vtab/table.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,MAAM,OAAgB,YAAY;IACjB,MAAM,CAAwB;IAC9B,EAAE,CAAW;IACb,SAAS,CAAS;IAClB,UAAU,CAAS;IAC5B,YAAY,CAAU;IACtB,WAAW,CAAe;IAEjC;;;;OAIG;IACI,cAAc,CAAW;IAEhC,YAAY,EAAY,EAAE,MAA6B,EAAE,UAAkB,EAAE,SAAiB;QAC7F,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC5B,CAAC;IAED;;;OAGG;IACO,eAAe,CAAC,OAA2B;QACpD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC;IAC7B,CAAC;CAqHD"}
|
package/package.json
CHANGED
package/src/common/logger.ts
CHANGED
|
@@ -19,5 +19,79 @@ const BASE_NAMESPACE = 'quereus';
|
|
|
19
19
|
* @returns A debug instance.
|
|
20
20
|
*/
|
|
21
21
|
export function createLogger(subNamespace: string): debug.Debugger {
|
|
22
|
-
|
|
22
|
+
return debug(`${BASE_NAMESPACE}:${subNamespace}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enable Quereus debug logging programmatically.
|
|
27
|
+
*
|
|
28
|
+
* This is particularly useful in environments like React Native where
|
|
29
|
+
* environment variables are not available at runtime.
|
|
30
|
+
*
|
|
31
|
+
* @param pattern - Debug pattern to enable (default: 'quereus:*')
|
|
32
|
+
* Examples:
|
|
33
|
+
* - 'quereus:*' - all Quereus logs
|
|
34
|
+
* - 'quereus:vtab:*' - virtual table operations
|
|
35
|
+
* - 'quereus:vtab:memory' - memory table operations only
|
|
36
|
+
* - 'quereus:planner' - query planner logs
|
|
37
|
+
* - 'quereus:runtime' - VDBE execution (very verbose)
|
|
38
|
+
* - 'quereus:*,-quereus:runtime' - all except verbose runtime
|
|
39
|
+
* @param logFn - Optional custom log function. Defaults to console.log.
|
|
40
|
+
* Useful in environments where console.debug may be filtered.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { enableLogging } from '@quereus/quereus';
|
|
45
|
+
*
|
|
46
|
+
* // Enable all Quereus logs
|
|
47
|
+
* enableLogging();
|
|
48
|
+
*
|
|
49
|
+
* // Enable only memory table logs
|
|
50
|
+
* enableLogging('quereus:vtab:memory');
|
|
51
|
+
*
|
|
52
|
+
* // Enable all logs with custom output (e.g., React Native)
|
|
53
|
+
* enableLogging('quereus:*', console.log.bind(console));
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function enableLogging(
|
|
57
|
+
pattern: string = `${BASE_NAMESPACE}:*`,
|
|
58
|
+
logFn?: (...args: unknown[]) => void
|
|
59
|
+
): void {
|
|
60
|
+
if (logFn) {
|
|
61
|
+
debug.log = logFn;
|
|
62
|
+
}
|
|
63
|
+
debug.enable(pattern);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Disable all Quereus debug logging.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* import { disableLogging } from '@quereus/quereus';
|
|
72
|
+
*
|
|
73
|
+
* disableLogging();
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function disableLogging(): void {
|
|
77
|
+
debug.disable();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if logging is enabled for a specific namespace.
|
|
82
|
+
*
|
|
83
|
+
* @param namespace - The namespace to check (without 'quereus:' prefix)
|
|
84
|
+
* @returns true if logging is enabled for this namespace
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { isLoggingEnabled } from '@quereus/quereus';
|
|
89
|
+
*
|
|
90
|
+
* if (isLoggingEnabled('vtab:memory')) {
|
|
91
|
+
* // Perform expensive debug operations
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function isLoggingEnabled(namespace: string): boolean {
|
|
96
|
+
return debug.enabled(`${BASE_NAMESPACE}:${namespace}`);
|
|
23
97
|
}
|
|
@@ -309,8 +309,9 @@ function processDateTimeArgs(args: ReadonlyArray<SqlValue>): Temporal.ZonedDateT
|
|
|
309
309
|
// --- Function Implementations --- //
|
|
310
310
|
|
|
311
311
|
// date(timestring, modifier, ...)
|
|
312
|
+
// NOTE: Marked as non-deterministic because it accepts 'now' as a timestring
|
|
312
313
|
export const dateFunc = createScalarFunction(
|
|
313
|
-
{ name: 'date', numArgs: -1, deterministic:
|
|
314
|
+
{ name: 'date', numArgs: -1, deterministic: false },
|
|
314
315
|
(...args: SqlValue[]): SqlValue => {
|
|
315
316
|
const finalDt = processDateTimeArgs(args);
|
|
316
317
|
if (!finalDt) return null;
|
|
@@ -319,8 +320,9 @@ export const dateFunc = createScalarFunction(
|
|
|
319
320
|
);
|
|
320
321
|
|
|
321
322
|
// time(timestring, modifier, ...)
|
|
323
|
+
// NOTE: Marked as non-deterministic because it accepts 'now' as a timestring
|
|
322
324
|
export const timeFunc = createScalarFunction(
|
|
323
|
-
{ name: 'time', numArgs: -1, deterministic:
|
|
325
|
+
{ name: 'time', numArgs: -1, deterministic: false },
|
|
324
326
|
(...args: SqlValue[]): SqlValue => {
|
|
325
327
|
const finalDt = processDateTimeArgs(args);
|
|
326
328
|
if (!finalDt) return null;
|
|
@@ -329,8 +331,9 @@ export const timeFunc = createScalarFunction(
|
|
|
329
331
|
);
|
|
330
332
|
|
|
331
333
|
// datetime(timestring, modifier, ...)
|
|
334
|
+
// NOTE: Marked as non-deterministic because it accepts 'now' as a timestring
|
|
332
335
|
export const datetimeFunc = createScalarFunction(
|
|
333
|
-
{ name: 'datetime', numArgs: -1, deterministic:
|
|
336
|
+
{ name: 'datetime', numArgs: -1, deterministic: false },
|
|
334
337
|
(...args: SqlValue[]): SqlValue => {
|
|
335
338
|
const finalDt = processDateTimeArgs(args);
|
|
336
339
|
if (!finalDt) return null;
|
|
@@ -341,8 +344,9 @@ export const datetimeFunc = createScalarFunction(
|
|
|
341
344
|
);
|
|
342
345
|
|
|
343
346
|
// julianday(timestring, modifier, ...)
|
|
347
|
+
// NOTE: Marked as non-deterministic because it accepts 'now' as a timestring
|
|
344
348
|
export const juliandayFunc = createScalarFunction(
|
|
345
|
-
{ name: 'julianday', numArgs: -1, deterministic:
|
|
349
|
+
{ name: 'julianday', numArgs: -1, deterministic: false },
|
|
346
350
|
(...args: SqlValue[]): SqlValue => {
|
|
347
351
|
const finalDt = processDateTimeArgs(args);
|
|
348
352
|
if (!finalDt) return null;
|
|
@@ -352,8 +356,9 @@ export const juliandayFunc = createScalarFunction(
|
|
|
352
356
|
);
|
|
353
357
|
|
|
354
358
|
// strftime(format, timestring, modifier, ...)
|
|
359
|
+
// NOTE: Marked as non-deterministic because it accepts 'now' as a timestring
|
|
355
360
|
export const strftimeFunc = createScalarFunction(
|
|
356
|
-
{ name: 'strftime', numArgs: -1, deterministic:
|
|
361
|
+
{ name: 'strftime', numArgs: -1, deterministic: false },
|
|
357
362
|
(format: SqlValue, ...timeArgs: SqlValue[]): SqlValue => {
|
|
358
363
|
if (typeof format !== 'string') return null;
|
|
359
364
|
const finalDt = processDateTimeArgs(timeArgs);
|
package/src/index.ts
CHANGED
|
@@ -138,3 +138,6 @@ export type {
|
|
|
138
138
|
// Debug and development utilities
|
|
139
139
|
export { serializePlanTree, formatPlanTree, formatPlanSummary, serializePlanTreeWithOptions } from './planner/debug.js';
|
|
140
140
|
export type { PlanDisplayOptions } from './planner/debug.js';
|
|
141
|
+
|
|
142
|
+
// Logging control (for environments like React Native where env vars aren't available)
|
|
143
|
+
export { enableLogging, disableLogging, isLoggingEnabled } from './common/logger.js';
|
|
@@ -9,6 +9,7 @@ import { PlanNodeType } from '../nodes/plan-node-type.js';
|
|
|
9
9
|
import { ColumnReferenceNode } from '../nodes/reference.js';
|
|
10
10
|
import type { ScalarPlanNode } from '../nodes/plan-node.js';
|
|
11
11
|
import * as AST from '../../parser/ast.js';
|
|
12
|
+
import { validateDeterministicConstraint } from '../validation/determinism-validator.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Determines if a constraint should be checked for the given operation
|
|
@@ -137,6 +138,10 @@ export function buildConstraintChecks(
|
|
|
137
138
|
constraint.expr
|
|
138
139
|
) as ScalarPlanNode;
|
|
139
140
|
|
|
141
|
+
// Validate that the constraint expression is deterministic
|
|
142
|
+
const constraintName = constraint.name ?? `_check_${tableSchema.name}`;
|
|
143
|
+
validateDeterministicConstraint(expression, constraintName, tableSchema.name);
|
|
144
|
+
|
|
140
145
|
// Heuristic: auto-defer if the expression contains a subquery
|
|
141
146
|
// or references a different relation via attribute bindings (NEW/OLD already localized).
|
|
142
147
|
const needsDeferred = containsSubquery(expression);
|
|
@@ -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,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
|
+
* Validates that an expression is deterministic (suitable for constraints and defaults).
|
|
10
|
+
* Non-deterministic expressions must be passed via mutation context instead.
|
|
11
|
+
*
|
|
12
|
+
* @param expr The expression plan node to validate
|
|
13
|
+
* @param context Description of where the expression is used (e.g., "DEFAULT for column 'created_at'")
|
|
14
|
+
* @throws QuereusError if the expression is non-deterministic
|
|
15
|
+
*/
|
|
16
|
+
export function validateDeterministicExpression(
|
|
17
|
+
expr: ScalarPlanNode,
|
|
18
|
+
context: string
|
|
19
|
+
): void {
|
|
20
|
+
log('Validating determinism for: %s', context);
|
|
21
|
+
|
|
22
|
+
// Check physical properties - this will recursively check all child nodes
|
|
23
|
+
const physical = expr.physical;
|
|
24
|
+
|
|
25
|
+
if (physical.deterministic === false) {
|
|
26
|
+
log('Non-deterministic expression detected in %s: %s', context, expr.toString());
|
|
27
|
+
|
|
28
|
+
throw new QuereusError(
|
|
29
|
+
`Non-deterministic expression not allowed in ${context}. ` +
|
|
30
|
+
`Expression: ${expr.toString()}. ` +
|
|
31
|
+
`Use mutation context to pass non-deterministic values (e.g., WITH CONTEXT (timestamp = datetime('now'))).`,
|
|
32
|
+
StatusCode.ERROR
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
log('Expression is deterministic: %s', expr.toString());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validates that a CHECK constraint expression is deterministic.
|
|
41
|
+
*
|
|
42
|
+
* @param expr The constraint expression plan node
|
|
43
|
+
* @param constraintName The name of the constraint (for error messages)
|
|
44
|
+
* @param tableName The name of the table (for error messages)
|
|
45
|
+
* @throws QuereusError if the expression is non-deterministic
|
|
46
|
+
*/
|
|
47
|
+
export function validateDeterministicConstraint(
|
|
48
|
+
expr: ScalarPlanNode,
|
|
49
|
+
constraintName: string,
|
|
50
|
+
tableName: string
|
|
51
|
+
): void {
|
|
52
|
+
validateDeterministicExpression(
|
|
53
|
+
expr,
|
|
54
|
+
`CHECK constraint '${constraintName}' on table '${tableName}'`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates that a DEFAULT expression is deterministic.
|
|
60
|
+
*
|
|
61
|
+
* @param expr The default value expression plan node
|
|
62
|
+
* @param columnName The name of the column (for error messages)
|
|
63
|
+
* @param tableName The name of the table (for error messages)
|
|
64
|
+
* @throws QuereusError if the expression is non-deterministic
|
|
65
|
+
*/
|
|
66
|
+
export function validateDeterministicDefault(
|
|
67
|
+
expr: ScalarPlanNode,
|
|
68
|
+
columnName: string,
|
|
69
|
+
tableName: string
|
|
70
|
+
): void {
|
|
71
|
+
validateDeterministicExpression(
|
|
72
|
+
expr,
|
|
73
|
+
`DEFAULT for column '${columnName}' in table '${tableName}'`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -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,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { DmlExecutorNode } from '../../planner/nodes/dml-executor-node.js';
|
|
2
|
-
import type { Instruction, RuntimeContext, InstructionRun } from '../types.js';
|
|
3
|
-
import { emitPlanNode } from '../emitters.js';
|
|
2
|
+
import type { Instruction, RuntimeContext, InstructionRun, OutputValue } from '../types.js';
|
|
3
|
+
import { emitPlanNode, emitCallFromPlan } from '../emitters.js';
|
|
4
4
|
import { QuereusError } from '../../common/errors.js';
|
|
5
5
|
import { StatusCode, type Row, type SqlValue } from '../../common/types.js';
|
|
6
6
|
import { getVTable, disconnectVTable } from '../utils.js';
|
|
7
7
|
import { ConflictResolution } from '../../common/constants.js';
|
|
8
8
|
import type { EmissionContext } from '../emission-context.js';
|
|
9
9
|
import { extractOldRowFromFlat, extractNewRowFromFlat } from '../../util/row-descriptor.js';
|
|
10
|
+
import { buildInsertStatement, buildUpdateStatement, buildDeleteStatement } from '../../util/mutation-statement.js';
|
|
11
|
+
import type { UpdateArgs } from '../../vtab/table.js';
|
|
10
12
|
|
|
11
13
|
export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): Instruction {
|
|
12
14
|
const tableSchema = plan.table.tableSchema;
|
|
@@ -14,15 +16,55 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
14
16
|
// Pre-calculate primary key column indices from schema (needed for update/delete)
|
|
15
17
|
const pkColumnIndicesInSchema = tableSchema.primaryKeyDefinition.map(pkColDef => pkColDef.index);
|
|
16
18
|
|
|
19
|
+
// Emit mutation context evaluators if present
|
|
20
|
+
const contextEvaluatorInstructions: Instruction[] = [];
|
|
21
|
+
if (plan.mutationContextValues && plan.contextAttributes) {
|
|
22
|
+
for (const attr of plan.contextAttributes) {
|
|
23
|
+
const valueNode = plan.mutationContextValues.get(attr.name);
|
|
24
|
+
if (!valueNode) {
|
|
25
|
+
throw new QuereusError(`Missing mutation context value for '${attr.name}'`, StatusCode.INTERNAL);
|
|
26
|
+
}
|
|
27
|
+
const instruction = emitCallFromPlan(valueNode, ctx);
|
|
28
|
+
contextEvaluatorInstructions.push(instruction);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
// --- Operation-specific run generators ------------------------------------
|
|
18
33
|
|
|
19
34
|
// INSERT ----------------------------------------------------
|
|
20
|
-
async function* runInsert(ctx: RuntimeContext, rows: AsyncIterable<Row>): AsyncIterable<Row> {
|
|
35
|
+
async function* runInsert(ctx: RuntimeContext, rows: AsyncIterable<Row>, ...contextEvaluators: Array<(ctx: RuntimeContext) => OutputValue>): AsyncIterable<Row> {
|
|
21
36
|
const vtab = await getVTable(ctx, tableSchema);
|
|
37
|
+
|
|
38
|
+
// Evaluate mutation context once per statement
|
|
39
|
+
let contextRow: Row | undefined;
|
|
40
|
+
if (contextEvaluators.length > 0) {
|
|
41
|
+
contextRow = [];
|
|
42
|
+
for (const evaluator of contextEvaluators) {
|
|
43
|
+
const value = await evaluator(ctx) as SqlValue;
|
|
44
|
+
contextRow.push(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
try {
|
|
23
49
|
for await (const flatRow of rows) {
|
|
24
50
|
const newRow = extractNewRowFromFlat(flatRow, tableSchema.columns.length);
|
|
25
|
-
|
|
51
|
+
|
|
52
|
+
// Build mutation statement if logging is enabled
|
|
53
|
+
let mutationStatement: string | undefined;
|
|
54
|
+
if (vtab.wantStatements) {
|
|
55
|
+
mutationStatement = buildInsertStatement(tableSchema, newRow, contextRow);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const args: UpdateArgs = {
|
|
59
|
+
operation: 'insert',
|
|
60
|
+
values: newRow,
|
|
61
|
+
oldKeyValues: undefined,
|
|
62
|
+
onConflict: plan.onConflict ?? ConflictResolution.ABORT,
|
|
63
|
+
mutationStatement
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await vtab.update!(args);
|
|
67
|
+
|
|
26
68
|
// Track change (INSERT): record NEW primary key
|
|
27
69
|
const pkValues = tableSchema.primaryKeyDefinition.map(def => newRow[def.index]);
|
|
28
70
|
ctx.db._recordInsert(`${tableSchema.schemaName}.${tableSchema.name}`, pkValues);
|
|
@@ -34,8 +76,19 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
// UPDATE ----------------------------------------------------
|
|
37
|
-
async function* runUpdate(ctx: RuntimeContext, rows: AsyncIterable<Row>): AsyncIterable<Row> {
|
|
79
|
+
async function* runUpdate(ctx: RuntimeContext, rows: AsyncIterable<Row>, ...contextEvaluators: Array<(ctx: RuntimeContext) => OutputValue>): AsyncIterable<Row> {
|
|
38
80
|
const vtab = await getVTable(ctx, tableSchema);
|
|
81
|
+
|
|
82
|
+
// Evaluate mutation context once per statement
|
|
83
|
+
let contextRow: Row | undefined;
|
|
84
|
+
if (contextEvaluators.length > 0) {
|
|
85
|
+
contextRow = [];
|
|
86
|
+
for (const evaluator of contextEvaluators) {
|
|
87
|
+
const value = await evaluator(ctx) as SqlValue;
|
|
88
|
+
contextRow.push(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
39
92
|
try {
|
|
40
93
|
for await (const flatRow of rows) {
|
|
41
94
|
const oldRow = extractOldRowFromFlat(flatRow, tableSchema.columns.length);
|
|
@@ -48,7 +101,23 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
48
101
|
}
|
|
49
102
|
return oldRow[pkColIdx];
|
|
50
103
|
});
|
|
51
|
-
|
|
104
|
+
|
|
105
|
+
// Build mutation statement if logging is enabled
|
|
106
|
+
let mutationStatement: string | undefined;
|
|
107
|
+
if (vtab.wantStatements) {
|
|
108
|
+
mutationStatement = buildUpdateStatement(tableSchema, newRow, keyValues, contextRow);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const args: UpdateArgs = {
|
|
112
|
+
operation: 'update',
|
|
113
|
+
values: newRow,
|
|
114
|
+
oldKeyValues: keyValues,
|
|
115
|
+
onConflict: ConflictResolution.ABORT,
|
|
116
|
+
mutationStatement
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await vtab.update!(args);
|
|
120
|
+
|
|
52
121
|
// Track change (UPDATE): record OLD and NEW primary keys
|
|
53
122
|
const newKeyValues: SqlValue[] = tableSchema.primaryKeyDefinition.map(pkColDef => newRow[pkColDef.index]);
|
|
54
123
|
ctx.db._recordUpdate(`${tableSchema.schemaName}.${tableSchema.name}`, keyValues, newKeyValues);
|
|
@@ -60,8 +129,19 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
60
129
|
}
|
|
61
130
|
|
|
62
131
|
// DELETE ----------------------------------------------------
|
|
63
|
-
async function* runDelete(ctx: RuntimeContext, rows: AsyncIterable<Row>): AsyncIterable<Row> {
|
|
132
|
+
async function* runDelete(ctx: RuntimeContext, rows: AsyncIterable<Row>, ...contextEvaluators: Array<(ctx: RuntimeContext) => OutputValue>): AsyncIterable<Row> {
|
|
64
133
|
const vtab = await getVTable(ctx, tableSchema);
|
|
134
|
+
|
|
135
|
+
// Evaluate mutation context once per statement
|
|
136
|
+
let contextRow: Row | undefined;
|
|
137
|
+
if (contextEvaluators.length > 0) {
|
|
138
|
+
contextRow = [];
|
|
139
|
+
for (const evaluator of contextEvaluators) {
|
|
140
|
+
const value = await evaluator(ctx) as SqlValue;
|
|
141
|
+
contextRow.push(value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
65
145
|
try {
|
|
66
146
|
for await (const flatRow of rows) {
|
|
67
147
|
const oldRow = extractOldRowFromFlat(flatRow, tableSchema.columns.length);
|
|
@@ -72,7 +152,23 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
72
152
|
}
|
|
73
153
|
return oldRow[pkColIdx];
|
|
74
154
|
});
|
|
75
|
-
|
|
155
|
+
|
|
156
|
+
// Build mutation statement if logging is enabled
|
|
157
|
+
let mutationStatement: string | undefined;
|
|
158
|
+
if (vtab.wantStatements) {
|
|
159
|
+
mutationStatement = buildDeleteStatement(tableSchema, keyValues, contextRow);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const args: UpdateArgs = {
|
|
163
|
+
operation: 'delete',
|
|
164
|
+
values: undefined,
|
|
165
|
+
oldKeyValues: keyValues,
|
|
166
|
+
onConflict: ConflictResolution.ABORT,
|
|
167
|
+
mutationStatement
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
await vtab.update!(args);
|
|
171
|
+
|
|
76
172
|
// Track change (DELETE): record OLD primary key
|
|
77
173
|
ctx.db._recordDelete(`${tableSchema.schemaName}.${tableSchema.name}`, keyValues);
|
|
78
174
|
yield flatRow;
|
|
@@ -95,7 +191,7 @@ export function emitDmlExecutor(plan: DmlExecutorNode, ctx: EmissionContext): In
|
|
|
95
191
|
const sourceInstruction = emitPlanNode(plan.source, ctx);
|
|
96
192
|
|
|
97
193
|
return {
|
|
98
|
-
params: [sourceInstruction],
|
|
194
|
+
params: [sourceInstruction, ...contextEvaluatorInstructions],
|
|
99
195
|
run,
|
|
100
196
|
note: `execute${plan.operation}(${plan.table.tableSchema.name})`
|
|
101
197
|
};
|
package/src/runtime/types.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { EmissionContext } from "./emission-context.js";
|
|
|
7
7
|
import type { VirtualTableConnection } from "../vtab/connection.js";
|
|
8
8
|
import type { PlanNode } from '../planner/nodes/plan-node.js';
|
|
9
9
|
|
|
10
|
+
// Re-export types from common/types.js for convenience
|
|
11
|
+
export type { OutputValue };
|
|
12
|
+
|
|
10
13
|
export type RuntimeContext = {
|
|
11
14
|
db: Database;
|
|
12
15
|
stmt: Statement | undefined; // Undefined for transient exec statements
|
package/src/schema/manager.ts
CHANGED
|
@@ -13,6 +13,13 @@ import type { ViewSchema } from './view.js';
|
|
|
13
13
|
import { createLogger } from '../common/logger.js';
|
|
14
14
|
import type * as AST from '../parser/ast.js';
|
|
15
15
|
import { SchemaChangeNotifier } from './change-events.js';
|
|
16
|
+
import { validateDeterministicDefault, validateDeterministicConstraint } from '../planner/validation/determinism-validator.js';
|
|
17
|
+
import { buildExpression } from '../planner/building/expression.js';
|
|
18
|
+
import type { PlanningContext } from '../planner/planning-context.js';
|
|
19
|
+
import { BuildTimeDependencyTracker } from '../planner/planning-context.js';
|
|
20
|
+
import { GlobalScope } from '../planner/scopes/global.js';
|
|
21
|
+
import { ParameterScope } from '../planner/scopes/param.js';
|
|
22
|
+
import type { ScalarPlanNode } from '../planner/nodes/plan-node.js';
|
|
16
23
|
|
|
17
24
|
const log = createLogger('schema:manager');
|
|
18
25
|
const warnLog = log.extend('warn');
|
|
@@ -651,6 +658,44 @@ export class SchemaManager {
|
|
|
651
658
|
? stmt.contextDefinitions.map(varDef => mutationContextVarToSchema(varDef, defaultNotNull))
|
|
652
659
|
: undefined;
|
|
653
660
|
|
|
661
|
+
// Validate that default expressions are deterministic
|
|
662
|
+
// We need to build them temporarily to check their physical properties
|
|
663
|
+
// Note: We only validate defaults here, not CHECK constraints, because CHECK constraints
|
|
664
|
+
// may reference table columns which don't exist yet at CREATE TABLE time.
|
|
665
|
+
// CHECK constraints are validated at INSERT/UPDATE time in constraint-builder.ts
|
|
666
|
+
const globalScope = new GlobalScope(this.db.schemaManager);
|
|
667
|
+
const parameterScope = new ParameterScope(globalScope);
|
|
668
|
+
const planningCtx: PlanningContext = {
|
|
669
|
+
db: this.db,
|
|
670
|
+
schemaManager: this.db.schemaManager,
|
|
671
|
+
parameters: {},
|
|
672
|
+
scope: parameterScope,
|
|
673
|
+
cteNodes: new Map(),
|
|
674
|
+
schemaDependencies: new BuildTimeDependencyTracker(),
|
|
675
|
+
schemaCache: new Map(),
|
|
676
|
+
cteReferenceCache: new Map(),
|
|
677
|
+
outputScopes: new Map()
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Validate default expressions
|
|
681
|
+
// Note: We can only validate defaults that don't reference table columns,
|
|
682
|
+
// since the table doesn't exist yet. Defaults that reference columns will be
|
|
683
|
+
// validated at INSERT time in insert.ts
|
|
684
|
+
for (const col of finalColumnSchemas) {
|
|
685
|
+
if (col.defaultValue && typeof col.defaultValue === 'object' && col.defaultValue !== null && 'type' in col.defaultValue) {
|
|
686
|
+
try {
|
|
687
|
+
// It's an AST expression - try to build and validate it
|
|
688
|
+
const defaultExpr = buildExpression(planningCtx, col.defaultValue as AST.Expression) as ScalarPlanNode;
|
|
689
|
+
validateDeterministicDefault(defaultExpr, col.name, tableName);
|
|
690
|
+
} catch (e) {
|
|
691
|
+
// If we can't build the expression (e.g., it references columns that don't exist yet),
|
|
692
|
+
// skip validation here. It will be validated at INSERT time.
|
|
693
|
+
log('Skipping determinism validation for default on column %s.%s at CREATE TABLE time (will validate at INSERT time): %s',
|
|
694
|
+
tableName, col.name, (e as Error).message);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
654
699
|
const baseTableSchema: TableSchema = {
|
|
655
700
|
name: tableName,
|
|
656
701
|
schemaName: targetSchemaName,
|
|
@@ -497,6 +497,13 @@ export function insertToString(stmt: AST.InsertStmt): string {
|
|
|
497
497
|
parts.push(`(${stmt.columns.map(quoteIdentifierIfNeeded).join(', ')})`);
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
if (stmt.contextValues && stmt.contextValues.length > 0) {
|
|
501
|
+
const contextAssignments = stmt.contextValues.map(assign =>
|
|
502
|
+
`${quoteIdentifierIfNeeded(assign.name)} = ${expressionToString(assign.value)}`
|
|
503
|
+
);
|
|
504
|
+
parts.push('with context', contextAssignments.join(', '));
|
|
505
|
+
}
|
|
506
|
+
|
|
500
507
|
if (stmt.values) {
|
|
501
508
|
const valueRows = stmt.values.map(row =>
|
|
502
509
|
`(${row.map(expressionToString).join(', ')})`
|
|
@@ -533,7 +540,16 @@ export function updateToString(stmt: AST.UpdateStmt): string {
|
|
|
533
540
|
parts.push(withClauseToString(stmt.withClause));
|
|
534
541
|
}
|
|
535
542
|
|
|
536
|
-
parts.push('update', expressionToString(stmt.table)
|
|
543
|
+
parts.push('update', expressionToString(stmt.table));
|
|
544
|
+
|
|
545
|
+
if (stmt.contextValues && stmt.contextValues.length > 0) {
|
|
546
|
+
const contextAssignments = stmt.contextValues.map(assign =>
|
|
547
|
+
`${quoteIdentifierIfNeeded(assign.name)} = ${expressionToString(assign.value)}`
|
|
548
|
+
);
|
|
549
|
+
parts.push('with context', contextAssignments.join(', '));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
parts.push('set');
|
|
537
553
|
|
|
538
554
|
const assignments = stmt.assignments.map(assign =>
|
|
539
555
|
`${quoteIdentifierIfNeeded(assign.column)} = ${expressionToString(assign.value)}`
|
|
@@ -573,6 +589,13 @@ export function deleteToString(stmt: AST.DeleteStmt): string {
|
|
|
573
589
|
|
|
574
590
|
parts.push('delete from', expressionToString(stmt.table));
|
|
575
591
|
|
|
592
|
+
if (stmt.contextValues && stmt.contextValues.length > 0) {
|
|
593
|
+
const contextAssignments = stmt.contextValues.map(assign =>
|
|
594
|
+
`${quoteIdentifierIfNeeded(assign.name)} = ${expressionToString(assign.value)}`
|
|
595
|
+
);
|
|
596
|
+
parts.push('with context', contextAssignments.join(', '));
|
|
597
|
+
}
|
|
598
|
+
|
|
576
599
|
if (stmt.where) {
|
|
577
600
|
parts.push('where', expressionToString(stmt.where));
|
|
578
601
|
}
|