@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.
- package/dist/src/common/type-inference.d.ts +9 -1
- package/dist/src/common/type-inference.d.ts.map +1 -1
- package/dist/src/common/type-inference.js +11 -3
- package/dist/src/common/type-inference.js.map +1 -1
- package/dist/src/core/database.d.ts +27 -1
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +39 -6
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/param.d.ts +17 -1
- package/dist/src/core/param.d.ts.map +1 -1
- package/dist/src/core/param.js +23 -1
- package/dist/src/core/param.js.map +1 -1
- package/dist/src/core/statement.d.ts +10 -1
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +71 -5
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/planner/scopes/param.d.ts +2 -2
- package/dist/src/planner/scopes/param.d.ts.map +1 -1
- package/dist/src/planner/scopes/param.js +9 -9
- package/dist/src/planner/scopes/param.js.map +1 -1
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/logical-type.d.ts +5 -0
- package/dist/src/types/logical-type.d.ts.map +1 -1
- package/dist/src/types/logical-type.js +15 -0
- package/dist/src/types/logical-type.js.map +1 -1
- package/package.json +1 -1
- package/src/common/type-inference.ts +11 -3
- package/src/core/database.ts +41 -6
- package/src/core/param.ts +23 -1
- package/src/core/statement.ts +89 -5
- package/src/planner/scopes/param.ts +9 -9
- package/src/types/index.ts +1 -1
- package/src/types/logical-type.ts +16 -0
package/src/core/statement.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
+
private readonly _parameterTypes: ReadonlyMap<string | number, ScalarType>;
|
|
24
24
|
|
|
25
25
|
constructor(
|
|
26
26
|
public readonly parentScope: Scope,
|
|
27
|
-
|
|
27
|
+
parameterTypes?: ReadonlyMap<string | number, ScalarType>
|
|
28
28
|
) {
|
|
29
29
|
super();
|
|
30
|
-
this.
|
|
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
|
|
46
|
-
if (this.
|
|
47
|
-
resolvedType = this.
|
|
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
|
|
66
|
+
// If already exists, its type was set at creation
|
|
67
67
|
} else {
|
|
68
|
-
if (this.
|
|
69
|
-
resolvedType = this.
|
|
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);
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
|