@quereus/quereus 0.6.4 → 0.6.6

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 (97) 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 +2 -1
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +3 -1
  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 +47 -0
  30. package/dist/src/planner/validation/determinism-validator.d.ts.map +1 -0
  31. package/dist/src/planner/validation/determinism-validator.js +63 -0
  32. package/dist/src/planner/validation/determinism-validator.js.map +1 -0
  33. package/dist/src/runtime/async-util.d.ts.map +1 -1
  34. package/dist/src/runtime/async-util.js +4 -3
  35. package/dist/src/runtime/async-util.js.map +1 -1
  36. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  37. package/dist/src/runtime/emit/add-constraint.js +3 -0
  38. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  39. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  40. package/dist/src/runtime/emit/dml-executor.js +84 -8
  41. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  42. package/dist/src/runtime/emitters.d.ts.map +1 -1
  43. package/dist/src/runtime/emitters.js +2 -1
  44. package/dist/src/runtime/emitters.js.map +1 -1
  45. package/dist/src/runtime/types.d.ts +1 -0
  46. package/dist/src/runtime/types.d.ts.map +1 -1
  47. package/dist/src/runtime/types.js.map +1 -1
  48. package/dist/src/runtime/utils.d.ts +14 -0
  49. package/dist/src/runtime/utils.d.ts.map +1 -1
  50. package/dist/src/runtime/utils.js +40 -1
  51. package/dist/src/runtime/utils.js.map +1 -1
  52. package/dist/src/schema/manager.d.ts.map +1 -1
  53. package/dist/src/schema/manager.js +50 -0
  54. package/dist/src/schema/manager.js.map +1 -1
  55. package/dist/src/util/ast-stringify.d.ts.map +1 -1
  56. package/dist/src/util/ast-stringify.js +14 -1
  57. package/dist/src/util/ast-stringify.js.map +1 -1
  58. package/dist/src/util/mutation-statement.d.ts +16 -0
  59. package/dist/src/util/mutation-statement.d.ts.map +1 -0
  60. package/dist/src/util/mutation-statement.js +92 -0
  61. package/dist/src/util/mutation-statement.js.map +1 -0
  62. package/dist/src/util/sql-literal.d.ts +11 -0
  63. package/dist/src/util/sql-literal.d.ts.map +1 -0
  64. package/dist/src/util/sql-literal.js +18 -0
  65. package/dist/src/util/sql-literal.js.map +1 -0
  66. package/dist/src/vtab/memory/table.d.ts +1 -2
  67. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  68. package/dist/src/vtab/memory/table.js +3 -3
  69. package/dist/src/vtab/memory/table.js.map +1 -1
  70. package/dist/src/vtab/table.d.ts +23 -5
  71. package/dist/src/vtab/table.d.ts.map +1 -1
  72. package/dist/src/vtab/table.js +6 -0
  73. package/dist/src/vtab/table.js.map +1 -1
  74. package/package.json +1 -1
  75. package/src/common/logger.ts +75 -1
  76. package/src/func/builtins/datetime.ts +10 -5
  77. package/src/index.ts +4 -1
  78. package/src/planner/building/constraint-builder.ts +178 -173
  79. package/src/planner/building/delete.ts +5 -1
  80. package/src/planner/building/insert.ts +8 -1
  81. package/src/planner/building/update.ts +10 -2
  82. package/src/planner/nodes/dml-executor-node.ts +8 -2
  83. package/src/planner/validation/determinism-validator.ts +104 -0
  84. package/src/runtime/async-util.ts +4 -3
  85. package/src/runtime/emit/add-constraint.ts +3 -0
  86. package/src/runtime/emit/dml-executor.ts +105 -9
  87. package/src/runtime/emitters.ts +2 -1
  88. package/src/runtime/types.ts +3 -0
  89. package/src/runtime/utils.ts +41 -1
  90. package/src/schema/manager.ts +800 -742
  91. package/src/util/ast-stringify.ts +24 -1
  92. package/src/util/hash.ts +90 -90
  93. package/src/util/mutation-statement.ts +129 -0
  94. package/src/util/plugin-helper.ts +110 -110
  95. package/src/util/sql-literal.ts +22 -0
  96. package/src/vtab/memory/table.ts +3 -8
  97. package/src/vtab/table.ts +25 -10
