@quereus/quereus 0.4.8 → 0.4.10

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 (40) hide show
  1. package/dist/src/config/loader.d.ts +41 -0
  2. package/dist/src/config/loader.d.ts.map +1 -0
  3. package/dist/src/config/loader.js +102 -0
  4. package/dist/src/config/loader.js.map +1 -0
  5. package/dist/src/core/database.d.ts +0 -7
  6. package/dist/src/core/database.d.ts.map +1 -1
  7. package/dist/src/core/database.js +48 -28
  8. package/dist/src/core/database.js.map +1 -1
  9. package/dist/src/index.d.ts +2 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +2 -0
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/parser/parser.d.ts.map +1 -1
  14. package/dist/src/parser/parser.js +32 -6
  15. package/dist/src/parser/parser.js.map +1 -1
  16. package/dist/src/planner/building/create-view.d.ts.map +1 -1
  17. package/dist/src/planner/building/create-view.js +3 -25
  18. package/dist/src/planner/building/create-view.js.map +1 -1
  19. package/dist/src/planner/building/select.d.ts.map +1 -1
  20. package/dist/src/planner/building/select.js +20 -1
  21. package/dist/src/planner/building/select.js.map +1 -1
  22. package/dist/src/schema/catalog.d.ts.map +1 -1
  23. package/dist/src/schema/catalog.js +18 -3
  24. package/dist/src/schema/catalog.js.map +1 -1
  25. package/dist/src/schema/schema-differ.js +3 -3
  26. package/dist/src/schema/schema-differ.js.map +1 -1
  27. package/dist/src/util/ast-stringify.d.ts +1 -0
  28. package/dist/src/util/ast-stringify.d.ts.map +1 -1
  29. package/dist/src/util/ast-stringify.js +1 -1
  30. package/dist/src/util/ast-stringify.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/config/loader.ts +140 -0
  33. package/src/core/database.ts +54 -34
  34. package/src/index.ts +9 -0
  35. package/src/parser/parser.ts +28 -6
  36. package/src/planner/building/create-view.ts +3 -30
  37. package/src/planner/building/select.ts +29 -1
  38. package/src/schema/catalog.ts +17 -3
  39. package/src/schema/schema-differ.ts +3 -3
  40. package/src/util/ast-stringify.ts +1 -1
