@quereus/quereus 0.6.1 → 0.6.2

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 (36) hide show
  1. package/dist/src/common/type-inference.d.ts +9 -1
  2. package/dist/src/common/type-inference.d.ts.map +1 -1
  3. package/dist/src/common/type-inference.js +11 -3
  4. package/dist/src/common/type-inference.js.map +1 -1
  5. package/dist/src/core/database.d.ts +27 -1
  6. package/dist/src/core/database.d.ts.map +1 -1
  7. package/dist/src/core/database.js +39 -6
  8. package/dist/src/core/database.js.map +1 -1
  9. package/dist/src/core/param.d.ts +17 -1
  10. package/dist/src/core/param.d.ts.map +1 -1
  11. package/dist/src/core/param.js +23 -1
  12. package/dist/src/core/param.js.map +1 -1
  13. package/dist/src/core/statement.d.ts +10 -1
  14. package/dist/src/core/statement.d.ts.map +1 -1
  15. package/dist/src/core/statement.js +71 -5
  16. package/dist/src/core/statement.js.map +1 -1
  17. package/dist/src/planner/scopes/param.d.ts +2 -2
  18. package/dist/src/planner/scopes/param.d.ts.map +1 -1
  19. package/dist/src/planner/scopes/param.js +9 -9
  20. package/dist/src/planner/scopes/param.js.map +1 -1
  21. package/dist/src/types/index.d.ts +1 -1
  22. package/dist/src/types/index.d.ts.map +1 -1
  23. package/dist/src/types/index.js +1 -1
  24. package/dist/src/types/index.js.map +1 -1
  25. package/dist/src/types/logical-type.d.ts +5 -0
  26. package/dist/src/types/logical-type.d.ts.map +1 -1
  27. package/dist/src/types/logical-type.js +15 -0
  28. package/dist/src/types/logical-type.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/common/type-inference.ts +11 -3
  31. package/src/core/database.ts +41 -6
  32. package/src/core/param.ts +23 -1
  33. package/src/core/statement.ts +89 -5
  34. package/src/planner/scopes/param.ts +9 -9
  35. package/src/types/index.ts +1 -1
  36. package/src/types/logical-type.ts +16 -0
@@ -14,6 +14,8 @@ import { isAsyncIterable } from '../runtime/utils.js';
14
14
  import { generateInstructionProgram, serializePlanTree } from '../planner/debug.js';
15
15
  import { EmissionContext } from '../runtime/emission-context.js';
16
16
  import type { SchemaDependency } from '../planner/planning-context.js';
17
+ import { getParameterTypes } from './param.js';
18
+ import { getPhysicalType, physicalTypeName } from '../types/logical-type.js';
17
19
 
18
20
  const log = createLogger('core:statement');
19
21
  const errorLog = log.extend('error');
