@minnowdb/core 0.6.8 → 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 +356 -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
|
|
@@ -8341,6 +8350,37 @@ function desugarFullJoin(parts, nextSequence) {
|
|
|
8341
8350
|
}, nextSequence);
|
|
8342
8351
|
}
|
|
8343
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
|
+
}
|
|
8344
8384
|
parts = { ...parts, orderBy: resolveOrderByOrdinals(parts.orderBy, parts.select) };
|
|
8345
8385
|
if (parts.joins.some((join) => join.full === true)) {
|
|
8346
8386
|
return desugarFullJoin(parts, nextSequence);
|
|
@@ -8583,6 +8623,12 @@ function withTiesPlan(plan) {
|
|
|
8583
8623
|
* table's own aliases, a set operation's first member's, and otherwise the input table's.
|
|
8584
8624
|
*/
|
|
8585
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
|
+
}
|
|
8586
8632
|
if (source.derived !== undefined)
|
|
8587
8633
|
return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
|
|
8588
8634
|
if (source.union !== undefined) {
|
|
@@ -8590,8 +8636,188 @@ function sourceWildcardColumns(source, columnsOf) {
|
|
|
8590
8636
|
.map((item) => item.alias)
|
|
8591
8637
|
.filter((name) => !name.startsWith("\0"));
|
|
8592
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
|
+
}
|
|
8593
8650
|
return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
|
|
8594
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
|
+
}
|
|
8595
8821
|
/** Whether any block of the plan still carries an unresolved NATURAL join marker. */
|
|
8596
8822
|
export function planHasNaturalJoins(plan) {
|
|
8597
8823
|
if (plan.joins.some((join) => join.natural === true))
|
|
@@ -8751,7 +8977,13 @@ function expandQualifiedWildcards(plan, columnsOf) {
|
|
|
8751
8977
|
}
|
|
8752
8978
|
return columns.map((name) => {
|
|
8753
8979
|
const output = multiple ? `${source.alias}.${name}` : name;
|
|
8754
|
-
|
|
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
|
+
};
|
|
8755
8987
|
});
|
|
8756
8988
|
});
|
|
8757
8989
|
const aliases = new Set();
|
|
@@ -8768,7 +9000,10 @@ function expandQualifiedWildcards(plan, columnsOf) {
|
|
|
8768
9000
|
/** Wraps compound members into the set-operation source the executor folds left to right. */
|
|
8769
9001
|
export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
8770
9002
|
// Set-operation output columns are the first member's, so ordinals resolve against them.
|
|
8771
|
-
|
|
9003
|
+
const pendingSetOrder = blocks.some(planHasPendingSelectShapes);
|
|
9004
|
+
if (!pendingSetOrder) {
|
|
9005
|
+
tail = { ...tail, orderBy: resolveOrderByOrdinals(tail.orderBy, blocks[0]?.select ?? []) };
|
|
9006
|
+
}
|
|
8772
9007
|
return {
|
|
8773
9008
|
sql,
|
|
8774
9009
|
base: {
|
|
@@ -8782,9 +9017,12 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
|
8782
9017
|
groupBy: [],
|
|
8783
9018
|
having: [],
|
|
8784
9019
|
orderBy: tail.orderBy,
|
|
9020
|
+
...(pendingSetOrder ? { pendingSetOrder: true } : {}),
|
|
8785
9021
|
...(tail.limit === undefined ? {} : { limit: tail.limit }),
|
|
8786
9022
|
...(tail.offset === undefined ? {} : { offset: tail.offset }),
|
|
8787
9023
|
...(tail.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
9024
|
+
...(tail.limitParameter === undefined ? {} : { limitParameter: tail.limitParameter }),
|
|
9025
|
+
...(tail.offsetParameter === undefined ? {} : { offsetParameter: tail.offsetParameter }),
|
|
8788
9026
|
};
|
|
8789
9027
|
}
|
|
8790
9028
|
/**
|
|
@@ -8792,9 +9030,10 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
|
8792
9030
|
* either reuses a structurally identical select item's alias or becomes a hidden "(order N)"
|
|
8793
9031
|
* select item, the ordering (and LIMIT/OFFSET) applies inside that block, and an outer block
|
|
8794
9032
|
* projects only the visible aliases away from a derived source. Runs in the shared assembly,
|
|
8795
|
-
* so the builder and SQL front ends produce identical plans.
|
|
8796
|
-
*
|
|
8797
|
-
*
|
|
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.
|
|
8798
9037
|
*/
|
|
8799
9038
|
/**
|
|
8800
9039
|
* Whether an ORDER BY item has to travel as a hidden select item: any expression, and also a
|
|
@@ -8851,17 +9090,39 @@ function assembleOrderByExpressionBlock(parts, nextSequence) {
|
|
|
8851
9090
|
expression: { kind: "column", reference: alias },
|
|
8852
9091
|
};
|
|
8853
9092
|
});
|
|
8854
|
-
const inner = assembleSelectBlock({ ...parts, select: [...parts.select, ...hiddenItems], orderBy: rewrittenOrder }, nextSequence);
|
|
8855
9093
|
// Every ordering expression matched a visible select item — no hidden columns, no wrap.
|
|
8856
|
-
if (hiddenItems.length === 0)
|
|
8857
|
-
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);
|
|
8858
9116
|
const source = derivedTableSource(inner, "(ordered)", nextSequence);
|
|
8859
9117
|
return {
|
|
8860
9118
|
sql: parts.sql,
|
|
8861
9119
|
base: source,
|
|
8862
9120
|
joins: [],
|
|
8863
|
-
select: parts.select.map((item) => ({
|
|
8864
|
-
expression: {
|
|
9121
|
+
select: parts.select.map((item, index) => ({
|
|
9122
|
+
expression: {
|
|
9123
|
+
kind: "column",
|
|
9124
|
+
reference: visibleInner[index]?.alias ?? item.alias,
|
|
9125
|
+
},
|
|
8865
9126
|
alias: item.alias,
|
|
8866
9127
|
})),
|
|
8867
9128
|
predicates: [],
|
|
@@ -8943,10 +9204,28 @@ export function validateLimit(limit) {
|
|
|
8943
9204
|
* which is later than this.
|
|
8944
9205
|
*/
|
|
8945
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
|
+
}
|
|
8946
9222
|
if (block.select.some((item) => item.expression.kind === "wildcard")) {
|
|
8947
|
-
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}`);
|
|
8948
9224
|
}
|
|
8949
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
|
+
}
|
|
8950
9229
|
throw new TypeError(`CTE ${name} declares ${String(columns.length)} columns but selects ${String(block.select.length)}`);
|
|
8951
9230
|
}
|
|
8952
9231
|
block.select = block.select.map((item, index) => ({
|
|
@@ -9037,7 +9316,10 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9037
9316
|
const readableWhenGrouped = (expression) => hasAggregate(expression) ||
|
|
9038
9317
|
expressionColumns(expression).length === 0 ||
|
|
9039
9318
|
groupExpressions.has(JSON.stringify(expression));
|
|
9040
|
-
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 }]);
|
|
9041
9323
|
const windows = [];
|
|
9042
9324
|
let hidden = 0;
|
|
9043
9325
|
/** A name for one more column the inner block computes for the wrapper to read back. */
|
|
@@ -9099,21 +9381,52 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9099
9381
|
}
|
|
9100
9382
|
return mapChildExpressions(expression, split);
|
|
9101
9383
|
};
|
|
9102
|
-
const projections = select.map((item) => {
|
|
9384
|
+
const projections = select.map((item, index) => {
|
|
9385
|
+
const internalAlias = internalAliases[index] ?? item.alias;
|
|
9103
9386
|
if (!containsWindow(item.expression)) {
|
|
9104
|
-
return {
|
|
9387
|
+
return {
|
|
9388
|
+
expression: { kind: "column", reference: internalAlias },
|
|
9389
|
+
alias: item.alias,
|
|
9390
|
+
};
|
|
9105
9391
|
}
|
|
9106
|
-
// A window that is the whole select item
|
|
9107
|
-
//
|
|
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.
|
|
9108
9394
|
if (item.expression.kind === "window") {
|
|
9109
|
-
registerWindow(item.expression,
|
|
9110
|
-
return {
|
|
9395
|
+
registerWindow(item.expression, internalAlias);
|
|
9396
|
+
return {
|
|
9397
|
+
expression: { kind: "column", reference: internalAlias },
|
|
9398
|
+
alias: item.alias,
|
|
9399
|
+
};
|
|
9111
9400
|
}
|
|
9112
9401
|
return { expression: split(item.expression), alias: item.alias };
|
|
9113
9402
|
});
|
|
9114
9403
|
if (innerSelect.length === 0) {
|
|
9115
9404
|
innerSelect.push({ expression: { kind: "literal", value: 1 }, alias: "(window 0)" });
|
|
9116
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
|
+
};
|
|
9117
9430
|
const inner = {
|
|
9118
9431
|
sql: "(window input)",
|
|
9119
9432
|
base,
|
|
@@ -9136,7 +9449,7 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9136
9449
|
predicates: [],
|
|
9137
9450
|
groupBy: [],
|
|
9138
9451
|
having: [],
|
|
9139
|
-
...
|
|
9452
|
+
...windowTail,
|
|
9140
9453
|
};
|
|
9141
9454
|
}
|
|
9142
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",
|