@quereus/quereus 0.6.1 → 0.6.3
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/runtime/emit/schema-declarative.js +1 -1
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/schema/schema-hasher.d.ts +3 -3
- package/dist/src/schema/schema-hasher.d.ts.map +1 -1
- package/dist/src/schema/schema-hasher.js +9 -27
- package/dist/src/schema/schema-hasher.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/dist/src/util/hash.d.ts +19 -0
- package/dist/src/util/hash.d.ts.map +1 -0
- package/dist/src/util/hash.js +76 -0
- package/dist/src/util/hash.js.map +1 -0
- 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/building/delete.ts +214 -214
- package/src/planner/building/insert.ts +428 -428
- package/src/planner/building/update.ts +319 -319
- package/src/planner/scopes/param.ts +9 -9
- package/src/runtime/emit/schema-declarative.ts +1 -1
- package/src/schema/schema-hasher.ts +9 -27
- package/src/types/index.ts +1 -1
- package/src/types/logical-type.ts +16 -0
- package/src/util/ast-stringify.ts +864 -864
- package/src/util/hash.ts +90 -0
- package/src/vtab/memory/table.ts +256 -256
- package/src/vtab/table.ts +162 -162
|
@@ -15,13 +15,21 @@ export function getLiteralSqlType(v: SqlValue): SqlDataType {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Infer LogicalType from a SqlValue
|
|
18
|
+
* Infer LogicalType from a SqlValue for parameters.
|
|
19
|
+
* Uses JavaScript type to determine the logical type:
|
|
20
|
+
* - null → NULL
|
|
21
|
+
* - number (integer) → INTEGER
|
|
22
|
+
* - number (float) → REAL
|
|
23
|
+
* - bigint → INTEGER
|
|
24
|
+
* - boolean → BOOLEAN
|
|
25
|
+
* - string → TEXT
|
|
26
|
+
* - Uint8Array → BLOB
|
|
19
27
|
*/
|
|
20
28
|
export function inferLogicalTypeFromValue(v: SqlValue): LogicalType {
|
|
21
29
|
if (v === null) return NULL_TYPE;
|
|
22
30
|
if (typeof v === 'number') {
|
|
23
|
-
//
|
|
24
|
-
return REAL_TYPE;
|
|
31
|
+
// Distinguish INTEGER from REAL based on Number.isInteger()
|
|
32
|
+
return Number.isInteger(v) ? INTEGER_TYPE : REAL_TYPE;
|
|
25
33
|
}
|
|
26
34
|
if (typeof v === 'bigint') return INTEGER_TYPE;
|
|
27
35
|
if (typeof v === 'boolean') return BOOLEAN_TYPE;
|
package/src/core/database.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { DeclaredSchemaManager } from '../schema/declared-schema-manager.js';
|
|
|
43
43
|
import { analyzeRowSpecific } from '../planner/analysis/constraint-extractor.js';
|
|
44
44
|
import { DeferredConstraintQueue } from '../runtime/deferred-constraint-queue.js';
|
|
45
45
|
import { type LogicalType } from '../types/logical-type.js';
|
|
46
|
+
import { getParameterTypes } from './param.js';
|
|
46
47
|
|
|
47
48
|
const log = createLogger('core:database');
|
|
48
49
|
const warnLog = log.extend('warn');
|
|
@@ -196,21 +197,50 @@ export class Database {
|
|
|
196
197
|
|
|
197
198
|
/**
|
|
198
199
|
* Prepares an SQL statement for execution.
|
|
200
|
+
*
|
|
199
201
|
* @param sql The SQL string to prepare.
|
|
202
|
+
* @param paramsOrTypes Optional parameter values (to infer types) or explicit type map.
|
|
203
|
+
* - If SqlParameters: Parameter types are inferred from the values
|
|
204
|
+
* - If Map<string|number, ScalarType>: Explicit type hints for parameters
|
|
205
|
+
* - If undefined: Parameters default to TEXT type
|
|
200
206
|
* @returns A Statement object.
|
|
201
207
|
* @throws QuereusError on failure (e.g., syntax error).
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* // Infer types from initial values
|
|
211
|
+
* const stmt = db.prepare('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Alice']);
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* // Explicit param types
|
|
215
|
+
* const types = new Map([
|
|
216
|
+
* [1, { typeClass: 'scalar', logicalType: INTEGER_TYPE, nullable: false }],
|
|
217
|
+
* [2, { typeClass: 'scalar', logicalType: TEXT_TYPE, nullable: false }]
|
|
218
|
+
* ]);
|
|
219
|
+
* const stmt = db.prepare('INSERT INTO users (id, name) VALUES (?, ?)', types);
|
|
202
220
|
*/
|
|
203
|
-
prepare(sql: string): Statement {
|
|
221
|
+
prepare(sql: string, paramsOrTypes?: SqlParameters | SqlValue[] | Map<string | number, ScalarType>): Statement {
|
|
204
222
|
this.checkOpen();
|
|
205
223
|
log('Preparing SQL (new runtime): %s', sql);
|
|
206
224
|
|
|
207
225
|
// Statement constructor defers planning/compilation until first step or explicit compile()
|
|
208
|
-
const stmt = new Statement(this, sql);
|
|
226
|
+
const stmt = new Statement(this, sql, 0, paramsOrTypes);
|
|
209
227
|
|
|
210
228
|
this.statements.add(stmt);
|
|
211
229
|
return stmt;
|
|
212
230
|
}
|
|
213
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Executes a query and returns the first result row as an object.
|
|
234
|
+
* @param sql The SQL query string to execute.
|
|
235
|
+
* @param params Optional parameters to bind.
|
|
236
|
+
* @returns A Promise resolving to the first result row as an object, or undefined if no rows.
|
|
237
|
+
* @throws QuereusError on failure.
|
|
238
|
+
*/
|
|
239
|
+
get(sql: string, params?: SqlParameters | SqlValue[]): Promise<Record<string, SqlValue> | undefined> {
|
|
240
|
+
const stmt = this.prepare(sql, params);
|
|
241
|
+
return stmt.get(params);
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
/**
|
|
215
245
|
* Executes one or more SQL statements directly.
|
|
216
246
|
* @param sql The SQL string(s) to execute.
|
|
@@ -902,17 +932,22 @@ export class Database {
|
|
|
902
932
|
}
|
|
903
933
|
|
|
904
934
|
/** @internal */
|
|
905
|
-
_buildPlan(statements: AST.Statement[],
|
|
935
|
+
_buildPlan(statements: AST.Statement[], paramsOrTypes?: SqlParameters | SqlValue[] | Map<string | number, ScalarType>) {
|
|
906
936
|
const globalScope = new GlobalScope(this.schemaManager);
|
|
907
937
|
|
|
908
|
-
//
|
|
938
|
+
// If we received parameter values, infer their types
|
|
939
|
+
// If we received explicit parameter types, use them as-is
|
|
940
|
+
const parameterTypes = paramsOrTypes instanceof Map
|
|
941
|
+
? paramsOrTypes
|
|
942
|
+
: getParameterTypes(paramsOrTypes);
|
|
943
|
+
|
|
909
944
|
// This ParameterScope is for the entire batch. It has globalScope as its parent.
|
|
910
|
-
const parameterScope = new ParameterScope(globalScope);
|
|
945
|
+
const parameterScope = new ParameterScope(globalScope, parameterTypes);
|
|
911
946
|
|
|
912
947
|
const ctx: PlanningContext = {
|
|
913
948
|
db: this,
|
|
914
949
|
schemaManager: this.schemaManager,
|
|
915
|
-
parameters:
|
|
950
|
+
parameters: paramsOrTypes instanceof Map ? {} : (paramsOrTypes ?? {}),
|
|
916
951
|
scope: parameterScope,
|
|
917
952
|
cteNodes: new Map(),
|
|
918
953
|
schemaDependencies: new BuildTimeDependencyTracker(),
|
package/src/core/param.ts
CHANGED
|
@@ -2,7 +2,23 @@ import type { ScalarType } from '../common/datatype.js';
|
|
|
2
2
|
import { type SqlParameters, type SqlValue } from '../common/types.js';
|
|
3
3
|
import { inferLogicalTypeFromValue } from '../common/type-inference.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Generate type hints for parameters based on their JavaScript values.
|
|
7
|
+
* This is used during planning to assign strong types to parameters.
|
|
8
|
+
*
|
|
9
|
+
* Type inference rules:
|
|
10
|
+
* - null → NULL
|
|
11
|
+
* - number (integer) → INTEGER
|
|
12
|
+
* - number (float) → REAL
|
|
13
|
+
* - bigint → INTEGER
|
|
14
|
+
* - boolean → BOOLEAN
|
|
15
|
+
* - string → TEXT
|
|
16
|
+
* - Uint8Array → BLOB
|
|
17
|
+
*
|
|
18
|
+
* @param params The parameter values (positional array or named object)
|
|
19
|
+
* @returns Map of parameter keys to their inferred ScalarTypes
|
|
20
|
+
*/
|
|
21
|
+
export function getParameterTypes(params: SqlParameters | undefined): Map<string | number, ScalarType> | undefined {
|
|
6
22
|
let results: Map<string | number, ScalarType> | undefined;
|
|
7
23
|
if (params) {
|
|
8
24
|
results = new Map<string | number, ScalarType>();
|
|
@@ -22,6 +38,12 @@ export function getParameterTypeHints(params: SqlParameters | undefined): Map<st
|
|
|
22
38
|
return results;
|
|
23
39
|
}
|
|
24
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Infer the ScalarType for a parameter value based on its JavaScript type.
|
|
43
|
+
*
|
|
44
|
+
* @param value The parameter value
|
|
45
|
+
* @returns The inferred ScalarType
|
|
46
|
+
*/
|
|
25
47
|
function getParameterScalarType(value: SqlValue): ScalarType {
|
|
26
48
|
const logicalType = inferLogicalTypeFromValue(value);
|
|
27
49
|
|
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.
|