@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 +3 -2
- package/dist/engine/database.js +174 -29
- package/dist/engine/query.d.ts +14 -1
- package/dist/engine/query.js +384 -43
- package/dist/plan/model.d.ts +18 -0
- package/package.json +1 -1
- package/sql-feature-matrix.json +25 -4
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
|
-
-
|
|
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
|
-
|
|
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
|
package/dist/engine/database.js
CHANGED
|
@@ -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
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
return
|
|
3360
|
-
|
|
3361
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
}
|
package/dist/engine/query.d.ts
CHANGED
|
@@ -105,10 +105,14 @@ export interface UniqueConstraintDefinition {
|
|
|
105
105
|
interface DefaultInsertValue {
|
|
106
106
|
readonly default: true;
|
|
107
107
|
}
|
|
108
|
-
|
|
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
|
/**
|
package/dist/engine/query.js
CHANGED
|
@@ -890,7 +890,11 @@ export function compileQuery(sql, options = {}) {
|
|
|
890
890
|
let compiled;
|
|
891
891
|
try {
|
|
892
892
|
resolvePlanExactNumericConstants(plan);
|
|
893
|
-
|
|
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 =
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
8768
|
-
*
|
|
8769
|
-
*
|
|
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
|
|
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: {
|
|
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
|
|
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
|
|
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 {
|
|
9387
|
+
return {
|
|
9388
|
+
expression: { kind: "column", reference: internalAlias },
|
|
9389
|
+
alias: item.alias,
|
|
9390
|
+
};
|
|
9077
9391
|
}
|
|
9078
|
-
// A window that is the whole select item
|
|
9079
|
-
//
|
|
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,
|
|
9082
|
-
return {
|
|
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
|
-
...
|
|
9452
|
+
...windowTail,
|
|
9112
9453
|
};
|
|
9113
9454
|
}
|
|
9114
9455
|
/**
|
package/dist/plan/model.d.ts
CHANGED
|
@@ -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.
|
|
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",
|
package/sql-feature-matrix.json
CHANGED
|
@@ -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
|
|
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
|
|
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",
|