@@ -217,6 +217,34 @@ export class Database {
217
217
  * @returns A Promise resolving when execution completes.
218
218
  * @throws QuereusError on failure.
219
219
  */
220
+ /**
221
+ * @internal
222
+ * Executes a single AST statement without transaction management.
223
+ * Used by both exec() and eval() to avoid code duplication.
224
+ */
225
+ private async _executeStatement(statementAst: AST.Statement, params?: SqlParameters | SqlValue[]): Promise<void> {
226
+ const plan = this._buildPlan([statementAst], params);
227
+
228
+ if (plan.statements.length === 0) return; // No-op for this AST
229
+
230
+ const optimizedPlan = this.optimizer.optimize(plan, this) as BlockNode;
231
+ const emissionContext = new EmissionContext(this);
232
+ const rootInstruction = emitPlanNode(optimizedPlan, emissionContext);
233
+ const scheduler = new Scheduler(rootInstruction);
234
+
235
+ const runtimeCtx: RuntimeContext = {
236
+ db: this,
237
+ stmt: undefined,
238
+ params: params ?? {},
239
+ context: new Map(),
240
+ tableContexts: new Map(),
241
+ tracer: this.instructionTracer,
242
+ enableMetrics: this.options.getBooleanOption('runtime_stats'),
243
+ };
244
+
245
+ await scheduler.run(runtimeCtx);
246
+ }
247
+
220
248
  async exec(
221
249
  sql: string,
222
250
  params?: SqlParameters,
@@ -250,39 +278,13 @@ export class Database {
250
278
  }
251
279
 
252
280
  for (let i = 0; i < batch.length; i++) {
253
- const statementAst = batch[i];
254
- let plan: BlockNode;
255
-
256
281
  try {
257
- plan = this._buildPlan([statementAst], params);
258
-
259
- if (plan.statements.length === 0) continue; // No-op for this AST
260
-
261
- const optimizedPlan = this.optimizer.optimize(plan, this) as BlockNode;
262
-
263
- const emissionContext = new EmissionContext(this);
264
- const rootInstruction = emitPlanNode(optimizedPlan, emissionContext);
265
-
266
- const scheduler = new Scheduler(rootInstruction);
267
-
268
- const runtimeCtx: RuntimeContext = {
269
- db: this,
270
- stmt: undefined,
271
- params: params ?? {},
272
- context: new Map(),
273
- tableContexts: new Map(),
274
- tracer: this.instructionTracer,
275
- enableMetrics: this.options.getBooleanOption('runtime_stats'),
276
- };
277
-
278
- void await scheduler.run(runtimeCtx);
279
- // Nothing to do with the result, this is executed for side effects only
282
+ await this._executeStatement(batch[i], params);
280
283
  } catch (err) {
281
284
  const error = err instanceof Error ? err : new Error(String(err));
282
285
  executionError = error instanceof QuereusError ? error : new QuereusError(error.message, StatusCode.ERROR, error);
283
286
  break; // Stop processing further statements on error
284
287
  }
285
- // No explicit finalize for transient plan/scheduler used in exec loop
286
288
  }
287
289
 
288
290
  } finally {
@@ -755,17 +757,35 @@ export class Database {
755
757
  let stmt: Statement | null = null;
756
758
  try {
757
759
  stmt = this.prepare(sql);
758
- if (stmt.astBatch.length > 1) {
759
- warnLog(`Database.eval called with multi-statement SQL. Only results from the first statement will be yielded.`);
760
- }
761
760
 
762
- if (stmt.astBatch.length > 0) { // Check if there are any statements to execute
763
- // If currentAstIndex defaults to 0 and astBatch is not empty, this will run the first statement.
764
- yield* stmt.all(params);
765
- } else {
761
+ if (stmt.astBatch.length === 0) {
766
762
  // No statements, yield nothing.
767
763
  return;
768
764
  }
765
+
766
+ if (stmt.astBatch.length > 1) {
767
+ // Multi-statement batch: execute all but the last statement,
768
+ // then yield results from the last statement
769
+ const parser = new Parser();
770
+ const batch = parser.parseAll(sql);
771
+
772
+ // Execute all statements except the last one
773
+ for (let i = 0; i < batch.length - 1; i++) {
774
+ await this._executeStatement(batch[i], params);
775
+ }
776
+
777
+ // Now prepare and execute the last statement to yield its results
778
+ const lastStmt = new Statement(this, [batch[batch.length - 1]]);
779
+ this.statements.add(lastStmt);
780
+ try {
781
+ yield* lastStmt.all(params);
782
+ } finally {
783
+ await lastStmt.finalize();
784
+ }
785
+ } else {
786
+ // Single statement: execute and yield results
787
+ yield* stmt.all(params);
788
+ }
769
789
  } finally {
770
790
  if (stmt) { await stmt.finalize(); }
771
791
  }
package/src/index.ts CHANGED
@@ -93,6 +93,15 @@ export type {
93
93
  PluginRegistrations
94
94
  } from './vtab/manifest.js';
95
95
 
96
+ // Re-export config system
97
+ export {
98
+ interpolateEnvVars,
99
+ interpolateConfigEnvVars,
100
+ loadPluginsFromConfig,
101
+ validateConfig
102
+ } from './config/loader.js';
103
+ export type { QuoombConfig, PluginConfig } from './config/loader.js';
104
+
96
105
  // Re-export virtual table framework
97
106
  export type { VirtualTableModule } from './vtab/module.js';
98
107
 
@@ -2048,12 +2048,23 @@ export class Parser {
2048
2048
  if (this.matchKeyword('USING')) {
2049
2049
  moduleName = this.consumeIdentifier("Expected module name after 'USING'.");
2050
2050
  if (this.match(TokenType.LPAREN)) {
2051
+ let positionalIndex = 0;
2051
2052
  if (!this.check(TokenType.RPAREN)) {
2052
2053
  do {
2053
- const nameValue = this.nameValueItem('module argument');
2054
- moduleArgs[nameValue.name] = nameValue.value && nameValue.value.type === 'literal'
2055
- ? getSyncLiteral(nameValue.value)
2056
- : (nameValue.value && nameValue.value.type === 'identifier' ? nameValue.value.name : nameValue.name);
2054
+ // Check if this is a positional argument (string/number literal) or named argument (identifier=value)
2055
+ if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT)) {
2056
+ // Positional argument
2057
+ const token = this.advance();
2058
+ moduleArgs[String(positionalIndex++)] = token.literal;
2059
+ } else if (this.check(TokenType.IDENTIFIER)) {
2060
+ // Could be named argument or identifier value
2061
+ const nameValue = this.nameValueItem('module argument');
2062
+ moduleArgs[nameValue.name] = nameValue.value && nameValue.value.type === 'literal'
2063
+ ? getSyncLiteral(nameValue.value)
2064
+ : (nameValue.value && nameValue.value.type === 'identifier' ? nameValue.value.name : nameValue.name);
2065
+ } else {
2066
+ throw this.error(this.peek(), "Expected module argument (string, number, or name=value pair).");
2067
+ }
2057
2068
  } while (this.match(TokenType.COMMA));
2058
2069
  }
2059
2070
  this.consume(TokenType.RPAREN, "Expected ')' after module arguments.");
@@ -2452,10 +2463,21 @@ export class Parser {
2452
2463
  }
2453
2464
  if (this.match(TokenType.LPAREN)) {
2454
2465
  moduleArgs = {};
2466
+ let positionalIndex = 0;
2455
2467
  if (!this.check(TokenType.RPAREN)) {
2456
2468
  do {
2457
- const nv = this.nameValueItem('module argument');
2458
- moduleArgs[nv.name] = nv.value && nv.value.type === 'literal' ? getSyncLiteral(nv.value) : (nv.value && nv.value.type === 'identifier' ? nv.value.name : null);
2469
+ // Check if this is a positional argument (string/number literal) or named argument (identifier=value)
2470
+ if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT)) {
2471
+ // Positional argument
2472
+ const token = this.advance();
2473
+ moduleArgs[String(positionalIndex++)] = token.literal;
2474
+ } else if (this.check(TokenType.IDENTIFIER)) {
2475
+ // Could be named argument or identifier value
2476
+ const nv = this.nameValueItem('module argument');
2477
+ moduleArgs[nv.name] = nv.value && nv.value.type === 'literal' ? getSyncLiteral(nv.value) : (nv.value && nv.value.type === 'identifier' ? nv.value.name : null);
2478
+ } else {
2479
+ throw this.error(this.peek(), "Expected module argument (string, number, or name=value pair).");
2480
+ }
2459
2481
  } while (this.match(TokenType.COMMA));
2460
2482
  }
2461
2483
  this.consume(TokenType.RPAREN, "Expected ')' after module arguments.");
@@ -1,6 +1,7 @@
1
1
  import type * as AST from '../../parser/ast.js';
2
2
  import type { PlanningContext } from '../planning-context.js';
3
3
  import { CreateViewNode } from '../nodes/create-view-node.js';
4
+ import { createViewToString } from '../../util/ast-stringify.js';
4
5
 
5
6
  /**
6
7
  * Builds a plan node for CREATE VIEW statements.
@@ -11,9 +12,8 @@ export function buildCreateViewStmt(ctx: PlanningContext, stmt: AST.CreateViewSt
11
12
  const viewName = stmt.view.name;
12
13
 
13
14
  // The original SQL text is needed for the view definition
14
- // For now, we'll reconstruct it from the AST
15
- // In a full implementation, this should be preserved from the original input
16
- const sql = reconstructViewSQL(stmt);
15
+ // Reconstruct it from the AST using the proper stringifier
16
+ const sql = createViewToString(stmt);
17
17
 
18
18
  return new CreateViewNode(
19
19
  ctx.scope,
@@ -26,31 +26,4 @@ export function buildCreateViewStmt(ctx: PlanningContext, stmt: AST.CreateViewSt
26
26
  );
27
27
  }
28
28
 
29
- /**
30
- * Reconstructs the original SQL text for a CREATE VIEW statement.
31
- * This is a simplified version - in production, the original text should be preserved.
32
- */
33
- function reconstructViewSQL(stmt: AST.CreateViewStmt): string {
34
- let sql = 'CREATE VIEW ';
35
-
36
- if (stmt.ifNotExists) {
37
- sql += 'IF NOT EXISTS ';
38
- }
39
-
40
- if (stmt.view.schema) {
41
- sql += `${stmt.view.schema}.`;
42
- }
43
- sql += stmt.view.name;
44
29
 
45
- if (stmt.columns && stmt.columns.length > 0) {
46
- sql += ` (${stmt.columns.join(', ')})`;
47
- }
48
-
49
- sql += ' AS ';
50
-
51
- // For now, just add a placeholder for the SELECT statement
52
- // In a full implementation, this would reconstruct the full SELECT
53
- sql += '(SELECT statement)';
54
-
55
- return sql;
56
- }
@@ -333,7 +333,35 @@ export function buildFrom(fromClause: AST.FromClause, parentContext: PlanningCon
333
333
 
334
334
  if (viewSchema) {
335
335
  // Build the view's SELECT statement
336
- fromTable = buildSelectStmt(parentContext, viewSchema.selectAst, cteNodes) as RelationalPlanNode;
336
+ let viewSelectNode = buildSelectStmt(parentContext, viewSchema.selectAst, cteNodes) as RelationalPlanNode;
337
+
338
+ // If the view has explicit column names, wrap with a projection to rename columns
339
+ if (viewSchema.columns && viewSchema.columns.length > 0) {
340
+ const viewAttributes = viewSelectNode.getAttributes();
341
+ const projections = viewSchema.columns.map((columnName, i) => {
342
+ if (i >= viewAttributes.length) {
343
+ throw new QuereusError(
344
+ `View '${viewSchema.name}' has more explicit column names than SELECT columns`,
345
+ StatusCode.ERROR
346
+ );
347
+ }
348
+ const attr = viewAttributes[i];
349
+ const columnRef = new ColumnReferenceNode(
350
+ parentContext.scope,
351
+ { type: 'column', name: attr.name } as AST.ColumnExpr,
352
+ attr.type,
353
+ attr.id,
354
+ i
355
+ );
356
+ return {
357
+ node: columnRef,
358
+ alias: columnName
359
+ };
360
+ });
361
+ fromTable = new ProjectNode(parentContext.scope, viewSelectNode, projections);
362
+ } else {
363
+ fromTable = viewSelectNode;
364
+ }
337
365
 
338
366
  // Create scope for view columns
339
367
  const viewScope = new RegisteredScope(parentContext.scope);
@@ -2,7 +2,7 @@ import type { Database } from '../core/database.js';
2
2
  import type { TableSchema } from './table.js';
3
3
  import type { ViewSchema } from './view.js';
4
4
  import type { IntegrityAssertionSchema } from './assertion.js';
5
- import { createTableToString } from '../util/ast-stringify.js';
5
+ import { createTableToString, createViewToString } from '../util/ast-stringify.js';
6
6
  import type * as AST from '../parser/ast.js';
7
7
 
8
8
  /**
@@ -196,9 +196,23 @@ export function generateDeclaredDDL(declaredSchema: AST.DeclareSchemaStmt, targe
196
196
  case 'declaredIndex':
197
197
  // TODO: Implement index DDL generation
198
198
  break;
199
- case 'declaredView':
200
- // TODO: Implement view DDL generation
199
+ case 'declaredView': {
200
+ // Qualify view name with schema if specified
201
+ const viewStmt = item.viewStmt;
202
+ if (targetSchema && targetSchema !== 'main' && !viewStmt.view.schema) {
203
+ const qualifiedStmt: AST.CreateViewStmt = {
204
+ ...viewStmt,
205
+ view: {
206
+ ...viewStmt.view,
207
+ schema: targetSchema
208
+ }
209
+ };
210
+ ddlStatements.push(createViewToString(qualifiedStmt));
211
+ } else {
212
+ ddlStatements.push(createViewToString(viewStmt));
213
+ }
201
214
  break;
215
+ }
202
216
  }
203
217
  }
204
218
 
@@ -1,6 +1,6 @@
1
1
  import type { SchemaCatalog } from './catalog.js';
2
2
  import type * as AST from '../parser/ast.js';
3
- import { createTableToString } from '../util/ast-stringify.js';
3
+ import { createTableToString, createViewToString } from '../util/ast-stringify.js';
4
4
 
5
5
  /**
6
6
  * Represents the difference between a declared schema and actual database state
@@ -104,8 +104,8 @@ export function computeSchemaDiff(
104
104
  // Find views to create/drop
105
105
  for (const [name, declaredView] of declaredViews) {
106
106
  if (!actualViews.has(name)) {
107
- // TODO: Generate view DDL
108
- diff.viewsToCreate.push(`CREATE VIEW "${declaredView.viewStmt.view.name}" AS SELECT 1`);
107
+ // Generate proper view DDL using AST stringifier
108
+ diff.viewsToCreate.push(createViewToString(declaredView.viewStmt));
109
109
  }
110
110
  }
111
111
 
@@ -629,7 +629,7 @@ function createIndexToString(stmt: AST.CreateIndexStmt): string {
629
629
  return parts.join(' ');
630
630
  }
631
631
 
632
- function createViewToString(stmt: AST.CreateViewStmt): string {
632
+ export function createViewToString(stmt: AST.CreateViewStmt): string {
633
633
  const parts: string[] = ['create'];
634
634
  if (stmt.isTemporary) parts.push('temp');
635
635
  parts.push('view');