@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.
Files changed (55) 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/runtime/emit/schema-declarative.js +1 -1
  22. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  23. package/dist/src/schema/schema-hasher.d.ts +3 -3
  24. package/dist/src/schema/schema-hasher.d.ts.map +1 -1
  25. package/dist/src/schema/schema-hasher.js +9 -27
  26. package/dist/src/schema/schema-hasher.js.map +1 -1
  27. package/dist/src/types/index.d.ts +1 -1
  28. package/dist/src/types/index.d.ts.map +1 -1
  29. package/dist/src/types/index.js +1 -1
  30. package/dist/src/types/index.js.map +1 -1
  31. package/dist/src/types/logical-type.d.ts +5 -0
  32. package/dist/src/types/logical-type.d.ts.map +1 -1
  33. package/dist/src/types/logical-type.js +15 -0
  34. package/dist/src/types/logical-type.js.map +1 -1
  35. package/dist/src/util/hash.d.ts +19 -0
  36. package/dist/src/util/hash.d.ts.map +1 -0
  37. package/dist/src/util/hash.js +76 -0
  38. package/dist/src/util/hash.js.map +1 -0
  39. package/package.json +1 -1
  40. package/src/common/type-inference.ts +11 -3
  41. package/src/core/database.ts +41 -6
  42. package/src/core/param.ts +23 -1
  43. package/src/core/statement.ts +89 -5
  44. package/src/planner/building/delete.ts +214 -214
  45. package/src/planner/building/insert.ts +428 -428
  46. package/src/planner/building/update.ts +319 -319
  47. package/src/planner/scopes/param.ts +9 -9
  48. package/src/runtime/emit/schema-declarative.ts +1 -1
  49. package/src/schema/schema-hasher.ts +9 -27
  50. package/src/types/index.ts +1 -1
  51. package/src/types/logical-type.ts +16 -0
  52. package/src/util/ast-stringify.ts +864 -864
  53. package/src/util/hash.ts +90 -0
  54. package/src/vtab/memory/table.ts +256 -256
  55. 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
- // For now, all numbers are REAL. Could check Number.isInteger() for INTEGER_TYPE
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;
@@ -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[], params?: SqlParameters | SqlValue[]) {
935
+ _buildPlan(statements: AST.Statement[], paramsOrTypes?: SqlParameters | SqlValue[] | Map<string | number, ScalarType>) {
906
936
  const globalScope = new GlobalScope(this.schemaManager);
907
937
 
908
- // TODO: way to generate type hints from parameters? Maybe we should extract that from the expression context?
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: params ?? {},
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
- export function getParameterTypeHints(params: SqlParameters | undefined): Map<string | number, ScalarType> | undefined {
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
 
@@ -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.