@minnowdb/core 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ npm install @minnowdb/core
17
17
  - A ready-made worker client with the same everyday database API.
18
18
  - TypeScript schema declarations and metadata-only migrations, including SQL domains,
19
19
  composite primary/foreign keys, and informational relationships.
20
- - Guarded columnar upserts, result-column SQL domains, typed catalog errors, and batch writes.
20
+ - Batch and upsert write APIs, typed catalog errors, and per-column result type metadata.
21
21
  - Pull-driven query cursors, live queries, snapshots, compaction, and configurable query memory.
22
22
 
23
23
  Use [the PostgreSQL compatibility page](https://minnowdb.com/docs/sql/feature-matrix/) for the
@@ -27,7 +27,8 @@ workers, transactions, and API details.
27
27
  The optional [Kysely dialect](https://minnowdb.com/docs/adapters/kysely/) connects Kysely's
28
28
  PostgreSQL compiler to the engine and derives its `DB` types from the same schema declaration.
29
29
 
30
- Every `@minnowdb` package in one application must use the same major version. See
30
+ Minnow is 0.x: breaking changes land in minor releases, so pin exact versions and upgrade
31
+ `@minnowdb` packages together. See
31
32
  [Versioning](https://minnowdb.com/docs/reference/versioning/).
32
33
 
33
34
  ## License
@@ -15,7 +15,7 @@ import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize
15
15
  import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, } from "../storage/types.js";
16
16
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
17
17
  import { Snapshot, TransactionManager, } from "../transactions/index.js";
18
- import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
18
+ import { applyWindowFunctions, bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
19
19
  import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
20
20
  import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
21
21
  import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
@@ -353,6 +353,99 @@ function insertStatementBatch(statement) {
353
353
  rowCount: statement.rows.length,
354
354
  };
355
355
  }
356
+ /** Resolves CURRENT_* in every retained DML expression with one statement-start clock. */
357
+ function resolveMutationStatementDatetimes(statement, now) {
358
+ const plan = (block) => resolveStatementDatetimes({ ...block, usesStatementDatetime: true }, now);
359
+ const expression = (value) => {
360
+ const resolved = plan({
361
+ sql: "(statement expression)",
362
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
363
+ joins: [],
364
+ select: [{ expression: value, alias: "value" }],
365
+ predicates: [],
366
+ groupBy: [],
367
+ having: [],
368
+ orderBy: [],
369
+ });
370
+ return resolved.select[0]?.expression ?? value;
371
+ };
372
+ const predicates = (values) => values.map((predicate) => ({
373
+ ...predicate,
374
+ left: expression(predicate.left),
375
+ right: expression(predicate.right),
376
+ }));
377
+ if (statement.kind === "insert") {
378
+ return {
379
+ ...statement,
380
+ rows: statement.rows.map((row) => row.map((value) => isDeferredInsertExpression(value) ? { expression: expression(value.expression) } : value)),
381
+ ...(statement.query === undefined ? {} : { query: plan(statement.query) }),
382
+ ...(statement.onConflict === undefined
383
+ ? {}
384
+ : {
385
+ onConflict: {
386
+ ...statement.onConflict,
387
+ ...(statement.onConflict.assignments === undefined
388
+ ? {}
389
+ : {
390
+ assignments: statement.onConflict.assignments.map((assignment) => ({
391
+ ...assignment,
392
+ expression: expression(assignment.expression),
393
+ })),
394
+ }),
395
+ ...(statement.onConflict.where === undefined
396
+ ? {}
397
+ : { where: expression(statement.onConflict.where) }),
398
+ },
399
+ }),
400
+ };
401
+ }
402
+ if (statement.kind === "update") {
403
+ return {
404
+ ...statement,
405
+ assignments: statement.assignments.map((assignment) => ({
406
+ ...assignment,
407
+ expression: expression(assignment.expression),
408
+ })),
409
+ predicates: predicates(statement.predicates),
410
+ };
411
+ }
412
+ if (statement.kind === "delete") {
413
+ return { ...statement, predicates: predicates(statement.predicates) };
414
+ }
415
+ if (statement.kind === "merge") {
416
+ return {
417
+ ...statement,
418
+ on: expression(statement.on),
419
+ branches: statement.branches.map((branch) => {
420
+ const condition = branch.condition === undefined ? {} : { condition: expression(branch.condition) };
421
+ if (branch.when === "not-matched") {
422
+ return {
423
+ ...branch,
424
+ ...condition,
425
+ action: { ...branch.action, values: branch.action.values.map(expression) },
426
+ };
427
+ }
428
+ return {
429
+ ...branch,
430
+ ...condition,
431
+ action: branch.action.kind === "update"
432
+ ? {
433
+ ...branch.action,
434
+ assignments: branch.action.assignments.map((assignment) => ({
435
+ ...assignment,
436
+ expression: expression(assignment.expression),
437
+ })),
438
+ }
439
+ : branch.action,
440
+ };
441
+ }),
442
+ };
443
+ }
444
+ if (statement.kind === "create-table-as") {
445
+ return { ...statement, query: plan(statement.query) };
446
+ }
447
+ return statement;
448
+ }
356
449
  /** Quotes a catalog identifier for internally generated SQL. */
357
450
  function quoteSqlIdentifier(identifier) {
358
451
  return `"${identifier.replaceAll('"', '""')}"`;
@@ -1697,6 +1790,41 @@ export class MinnowDatabase {
1697
1790
  }
1698
1791
  throw new TypeError(`DEFAULT produced an unsupported value: ${sql}`);
1699
1792
  }
1793
+ /** Materializes volatile/catalog-backed INSERT values once per execution, never in the cache. */
1794
+ async #materializeInsertExpressions(statement, statementNow) {
1795
+ if (!statement.rows.some((row) => row.some(isDeferredInsertExpression)))
1796
+ return statement;
1797
+ const rows = [];
1798
+ for (const row of statement.rows) {
1799
+ const materialized = [];
1800
+ for (const value of row) {
1801
+ if (!isDeferredInsertExpression(value)) {
1802
+ materialized.push(value);
1803
+ continue;
1804
+ }
1805
+ let plan = {
1806
+ sql: "(insert value)",
1807
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
1808
+ joins: [],
1809
+ select: [{ expression: value.expression, alias: "value" }],
1810
+ predicates: [],
1811
+ groupBy: [],
1812
+ having: [],
1813
+ orderBy: [],
1814
+ usesStatementDatetime: true,
1815
+ usesSequenceCalls: true,
1816
+ usesVolatileFunctions: true,
1817
+ };
1818
+ // Every CURRENT_* occurrence in all rows sees the same statement clock. RANDOM,
1819
+ // GEN_RANDOM_UUID, and NEXTVAL remain per expression, as SQL requires.
1820
+ plan = resolveStatementDatetimes(plan, statementNow);
1821
+ const result = await this.#queryCompiled(plan, { memoize: false });
1822
+ materialized.push(result.rows[0]?.value ?? null);
1823
+ }
1824
+ rows.push(materialized);
1825
+ }
1826
+ return { ...statement, rows };
1827
+ }
1700
1828
  async insert(tableName, row) {
1701
1829
  return this.insertBatch(tableName, [row]);
1702
1830
  }
@@ -3352,28 +3480,31 @@ export class MinnowDatabase {
3352
3480
  if (domains.size > 0 && planReadsTable(rewritten, (name) => domains.has(name))) {
3353
3481
  rewritten = normalizePlanDomainLiterals(rewritten, domains);
3354
3482
  }
3355
- const qualified = qualifyCorrelatedReferences(rewritten, catalogColumns);
3356
- if (qualified !== rewritten)
3357
- rewritten = optimizePlan(qualified);
3358
- if (!aliased && !natural)
3359
- return rewritten;
3360
- // These two need column *order*, which only the records carry; both are rare enough that
3361
- // reading the catalog for them is fine.
3362
- const columns = new Map((await this.store.listTables()).map((table) => [
3363
- table.name,
3364
- table.columns.map(({ name }) => name),
3365
- ]));
3366
- const columnsOf = (name) => columns.get(name);
3483
+ const columnsOf = (name) => {
3484
+ const columns = catalogColumns.get(name);
3485
+ if (columns === undefined)
3486
+ throw new UnknownTableError(name);
3487
+ return columns;
3488
+ };
3489
+ rewritten = bindPendingSelectShapes(rewritten, columnsOf);
3367
3490
  if (aliased)
3368
3491
  rewritten = expandSourceColumnAliases(rewritten, columnsOf);
3369
- return natural ? expandNaturalJoins(rewritten, columnsOf) : rewritten;
3492
+ if (natural)
3493
+ rewritten = expandNaturalJoins(rewritten, columnsOf);
3494
+ const qualified = qualifyCorrelatedReferences(rewritten, catalogColumns);
3495
+ return qualified === rewritten ? rewritten : optimizePlan(qualified);
3370
3496
  }
3371
3497
  /** Resolves connection-local sequence calls before either synchronous executor sees the plan. */
3372
3498
  async #resolveSequenceCalls(plan) {
3373
3499
  const usesSequence = (expression) => (expression.kind === "call" &&
3374
3500
  (expression.name === "NEXTVAL" || expression.name === "CURRVAL")) ||
3375
3501
  childExpressions(expression).some(usesSequence);
3376
- const selected = new Set(plan.select.map((item) => item.expression));
3502
+ const selected = new Set();
3503
+ const markSelected = (expression) => {
3504
+ selected.add(expression);
3505
+ childExpressions(expression).forEach(markSelected);
3506
+ };
3507
+ plan.select.forEach((item) => markSelected(item.expression));
3377
3508
  const sequenceExpressions = [];
3378
3509
  forEachBlockExpression(plan, (expression) => {
3379
3510
  if (!usesSequence(expression))
@@ -4785,7 +4916,7 @@ export class MinnowDatabase {
4785
4916
  }
4786
4917
  for (const trigger of triggers) {
4787
4918
  for (const statement of trigger.statements) {
4788
- const compiled = compileStatement(statement.sql);
4919
+ const compiled = resolveMutationStatementDatetimes(compileStatement(statement.sql), this.#now());
4789
4920
  if (compiled.kind === "insert") {
4790
4921
  await this.#applyTriggerInsertBody(transaction, compiled, statement.bindings, rowCount, valueAt, cascadeBudget);
4791
4922
  continue;
@@ -4810,8 +4941,9 @@ export class MinnowDatabase {
4810
4941
  if (target.uniqueKeyColumnId !== undefined) {
4811
4942
  throw new TypeError(`Trigger bodies insert into keyless tables only: ${target.name}`);
4812
4943
  }
4813
- const input = insertStatementBatch({ ...compiled, rows: derivedRows });
4814
4944
  const statementNow = this.#now();
4945
+ const materialized = await this.#materializeInsertExpressions({ ...compiled, rows: derivedRows }, statementNow);
4946
+ const input = insertStatementBatch(materialized);
4815
4947
  const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
4816
4948
  normalizeDomainBatch(target, filled.batch);
4817
4949
  fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
@@ -6961,9 +7093,10 @@ export class MinnowDatabase {
6961
7093
  if (statement.query === undefined)
6962
7094
  return statement;
6963
7095
  const target = await this.#findTable(statement.table);
7096
+ const plan = await this.#applyCatalogRewrites(statement.query);
6964
7097
  let result;
6965
7098
  if (writer === undefined) {
6966
- const prepared = await this.#prepareCompiledPlan(statement.query);
7099
+ const prepared = await this.#prepareCompiledPlan(plan);
6967
7100
  try {
6968
7101
  result = prepared.execute();
6969
7102
  }
@@ -6972,14 +7105,22 @@ export class MinnowDatabase {
6972
7105
  }
6973
7106
  }
6974
7107
  else {
6975
- const plan = await this.#applyCatalogRewrites(statement.query);
6976
7108
  result = await writer.queryPlan(plan);
6977
7109
  }
7110
+ const insertColumns = statement.columns.length > 0
7111
+ ? statement.columns
7112
+ : visibleTableColumns(target).map(({ name }) => name);
7113
+ if (result.columns.length !== insertColumns.length) {
7114
+ throw new TypeError(statement.columns.length > 0
7115
+ ? "INSERT ... SELECT must produce exactly the insert column count"
7116
+ : "INSERT ... SELECT must produce exactly the table column count");
7117
+ }
6978
7118
  const { query, ...rest } = statement;
6979
7119
  void query;
6980
- const targetColumns = statement.columns.map((name) => target.columns.find((column) => column.name === name));
7120
+ const targetColumns = insertColumns.map((name) => target.columns.find((column) => column.name === name));
6981
7121
  return {
6982
7122
  ...rest,
7123
+ columns: insertColumns,
6983
7124
  rows: result.rows.map((row) => result.columns.map((column, position) => storedSqlValueFromExecution(targetColumns[position], row[column] ?? null))),
6984
7125
  };
6985
7126
  }
@@ -6991,6 +7132,8 @@ export class MinnowDatabase {
6991
7132
  * (the snapshot row with assignments applied — the exact values the mutation wrote).
6992
7133
  */
6993
7134
  async runStatement(statement, options = {}) {
7135
+ const statementNow = this.#now();
7136
+ statement = resolveMutationStatementDatetimes(statement, statementNow);
6994
7137
  if (statement.kind === "select") {
6995
7138
  return { kind: "rows", result: await this.query(statement.sql) };
6996
7139
  }
@@ -7110,7 +7253,8 @@ export class MinnowDatabase {
7110
7253
  ...(sqlDomain === undefined ? {} : { sqlDomain }),
7111
7254
  })),
7112
7255
  ]));
7113
- const tableColumns = inferBlockSchema(statement.query, schemas).map(({ name, type, integer, sqlDomain }) => ({
7256
+ const plan = await this.#applyCatalogRewrites(statement.query, before);
7257
+ const tableColumns = inferBlockSchema(plan, schemas).map(({ name, type, integer, sqlDomain }) => ({
7114
7258
  id: this.#createId(),
7115
7259
  name: validateName(name, "Column"),
7116
7260
  type,
@@ -7128,7 +7272,6 @@ export class MinnowDatabase {
7128
7272
  };
7129
7273
  const transaction = await this.#transactions.beginWithPendingTable(pendingTable, before.catalogEpoch);
7130
7274
  try {
7131
- const plan = await this.#applyCatalogRewrites(statement.query);
7132
7275
  const executionMemoryBudgetBytes = Math.min(this.#queryExecutionMemoryBudgetBytes, DEFAULT_QUERY_MEMORY_BUDGET_BYTES);
7133
7276
  const stageBatchRows = 16_384;
7134
7277
  let nextRowId = 1n;
@@ -7393,6 +7536,9 @@ export class MinnowDatabase {
7393
7536
  if (statement.kind === "transaction") {
7394
7537
  return this.#runTransactionStatement(statement.action, statement.name);
7395
7538
  }
7539
+ if (statement.kind === "insert") {
7540
+ statement = await this.#materializeInsertExpressions(statement, statementNow);
7541
+ }
7396
7542
  if (options.writer === undefined) {
7397
7543
  // Selection, assignment evaluation, constraint proofs, trigger derivation, and the write
7398
7544
  // itself are one statement transaction. A genuine concurrent data commit does not rebase
@@ -7421,13 +7567,12 @@ export class MinnowDatabase {
7421
7567
  }
7422
7568
  }
7423
7569
  }
7570
+ if (statement.kind === "insert" && statement.query !== undefined) {
7571
+ statement = await this.#materializeInsertSelect(statement, options.writer);
7572
+ }
7424
7573
  if (statement.kind === "insert" && statement.columns.length === 0) {
7425
7574
  const table = await this.#findTable(statement.table);
7426
7575
  const columns = visibleTableColumns(table).map((column) => column.name);
7427
- const producedColumns = statement.query?.select.length;
7428
- if (producedColumns !== undefined && producedColumns !== columns.length) {
7429
- throw new TypeError("INSERT ... SELECT must produce exactly the table column count");
7430
- }
7431
7576
  if (statement.defaultValues === true) {
7432
7577
  // Keep the empty column list: omission, not NULL, is what invokes catalog defaults.
7433
7578
  }
@@ -7438,13 +7583,13 @@ export class MinnowDatabase {
7438
7583
  statement = { ...statement, columns };
7439
7584
  }
7440
7585
  }
7586
+ if (statement.kind === "insert") {
7587
+ statement = await this.#materializeInsertExpressions(statement, statementNow);
7588
+ }
7441
7589
  // A RETURNING clause parsed from SQL text applies unless the caller overrides it.
7442
7590
  if (statement.returning !== undefined && options.returning === undefined) {
7443
7591
  options = { ...options, returning: statement.returning };
7444
7592
  }
7445
- if (statement.kind === "insert" && statement.query !== undefined) {
7446
- statement = await this.#materializeInsertSelect(statement, options.writer);
7447
- }
7448
7593
  if (statement.kind === "insert" && statement.onConflict?.action === "nothing") {
7449
7594
  statement = await this.#filterConflictingInsertRows(statement, options.writer);
7450
7595
  }
@@ -105,10 +105,14 @@ export interface UniqueConstraintDefinition {
105
105
  interface DefaultInsertValue {
106
106
  readonly default: true;
107
107
  }
108
- export type InsertValue = QueryValue | DefaultInsertValue | {
108
+ interface DeferredInsertExpression {
109
+ readonly expression: Expression;
110
+ }
111
+ export type InsertValue = QueryValue | DefaultInsertValue | DeferredInsertExpression | {
109
112
  parameter: number;
110
113
  };
111
114
  export declare function isDefaultInsertValue(value: InsertValue): value is DefaultInsertValue;
115
+ export declare function isDeferredInsertExpression(value: InsertValue): value is DeferredInsertExpression;
112
116
  export type CompiledStatement = {
113
117
  kind: "select";
114
118
  sql: string;
@@ -626,6 +630,15 @@ export declare function planReadsViews(plan: CompiledQuery, isView: (name: strin
626
630
  */
627
631
  export declare function expandViewSources(plan: CompiledQuery, viewFor: (tableName: string) => CompiledQuery | undefined, maxDepth?: number): CompiledQuery;
628
632
  export declare function planHasSourceColumnAliases(plan: CompiledQuery): boolean;
633
+ /** Whether any block still needs source schemas before its SELECT shape can be lowered. */
634
+ export declare function planHasPendingSelectShapes(plan: CompiledQuery): boolean;
635
+ /**
636
+ * Expands schema-dependent SELECT wildcards, then sends the now-concrete block through the same
637
+ * lowering used by ordinary named select lists. This is the single boundary at which wildcard
638
+ * width becomes plan shape: DISTINCT, windows, ORDER BY expressions/ordinals, set operations,
639
+ * CTE/derived column lists, grouping sets, and FULL JOIN therefore cannot disagree about it.
640
+ */
641
+ export declare function bindPendingSelectShapes(plan: CompiledQuery, columnsOf: (tableName: string) => readonly string[] | undefined): CompiledQuery;
629
642
  /** Whether any block of the plan still carries an unresolved NATURAL join marker. */
630
643
  export declare function planHasNaturalJoins(plan: CompiledQuery): boolean;
631
644
  /**
@@ -890,7 +890,11 @@ export function compileQuery(sql, options = {}) {
890
890
  let compiled;
891
891
  try {
892
892
  resolvePlanExactNumericConstants(plan);
893
- compiled = options.optimize === false ? plan : optimizePlan(plan);
893
+ if (options.optimize === false && planHasPendingSelectShapes(plan)) {
894
+ plan.preserveUnoptimizedShape = true;
895
+ }
896
+ compiled =
897
+ options.optimize === false || planHasPendingSelectShapes(plan) ? plan : optimizePlan(plan);
894
898
  }
895
899
  catch (error) {
896
900
  // Compile-time rewrites (for example decorrelation) reject unsupported shapes; those
@@ -925,6 +929,9 @@ function columnDefaultFor(expression, sql) {
925
929
  export function isDefaultInsertValue(value) {
926
930
  return (typeof value === "object" && value !== null && !(value instanceof Date) && "default" in value);
927
931
  }
932
+ export function isDeferredInsertExpression(value) {
933
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "expression" in value);
934
+ }
928
935
  /**
929
936
  * Parses CREATE TRIGGER name AFTER INSERT|UPDATE|DELETE ON table [FOR EACH ROW]
930
937
  * BEGIN insert; ... END. Body statements are INSERT ... VALUES with NEW.col / OLD.col
@@ -2365,8 +2372,9 @@ function trimPreparedResults(prepared, trim) {
2365
2372
  }
2366
2373
  export function createPreparedQuery(plan, tables, options = {}) {
2367
2374
  assertTailParametersBound(plan);
2368
- validateGrouping(plan);
2369
2375
  plan = resolveStatementDatetimes(plan);
2376
+ plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
2377
+ validateGrouping(plan);
2370
2378
  // The schema-dependent rewrites run before derived sources materialize, because both can
2371
2379
  // turn a scanned table into one more derived block.
2372
2380
  plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
@@ -2411,16 +2419,17 @@ export function createPreparedQuery(plan, tables, options = {}) {
2411
2419
  /** Internal columnar entry point used after MinnowDatabase materializes a stable snapshot. */
2412
2420
  export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), preparedOptions = {}) {
2413
2421
  plan = resolveStatementDatetimes(plan);
2414
- const ties = withTiesPlan(plan);
2415
- if (ties.plan !== plan) {
2416
- return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
2417
- }
2418
2422
  const columnarColumns = (tableName) => {
2419
2423
  const table = tables.get(tableName);
2420
2424
  return table === undefined
2421
2425
  ? undefined
2422
2426
  : [...table.columns.keys()].filter((name) => !name.startsWith("\0"));
2423
2427
  };
2428
+ plan = bindPendingSelectShapes(plan, columnarColumns);
2429
+ const ties = withTiesPlan(plan);
2430
+ if (ties.plan !== plan) {
2431
+ return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
2432
+ }
2424
2433
  plan = expandSourceColumnAliases(plan, columnarColumns);
2425
2434
  plan = expandNaturalJoins(plan, columnarColumns);
2426
2435
  plan = expandQualifiedWildcards(plan, columnarColumns);
@@ -3691,8 +3700,9 @@ export function executeRowQuery(plan, tables) {
3691
3700
  }
3692
3701
  function executeRowQueryInternal(plan, tables, memory) {
3693
3702
  assertTailParametersBound(plan);
3694
- validateGrouping(plan);
3695
3703
  plan = resolveStatementDatetimes(plan);
3704
+ plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
3705
+ validateGrouping(plan);
3696
3706
  plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
3697
3707
  plan = expandNaturalJoins(plan, wildcardRowColumns(tables));
3698
3708
  plan = expandQualifiedWildcards(plan, wildcardRowColumns(tables));
@@ -5634,11 +5644,12 @@ class Parser {
5634
5644
  if (this.#isKeyword("SELECT")) {
5635
5645
  const insertSource = this.#selectBlock("(insert select)");
5636
5646
  resolvePlanExactNumericConstants(insertSource);
5637
- const query = optimizePlan(insertSource);
5638
- if (query.select.some((item) => item.expression.kind === "wildcard")) {
5639
- throw new TypeError("INSERT ... SELECT requires an explicit select list");
5640
- }
5641
- if (columns.length > 0 && query.select.length !== columns.length) {
5647
+ const query = planHasPendingSelectShapes(insertSource)
5648
+ ? insertSource
5649
+ : optimizePlan(insertSource);
5650
+ if (!planHasPendingSelectShapes(query) &&
5651
+ columns.length > 0 &&
5652
+ query.select.length !== columns.length) {
5642
5653
  throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
5643
5654
  }
5644
5655
  return {
@@ -5959,6 +5970,15 @@ class Parser {
5959
5970
  if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
5960
5971
  throw new TypeError(`${label} must be constant expressions`);
5961
5972
  }
5973
+ const needsExecution = (value) => (value.kind === "call" &&
5974
+ (statementDatetimeNames.has(value.name) ||
5975
+ value.name === "NEXTVAL" ||
5976
+ value.name === "CURRVAL" ||
5977
+ volatileScalarFunctionNames.has(value.name))) ||
5978
+ childExpressions(value).some(needsExecution);
5979
+ if (needsExecution(expression)) {
5980
+ return { expression: resolveExactNumericConstants(expression) };
5981
+ }
5962
5982
  return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
5963
5983
  }
5964
5984
  #unionMember(sql) {
@@ -6515,18 +6535,7 @@ class Parser {
6515
6535
  break;
6516
6536
  }
6517
6537
  this.#expectPunctuation(")");
6518
- // Set-operation output takes the first member's aliases, so renaming targets it.
6519
- const target = derived.base.union !== undefined && derived.select[0]?.expression.kind === "wildcard"
6520
- ? derived.base.union.blocks[0]
6521
- : derived;
6522
- if (target?.select.length !== names.length) {
6523
- throw new TypeError("Column alias list must match the derived table's column count");
6524
- }
6525
- names.forEach((name, index) => {
6526
- const item = target.select[index];
6527
- if (item !== undefined)
6528
- item.alias = name;
6529
- });
6538
+ renameBlockOutputs(derived, names, "derived table");
6530
6539
  }
6531
6540
  /**
6532
6541
  * `JOIN t USING (a, b)`: the named columns must exist on both sides, and the join condition
@@ -8281,6 +8290,34 @@ function desugarFullJoin(parts, nextSequence) {
8281
8290
  if (join.on !== undefined) {
8282
8291
  throw new TypeError("FULL JOIN requires a single equality ON condition");
8283
8292
  }
8293
+ // The UNION ALL this desugars into resolves ORDER BY against the compound's output columns,
8294
+ // where the branches' table aliases no longer exist. Qualified references that name a select
8295
+ // item are rewritten to its output alias, and everything else — expressions, and columns the
8296
+ // select list does not carry — takes the shared hidden-column desugar, which re-enters this
8297
+ // function with alias-only ordering.
8298
+ const outputAlias = (reference) => {
8299
+ const exact = parts.select.find((item) => item.alias === reference ||
8300
+ (item.expression.kind === "column" && item.expression.reference === reference));
8301
+ if (exact !== undefined)
8302
+ return exact.alias;
8303
+ const bare = reference.split(".").at(-1) ?? reference;
8304
+ return parts.select.find((item) => item.alias === bare)?.alias;
8305
+ };
8306
+ parts = {
8307
+ ...parts,
8308
+ orderBy: parts.orderBy.map((order) => {
8309
+ if (order.expression.kind !== "column" || !order.expression.reference.includes(".")) {
8310
+ return order;
8311
+ }
8312
+ const alias = outputAlias(order.expression.reference);
8313
+ if (alias === undefined)
8314
+ return order;
8315
+ return { ...order, expression: { kind: "column", reference: alias } };
8316
+ }),
8317
+ };
8318
+ if (parts.orderBy.some((order) => orderNeedsHiddenColumn(order.expression, parts))) {
8319
+ return assembleOrderByExpressionBlock(parts, nextSequence);
8320
+ }
8284
8321
  // `on` is absent here: the guard above rejected non-equi conditions.
8285
8322
  const { full, kind, left, right, ...joinSource } = join;
8286
8323
  void full;
@@ -8313,6 +8350,37 @@ function desugarFullJoin(parts, nextSequence) {
8313
8350
  }, nextSequence);
8314
8351
  }
8315
8352
  export function assembleSelectBlock(parts, nextSequence) {
8353
+ if (parts.select.some((item) => item.expression.kind === "wildcard")) {
8354
+ if (parts.joins.some((join) => join.full === true)) {
8355
+ throw new TypeError("FULL JOIN cannot be combined with SELECT *");
8356
+ }
8357
+ // A wildcard's width and names come from source schemas, but ordinals, hidden ORDER BY
8358
+ // columns, DISTINCT grouping, windows, grouping sets, and FULL JOIN lowering all depend on
8359
+ // that width. Preserve the raw block until schema binding instead of teaching each lowering
8360
+ // a different late wildcard exception.
8361
+ return {
8362
+ sql: parts.sql,
8363
+ base: parts.base,
8364
+ joins: parts.joins,
8365
+ select: parts.select,
8366
+ predicates: parts.predicates,
8367
+ groupBy: parts.groupBy,
8368
+ having: parts.having,
8369
+ orderBy: parts.orderBy,
8370
+ ...(parts.limit === undefined ? {} : { limit: parts.limit }),
8371
+ ...(parts.offset === undefined ? {} : { offset: parts.offset }),
8372
+ ...(parts.limitParameter === undefined ? {} : { limitParameter: parts.limitParameter }),
8373
+ ...(parts.offsetParameter === undefined ? {} : { offsetParameter: parts.offsetParameter }),
8374
+ ...(parts.limitWithTies === true ? { limitWithTies: true } : {}),
8375
+ pendingSelectShape: {
8376
+ distinct: parts.distinct,
8377
+ ...(parts.groupingSets === undefined
8378
+ ? {}
8379
+ : { groupingSets: structuredClone(parts.groupingSets) }),
8380
+ },
8381
+ ...(parts.distinct ? { distinctWildcard: true } : {}),
8382
+ };
8383
+ }
8316
8384
  parts = { ...parts, orderBy: resolveOrderByOrdinals(parts.orderBy, parts.select) };
8317
8385
  if (parts.joins.some((join) => join.full === true)) {
8318
8386
  return desugarFullJoin(parts, nextSequence);
@@ -8555,6 +8623,12 @@ function withTiesPlan(plan) {
8555
8623
  * table's own aliases, a set operation's first member's, and otherwise the input table's.
8556
8624
  */
8557
8625
  function sourceWildcardColumns(source, columnsOf) {
8626
+ if (source.derived?.base.union !== undefined &&
8627
+ source.derived.select[0]?.expression.kind === "wildcard") {
8628
+ return source.derived.base.union.blocks[0]?.select
8629
+ .map((item) => item.alias)
8630
+ .filter((name) => !name.startsWith("\0"));
8631
+ }
8558
8632
  if (source.derived !== undefined)
8559
8633
  return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
8560
8634
  if (source.union !== undefined) {
@@ -8562,8 +8636,188 @@ function sourceWildcardColumns(source, columnsOf) {
8562
8636
  .map((item) => item.alias)
8563
8637
  .filter((name) => !name.startsWith("\0"));
8564
8638
  }
8639
+ if (source.windowed !== undefined) {
8640
+ return [
8641
+ ...source.windowed.block.select.map((item) => item.alias),
8642
+ ...source.windowed.windows.map((window) => window.alias),
8643
+ ].filter((name) => !name.startsWith("\0"));
8644
+ }
8645
+ if (source.recursive !== undefined) {
8646
+ return source.recursive.base.select
8647
+ .map((item) => item.alias)
8648
+ .filter((name) => !name.startsWith("\0"));
8649
+ }
8565
8650
  return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
8566
8651
  }
8652
+ /** Whether any block still needs source schemas before its SELECT shape can be lowered. */
8653
+ export function planHasPendingSelectShapes(plan) {
8654
+ if (plan.pendingSelectShape !== undefined ||
8655
+ plan.pendingOutputAliases !== undefined ||
8656
+ plan.pendingSetOrder === true) {
8657
+ return true;
8658
+ }
8659
+ let pending = false;
8660
+ forEachNestedBlock(plan, (nested) => {
8661
+ pending ||= planHasPendingSelectShapes(nested);
8662
+ });
8663
+ const inspect = (expression) => {
8664
+ if (expression.kind === "subquery" || expression.kind === "exists") {
8665
+ pending ||= planHasPendingSelectShapes(expression.block);
8666
+ return;
8667
+ }
8668
+ childExpressions(expression).forEach(inspect);
8669
+ };
8670
+ forEachBlockExpression(plan, inspect);
8671
+ return pending;
8672
+ }
8673
+ /**
8674
+ * Expands schema-dependent SELECT wildcards, then sends the now-concrete block through the same
8675
+ * lowering used by ordinary named select lists. This is the single boundary at which wildcard
8676
+ * width becomes plan shape: DISTINCT, windows, ORDER BY expressions/ordinals, set operations,
8677
+ * CTE/derived column lists, grouping sets, and FULL JOIN therefore cannot disagree about it.
8678
+ */
8679
+ export function bindPendingSelectShapes(plan, columnsOf) {
8680
+ if (!planHasPendingSelectShapes(plan))
8681
+ return plan;
8682
+ const bound = structuredClone(plan);
8683
+ const preserveUnoptimizedShape = bound.preserveUnoptimizedShape === true;
8684
+ delete bound.preserveUnoptimizedShape;
8685
+ let sequence = 0;
8686
+ const scanSequence = (block) => {
8687
+ for (const source of [block.base, ...block.joins]) {
8688
+ const match = /\((?:derived|window|union) (\d+)\)/.exec(source.table);
8689
+ if (match?.[1] !== undefined)
8690
+ sequence = Math.max(sequence, Number(match[1]));
8691
+ }
8692
+ forEachNestedBlock(block, scanSequence);
8693
+ };
8694
+ scanSequence(bound);
8695
+ const nextSequence = () => {
8696
+ sequence += 1;
8697
+ return sequence;
8698
+ };
8699
+ const bindExpressionBlocks = (expression) => {
8700
+ if (expression.kind === "subquery" || expression.kind === "exists") {
8701
+ return { ...expression, block: bindBlock(expression.block) };
8702
+ }
8703
+ if (expression.kind === "window") {
8704
+ return {
8705
+ ...expression,
8706
+ partitionBy: expression.partitionBy.map(bindExpressionBlocks),
8707
+ orderBy: expression.orderBy.map((order) => ({
8708
+ ...order,
8709
+ expression: bindExpressionBlocks(order.expression),
8710
+ })),
8711
+ ...(expression.argument === undefined
8712
+ ? {}
8713
+ : { argument: bindExpressionBlocks(expression.argument) }),
8714
+ };
8715
+ }
8716
+ return mapChildExpressions(expression, bindExpressionBlocks);
8717
+ };
8718
+ const bindSource = (source) => {
8719
+ if (source.derived !== undefined)
8720
+ source.derived = bindBlock(source.derived);
8721
+ if (source.union !== undefined) {
8722
+ source.union.blocks = source.union.blocks.map(bindBlock);
8723
+ }
8724
+ if (source.windowed !== undefined) {
8725
+ source.windowed.block = bindBlock(source.windowed.block);
8726
+ }
8727
+ if (source.recursive !== undefined) {
8728
+ source.recursive.base = bindBlock(source.recursive.base);
8729
+ source.recursive.step = bindBlock(source.recursive.step);
8730
+ }
8731
+ };
8732
+ const expandedSelect = (block) => {
8733
+ const sources = [block.base, ...block.joins];
8734
+ const shaped = sources.map((source) => ({
8735
+ source,
8736
+ columns: source.columnAliases ?? sourceWildcardColumns(source, columnsOf),
8737
+ }));
8738
+ const unknown = shaped.find(({ columns }) => columns === undefined);
8739
+ if (unknown !== undefined) {
8740
+ throw new TypeError(`SELECT * requires known columns for: ${unknown.source.table}`);
8741
+ }
8742
+ const multiple = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0).length > 1;
8743
+ const items = block.select.flatMap((item) => {
8744
+ if (item.expression.kind !== "wildcard")
8745
+ return [item];
8746
+ const table = item.expression.table;
8747
+ const selected = table === undefined ? shaped : shaped.filter(({ source }) => source.alias === table);
8748
+ if (table !== undefined && selected.length === 0) {
8749
+ throw new TypeError(`Unknown table for ${table}.*: ${table}`);
8750
+ }
8751
+ return selected.flatMap(({ source, columns }) => (columns ?? []).map((name) => ({
8752
+ expression: { kind: "column", reference: `${source.alias}.${name}` },
8753
+ alias: multiple ? `${source.alias}.${name}` : name,
8754
+ })));
8755
+ });
8756
+ const aliases = new Set();
8757
+ for (const item of items) {
8758
+ if (aliases.has(item.alias))
8759
+ throw new TypeError(`Duplicate output column: ${item.alias}`);
8760
+ aliases.add(item.alias);
8761
+ }
8762
+ return items;
8763
+ };
8764
+ const carryPlanFlags = (from, to) => {
8765
+ for (const key of [
8766
+ "parameterCount",
8767
+ "usesStatementDatetime",
8768
+ "usesSequenceCalls",
8769
+ "usesVolatileFunctions",
8770
+ ]) {
8771
+ const value = from[key];
8772
+ if (value !== undefined)
8773
+ Object.assign(to, { [key]: value });
8774
+ }
8775
+ };
8776
+ function bindBlock(block) {
8777
+ for (const source of [block.base, ...block.joins])
8778
+ bindSource(source);
8779
+ mapBlockExpressions(block, bindExpressionBlocks);
8780
+ let lowered = block;
8781
+ const pending = block.pendingSelectShape;
8782
+ if (pending !== undefined) {
8783
+ const { pendingSelectShape, pendingOutputAliases, distinctWildcard, ...rest } = block;
8784
+ void pendingSelectShape;
8785
+ void pendingOutputAliases;
8786
+ void distinctWildcard;
8787
+ lowered = assembleSelectBlock({
8788
+ sql: rest.sql,
8789
+ base: rest.base,
8790
+ joins: rest.joins,
8791
+ select: expandedSelect(block),
8792
+ distinct: pending.distinct,
8793
+ predicates: rest.predicates,
8794
+ groupBy: rest.groupBy,
8795
+ having: rest.having,
8796
+ orderBy: rest.orderBy,
8797
+ ...(rest.limit === undefined ? {} : { limit: rest.limit }),
8798
+ ...(rest.offset === undefined ? {} : { offset: rest.offset }),
8799
+ ...(rest.limitParameter === undefined ? {} : { limitParameter: rest.limitParameter }),
8800
+ ...(rest.offsetParameter === undefined ? {} : { offsetParameter: rest.offsetParameter }),
8801
+ ...(rest.limitWithTies === true ? { limitWithTies: true } : {}),
8802
+ ...(pending.groupingSets === undefined ? {} : { groupingSets: pending.groupingSets }),
8803
+ }, nextSequence);
8804
+ carryPlanFlags(block, lowered);
8805
+ }
8806
+ if (lowered.pendingSetOrder === true) {
8807
+ const first = lowered.base.union?.blocks[0];
8808
+ lowered.orderBy = resolveOrderByOrdinals(lowered.orderBy, first?.select ?? []);
8809
+ delete lowered.pendingSetOrder;
8810
+ }
8811
+ const aliases = block.pendingOutputAliases;
8812
+ if (aliases !== undefined) {
8813
+ renameBlockOutputs(lowered, aliases.columns, aliases.sourceName);
8814
+ delete lowered.pendingOutputAliases;
8815
+ }
8816
+ return lowered;
8817
+ }
8818
+ const lowered = bindBlock(bound);
8819
+ return preserveUnoptimizedShape ? lowered : optimizePlan(lowered);
8820
+ }
8567
8821
  /** Whether any block of the plan still carries an unresolved NATURAL join marker. */
8568
8822
  export function planHasNaturalJoins(plan) {
8569
8823
  if (plan.joins.some((join) => join.natural === true))
@@ -8723,7 +8977,13 @@ function expandQualifiedWildcards(plan, columnsOf) {
8723
8977
  }
8724
8978
  return columns.map((name) => {
8725
8979
  const output = multiple ? `${source.alias}.${name}` : name;
8726
- return { expression: { kind: "column", reference: output }, alias: output };
8980
+ // Keep the source-qualified input reference even though one source exposes a bare output
8981
+ // name. ORDER BY can legally spell either form (`id` or `orders.id`), and the latter must
8982
+ // still match this select item after the wildcard has expanded.
8983
+ return {
8984
+ expression: { kind: "column", reference: `${source.alias}.${name}` },
8985
+ alias: output,
8986
+ };
8727
8987
  });
8728
8988
  });
8729
8989
  const aliases = new Set();
@@ -8740,7 +9000,10 @@ function expandQualifiedWildcards(plan, columnsOf) {
8740
9000
  /** Wraps compound members into the set-operation source the executor folds left to right. */
8741
9001
  export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
8742
9002
  // Set-operation output columns are the first member's, so ordinals resolve against them.
8743
- tail = { ...tail, orderBy: resolveOrderByOrdinals(tail.orderBy, blocks[0]?.select ?? []) };
9003
+ const pendingSetOrder = blocks.some(planHasPendingSelectShapes);
9004
+ if (!pendingSetOrder) {
9005
+ tail = { ...tail, orderBy: resolveOrderByOrdinals(tail.orderBy, blocks[0]?.select ?? []) };
9006
+ }
8744
9007
  return {
8745
9008
  sql,
8746
9009
  base: {
@@ -8754,9 +9017,12 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
8754
9017
  groupBy: [],
8755
9018
  having: [],
8756
9019
  orderBy: tail.orderBy,
9020
+ ...(pendingSetOrder ? { pendingSetOrder: true } : {}),
8757
9021
  ...(tail.limit === undefined ? {} : { limit: tail.limit }),
8758
9022
  ...(tail.offset === undefined ? {} : { offset: tail.offset }),
8759
9023
  ...(tail.limitWithTies === true ? { limitWithTies: true } : {}),
9024
+ ...(tail.limitParameter === undefined ? {} : { limitParameter: tail.limitParameter }),
9025
+ ...(tail.offsetParameter === undefined ? {} : { offsetParameter: tail.offsetParameter }),
8760
9026
  };
8761
9027
  }
8762
9028
  /**
@@ -8764,9 +9030,10 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
8764
9030
  * either reuses a structurally identical select item's alias or becomes a hidden "(order N)"
8765
9031
  * select item, the ordering (and LIMIT/OFFSET) applies inside that block, and an outer block
8766
9032
  * projects only the visible aliases away from a derived source. Runs in the shared assembly,
8767
- * so the builder and SQL front ends produce identical plans. A wildcard select has no named
8768
- * output list to hide items behind, and DISTINCT's output would change if hidden expressions
8769
- * joined its grouping, so both keep the named-column restriction.
9033
+ * so the builder and SQL front ends produce identical plans. Schema-bound wildcard blocks reach
9034
+ * this function with concrete items; only a manually constructed unresolved wildcard plan keeps
9035
+ * the named-column restriction. DISTINCT's output would change if hidden expressions joined its
9036
+ * grouping, so it keeps the selected-column restriction.
8770
9037
  */
8771
9038
  /**
8772
9039
  * Whether an ORDER BY item has to travel as a hidden select item: any expression, and also a
@@ -8823,17 +9090,39 @@ function assembleOrderByExpressionBlock(parts, nextSequence) {
8823
9090
  expression: { kind: "column", reference: alias },
8824
9091
  };
8825
9092
  });
8826
- const inner = assembleSelectBlock({ ...parts, select: [...parts.select, ...hiddenItems], orderBy: rewrittenOrder }, nextSequence);
8827
9093
  // Every ordering expression matched a visible select item — no hidden columns, no wrap.
8828
- if (hiddenItems.length === 0)
8829
- return inner;
9094
+ if (hiddenItems.length === 0) {
9095
+ return assembleSelectBlock({ ...parts, orderBy: rewrittenOrder }, nextSequence);
9096
+ }
9097
+ // Once hidden columns exist, the visible projection is wrapped in a derived table. Visible
9098
+ // output names such as `people.id` are legal result labels but are not legal internal column
9099
+ // references there — the dot would be read as the vanished `people` source qualifier. Carry
9100
+ // every visible value through a private dot-free name and restore its public label outside.
9101
+ const visibleInner = parts.select.map((item, index) => ({
9102
+ ...item,
9103
+ alias: item.alias.includes(".") ? `(order visible ${String(index + 1)})` : item.alias,
9104
+ }));
9105
+ const visibleAlias = new Map(parts.select.map((item, index) => [item.alias, visibleInner[index]?.alias ?? item.alias]));
9106
+ const innerOrder = rewrittenOrder.map((order) => {
9107
+ if (order.expression.kind !== "column" || order.expression.reference.includes(".")) {
9108
+ return order;
9109
+ }
9110
+ const alias = visibleAlias.get(order.expression.reference);
9111
+ return alias === undefined
9112
+ ? order
9113
+ : { ...order, expression: { kind: "column", reference: alias } };
9114
+ });
9115
+ const inner = assembleSelectBlock({ ...parts, select: [...visibleInner, ...hiddenItems], orderBy: innerOrder }, nextSequence);
8830
9116
  const source = derivedTableSource(inner, "(ordered)", nextSequence);
8831
9117
  return {
8832
9118
  sql: parts.sql,
8833
9119
  base: source,
8834
9120
  joins: [],
8835
- select: parts.select.map((item) => ({
8836
- expression: { kind: "column", reference: item.alias },
9121
+ select: parts.select.map((item, index) => ({
9122
+ expression: {
9123
+ kind: "column",
9124
+ reference: visibleInner[index]?.alias ?? item.alias,
9125
+ },
8837
9126
  alias: item.alias,
8838
9127
  })),
8839
9128
  predicates: [],
@@ -8915,10 +9204,28 @@ export function validateLimit(limit) {
8915
9204
  * which is later than this.
8916
9205
  */
8917
9206
  function renameBlockOutputs(block, columns, name) {
9207
+ // Set-operation output names come from the first member. Its width may itself be pending on a
9208
+ // wildcard, so preserve the alias list on the compound until all members have schema-bound.
9209
+ if (block.base.union !== undefined && block.select[0]?.expression.kind === "wildcard") {
9210
+ const first = block.base.union.blocks[0];
9211
+ if (first === undefined || planHasPendingSelectShapes(first)) {
9212
+ block.pendingOutputAliases = { columns: [...columns], sourceName: name };
9213
+ return;
9214
+ }
9215
+ renameBlockOutputs(first, columns, name);
9216
+ return;
9217
+ }
9218
+ if (block.pendingSelectShape !== undefined) {
9219
+ block.pendingOutputAliases = { columns: [...columns], sourceName: name };
9220
+ return;
9221
+ }
8918
9222
  if (block.select.some((item) => item.expression.kind === "wildcard")) {
8919
- throw new TypeError(`A column list needs named columns in the CTE body: ${name}`);
9223
+ throw new TypeError(`A column list needs named columns in the query body: ${name}`);
8920
9224
  }
8921
9225
  if (block.select.length !== columns.length) {
9226
+ if (name === "derived table") {
9227
+ throw new TypeError("Column alias list must match the derived table's column count");
9228
+ }
8922
9229
  throw new TypeError(`CTE ${name} declares ${String(columns.length)} columns but selects ${String(block.select.length)}`);
8923
9230
  }
8924
9231
  block.select = block.select.map((item, index) => ({
@@ -9009,7 +9316,10 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
9009
9316
  const readableWhenGrouped = (expression) => hasAggregate(expression) ||
9010
9317
  expressionColumns(expression).length === 0 ||
9011
9318
  groupExpressions.has(JSON.stringify(expression));
9012
- const innerSelect = select.filter((item) => !containsWindow(item.expression));
9319
+ const internalAliases = select.map((item, index) => item.alias.includes(".") ? `(window visible ${String(index + 1)})` : item.alias);
9320
+ const innerSelect = select.flatMap((item, index) => containsWindow(item.expression)
9321
+ ? []
9322
+ : [{ ...item, alias: internalAliases[index] ?? item.alias }]);
9013
9323
  const windows = [];
9014
9324
  let hidden = 0;
9015
9325
  /** A name for one more column the inner block computes for the wrapper to read back. */
@@ -9071,21 +9381,52 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
9071
9381
  }
9072
9382
  return mapChildExpressions(expression, split);
9073
9383
  };
9074
- const projections = select.map((item) => {
9384
+ const projections = select.map((item, index) => {
9385
+ const internalAlias = internalAliases[index] ?? item.alias;
9075
9386
  if (!containsWindow(item.expression)) {
9076
- return { expression: { kind: "column", reference: item.alias }, alias: item.alias };
9387
+ return {
9388
+ expression: { kind: "column", reference: internalAlias },
9389
+ alias: item.alias,
9390
+ };
9077
9391
  }
9078
- // A window that is the whole select item keeps carrying that item's own name, which is what
9079
- // the executor's windowed source and every existing plan already expect.
9392
+ // A window that is the whole select item carries a private source-safe name through the
9393
+ // windowed source, then the wrapper restores the public output alias.
9080
9394
  if (item.expression.kind === "window") {
9081
- registerWindow(item.expression, item.alias);
9082
- return { expression: { kind: "column", reference: item.alias }, alias: item.alias };
9395
+ registerWindow(item.expression, internalAlias);
9396
+ return {
9397
+ expression: { kind: "column", reference: internalAlias },
9398
+ alias: item.alias,
9399
+ };
9083
9400
  }
9084
9401
  return { expression: split(item.expression), alias: item.alias };
9085
9402
  });
9086
9403
  if (innerSelect.length === 0) {
9087
9404
  innerSelect.push({ expression: { kind: "literal", value: 1 }, alias: "(window 0)" });
9088
9405
  }
9406
+ // Window evaluation replaces the FROM sources with one synthetic source. A qualified ORDER
9407
+ // BY that names a selected column must therefore follow that value through its output alias;
9408
+ // leaving `a.id` in the wrapper would try to resolve the vanished table alias `a`.
9409
+ const windowTail = {
9410
+ ...tail,
9411
+ orderBy: tail.orderBy.map((order) => {
9412
+ if (order.expression.kind !== "column")
9413
+ return order;
9414
+ const reference = order.expression.reference;
9415
+ const bare = reference.split(".").at(-1) ?? reference;
9416
+ const selectedIndex = select.findIndex((item) => item.alias === reference ||
9417
+ item.alias === bare ||
9418
+ (item.expression.kind === "column" && item.expression.reference === reference));
9419
+ const selected = selectedIndex < 0 ? undefined : select[selectedIndex];
9420
+ const projected = selectedIndex < 0 ? undefined : projections[selectedIndex];
9421
+ const internalReference = projected?.expression.kind === "column" ? projected.expression.reference : selected?.alias;
9422
+ return selected === undefined || internalReference === undefined
9423
+ ? order
9424
+ : {
9425
+ ...order,
9426
+ expression: { kind: "column", reference: internalReference },
9427
+ };
9428
+ }),
9429
+ };
9089
9430
  const inner = {
9090
9431
  sql: "(window input)",
9091
9432
  base,
@@ -9108,7 +9449,7 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
9108
9449
  predicates: [],
9109
9450
  groupBy: [],
9110
9451
  having: [],
9111
- ...tail,
9452
+ ...windowTail,
9112
9453
  };
9113
9454
  }
9114
9455
  /**
@@ -226,6 +226,24 @@ export interface CompiledQuery {
226
226
  }>;
227
227
  limit?: number;
228
228
  offset?: number;
229
+ /**
230
+ * A SELECT whose output width depends on an input wildcard. The parser preserves the raw
231
+ * shape until source schemas are available; the schema-binding pass then expands the
232
+ * wildcard and runs the ordinary SELECT lowering exactly once.
233
+ */
234
+ pendingSelectShape?: {
235
+ distinct: boolean;
236
+ groupingSets?: Expression[][];
237
+ };
238
+ /** Positional output names whose count cannot be checked until a wildcard is expanded. */
239
+ pendingOutputAliases?: {
240
+ columns: string[];
241
+ sourceName: string;
242
+ };
243
+ /** A compound tail whose ORDER BY ordinals depend on its first member's wildcard width. */
244
+ pendingSetOrder?: true;
245
+ /** Preserve `{ optimize: false }` after a deferred wildcard shape becomes concrete. */
246
+ preserveUnoptimizedShape?: true;
229
247
  distinctWildcard?: boolean;
230
248
  limitParameter?: number;
231
249
  offsetParameter?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",
@@ -170,6 +170,12 @@
170
170
  "status": "supported",
171
171
  "example": "SELECT * FROM rows ORDER BY amount"
172
172
  },
173
+ {
174
+ "id": "order-by.qualified-wildcard-reference",
175
+ "status": "supported",
176
+ "example": "SELECT rows.* FROM rows ORDER BY rows.amount DESC",
177
+ "notes": "Both the wildcard and the ordering reference may use a table name or alias, quoted or unquoted. The conformance corpus crosses those spellings and compares them with SQLite and PostgreSQL."
178
+ },
173
179
  {
174
180
  "id": "limit",
175
181
  "status": "supported",
@@ -242,6 +248,15 @@
242
248
  "example": "INSERT INTO defaulted_insert DEFAULT VALUES",
243
249
  "notes": "DEFAULT VALUES inserts one row using catalog defaults. DEFAULT is also accepted in an individual VALUES slot."
244
250
  },
251
+ {
252
+ "id": "mutation.insert-runtime-values",
253
+ "status": "supported",
254
+ "setup": [
255
+ "CREATE TABLE runtime_values (id INTEGER PRIMARY KEY, noted_at TIMESTAMP, sample DOUBLE PRECISION, token UUID)"
256
+ ],
257
+ "example": "INSERT INTO runtime_values VALUES (1, CURRENT_TIMESTAMP, RANDOM(), GEN_RANDOM_UUID()) RETURNING id",
258
+ "notes": "Statement-time, random, UUID, and sequence calls in VALUES are evaluated at execution rather than frozen in the compiled-statement cache. Every CURRENT_* call in one statement shares one clock."
259
+ },
245
260
  {
246
261
  "id": "mutation.update-keyed",
247
262
  "status": "supported",
@@ -605,7 +620,7 @@
605
620
  "id": "select.distinct-wildcard",
606
621
  "status": "supported",
607
622
  "example": "SELECT DISTINCT * FROM rows",
608
- "notes": "Expands to DISTINCT over every wildcard output column once input schemas are known."
623
+ "notes": "Expands bare or qualified wildcards to exactly their selected source columns before DISTINCT grouping is planned."
609
624
  },
610
625
  {
611
626
  "id": "limit.offset",
@@ -806,7 +821,7 @@
806
821
  "id": "order-by.ordinal",
807
822
  "status": "supported",
808
823
  "example": "SELECT region, amount FROM rows ORDER BY 2 DESC",
809
- "notes": "Ordinals resolve to the select list at compile time; out-of-range ordinals are an error."
824
+ "notes": "Ordinals resolve after schema-bound wildcard expansion when needed; out-of-range ordinals are an error."
810
825
  },
811
826
  {
812
827
  "id": "window.lag-lead",
@@ -818,7 +833,7 @@
818
833
  "id": "mutation.insert-select",
819
834
  "status": "supported",
820
835
  "example": "INSERT INTO keyed (name, score) SELECT name || '2' AS name, score + 1 AS score FROM keyed",
821
- "notes": "The SELECT runs at one snapshot and materializes before the batch write."
836
+ "notes": "The SELECT runs at one snapshot and materializes before the batch write. Bare and qualified wildcard select lists are supported and their expanded width is checked against the target columns."
822
837
  },
823
838
  {
824
839
  "id": "mutation.merge",
@@ -899,7 +914,13 @@
899
914
  "id": "select.qualified-wildcard",
900
915
  "status": "supported",
901
916
  "example": "SELECT rows.* FROM rows",
902
- "notes": "Output names follow the rule a bare * uses: the column's own name from one source, alias-qualified from several."
917
+ "notes": "Output names follow the rule a bare * uses: the column's own name from one source, alias-qualified from several. Expansion happens before shape-dependent planning, so qualified wildcards compose with DISTINCT, windows, set operations, CTE/derived column lists, ORDER BY expressions and ordinals, and INSERT SELECT."
918
+ },
919
+ {
920
+ "id": "select.wildcard-window-composition",
921
+ "status": "supported",
922
+ "example": "SELECT rows.*, ROW_NUMBER() OVER (ORDER BY amount) AS position FROM rows ORDER BY amount",
923
+ "notes": "Wildcard columns are schema-bound before window lowering. The generated conformance corpus also crosses wildcards with DISTINCT, set operations, column lists, and ORDER BY expressions and ordinals."
903
924
  },
904
925
  {
905
926
  "id": "from.column-alias-list",