@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.
Files changed (84) 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 +1 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +2 -0
  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 +29 -0
  30. package/dist/src/planner/validation/determinism-validator.d.ts.map +1 -0
  31. package/dist/src/planner/validation/determinism-validator.js +47 -0
  32. package/dist/src/planner/validation/determinism-validator.js.map +1 -0
  33. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  34. package/dist/src/runtime/emit/add-constraint.js +3 -0
  35. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  36. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  37. package/dist/src/runtime/emit/dml-executor.js +84 -8
  38. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  39. package/dist/src/runtime/types.d.ts +1 -0
  40. package/dist/src/runtime/types.d.ts.map +1 -1
  41. package/dist/src/runtime/types.js.map +1 -1
  42. package/dist/src/schema/manager.d.ts.map +1 -1
  43. package/dist/src/schema/manager.js +41 -0
  44. package/dist/src/schema/manager.js.map +1 -1
  45. package/dist/src/util/ast-stringify.d.ts.map +1 -1
  46. package/dist/src/util/ast-stringify.js +14 -1
  47. package/dist/src/util/ast-stringify.js.map +1 -1
  48. package/dist/src/util/mutation-statement.d.ts +16 -0
  49. package/dist/src/util/mutation-statement.d.ts.map +1 -0
  50. package/dist/src/util/mutation-statement.js +92 -0
  51. package/dist/src/util/mutation-statement.js.map +1 -0
  52. package/dist/src/util/sql-literal.d.ts +11 -0
  53. package/dist/src/util/sql-literal.d.ts.map +1 -0
  54. package/dist/src/util/sql-literal.js +18 -0
  55. package/dist/src/util/sql-literal.js.map +1 -0
  56. package/dist/src/vtab/memory/table.d.ts +1 -2
  57. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  58. package/dist/src/vtab/memory/table.js +3 -3
  59. package/dist/src/vtab/memory/table.js.map +1 -1
  60. package/dist/src/vtab/table.d.ts +23 -5
  61. package/dist/src/vtab/table.d.ts.map +1 -1
  62. package/dist/src/vtab/table.js +6 -0
  63. package/dist/src/vtab/table.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/common/logger.ts +75 -1
  66. package/src/func/builtins/datetime.ts +10 -5
  67. package/src/index.ts +3 -0
  68. package/src/planner/building/constraint-builder.ts +5 -0
  69. package/src/planner/building/delete.ts +5 -1
  70. package/src/planner/building/insert.ts +8 -1
  71. package/src/planner/building/update.ts +10 -2
  72. package/src/planner/nodes/dml-executor-node.ts +8 -2
  73. package/src/planner/validation/determinism-validator.ts +76 -0
  74. package/src/runtime/emit/add-constraint.ts +3 -0
  75. package/src/runtime/emit/dml-executor.ts +105 -9
  76. package/src/runtime/types.ts +3 -0
  77. package/src/schema/manager.ts +45 -0
  78. package/src/util/ast-stringify.ts +24 -1
  79. package/src/util/hash.ts +90 -90
  80. package/src/util/mutation-statement.ts +129 -0
  81. package/src/util/plugin-helper.ts +110 -110
  82. package/src/util/sql-literal.ts +22 -0
  83. package/src/vtab/memory/table.ts +3 -8
  84. package/src/vtab/table.ts +25 -10
@@ -1 +1 @@
1
- {"version":3,"file":"table.js","sourceRoot":"","sources":["../../../src/vtab/table.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,OAAgB,YAAY;IACjB,MAAM,CAAwB;IAC9B,EAAE,CAAW;IACb,SAAS,CAAS;IAClB,UAAU,CAAS;IAC5B,YAAY,CAAU;IACtB,WAAW,CAAe;IAEjC,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;CA6HD"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quereus/quereus",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "description": "Federated SQL query processor",
6
6
  "publisher": "Got Choices Foundation",
@@ -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
- return debug(`${BASE_NAMESPACE}:${subNamespace}`);
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: true },
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: true },
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: true },
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: true },
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: true },
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
- await vtab.update!('insert', newRow, undefined, plan.onConflict ?? ConflictResolution.ABORT);
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
- await vtab.update!('update', newRow, keyValues, ConflictResolution.ABORT);
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
- await vtab.update!('delete', undefined, keyValues, ConflictResolution.ABORT);
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
  };
@@ -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
@@ -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), 'set');
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
  }