@@ -1,742 +1,800 @@
1
- import { Schema } from './schema.js';
2
- import type { IntegrityAssertionSchema } from './assertion.js';
3
- import type { Database } from '../core/database.js';
4
- import type { TableSchema, RowConstraintSchema, IndexSchema } from './table.js';
5
- import type { FunctionSchema } from './function.js';
6
- import { quereusError, QuereusError } from '../common/errors.js';
7
- import { StatusCode, type SqlValue } from '../common/types.js';
8
- import type { AnyVirtualTableModule, BaseModuleConfig } from '../vtab/module.js';
9
- import type { VirtualTable } from '../vtab/table.js';
10
- import type { ColumnSchema } from './column.js';
11
- import { buildColumnIndexMap, columnDefToSchema, findPKDefinition, opsToMask, mutationContextVarToSchema } from './table.js';
12
- import type { ViewSchema } from './view.js';
13
- import { createLogger } from '../common/logger.js';
14
- import type * as AST from '../parser/ast.js';
15
- import { SchemaChangeNotifier } from './change-events.js';
16
-
17
- const log = createLogger('schema:manager');
18
- const warnLog = log.extend('warn');
19
- const errorLog = log.extend('error');
20
-
21
- /**
22
- * Generic options passed to VTab modules during CREATE TABLE.
23
- * Modules are responsible for interpreting these.
24
- */
25
- export interface GenericModuleCallOptions extends BaseModuleConfig {
26
- moduleArgs?: readonly string[];
27
- statementColumns?: readonly AST.ColumnDef[];
28
- statementConstraints?: readonly AST.TableConstraint[];
29
- isTemporary?: boolean;
30
- }
31
-
32
- /**
33
- * Manages all schemas associated with a database connection (main, temp, attached).
34
- * Handles lookup resolution according to SQLite's rules.
35
- */
36
- export class SchemaManager {
37
- private schemas: Map<string, Schema> = new Map();
38
- private currentSchemaName: string = 'main';
39
- private modules: Map<string, { module: AnyVirtualTableModule, auxData?: unknown }> = new Map();
40
- private defaultVTabModuleName: string = 'memory';
41
- private defaultVTabModuleArgs: Record<string, SqlValue> = {};
42
- private db: Database;
43
- private changeNotifier = new SchemaChangeNotifier();
44
-
45
- /**
46
- * Creates a new schema manager
47
- *
48
- * @param db Reference to the parent Database instance
49
- */
50
- constructor(db: Database) {
51
- this.db = db;
52
- // Ensure 'main' and 'temp' schemas always exist
53
- this.schemas.set('main', new Schema('main'));
54
- this.schemas.set('temp', new Schema('temp'));
55
- }
56
-
57
- /**
58
- * Sets the current default schema for unqualified names
59
- *
60
- * @param name Schema name to set as current
61
- */
62
- setCurrentSchema(name: string): void {
63
- if (this.schemas.has(name.toLowerCase())) {
64
- this.currentSchemaName = name.toLowerCase();
65
- } else {
66
- warnLog(`Attempted to set current schema to non-existent schema: %s`, name);
67
- }
68
- }
69
-
70
- /**
71
- * Gets the name of the current default schema
72
- *
73
- * @returns Current schema name
74
- */
75
- getCurrentSchemaName(): string {
76
- return this.currentSchemaName;
77
- }
78
-
79
- /**
80
- * Registers a virtual table module
81
- *
82
- * @param name Module name
83
- * @param module Module implementation
84
- * @param auxData Optional client data associated with the module registration
85
- */
86
- registerModule(name: string, module: AnyVirtualTableModule, auxData?: unknown): void {
87
- const lowerName = name.toLowerCase();
88
- if (this.modules.has(lowerName)) {
89
- warnLog(`Replacing existing virtual table module: %s`, lowerName);
90
- }
91
- this.modules.set(lowerName, { module, auxData });
92
- log(`Registered VTab module: %s`, lowerName);
93
- }
94
-
95
- /**
96
- * Retrieves a registered virtual table module by name
97
- *
98
- * @param name Module name to look up
99
- * @returns The module and its auxData, or undefined if not found
100
- */
101
- getModule(name: string): { module: AnyVirtualTableModule, auxData?: unknown } | undefined {
102
- return this.modules.get(name.toLowerCase());
103
- }
104
-
105
- /**
106
- * Sets the default virtual table module to use when USING is omitted
107
- *
108
- * @param name Module name. Must be a registered module.
109
- * @throws QuereusError if the module name is not registered
110
- */
111
- setDefaultVTabModuleName(name: string): void {
112
- const lowerName = name.toLowerCase();
113
- if (this.modules.has(lowerName)) {
114
- this.defaultVTabModuleName = lowerName;
115
- log(`Default VTab module name set to: %s`, lowerName);
116
- } else {
117
- warnLog(`Setting default VTab module to '${lowerName}', which is not currently registered in SchemaManager. Ensure it gets registered.`);
118
- this.defaultVTabModuleName = lowerName;
119
- }
120
- }
121
-
122
- /**
123
- * Gets the currently configured default virtual table module name
124
- *
125
- * @returns The default module name
126
- */
127
- getDefaultVTabModuleName(): string {
128
- return this.defaultVTabModuleName;
129
- }
130
-
131
- /** @internal Sets the default VTab args directly */
132
- setDefaultVTabArgs(args: Record<string, SqlValue>): void {
133
- this.defaultVTabModuleArgs = args;
134
- log('Default VTab module args set to: %o', args);
135
- }
136
-
137
- /** @internal Sets the default VTab args by parsing a JSON string */
138
- setDefaultVTabArgsFromJson(argsJsonString: string): void {
139
- try {
140
- const parsedArgs = JSON.parse(argsJsonString);
141
- if (typeof parsedArgs !== 'object') {
142
- quereusError("JSON value must be an object.", StatusCode.MISUSE);
143
- }
144
- this.setDefaultVTabArgs(parsedArgs);
145
- } catch (e) {
146
- const msg = e instanceof Error ? e.message : String(e);
147
- quereusError(`Invalid JSON for default_vtab_args: ${msg}`, StatusCode.ERROR);
148
- }
149
- }
150
-
151
- /**
152
- * Gets the default virtual table module arguments.
153
- * @returns A copy of the default arguments array.
154
- */
155
- getDefaultVTabArgs(): Record<string, SqlValue> {
156
- return { ...this.defaultVTabModuleArgs };
157
- }
158
-
159
- /**
160
- * Gets the default virtual table module name and arguments.
161
- * @returns An object containing the module name and arguments.
162
- */
163
- getDefaultVTabModule(): { name: string; args: Record<string, SqlValue> } {
164
- return {
165
- name: this.defaultVTabModuleName,
166
- args: this.defaultVTabModuleArgs,
167
- };
168
- }
169
-
170
- /**
171
- * Gets a specific schema by name
172
- *
173
- * @param name Schema name to retrieve
174
- * @returns The schema or undefined if not found
175
- */
176
- getSchema(name: string): Schema | undefined {
177
- return this.schemas.get(name.toLowerCase());
178
- }
179
-
180
- /**
181
- * Gets the 'main' schema
182
- *
183
- * @returns The main schema
184
- */
185
- getMainSchema(): Schema {
186
- return this.schemas.get('main')!;
187
- }
188
-
189
- /**
190
- * Gets the 'temp' schema
191
- *
192
- * @returns The temp schema
193
- */
194
- getTempSchema(): Schema {
195
- return this.schemas.get('temp')!;
196
- }
197
-
198
- /**
199
- * @internal Returns iterator over all managed schemas
200
- */
201
- _getAllSchemas(): IterableIterator<Schema> {
202
- return this.schemas.values();
203
- }
204
-
205
- /**
206
- * Returns all assertions across all schemas
207
- */
208
- getAllAssertions(): IntegrityAssertionSchema[] {
209
- const result: IntegrityAssertionSchema[] = [];
210
- for (const schema of this._getAllSchemas()) {
211
- for (const a of schema.getAllAssertions()) {
212
- result.push(a);
213
- }
214
- }
215
- return result;
216
- }
217
-
218
- /**
219
- * Gets the schema change notifier for listening to schema changes
220
- */
221
- getChangeNotifier(): SchemaChangeNotifier {
222
- return this.changeNotifier;
223
- }
224
-
225
- /**
226
- * Adds a new schema (e.g., for ATTACH)
227
- *
228
- * @param name Name of the schema to add
229
- * @returns The newly created schema
230
- * @throws QuereusError if the name conflicts with an existing schema
231
- */
232
- addSchema(name: string): Schema {
233
- const lowerName = name.toLowerCase();
234
- if (this.schemas.has(lowerName)) {
235
- throw new QuereusError(`Schema '${name}' already exists`, StatusCode.ERROR);
236
- }
237
- const schema = new Schema(name);
238
- this.schemas.set(lowerName, schema);
239
- log(`Added schema '%s'`, name);
240
- return schema;
241
- }
242
-
243
- /**
244
- * Removes a schema (e.g., for DETACH)
245
- *
246
- * @param name Name of the schema to remove
247
- * @returns true if found and removed, false otherwise
248
- * @throws QuereusError if attempting to remove 'main' or 'temp'
249
- */
250
- removeSchema(name: string): boolean {
251
- const lowerName = name.toLowerCase();
252
- if (lowerName === 'main' || lowerName === 'temp') {
253
- throw new QuereusError(`Cannot detach schema '${name}'`, StatusCode.ERROR);
254
- }
255
- const schema = this.schemas.get(lowerName);
256
- if (schema) {
257
- schema.clearFunctions();
258
- schema.clearTables();
259
- schema.clearViews();
260
- this.schemas.delete(lowerName);
261
- log(`Removed schema '%s'`, name);
262
- return true;
263
- }
264
- return false;
265
- }
266
-
267
- /**
268
- * @internal Finds a table or virtual table by name across schemas
269
- */
270
- _findTable(tableName: string, dbName?: string): TableSchema | undefined {
271
- const lowerTableName = tableName.toLowerCase();
272
-
273
-
274
-
275
- if (dbName) {
276
- // Search specific schema
277
- const schema = this.schemas.get(dbName.toLowerCase());
278
- return schema?.getTable(lowerTableName);
279
- } else {
280
- // Search order: main, then temp (and attached later)
281
- const mainSchema = this.schemas.get('main');
282
- let table = mainSchema?.getTable(lowerTableName);
283
- if (table) return table;
284
-
285
- const tempSchema = this.schemas.get('temp');
286
- table = tempSchema?.getTable(lowerTableName);
287
- return table;
288
- }
289
- }
290
-
291
- /**
292
- * Finds a table by name, searching schemas according to SQLite rules
293
- *
294
- * @param tableName Name of the table
295
- * @param dbName Optional specific schema name to search
296
- * @returns The TableSchema or undefined if not found
297
- */
298
- findTable(tableName: string, dbName?: string): TableSchema | undefined {
299
- return this._findTable(tableName, dbName);
300
- }
301
-
302
- /**
303
- * Finds a function by name and arg count, searching schemas
304
- *
305
- * @param funcName Name of the function
306
- * @param nArg Number of arguments
307
- * @returns The FunctionSchema or undefined if not found
308
- */
309
- findFunction(funcName: string, nArg: number): FunctionSchema | undefined {
310
- return this.getMainSchema().getFunction(funcName, nArg);
311
- }
312
-
313
- /**
314
- * Retrieves a view schema definition
315
- *
316
- * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
317
- * @param viewName The name of the view
318
- * @returns The ViewSchema or undefined if not found
319
- */
320
- getView(schemaName: string | null, viewName: string): ViewSchema | undefined {
321
- const targetSchemaName = schemaName ?? this.currentSchemaName;
322
- const schema = this.schemas.get(targetSchemaName);
323
- return schema?.getView(viewName);
324
- }
325
-
326
- /**
327
- * Retrieves any schema item (table or view) by name. Checks views first
328
- *
329
- * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
330
- * @param itemName The name of the table or view
331
- * @returns The TableSchema or ViewSchema, or undefined if not found
332
- */
333
- getSchemaItem(schemaName: string | null, itemName: string): TableSchema | ViewSchema | undefined {
334
- const targetSchemaName = schemaName ?? this.currentSchemaName;
335
- const schema = this.schemas.get(targetSchemaName);
336
- if (!schema) return undefined;
337
-
338
- // Prioritize views over tables if names conflict
339
- const view = schema.getView(itemName);
340
- if (view) return view;
341
- return schema.getTable(itemName);
342
- }
343
-
344
- /**
345
- * Drops a table from the specified schema
346
- *
347
- * @param schemaName The name of the schema
348
- * @param tableName The name of the table to drop
349
- * @param ifExists If true, do not throw an error if the table does not exist.
350
- * @returns True if the table was found and dropped, false otherwise.
351
- */
352
- dropTable(schemaName: string, tableName: string, ifExists: boolean = false): boolean {
353
- const schema = this.schemas.get(schemaName.toLowerCase()); // Ensure schemaName is lowercased for lookup
354
- if (!schema) {
355
- if (ifExists) return false; // Schema not found, but IF EXISTS specified
356
- throw new QuereusError(`Schema not found: ${schemaName}`, StatusCode.ERROR);
357
- }
358
-
359
- const tableSchema = schema.getTable(tableName); // getTable should handle case-insensitivity
360
-
361
- if (!tableSchema) {
362
- if (ifExists) {
363
- log(`Table %s.%s not found, but IF EXISTS was specified.`, schemaName, tableName);
364
- return false; // Not found, but IF EXISTS means no error, not dropped.
365
- }
366
- throw new QuereusError(`Table ${tableName} not found in schema ${schemaName}`, StatusCode.NOTFOUND);
367
- }
368
-
369
- let destroyPromise: Promise<void> | null = null;
370
-
371
- // Call destroy on the module, providing table details
372
- if (tableSchema.vtabModuleName) { // tableSchema is guaranteed to be defined here
373
- const moduleRegistration = this.getModule(tableSchema.vtabModuleName);
374
- if (moduleRegistration && moduleRegistration.module && moduleRegistration.module.destroy) {
375
- log(`Calling destroy for VTab %s.%s via module %s`, schemaName, tableName, tableSchema.vtabModuleName);
376
- destroyPromise = moduleRegistration.module.destroy(
377
- this.db,
378
- moduleRegistration.auxData,
379
- tableSchema.vtabModuleName,
380
- schemaName,
381
- tableName
382
- ).catch(err => {
383
- errorLog(`Error during VTab module destroy for %s.%s: %O`, schemaName, tableName, err);
384
- // Potentially re-throw or handle as a critical error if destroy failure is problematic
385
- });
386
- } else {
387
- warnLog(`VTab module %s (for table %s.%s) or its destroy method not found during dropTable.`, tableSchema.vtabModuleName, schemaName, tableName);
388
- }
389
- }
390
-
391
- // Remove from schema map immediately
392
- const removed = schema.removeTable(tableName);
393
- if (!removed && !ifExists) {
394
- // This should ideally not be reached if tableSchema was found above.
395
- // But as a safeguard if removeTable could fail for other reasons.
396
- throw new QuereusError(`Failed to remove table ${tableName} from schema ${schemaName}, though it was initially found.`, StatusCode.INTERNAL);
397
- }
398
-
399
- // Notify schema change listeners if table was removed
400
- if (removed) {
401
- this.changeNotifier.notifyChange({
402
- type: 'table_removed',
403
- schemaName: schemaName,
404
- objectName: tableName,
405
- oldObject: tableSchema
406
- });
407
- }
408
-
409
- // Process destruction asynchronously
410
- if (destroyPromise) {
411
- void destroyPromise.then(() => log(`destroy completed for VTab %s.%s`, schemaName, tableName));
412
- }
413
-
414
- return removed; // True if removed from schema, false if not found and ifExists was true.
415
- }
416
-
417
- /**
418
- * Drops a view from the specified schema
419
- *
420
- * @param schemaName The name of the schema
421
- * @param viewName The name of the view to drop
422
- * @returns True if the view was found and dropped, false otherwise
423
- */
424
- dropView(schemaName: string, viewName: string): boolean {
425
- const schema = this.schemas.get(schemaName);
426
- if (!schema) return false;
427
- return schema.removeView(viewName);
428
- }
429
-
430
- /**
431
- * Clears all schema items (tables, functions, views)
432
- */
433
- clearAll(): void {
434
- this.schemas.forEach(schema => {
435
- schema.clearTables();
436
- schema.clearFunctions();
437
- schema.clearViews();
438
- });
439
- log("Cleared all schemas.");
440
- }
441
-
442
- /**
443
- * Retrieves a schema object, throwing if it doesn't exist
444
- *
445
- * @param name Schema name ('main', 'temp', or custom). Case-insensitive
446
- * @returns The Schema object
447
- * @throws QuereusError if the schema does not exist
448
- */
449
- getSchemaOrFail(name: string): Schema {
450
- const schema = this.schemas.get(name.toLowerCase());
451
- if (!schema) {
452
- throw new QuereusError(`Schema not found: ${name}`);
453
- }
454
- return schema;
455
- }
456
-
457
- /**
458
- * Retrieves a table from the specified schema
459
- *
460
- * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
461
- * @param tableName The name of the table
462
- * @returns The TableSchema or undefined if not found
463
- */
464
- getTable(schemaName: string | undefined, tableName: string): TableSchema | undefined {
465
- const targetSchemaName = schemaName ?? this.currentSchemaName;
466
- const schema = this.schemas.get(targetSchemaName);
467
- return schema?.getTable(tableName);
468
- }
469
-
470
- /**
471
- * Creates a new index on an existing table based on an AST.CreateIndexStmt.
472
- * This method validates the index definition and calls the virtual table's createIndex method.
473
- *
474
- * @param stmt The AST node for the CREATE INDEX statement.
475
- * @returns A Promise that resolves when the index is created.
476
- * @throws QuereusError on errors (e.g., table not found, column not found, createIndex fails).
477
- */
478
- async createIndex(stmt: AST.CreateIndexStmt): Promise<void> {
479
- const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
480
- const tableName = stmt.table.name;
481
- const indexName = stmt.index.name;
482
-
483
- // Find the table schema
484
- const tableSchema = this.getTable(targetSchemaName, tableName);
485
- if (!tableSchema) {
486
- throw new QuereusError(`no such table: ${tableName}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
487
- }
488
-
489
- // Check if the virtual table module supports createIndex
490
- if (!tableSchema.vtabModule.createIndex) {
491
- throw new QuereusError(`Virtual table module '${tableSchema.vtabModuleName}' for table '${tableName}' does not support CREATE INDEX.`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
492
- }
493
-
494
- // Check if index already exists (if not IF NOT EXISTS)
495
- const existingIndex = tableSchema.indexes?.find(idx => idx.name.toLowerCase() === indexName.toLowerCase());
496
- if (existingIndex) {
497
- if (stmt.ifNotExists) {
498
- log(`Skipping CREATE INDEX: Index %s.%s already exists (IF NOT EXISTS).`, targetSchemaName, indexName);
499
- return;
500
- } else {
501
- throw new QuereusError(`Index ${indexName} already exists on table ${tableName}`, StatusCode.CONSTRAINT, undefined, stmt.index.loc?.start.line, stmt.index.loc?.start.column);
502
- }
503
- }
504
-
505
- // Convert AST columns to IndexSchema columns
506
- const indexColumns = stmt.columns.map((indexedCol: AST.IndexedColumn) => {
507
- if (indexedCol.expr) {
508
- throw new QuereusError(`Indices on expressions are not supported yet.`, StatusCode.ERROR, undefined, indexedCol.expr.loc?.start.line, indexedCol.expr.loc?.start.column);
509
- }
510
- const colName = indexedCol.name;
511
- if (!colName) {
512
- // Should not happen if expr is checked first
513
- throw new QuereusError(`Indexed column must be a simple column name.`, StatusCode.ERROR);
514
- }
515
- const tableColIndex = tableSchema.columnIndexMap.get(colName.toLowerCase());
516
- if (tableColIndex === undefined) {
517
- throw new QuereusError(`Column '${colName}' not found in table '${tableName}'`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
518
- }
519
- const tableColSchema = tableSchema.columns[tableColIndex];
520
- return {
521
- index: tableColIndex,
522
- desc: indexedCol.direction === 'desc',
523
- collation: indexedCol.collation || tableColSchema.collation // Use specified collation or inherit from table column
524
- };
525
- });
526
-
527
- // Construct the IndexSchema object
528
- const indexSchema: IndexSchema = {
529
- name: indexName,
530
- columns: Object.freeze(indexColumns),
531
- };
532
-
533
- try {
534
- // Call createIndex on the virtual table module
535
- await tableSchema.vtabModule.createIndex(
536
- this.db,
537
- targetSchemaName,
538
- tableName,
539
- indexSchema
540
- );
541
-
542
- // Update the table schema with the new index by creating a new schema object
543
- const updatedIndexes = [...(tableSchema.indexes || []), indexSchema];
544
- const updatedTableSchema: TableSchema = {
545
- ...tableSchema,
546
- indexes: Object.freeze(updatedIndexes),
547
- };
548
-
549
- // Replace the table schema in the schema
550
- const schema = this.getSchemaOrFail(targetSchemaName);
551
- schema.addTable(updatedTableSchema);
552
-
553
- // Notify schema change listeners that the table was modified
554
- this.changeNotifier.notifyChange({
555
- type: 'table_modified',
556
- schemaName: targetSchemaName,
557
- objectName: tableName,
558
- oldObject: tableSchema,
559
- newObject: updatedTableSchema
560
- });
561
-
562
- log(`Successfully created index %s on table %s.%s`, indexName, targetSchemaName, tableName);
563
- } catch (e: unknown) {
564
- const message = e instanceof Error ? e.message : String(e);
565
- const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
566
- throw new QuereusError(`createIndex failed for index '${indexName}' on table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
567
- }
568
- }
569
-
570
- /**
571
- * Defines a new table in the schema based on an AST.CreateTableStmt.
572
- * This method encapsulates the logic for interacting with VTab modules (create)
573
- * and registering the new table schema.
574
- *
575
- * @param stmt The AST node for the CREATE TABLE statement.
576
- * @returns A Promise that resolves to the created TableSchema.
577
- * @throws QuereusError on errors (e.g., module not found, create fails, table exists).
578
- */
579
- async createTable(stmt: AST.CreateTableStmt): Promise<TableSchema> {
580
- const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
581
- const tableName = stmt.table.name;
582
- let moduleName: string;
583
- let effectiveModuleArgs: Record<string, SqlValue>;
584
-
585
- if (stmt.moduleName) {
586
- moduleName = stmt.moduleName;
587
- effectiveModuleArgs = Object.freeze(stmt.moduleArgs || {});
588
- } else {
589
- const defaultVtab = this.getDefaultVTabModule();
590
- moduleName = defaultVtab.name;
591
- effectiveModuleArgs = Object.freeze(defaultVtab.args || {});
592
- }
593
-
594
- const moduleInfo = this.getModule(moduleName);
595
- if (!moduleInfo || !moduleInfo.module) {
596
- throw new QuereusError(`No virtual table module named '${moduleName}'`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
597
- }
598
-
599
- const astColumnsToProcess = stmt.columns || [];
600
- const astConstraintsToProcess = stmt.constraints;
601
-
602
- // Get default nullability setting from database options
603
- const defaultNullability = this.db.options.getStringOption('default_column_nullability');
604
- const defaultNotNull = defaultNullability === 'not_null';
605
-
606
- const preliminaryColumnSchemas: ColumnSchema[] = astColumnsToProcess.map(colDef => columnDefToSchema(colDef, defaultNotNull));
607
- const pkDefinition = findPKDefinition(preliminaryColumnSchemas, astConstraintsToProcess);
608
-
609
- const finalColumnSchemas = preliminaryColumnSchemas.map((col, idx) => {
610
- const isPkColumn = pkDefinition.some(pkCol => pkCol.index === idx);
611
- let pkOrder = 0;
612
- if (isPkColumn) {
613
- pkOrder = pkDefinition.findIndex(pkC => pkC.index === idx) + 1;
614
- }
615
- return {
616
- ...col,
617
- primaryKey: isPkColumn,
618
- pkOrder: pkOrder,
619
- notNull: isPkColumn ? true : col.notNull,
620
- };
621
- });
622
-
623
- const checkConstraintsSchema: RowConstraintSchema[] = [];
624
- astColumnsToProcess.forEach(colDef => {
625
- colDef.constraints?.forEach(con => {
626
- if (con.type === 'check' && con.expr) {
627
- checkConstraintsSchema.push({
628
- name: con.name ?? `_check_${colDef.name}`,
629
- expr: con.expr,
630
- operations: opsToMask(con.operations),
631
- deferrable: con.deferrable,
632
- initiallyDeferred: con.initiallyDeferred
633
- });
634
- }
635
- });
636
- });
637
- (astConstraintsToProcess || []).forEach(con => {
638
- if (con.type === 'check' && con.expr) {
639
- checkConstraintsSchema.push({
640
- name: con.name,
641
- expr: con.expr,
642
- operations: opsToMask(con.operations),
643
- deferrable: con.deferrable,
644
- initiallyDeferred: con.initiallyDeferred
645
- });
646
- }
647
- });
648
-
649
- // Process mutation context definitions if present
650
- const mutationContextSchemas = stmt.contextDefinitions
651
- ? stmt.contextDefinitions.map(varDef => mutationContextVarToSchema(varDef, defaultNotNull))
652
- : undefined;
653
-
654
- const baseTableSchema: TableSchema = {
655
- name: tableName,
656
- schemaName: targetSchemaName,
657
- columns: Object.freeze(finalColumnSchemas),
658
- columnIndexMap: buildColumnIndexMap(finalColumnSchemas),
659
- primaryKeyDefinition: pkDefinition,
660
- checkConstraints: Object.freeze(checkConstraintsSchema),
661
- isTemporary: !!stmt.isTemporary,
662
- isView: false,
663
- vtabModuleName: moduleName,
664
- vtabArgs: effectiveModuleArgs,
665
- vtabModule: moduleInfo.module,
666
- vtabAuxData: moduleInfo.auxData,
667
- estimatedRows: 0,
668
- mutationContext: mutationContextSchemas ? Object.freeze(mutationContextSchemas) : undefined,
669
- };
670
-
671
- let tableInstance: VirtualTable;
672
- try {
673
- tableInstance = moduleInfo.module.create(
674
- this.db,
675
- baseTableSchema
676
- );
677
- } catch (e: unknown) {
678
- const message = e instanceof Error ? e.message : String(e);
679
- const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
680
- throw new QuereusError(`Module '${moduleName}' create failed for table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
681
- }
682
-
683
- const schema = this.getSchema(targetSchemaName);
684
- if (!schema) {
685
- throw new QuereusError(`Internal error: Schema '${targetSchemaName}' not found.`, StatusCode.INTERNAL);
686
- }
687
-
688
- const finalRegisteredSchema = tableInstance.tableSchema;
689
- if (!finalRegisteredSchema) {
690
- throw new QuereusError(`Module '${moduleName}' create did not provide a tableSchema for '${tableName}'.`, StatusCode.INTERNAL);
691
- }
692
-
693
- // Create a properly typed schema object instead of mutating properties
694
- let correctedSchema = finalRegisteredSchema;
695
- if (finalRegisteredSchema.name.toLowerCase() !== tableName.toLowerCase() ||
696
- finalRegisteredSchema.schemaName.toLowerCase() !== targetSchemaName.toLowerCase()) {
697
- warnLog(`Module ${moduleName} returned schema for ${finalRegisteredSchema.schemaName}.${finalRegisteredSchema.name} but expected ${targetSchemaName}.${tableName}. Correcting name/schemaName.`);
698
- correctedSchema = {
699
- ...finalRegisteredSchema,
700
- name: tableName,
701
- schemaName: targetSchemaName,
702
- };
703
- }
704
-
705
- // Ensure all required properties are properly set
706
- const completeTableSchema: TableSchema = {
707
- ...correctedSchema,
708
- vtabModuleName: moduleName,
709
- vtabArgs: effectiveModuleArgs,
710
- vtabModule: moduleInfo.module,
711
- vtabAuxData: moduleInfo.auxData,
712
- estimatedRows: correctedSchema.estimatedRows ?? 0,
713
- };
714
-
715
- const existingTable = schema.getTable(tableName);
716
- const existingView = schema.getView(tableName);
717
-
718
- if (existingTable || existingView) {
719
- if (stmt.ifNotExists) {
720
- log(`Skipping CREATE TABLE: Item %s.%s already exists (IF NOT EXISTS).`, targetSchemaName, tableName);
721
- if (existingTable) return existingTable;
722
- throw new QuereusError(`Cannot CREATE TABLE ${targetSchemaName}.${tableName}: a VIEW with the same name already exists.`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
723
- } else {
724
- const itemType = existingTable ? 'Table' : 'View';
725
- throw new QuereusError(`${itemType} ${targetSchemaName}.${tableName} already exists`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
726
- }
727
- }
728
-
729
- schema.addTable(completeTableSchema);
730
- log(`Successfully created table %s.%s using module %s`, targetSchemaName, tableName, moduleName);
731
-
732
- // Notify schema change listeners
733
- this.changeNotifier.notifyChange({
734
- type: 'table_added',
735
- schemaName: targetSchemaName,
736
- objectName: tableName,
737
- newObject: completeTableSchema
738
- });
739
-
740
- return completeTableSchema;
741
- }
742
- }
1
+ import { Schema } from './schema.js';
2
+ import type { IntegrityAssertionSchema } from './assertion.js';
3
+ import type { Database } from '../core/database.js';
4
+ import type { TableSchema, RowConstraintSchema, IndexSchema } from './table.js';
5
+ import type { FunctionSchema } from './function.js';
6
+ import { quereusError, QuereusError } from '../common/errors.js';
7
+ import { StatusCode, type SqlValue } from '../common/types.js';
8
+ import type { AnyVirtualTableModule, BaseModuleConfig } from '../vtab/module.js';
9
+ import type { VirtualTable } from '../vtab/table.js';
10
+ import type { ColumnSchema } from './column.js';
11
+ import { buildColumnIndexMap, columnDefToSchema, findPKDefinition, opsToMask, mutationContextVarToSchema } from './table.js';
12
+ import type { ViewSchema } from './view.js';
13
+ import { createLogger } from '../common/logger.js';
14
+ import type * as AST from '../parser/ast.js';
15
+ import { SchemaChangeNotifier } from './change-events.js';
16
+ import { checkDeterministic } 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';
23
+
24
+ const log = createLogger('schema:manager');
25
+ const warnLog = log.extend('warn');
26
+ const errorLog = log.extend('error');
27
+
28
+ /**
29
+ * Generic options passed to VTab modules during CREATE TABLE.
30
+ * Modules are responsible for interpreting these.
31
+ */
32
+ export interface GenericModuleCallOptions extends BaseModuleConfig {
33
+ moduleArgs?: readonly string[];
34
+ statementColumns?: readonly AST.ColumnDef[];
35
+ statementConstraints?: readonly AST.TableConstraint[];
36
+ isTemporary?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Manages all schemas associated with a database connection (main, temp, attached).
41
+ * Handles lookup resolution according to SQLite's rules.
42
+ */
43
+ export class SchemaManager {
44
+ private schemas: Map<string, Schema> = new Map();
45
+ private currentSchemaName: string = 'main';
46
+ private modules: Map<string, { module: AnyVirtualTableModule, auxData?: unknown }> = new Map();
47
+ private defaultVTabModuleName: string = 'memory';
48
+ private defaultVTabModuleArgs: Record<string, SqlValue> = {};
49
+ private db: Database;
50
+ private changeNotifier = new SchemaChangeNotifier();
51
+
52
+ /**
53
+ * Creates a new schema manager
54
+ *
55
+ * @param db Reference to the parent Database instance
56
+ */
57
+ constructor(db: Database) {
58
+ this.db = db;
59
+ // Ensure 'main' and 'temp' schemas always exist
60
+ this.schemas.set('main', new Schema('main'));
61
+ this.schemas.set('temp', new Schema('temp'));
62
+ }
63
+
64
+ /**
65
+ * Sets the current default schema for unqualified names
66
+ *
67
+ * @param name Schema name to set as current
68
+ */
69
+ setCurrentSchema(name: string): void {
70
+ if (this.schemas.has(name.toLowerCase())) {
71
+ this.currentSchemaName = name.toLowerCase();
72
+ } else {
73
+ warnLog(`Attempted to set current schema to non-existent schema: %s`, name);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Gets the name of the current default schema
79
+ *
80
+ * @returns Current schema name
81
+ */
82
+ getCurrentSchemaName(): string {
83
+ return this.currentSchemaName;
84
+ }
85
+
86
+ /**
87
+ * Registers a virtual table module
88
+ *
89
+ * @param name Module name
90
+ * @param module Module implementation
91
+ * @param auxData Optional client data associated with the module registration
92
+ */
93
+ registerModule(name: string, module: AnyVirtualTableModule, auxData?: unknown): void {
94
+ const lowerName = name.toLowerCase();
95
+ if (this.modules.has(lowerName)) {
96
+ warnLog(`Replacing existing virtual table module: %s`, lowerName);
97
+ }
98
+ this.modules.set(lowerName, { module, auxData });
99
+ log(`Registered VTab module: %s`, lowerName);
100
+ }
101
+
102
+ /**
103
+ * Retrieves a registered virtual table module by name
104
+ *
105
+ * @param name Module name to look up
106
+ * @returns The module and its auxData, or undefined if not found
107
+ */
108
+ getModule(name: string): { module: AnyVirtualTableModule, auxData?: unknown } | undefined {
109
+ return this.modules.get(name.toLowerCase());
110
+ }
111
+
112
+ /**
113
+ * Sets the default virtual table module to use when USING is omitted
114
+ *
115
+ * @param name Module name. Must be a registered module.
116
+ * @throws QuereusError if the module name is not registered
117
+ */
118
+ setDefaultVTabModuleName(name: string): void {
119
+ const lowerName = name.toLowerCase();
120
+ if (this.modules.has(lowerName)) {
121
+ this.defaultVTabModuleName = lowerName;
122
+ log(`Default VTab module name set to: %s`, lowerName);
123
+ } else {
124
+ warnLog(`Setting default VTab module to '${lowerName}', which is not currently registered in SchemaManager. Ensure it gets registered.`);
125
+ this.defaultVTabModuleName = lowerName;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Gets the currently configured default virtual table module name
131
+ *
132
+ * @returns The default module name
133
+ */
134
+ getDefaultVTabModuleName(): string {
135
+ return this.defaultVTabModuleName;
136
+ }
137
+
138
+ /** @internal Sets the default VTab args directly */
139
+ setDefaultVTabArgs(args: Record<string, SqlValue>): void {
140
+ this.defaultVTabModuleArgs = args;
141
+ log('Default VTab module args set to: %o', args);
142
+ }
143
+
144
+ /** @internal Sets the default VTab args by parsing a JSON string */
145
+ setDefaultVTabArgsFromJson(argsJsonString: string): void {
146
+ try {
147
+ const parsedArgs = JSON.parse(argsJsonString);
148
+ if (typeof parsedArgs !== 'object') {
149
+ quereusError("JSON value must be an object.", StatusCode.MISUSE);
150
+ }
151
+ this.setDefaultVTabArgs(parsedArgs);
152
+ } catch (e) {
153
+ const msg = e instanceof Error ? e.message : String(e);
154
+ quereusError(`Invalid JSON for default_vtab_args: ${msg}`, StatusCode.ERROR);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Gets the default virtual table module arguments.
160
+ * @returns A copy of the default arguments array.
161
+ */
162
+ getDefaultVTabArgs(): Record<string, SqlValue> {
163
+ return { ...this.defaultVTabModuleArgs };
164
+ }
165
+
166
+ /**
167
+ * Gets the default virtual table module name and arguments.
168
+ * @returns An object containing the module name and arguments.
169
+ */
170
+ getDefaultVTabModule(): { name: string; args: Record<string, SqlValue> } {
171
+ return {
172
+ name: this.defaultVTabModuleName,
173
+ args: this.defaultVTabModuleArgs,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Gets a specific schema by name
179
+ *
180
+ * @param name Schema name to retrieve
181
+ * @returns The schema or undefined if not found
182
+ */
183
+ getSchema(name: string): Schema | undefined {
184
+ return this.schemas.get(name.toLowerCase());
185
+ }
186
+
187
+ /**
188
+ * Gets the 'main' schema
189
+ *
190
+ * @returns The main schema
191
+ */
192
+ getMainSchema(): Schema {
193
+ return this.schemas.get('main')!;
194
+ }
195
+
196
+ /**
197
+ * Gets the 'temp' schema
198
+ *
199
+ * @returns The temp schema
200
+ */
201
+ getTempSchema(): Schema {
202
+ return this.schemas.get('temp')!;
203
+ }
204
+
205
+ /**
206
+ * @internal Returns iterator over all managed schemas
207
+ */
208
+ _getAllSchemas(): IterableIterator<Schema> {
209
+ return this.schemas.values();
210
+ }
211
+
212
+ /**
213
+ * Returns all assertions across all schemas
214
+ */
215
+ getAllAssertions(): IntegrityAssertionSchema[] {
216
+ const result: IntegrityAssertionSchema[] = [];
217
+ for (const schema of this._getAllSchemas()) {
218
+ for (const a of schema.getAllAssertions()) {
219
+ result.push(a);
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+
225
+ /**
226
+ * Gets the schema change notifier for listening to schema changes
227
+ */
228
+ getChangeNotifier(): SchemaChangeNotifier {
229
+ return this.changeNotifier;
230
+ }
231
+
232
+ /**
233
+ * Adds a new schema (e.g., for ATTACH)
234
+ *
235
+ * @param name Name of the schema to add
236
+ * @returns The newly created schema
237
+ * @throws QuereusError if the name conflicts with an existing schema
238
+ */
239
+ addSchema(name: string): Schema {
240
+ const lowerName = name.toLowerCase();
241
+ if (this.schemas.has(lowerName)) {
242
+ throw new QuereusError(`Schema '${name}' already exists`, StatusCode.ERROR);
243
+ }
244
+ const schema = new Schema(name);
245
+ this.schemas.set(lowerName, schema);
246
+ log(`Added schema '%s'`, name);
247
+ return schema;
248
+ }
249
+
250
+ /**
251
+ * Removes a schema (e.g., for DETACH)
252
+ *
253
+ * @param name Name of the schema to remove
254
+ * @returns true if found and removed, false otherwise
255
+ * @throws QuereusError if attempting to remove 'main' or 'temp'
256
+ */
257
+ removeSchema(name: string): boolean {
258
+ const lowerName = name.toLowerCase();
259
+ if (lowerName === 'main' || lowerName === 'temp') {
260
+ throw new QuereusError(`Cannot detach schema '${name}'`, StatusCode.ERROR);
261
+ }
262
+ const schema = this.schemas.get(lowerName);
263
+ if (schema) {
264
+ schema.clearFunctions();
265
+ schema.clearTables();
266
+ schema.clearViews();
267
+ this.schemas.delete(lowerName);
268
+ log(`Removed schema '%s'`, name);
269
+ return true;
270
+ }
271
+ return false;
272
+ }
273
+
274
+ /**
275
+ * @internal Finds a table or virtual table by name across schemas
276
+ */
277
+ _findTable(tableName: string, dbName?: string): TableSchema | undefined {
278
+ const lowerTableName = tableName.toLowerCase();
279
+
280
+
281
+
282
+ if (dbName) {
283
+ // Search specific schema
284
+ const schema = this.schemas.get(dbName.toLowerCase());
285
+ return schema?.getTable(lowerTableName);
286
+ } else {
287
+ // Search order: main, then temp (and attached later)
288
+ const mainSchema = this.schemas.get('main');
289
+ let table = mainSchema?.getTable(lowerTableName);
290
+ if (table) return table;
291
+
292
+ const tempSchema = this.schemas.get('temp');
293
+ table = tempSchema?.getTable(lowerTableName);
294
+ return table;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Finds a table by name, searching schemas according to SQLite rules
300
+ *
301
+ * @param tableName Name of the table
302
+ * @param dbName Optional specific schema name to search
303
+ * @returns The TableSchema or undefined if not found
304
+ */
305
+ findTable(tableName: string, dbName?: string): TableSchema | undefined {
306
+ return this._findTable(tableName, dbName);
307
+ }
308
+
309
+ /**
310
+ * Finds a function by name and arg count, searching schemas
311
+ *
312
+ * @param funcName Name of the function
313
+ * @param nArg Number of arguments
314
+ * @returns The FunctionSchema or undefined if not found
315
+ */
316
+ findFunction(funcName: string, nArg: number): FunctionSchema | undefined {
317
+ return this.getMainSchema().getFunction(funcName, nArg);
318
+ }
319
+
320
+ /**
321
+ * Retrieves a view schema definition
322
+ *
323
+ * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
324
+ * @param viewName The name of the view
325
+ * @returns The ViewSchema or undefined if not found
326
+ */
327
+ getView(schemaName: string | null, viewName: string): ViewSchema | undefined {
328
+ const targetSchemaName = schemaName ?? this.currentSchemaName;
329
+ const schema = this.schemas.get(targetSchemaName);
330
+ return schema?.getView(viewName);
331
+ }
332
+
333
+ /**
334
+ * Retrieves any schema item (table or view) by name. Checks views first
335
+ *
336
+ * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
337
+ * @param itemName The name of the table or view
338
+ * @returns The TableSchema or ViewSchema, or undefined if not found
339
+ */
340
+ getSchemaItem(schemaName: string | null, itemName: string): TableSchema | ViewSchema | undefined {
341
+ const targetSchemaName = schemaName ?? this.currentSchemaName;
342
+ const schema = this.schemas.get(targetSchemaName);
343
+ if (!schema) return undefined;
344
+
345
+ // Prioritize views over tables if names conflict
346
+ const view = schema.getView(itemName);
347
+ if (view) return view;
348
+ return schema.getTable(itemName);
349
+ }
350
+
351
+ /**
352
+ * Drops a table from the specified schema
353
+ *
354
+ * @param schemaName The name of the schema
355
+ * @param tableName The name of the table to drop
356
+ * @param ifExists If true, do not throw an error if the table does not exist.
357
+ * @returns True if the table was found and dropped, false otherwise.
358
+ */
359
+ dropTable(schemaName: string, tableName: string, ifExists: boolean = false): boolean {
360
+ const schema = this.schemas.get(schemaName.toLowerCase()); // Ensure schemaName is lowercased for lookup
361
+ if (!schema) {
362
+ if (ifExists) return false; // Schema not found, but IF EXISTS specified
363
+ throw new QuereusError(`Schema not found: ${schemaName}`, StatusCode.ERROR);
364
+ }
365
+
366
+ const tableSchema = schema.getTable(tableName); // getTable should handle case-insensitivity
367
+
368
+ if (!tableSchema) {
369
+ if (ifExists) {
370
+ log(`Table %s.%s not found, but IF EXISTS was specified.`, schemaName, tableName);
371
+ return false; // Not found, but IF EXISTS means no error, not dropped.
372
+ }
373
+ throw new QuereusError(`Table ${tableName} not found in schema ${schemaName}`, StatusCode.NOTFOUND);
374
+ }
375
+
376
+ let destroyPromise: Promise<void> | null = null;
377
+
378
+ // Call destroy on the module, providing table details
379
+ if (tableSchema.vtabModuleName) { // tableSchema is guaranteed to be defined here
380
+ const moduleRegistration = this.getModule(tableSchema.vtabModuleName);
381
+ if (moduleRegistration && moduleRegistration.module && moduleRegistration.module.destroy) {
382
+ log(`Calling destroy for VTab %s.%s via module %s`, schemaName, tableName, tableSchema.vtabModuleName);
383
+ destroyPromise = moduleRegistration.module.destroy(
384
+ this.db,
385
+ moduleRegistration.auxData,
386
+ tableSchema.vtabModuleName,
387
+ schemaName,
388
+ tableName
389
+ ).catch(err => {
390
+ errorLog(`Error during VTab module destroy for %s.%s: %O`, schemaName, tableName, err);
391
+ // Potentially re-throw or handle as a critical error if destroy failure is problematic
392
+ });
393
+ } else {
394
+ warnLog(`VTab module %s (for table %s.%s) or its destroy method not found during dropTable.`, tableSchema.vtabModuleName, schemaName, tableName);
395
+ }
396
+ }
397
+
398
+ // Remove from schema map immediately
399
+ const removed = schema.removeTable(tableName);
400
+ if (!removed && !ifExists) {
401
+ // This should ideally not be reached if tableSchema was found above.
402
+ // But as a safeguard if removeTable could fail for other reasons.
403
+ throw new QuereusError(`Failed to remove table ${tableName} from schema ${schemaName}, though it was initially found.`, StatusCode.INTERNAL);
404
+ }
405
+
406
+ // Notify schema change listeners if table was removed
407
+ if (removed) {
408
+ this.changeNotifier.notifyChange({
409
+ type: 'table_removed',
410
+ schemaName: schemaName,
411
+ objectName: tableName,
412
+ oldObject: tableSchema
413
+ });
414
+ }
415
+
416
+ // Process destruction asynchronously
417
+ if (destroyPromise) {
418
+ void destroyPromise.then(() => log(`destroy completed for VTab %s.%s`, schemaName, tableName));
419
+ }
420
+
421
+ return removed; // True if removed from schema, false if not found and ifExists was true.
422
+ }
423
+
424
+ /**
425
+ * Drops a view from the specified schema
426
+ *
427
+ * @param schemaName The name of the schema
428
+ * @param viewName The name of the view to drop
429
+ * @returns True if the view was found and dropped, false otherwise
430
+ */
431
+ dropView(schemaName: string, viewName: string): boolean {
432
+ const schema = this.schemas.get(schemaName);
433
+ if (!schema) return false;
434
+ return schema.removeView(viewName);
435
+ }
436
+
437
+ /**
438
+ * Clears all schema items (tables, functions, views)
439
+ */
440
+ clearAll(): void {
441
+ this.schemas.forEach(schema => {
442
+ schema.clearTables();
443
+ schema.clearFunctions();
444
+ schema.clearViews();
445
+ });
446
+ log("Cleared all schemas.");
447
+ }
448
+
449
+ /**
450
+ * Retrieves a schema object, throwing if it doesn't exist
451
+ *
452
+ * @param name Schema name ('main', 'temp', or custom). Case-insensitive
453
+ * @returns The Schema object
454
+ * @throws QuereusError if the schema does not exist
455
+ */
456
+ getSchemaOrFail(name: string): Schema {
457
+ const schema = this.schemas.get(name.toLowerCase());
458
+ if (!schema) {
459
+ throw new QuereusError(`Schema not found: ${name}`);
460
+ }
461
+ return schema;
462
+ }
463
+
464
+ /**
465
+ * Retrieves a table from the specified schema
466
+ *
467
+ * @param schemaName The name of the schema ('main', 'temp', etc.). Defaults to current schema
468
+ * @param tableName The name of the table
469
+ * @returns The TableSchema or undefined if not found
470
+ */
471
+ getTable(schemaName: string | undefined, tableName: string): TableSchema | undefined {
472
+ const targetSchemaName = schemaName ?? this.currentSchemaName;
473
+ const schema = this.schemas.get(targetSchemaName);
474
+ return schema?.getTable(tableName);
475
+ }
476
+
477
+ /**
478
+ * Creates a new index on an existing table based on an AST.CreateIndexStmt.
479
+ * This method validates the index definition and calls the virtual table's createIndex method.
480
+ *
481
+ * @param stmt The AST node for the CREATE INDEX statement.
482
+ * @returns A Promise that resolves when the index is created.
483
+ * @throws QuereusError on errors (e.g., table not found, column not found, createIndex fails).
484
+ */
485
+ async createIndex(stmt: AST.CreateIndexStmt): Promise<void> {
486
+ const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
487
+ const tableName = stmt.table.name;
488
+ const indexName = stmt.index.name;
489
+
490
+ // Find the table schema
491
+ const tableSchema = this.getTable(targetSchemaName, tableName);
492
+ if (!tableSchema) {
493
+ throw new QuereusError(`no such table: ${tableName}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
494
+ }
495
+
496
+ // Check if the virtual table module supports createIndex
497
+ if (!tableSchema.vtabModule.createIndex) {
498
+ throw new QuereusError(`Virtual table module '${tableSchema.vtabModuleName}' for table '${tableName}' does not support CREATE INDEX.`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
499
+ }
500
+
501
+ // Check if index already exists (if not IF NOT EXISTS)
502
+ const existingIndex = tableSchema.indexes?.find(idx => idx.name.toLowerCase() === indexName.toLowerCase());
503
+ if (existingIndex) {
504
+ if (stmt.ifNotExists) {
505
+ log(`Skipping CREATE INDEX: Index %s.%s already exists (IF NOT EXISTS).`, targetSchemaName, indexName);
506
+ return;
507
+ } else {
508
+ throw new QuereusError(`Index ${indexName} already exists on table ${tableName}`, StatusCode.CONSTRAINT, undefined, stmt.index.loc?.start.line, stmt.index.loc?.start.column);
509
+ }
510
+ }
511
+
512
+ // Convert AST columns to IndexSchema columns
513
+ const indexColumns = stmt.columns.map((indexedCol: AST.IndexedColumn) => {
514
+ if (indexedCol.expr) {
515
+ throw new QuereusError(`Indices on expressions are not supported yet.`, StatusCode.ERROR, undefined, indexedCol.expr.loc?.start.line, indexedCol.expr.loc?.start.column);
516
+ }
517
+ const colName = indexedCol.name;
518
+ if (!colName) {
519
+ // Should not happen if expr is checked first
520
+ throw new QuereusError(`Indexed column must be a simple column name.`, StatusCode.ERROR);
521
+ }
522
+ const tableColIndex = tableSchema.columnIndexMap.get(colName.toLowerCase());
523
+ if (tableColIndex === undefined) {
524
+ throw new QuereusError(`Column '${colName}' not found in table '${tableName}'`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
525
+ }
526
+ const tableColSchema = tableSchema.columns[tableColIndex];
527
+ return {
528
+ index: tableColIndex,
529
+ desc: indexedCol.direction === 'desc',
530
+ collation: indexedCol.collation || tableColSchema.collation // Use specified collation or inherit from table column
531
+ };
532
+ });
533
+
534
+ // Construct the IndexSchema object
535
+ const indexSchema: IndexSchema = {
536
+ name: indexName,
537
+ columns: Object.freeze(indexColumns),
538
+ };
539
+
540
+ try {
541
+ // Call createIndex on the virtual table module
542
+ await tableSchema.vtabModule.createIndex(
543
+ this.db,
544
+ targetSchemaName,
545
+ tableName,
546
+ indexSchema
547
+ );
548
+
549
+ // Update the table schema with the new index by creating a new schema object
550
+ const updatedIndexes = [...(tableSchema.indexes || []), indexSchema];
551
+ const updatedTableSchema: TableSchema = {
552
+ ...tableSchema,
553
+ indexes: Object.freeze(updatedIndexes),
554
+ };
555
+
556
+ // Replace the table schema in the schema
557
+ const schema = this.getSchemaOrFail(targetSchemaName);
558
+ schema.addTable(updatedTableSchema);
559
+
560
+ // Notify schema change listeners that the table was modified
561
+ this.changeNotifier.notifyChange({
562
+ type: 'table_modified',
563
+ schemaName: targetSchemaName,
564
+ objectName: tableName,
565
+ oldObject: tableSchema,
566
+ newObject: updatedTableSchema
567
+ });
568
+
569
+ log(`Successfully created index %s on table %s.%s`, indexName, targetSchemaName, tableName);
570
+ } catch (e: unknown) {
571
+ const message = e instanceof Error ? e.message : String(e);
572
+ const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
573
+ throw new QuereusError(`createIndex failed for index '${indexName}' on table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Defines a new table in the schema based on an AST.CreateTableStmt.
579
+ * This method encapsulates the logic for interacting with VTab modules (create)
580
+ * and registering the new table schema.
581
+ *
582
+ * @param stmt The AST node for the CREATE TABLE statement.
583
+ * @returns A Promise that resolves to the created TableSchema.
584
+ * @throws QuereusError on errors (e.g., module not found, create fails, table exists).
585
+ */
586
+ async createTable(stmt: AST.CreateTableStmt): Promise<TableSchema> {
587
+ const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
588
+ const tableName = stmt.table.name;
589
+ let moduleName: string;
590
+ let effectiveModuleArgs: Record<string, SqlValue>;
591
+
592
+ if (stmt.moduleName) {
593
+ moduleName = stmt.moduleName;
594
+ effectiveModuleArgs = Object.freeze(stmt.moduleArgs || {});
595
+ } else {
596
+ const defaultVtab = this.getDefaultVTabModule();
597
+ moduleName = defaultVtab.name;
598
+ effectiveModuleArgs = Object.freeze(defaultVtab.args || {});
599
+ }
600
+
601
+ const moduleInfo = this.getModule(moduleName);
602
+ if (!moduleInfo || !moduleInfo.module) {
603
+ throw new QuereusError(`No virtual table module named '${moduleName}'`, StatusCode.ERROR, undefined, stmt.loc?.start.line, stmt.loc?.start.column);
604
+ }
605
+
606
+ const astColumnsToProcess = stmt.columns || [];
607
+ const astConstraintsToProcess = stmt.constraints;
608
+
609
+ // Get default nullability setting from database options
610
+ const defaultNullability = this.db.options.getStringOption('default_column_nullability');
611
+ const defaultNotNull = defaultNullability === 'not_null';
612
+
613
+ const preliminaryColumnSchemas: ColumnSchema[] = astColumnsToProcess.map(colDef => columnDefToSchema(colDef, defaultNotNull));
614
+ const pkDefinition = findPKDefinition(preliminaryColumnSchemas, astConstraintsToProcess);
615
+
616
+ const finalColumnSchemas = preliminaryColumnSchemas.map((col, idx) => {
617
+ const isPkColumn = pkDefinition.some(pkCol => pkCol.index === idx);
618
+ let pkOrder = 0;
619
+ if (isPkColumn) {
620
+ pkOrder = pkDefinition.findIndex(pkC => pkC.index === idx) + 1;
621
+ }
622
+ return {
623
+ ...col,
624
+ primaryKey: isPkColumn,
625
+ pkOrder: pkOrder,
626
+ notNull: isPkColumn ? true : col.notNull,
627
+ };
628
+ });
629
+
630
+ const checkConstraintsSchema: RowConstraintSchema[] = [];
631
+ astColumnsToProcess.forEach(colDef => {
632
+ colDef.constraints?.forEach(con => {
633
+ if (con.type === 'check' && con.expr) {
634
+ checkConstraintsSchema.push({
635
+ name: con.name ?? `_check_${colDef.name}`,
636
+ expr: con.expr,
637
+ operations: opsToMask(con.operations),
638
+ deferrable: con.deferrable,
639
+ initiallyDeferred: con.initiallyDeferred
640
+ });
641
+ }
642
+ });
643
+ });
644
+ (astConstraintsToProcess || []).forEach(con => {
645
+ if (con.type === 'check' && con.expr) {
646
+ checkConstraintsSchema.push({
647
+ name: con.name,
648
+ expr: con.expr,
649
+ operations: opsToMask(con.operations),
650
+ deferrable: con.deferrable,
651
+ initiallyDeferred: con.initiallyDeferred
652
+ });
653
+ }
654
+ });
655
+
656
+ // Process mutation context definitions if present
657
+ const mutationContextSchemas = stmt.contextDefinitions
658
+ ? stmt.contextDefinitions.map(varDef => mutationContextVarToSchema(varDef, defaultNotNull))
659
+ : undefined;
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
+ let defaultExpr: ScalarPlanNode | undefined;
687
+ try {
688
+ // Try to build the expression - may fail if it references columns that don't exist yet
689
+ defaultExpr = buildExpression(planningCtx, col.defaultValue as AST.Expression) as ScalarPlanNode;
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
+ // If expression built successfully, check determinism (non-throwing)
698
+ if (defaultExpr) {
699
+ const result = checkDeterministic(defaultExpr);
700
+ if (!result.valid) {
701
+ throw new QuereusError(
702
+ `Non-deterministic expression not allowed in DEFAULT for column '${col.name}' in table '${tableName}'. ` +
703
+ `Expression: ${result.expression}. ` +
704
+ `Use mutation context to pass non-deterministic values (e.g., WITH CONTEXT (timestamp = datetime('now'))).`,
705
+ StatusCode.ERROR
706
+ );
707
+ }
708
+ }
709
+ }
710
+ }
711
+
712
+ const baseTableSchema: TableSchema = {
713
+ name: tableName,
714
+ schemaName: targetSchemaName,
715
+ columns: Object.freeze(finalColumnSchemas),
716
+ columnIndexMap: buildColumnIndexMap(finalColumnSchemas),
717
+ primaryKeyDefinition: pkDefinition,
718
+ checkConstraints: Object.freeze(checkConstraintsSchema),
719
+ isTemporary: !!stmt.isTemporary,
720
+ isView: false,
721
+ vtabModuleName: moduleName,
722
+ vtabArgs: effectiveModuleArgs,
723
+ vtabModule: moduleInfo.module,
724
+ vtabAuxData: moduleInfo.auxData,
725
+ estimatedRows: 0,
726
+ mutationContext: mutationContextSchemas ? Object.freeze(mutationContextSchemas) : undefined,
727
+ };
728
+
729
+ let tableInstance: VirtualTable;
730
+ try {
731
+ tableInstance = moduleInfo.module.create(
732
+ this.db,
733
+ baseTableSchema
734
+ );
735
+ } catch (e: unknown) {
736
+ const message = e instanceof Error ? e.message : String(e);
737
+ const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
738
+ throw new QuereusError(`Module '${moduleName}' create failed for table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
739
+ }
740
+
741
+ const schema = this.getSchema(targetSchemaName);
742
+ if (!schema) {
743
+ throw new QuereusError(`Internal error: Schema '${targetSchemaName}' not found.`, StatusCode.INTERNAL);
744
+ }
745
+
746
+ const finalRegisteredSchema = tableInstance.tableSchema;
747
+ if (!finalRegisteredSchema) {
748
+ throw new QuereusError(`Module '${moduleName}' create did not provide a tableSchema for '${tableName}'.`, StatusCode.INTERNAL);
749
+ }
750
+
751
+ // Create a properly typed schema object instead of mutating properties
752
+ let correctedSchema = finalRegisteredSchema;
753
+ if (finalRegisteredSchema.name.toLowerCase() !== tableName.toLowerCase() ||
754
+ finalRegisteredSchema.schemaName.toLowerCase() !== targetSchemaName.toLowerCase()) {
755
+ warnLog(`Module ${moduleName} returned schema for ${finalRegisteredSchema.schemaName}.${finalRegisteredSchema.name} but expected ${targetSchemaName}.${tableName}. Correcting name/schemaName.`);
756
+ correctedSchema = {
757
+ ...finalRegisteredSchema,
758
+ name: tableName,
759
+ schemaName: targetSchemaName,
760
+ };
761
+ }
762
+
763
+ // Ensure all required properties are properly set
764
+ const completeTableSchema: TableSchema = {
765
+ ...correctedSchema,
766
+ vtabModuleName: moduleName,
767
+ vtabArgs: effectiveModuleArgs,
768
+ vtabModule: moduleInfo.module,
769
+ vtabAuxData: moduleInfo.auxData,
770
+ estimatedRows: correctedSchema.estimatedRows ?? 0,
771
+ };
772
+
773
+ const existingTable = schema.getTable(tableName);
774
+ const existingView = schema.getView(tableName);
775
+
776
+ if (existingTable || existingView) {
777
+ if (stmt.ifNotExists) {
778
+ log(`Skipping CREATE TABLE: Item %s.%s already exists (IF NOT EXISTS).`, targetSchemaName, tableName);
779
+ if (existingTable) return existingTable;
780
+ throw new QuereusError(`Cannot CREATE TABLE ${targetSchemaName}.${tableName}: a VIEW with the same name already exists.`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
781
+ } else {
782
+ const itemType = existingTable ? 'Table' : 'View';
783
+ throw new QuereusError(`${itemType} ${targetSchemaName}.${tableName} already exists`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
784
+ }
785
+ }
786
+
787
+ schema.addTable(completeTableSchema);
788
+ log(`Successfully created table %s.%s using module %s`, targetSchemaName, tableName, moduleName);
789
+
790
+ // Notify schema change listeners
791
+ this.changeNotifier.notifyChange({
792
+ type: 'table_added',
793
+ schemaName: targetSchemaName,
794
+ objectName: tableName,
795
+ newObject: completeTableSchema
796
+ });
797
+
798
+ return completeTableSchema;
799
+ }
800
+ }