@@ -35,13 +37,21 @@ export class Statement {
35
37
  private needsCompile = true;
36
38
  private columnDefCache = new Cached<DeepReadonly<ColumnDef>[]>(() => this.getColumnDefs());
37
39
  private schemaChangeUnsubscriber: (() => void) | null = null;
40
+ /** Parameter types established at prepare time (either explicit or inferred from initial values) */
41
+ private parameterTypes: Map<string | number, ScalarType> | undefined = undefined;
38
42
 
39
43
  /**
40
44
  * @internal - Use db.prepare().
41
45
  * The `sqlOrAstBatch` can be a single SQL string (parsed internally) or a pre-parsed batch.
42
46
  * `initialAstIndex` is for internal use when db.prepare might create one Statement per AST in a batch.
47
+ * `paramsOrTypes` can be initial parameter values (to infer types) or explicit types.
43
48
  */
44
- constructor(db: Database, sqlOrAstBatch: string | ASTStatement[], initialAstIndex: number = 0) {
49
+ constructor(
50
+ db: Database,
51
+ sqlOrAstBatch: string | ASTStatement[],
52
+ initialAstIndex: number = 0,
53
+ paramsOrTypes?: SqlParameters | SqlValue[] | Map<string | number, ScalarType>
54
+ ) {
45
55
  this.db = db;
46
56
  if (typeof sqlOrAstBatch === 'string') {
47
57
  this.originalSql = sqlOrAstBatch;
@@ -58,6 +68,23 @@ export class Statement {
58
68
  this.originalSql = this.astBatch.map(s => s.toString()).join('; '); // TODO: replace with better AST stringification
59
69
  }
60
70
 
71
+ // Handle explicit parameter types or initial values
72
+ if (paramsOrTypes instanceof Map) {
73
+ // Explicit parameter types provided
74
+ this.parameterTypes = paramsOrTypes;
75
+ } else if (paramsOrTypes !== undefined) {
76
+ // Initial parameter values - infer types and bind them
77
+ this.parameterTypes = getParameterTypes(paramsOrTypes);
78
+ // Also bind the initial values
79
+ if (Array.isArray(paramsOrTypes)) {
80
+ paramsOrTypes.forEach((value, index) => {
81
+ this.boundArgs[index + 1] = value;
82
+ });
83
+ } else {
84
+ Object.assign(this.boundArgs, paramsOrTypes);
85
+ }
86
+ }
87
+
61
88
  if (this.astBatch.length === 0 && initialAstIndex === 0) {
62
89
  // No statements to run, effectively. nextStatement will return false.
63
90
  this.astBatchIndex = -1;
@@ -80,6 +107,7 @@ export class Statement {
80
107
  this.emissionContext = null;
81
108
  this.needsCompile = true;
82
109
  this.columnDefCache.clear();
110
+ this.parameterTypes = undefined;
83
111
  return true;
84
112
  } else {
85
113
  return false;
@@ -105,7 +133,16 @@ export class Statement {
105
133
  let plan: BlockNode | undefined;
106
134
  try {
107
135
  const currentAst = this.getAstStatement();
108
- const planResult = this.db._buildPlan([currentAst], this.boundArgs);
136
+
137
+ // On first compilation, establish the parameter types
138
+ // Use explicit types if provided, otherwise infer from bound args
139
+ if (this.parameterTypes === undefined) {
140
+ // Infer types from current bound args
141
+ this.parameterTypes = getParameterTypes(this.boundArgs);
142
+ }
143
+
144
+ // Pass parameter types directly to planning
145
+ const planResult = this.db._buildPlan([currentAst], this.parameterTypes);
109
146
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
147
  const dependencies = (planResult as any).schemaDependencies; // Extract dependencies from planning context
111
148
  plan = this.db.optimizer.optimize(planResult, this.db) as BlockNode;
@@ -227,6 +264,9 @@ export class Statement {
227
264
 
228
265
  if (params) this.bindAll(params);
229
266
 
267
+ // Validate parameter types before execution
268
+ this.validateParameterTypes();
269
+
230
270
  this.busy = true;
231
271
  try {
232
272
  const blockPlanNode = this.compile();
@@ -283,14 +323,14 @@ export class Statement {
283
323
  }
284
324
 
285
325
  /**
286
- * Clears all bound parameter values, setting them to NULL.
326
+ * Clears all bound parameter values.
327
+ * Note: This does NOT trigger recompilation - parameter types are preserved.
287
328
  */
288
329
  clearBindings(): this {
289
330
  this.validateStatement("clear bindings for");
290
331
  if (this.busy) throw new MisuseError("Statement busy, reset first");
291
332
  this.boundArgs = {};
292
- this.emissionContext = null;
293
- this.needsCompile = true;
333
+ // Don't set needsCompile - parameter types are preserved
294
334
  return this;
295
335
  }
296
336
 
@@ -417,6 +457,50 @@ export class Statement {
417
457
  return this.astBatch[this.astBatchIndex];
418
458
  }
419
459
 
460
+ /**
461
+ * Validates that bound parameters match the expected types from compilation.
462
+ * Validates that the JavaScript value is compatible with the physical type of the declared logical type.
463
+ * @throws QuereusError if parameter types don't match
464
+ */
465
+ private validateParameterTypes(): void {
466
+ if (!this.parameterTypes) return; // No parameter types established yet
467
+
468
+ for (const [key, expectedType] of this.parameterTypes.entries()) {
469
+ const value = this.boundArgs[key];
470
+
471
+ // Allow undefined/missing parameters (they'll be caught at runtime if required)
472
+ if (value === undefined) continue;
473
+
474
+ // NULL is compatible with any nullable type
475
+ if (value === null) {
476
+ if (!expectedType.nullable) {
477
+ throw new QuereusError(
478
+ `Parameter type mismatch for ${typeof key === 'number' ? `?${key}` : `:${key}`}: ` +
479
+ `expected non-nullable ${expectedType.logicalType.name}, got NULL`,
480
+ StatusCode.MISMATCH
481
+ );
482
+ }
483
+ continue;
484
+ }
485
+
486
+ // Get the physical type of the declared logical type
487
+ const expectedPhysicalType = expectedType.logicalType.physicalType;
488
+
489
+ // Get the physical type directly from the JavaScript value
490
+ const actualPhysicalType = getPhysicalType(value);
491
+
492
+ // Check if physical types are compatible
493
+ if (actualPhysicalType !== expectedPhysicalType) {
494
+ throw new QuereusError(
495
+ `Parameter type mismatch for ${typeof key === 'number' ? `?${key}` : `:${key}`}: ` +
496
+ `expected ${expectedType.logicalType.name} (physical: ${physicalTypeName(expectedPhysicalType)}), ` +
497
+ `got value with physical type ${physicalTypeName(actualPhysicalType)}`,
498
+ StatusCode.MISMATCH
499
+ );
500
+ }
501
+ }
502
+ }
503
+
420
504
  /**
421
505
  * Gets a detailed JSON representation of the query plan for debugging.
422
506
  * @returns JSON string containing the detailed plan tree.
@@ -20,14 +20,14 @@ const DEFAULT_PARAMETER_TYPE: ScalarType = {
20
20
  export class ParameterScope extends BaseScope {
21
21
  private _nextAnonymousIndex: number = 1;
22
22
  private readonly _parameters: Map<string | number, ParameterReferenceNode> = new Map();
23
- private readonly _parameterTypeHints: ReadonlyMap<string | number, ScalarType>;
23
+ private readonly _parameterTypes: ReadonlyMap<string | number, ScalarType>;
24
24
 
25
25
  constructor(
26
26
  public readonly parentScope: Scope,
27
- parameterTypeHints?: ReadonlyMap<string | number, ScalarType>
27
+ parameterTypes?: ReadonlyMap<string | number, ScalarType>
28
28
  ) {
29
29
  super();
30
- this._parameterTypeHints = parameterTypeHints || new Map();
30
+ this._parameterTypes = parameterTypes || new Map();
31
31
  }
32
32
 
33
33
  resolveSymbol(symbolKey: string, expression: AST.Expression): PlanNode | typeof Ambiguous | undefined {
@@ -42,9 +42,9 @@ export class ParameterScope extends BaseScope {
42
42
  // Use the current _nextAnonymousIndex as the potential identifier for this '?'
43
43
  const currentAnonymousId = this._nextAnonymousIndex;
44
44
 
45
- // Check if this specific anonymous parameter (by its future index) has a type hint
46
- if (this._parameterTypeHints.has(currentAnonymousId)) {
47
- resolvedType = this._parameterTypeHints.get(currentAnonymousId)!;
45
+ // Check if this specific anonymous parameter (by its future index) has a declared type
46
+ if (this._parameterTypes.has(currentAnonymousId)) {
47
+ resolvedType = this._parameterTypes.get(currentAnonymousId)!;
48
48
  }
49
49
  // Note: We don't check _parameters here for '?' because each '?' AST node should resolve,
50
50
  // potentially creating a new ParameterReferenceNode if it's a new '?' instance in the query,
@@ -63,10 +63,10 @@ export class ParameterScope extends BaseScope {
63
63
 
64
64
  if (this._parameters.has(identifier)) {
65
65
  parameterNode = this._parameters.get(identifier)!;
66
- // If already exists, its type was set at creation. Type hints are for new nodes.
66
+ // If already exists, its type was set at creation
67
67
  } else {
68
- if (this._parameterTypeHints.has(identifier)) {
69
- resolvedType = this._parameterTypeHints.get(identifier)!;
68
+ if (this._parameterTypes.has(identifier)) {
69
+ resolvedType = this._parameterTypes.get(identifier)!;
70
70
  }
71
71
  parameterNode = new ParameterReferenceNode(this, parameterExpression, identifier, resolvedType);
72
72
  this._parameters.set(identifier, parameterNode);
@@ -1,5 +1,5 @@
1
1
  // Core type system exports
2
- export { PhysicalType, type LogicalType, getPhysicalType } from './logical-type.js';
2
+ export { PhysicalType, type LogicalType, getPhysicalType, physicalTypeName } from './logical-type.js';
3
3
 
4
4
  // Built-in types
5
5
  export { NULL_TYPE, INTEGER_TYPE, REAL_TYPE, TEXT_TYPE, BLOB_TYPE, BOOLEAN_TYPE, NUMERIC_TYPE, ANY_TYPE } from './builtin-types.js';
@@ -73,3 +73,19 @@ export function getPhysicalType(value: SqlValue): PhysicalType {
73
73
  return PhysicalType.NULL;
74
74
  }
75
75
 
76
+ /**
77
+ * Get a human-readable name for a physical type code.
78
+ * Useful for error messages and debugging.
79
+ */
80
+ export function physicalTypeName(physicalType: PhysicalType): string {
81
+ switch (physicalType) {
82
+ case PhysicalType.NULL: return 'NULL';
83
+ case PhysicalType.INTEGER: return 'INTEGER';
84
+ case PhysicalType.REAL: return 'REAL';
85
+ case PhysicalType.TEXT: return 'TEXT';
86
+ case PhysicalType.BLOB: return 'BLOB';
87
+ case PhysicalType.BOOLEAN: return 'BOOLEAN';
88
+ default: return 'UNKNOWN';
89
+ }
90
+ }
91
+