@minnowdb/core 0.4.1 → 0.6.0
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 +5 -5
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +5 -1
- package/dist/engine/catalog.js +5 -1
- package/dist/engine/client.d.ts +34 -6
- package/dist/engine/client.js +87 -19
- package/dist/engine/database.d.ts +43 -20
- package/dist/engine/database.js +823 -164
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +19 -0
- package/dist/engine/errors.js +31 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +12 -13
- package/dist/engine/optimizer.js +546 -39
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +16 -278
- package/dist/engine/query.js +260 -74
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +14 -1
- package/dist/engine/schema-wire.js +7 -1
- package/dist/engine/schema.d.ts +83 -32
- package/dist/engine/schema.js +180 -14
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +65 -1
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +328 -79
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +218 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +26 -8
- package/dist/storage/types.js +85 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +75 -19
package/dist/engine/database.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { toColumnarBatch, } from "./batch.js";
|
|
2
2
|
import { ArtifactCache } from "./artifact-cache.js";
|
|
3
|
+
import { throwIfAborted } from "./cancellation.js";
|
|
3
4
|
import { BufferedTableWriter } from "./buffered-writer.js";
|
|
4
5
|
export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_ADDS, } from "./buffered-writer.js";
|
|
5
|
-
import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
|
|
6
|
+
import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, DatabaseReadBacklogError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
|
|
7
|
+
export { DatabaseReadBacklogError } from "./errors.js";
|
|
6
8
|
import { BLOCK_HEADER_LENGTH, buildPhysicalColumnFromRanges, decodeBlock, decodePhysicalBlock, encodeBlock, encodePhysicalBlock, getCompressionMemoryBound, inspectBlock, maximumPhysicalBlockByteLength, MAX_BLOCK_METADATA_BYTE_LENGTH, MAX_BLOCK_ROW_COUNT, MAX_PHYSICAL_COLUMN_BYTE_LENGTH, MAX_STORED_BLOCK_BYTE_LENGTH, measurePhysicalColumnRanges, physicalColumnByteLength, slicePhysicalColumn, StoredBlockPayloadTooLargeError, wellFormedUtf8ByteLength, } from "../block-format/index.js";
|
|
7
9
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
8
10
|
import { estimateCompactionRowsPerOutput, planAlignedWriteBlockRanges as writeBlockRanges, } from "./write-block-planner.js";
|
|
9
|
-
import { MAX_CACHEABLE_TEXT_CHARACTERS } from "./cache-limits.js";
|
|
11
|
+
import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_PARAMETERS } from "./cache-limits.js";
|
|
10
12
|
import { fillColumnDefaults, patchAutoIncrementValues, } from "./defaults.js";
|
|
11
13
|
import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize as ftsTokenize, } from "./fts.js";
|
|
12
14
|
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";
|
|
13
15
|
import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
|
|
14
16
|
import { Snapshot, TransactionManager, } from "../transactions/index.js";
|
|
15
|
-
import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, compileStatement, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
|
|
17
|
+
import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, 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";
|
|
16
18
|
import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
|
|
17
19
|
import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
|
|
18
20
|
import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
|
|
19
21
|
import { chooseJoinOrder, renderPlan } from "./optimizer.js";
|
|
20
22
|
import { encodeSqlEqualityValue } from "./sql-semantics.js";
|
|
21
|
-
import { externalSqlDomainValue, externalSqlTextValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
23
|
+
import { externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
22
24
|
import { toCatalog } from "./catalog.js";
|
|
23
|
-
import { applyColumnSteps, assertColumnDroppable, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
|
|
25
|
+
import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
|
|
24
26
|
import { columnarTableFromRows, createColumnarTable, vectorValue, } from "./vector.js";
|
|
25
27
|
// The on-disk format is little-endian; the bulk Float64 copy reads platform order.
|
|
26
28
|
const PLATFORM_LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
@@ -103,14 +105,6 @@ export const MAX_TRANSACTION_SAVEPOINT_BYTES = 8 * 1024 * 1024;
|
|
|
103
105
|
export const MAX_DATABASE_PENDING_WRITES = 64;
|
|
104
106
|
/** Concurrent direct reads retained by one database instance. Reads still execute in parallel. */
|
|
105
107
|
export const MAX_DATABASE_ACTIVE_READS = 256;
|
|
106
|
-
export class DatabaseReadBacklogError extends Error {
|
|
107
|
-
limit;
|
|
108
|
-
name = "DatabaseReadBacklogError";
|
|
109
|
-
constructor(limit = MAX_DATABASE_ACTIVE_READS) {
|
|
110
|
-
super(`A database cannot retain more than ${String(limit)} active reads; await a read`);
|
|
111
|
-
this.limit = limit;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
108
|
/** Commits tolerated while collection fails before foreground writes assist and backpressure. */
|
|
115
109
|
const AUTO_COLLECT_MAX_DEBT_COMMITS = 4_096;
|
|
116
110
|
/** Failed background passes retry without another write, with bounded exponential backoff. */
|
|
@@ -272,7 +266,7 @@ function migrationResult(steps) {
|
|
|
272
266
|
};
|
|
273
267
|
}
|
|
274
268
|
export const MAX_GARBAGE_COLLECTION_RETAIN_RECENT_VERSIONS = 1_024;
|
|
275
|
-
export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
|
|
269
|
+
export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
|
|
276
270
|
/**
|
|
277
271
|
* How deep a delete's referential actions may cascade. A chain of CASCADE keys can reach further
|
|
278
272
|
* tables; the bound stops a cycle from recursing, the same way the trigger chain is bounded.
|
|
@@ -291,13 +285,29 @@ function isTransactionalStatement(statement) {
|
|
|
291
285
|
statement.kind === "delete" ||
|
|
292
286
|
statement.kind === "select");
|
|
293
287
|
}
|
|
288
|
+
function returningExecuteFields(table, columns, rows) {
|
|
289
|
+
return {
|
|
290
|
+
returnedRows: rows,
|
|
291
|
+
returnedColumns: [...columns],
|
|
292
|
+
returnedColumnDomains: columns.map((name) => table.columns.find((column) => column.name === name)?.sqlDomain ?? null),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
294
295
|
function externalizeExecuteResult(result) {
|
|
295
296
|
if (!("returnedRows" in result) || !Array.isArray(result.returnedRows))
|
|
296
297
|
return result;
|
|
297
|
-
const columns =
|
|
298
|
+
const columns = result.returnedColumns ?? [
|
|
299
|
+
...new Set(result.returnedRows.flatMap((row) => Object.keys(row))),
|
|
300
|
+
];
|
|
301
|
+
const columnDomains = result.returnedColumnDomains ?? unknownColumnDomains(columns);
|
|
298
302
|
return {
|
|
299
303
|
...result,
|
|
300
|
-
returnedRows: externalizeQueryResult({
|
|
304
|
+
returnedRows: externalizeQueryResult({
|
|
305
|
+
columns,
|
|
306
|
+
columnDomains,
|
|
307
|
+
rows: result.returnedRows,
|
|
308
|
+
}).rows,
|
|
309
|
+
returnedColumns: [...columns],
|
|
310
|
+
returnedColumnDomains: structuredClone(columnDomains),
|
|
301
311
|
};
|
|
302
312
|
}
|
|
303
313
|
/** Rejects an INSERT value that is still an unbound `?`/`$n` slot. */
|
|
@@ -567,6 +577,8 @@ export class MinnowDatabase {
|
|
|
567
577
|
#droppingTables = new Set();
|
|
568
578
|
/** Per table: the visible segment count a failed auto-compaction must see before retrying. */
|
|
569
579
|
#autoCompactionBackoff = new Map();
|
|
580
|
+
/** Exact current-layout counters; local commits advance them without rescanning history. */
|
|
581
|
+
#autoCompactionHints = new Map();
|
|
570
582
|
/** Data commits per table since its last write-path auto-compaction check. */
|
|
571
583
|
#commitsSinceCompactionCheck = new Map();
|
|
572
584
|
/** The compaction step in flight per table, so steps on one table run one at a time. */
|
|
@@ -745,6 +757,7 @@ export class MinnowDatabase {
|
|
|
745
757
|
this.#autoCompactionsRequested.clear();
|
|
746
758
|
this.#idleCompactionTableIds.clear();
|
|
747
759
|
this.#autoCompactionBackoff.clear();
|
|
760
|
+
this.#autoCompactionHints.clear();
|
|
748
761
|
this.#commitsSinceCompactionCheck.clear();
|
|
749
762
|
this.#artifactCache.clear();
|
|
750
763
|
}
|
|
@@ -809,6 +822,9 @@ export class MinnowDatabase {
|
|
|
809
822
|
...(sqlDomain === undefined ? {} : { sqlDomain: structuredClone(sqlDomain) }),
|
|
810
823
|
nullable: column.nullable ?? false,
|
|
811
824
|
...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
|
|
825
|
+
...(column.generatedValue === undefined
|
|
826
|
+
? {}
|
|
827
|
+
: { generatedValue: structuredClone(column.generatedValue) }),
|
|
812
828
|
...(column.enumValues === undefined
|
|
813
829
|
? {}
|
|
814
830
|
: { enumValues: validateEnumValues(column.enumValues, columnName) }),
|
|
@@ -867,6 +883,15 @@ export class MinnowDatabase {
|
|
|
867
883
|
throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
|
|
868
884
|
}
|
|
869
885
|
for (const column of columns) {
|
|
886
|
+
if (column.generatedValue !== undefined) {
|
|
887
|
+
if (column.defaultValue !== undefined || column.backfill !== undefined) {
|
|
888
|
+
throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${column.name}`);
|
|
889
|
+
}
|
|
890
|
+
compileGeneratedColumnExpression(name, column.name, column.generatedValue.sql, columns);
|
|
891
|
+
if (column === uniqueKeyColumn || compositePrimaryColumns.includes(column)) {
|
|
892
|
+
throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${column.name}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
870
895
|
if (column.defaultValue !== undefined) {
|
|
871
896
|
validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
|
|
872
897
|
if (column.defaultValue.kind === "literal" && column.sqlDomain !== undefined) {
|
|
@@ -894,6 +919,9 @@ export class MinnowDatabase {
|
|
|
894
919
|
const checkNames = (input.checks ?? []).map((check) => claimConstraintName(check.name));
|
|
895
920
|
const uniqueConstraintNames = (input.uniqueConstraints ?? []).map((constraint) => claimConstraintName(constraint.name));
|
|
896
921
|
const foreignKeys = await Promise.all((input.foreignKeys ?? []).map(async (key, keyIndex) => {
|
|
922
|
+
if (key.enforced === false && key.onDelete !== "restrict") {
|
|
923
|
+
throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
|
|
924
|
+
}
|
|
897
925
|
const childNames = key.columns ?? [key.column];
|
|
898
926
|
const children = childNames.map((columnName) => {
|
|
899
927
|
const column = columns.find((candidate) => candidate.name === columnName);
|
|
@@ -959,6 +987,7 @@ export class MinnowDatabase {
|
|
|
959
987
|
parentTable: key.parentTable,
|
|
960
988
|
parentColumns: parentNames,
|
|
961
989
|
onDelete: key.onDelete,
|
|
990
|
+
...(key.enforced === false ? { enforced: false } : {}),
|
|
962
991
|
};
|
|
963
992
|
}));
|
|
964
993
|
const checks = (input.checks ?? []).map((check, checkIndex) => {
|
|
@@ -1051,6 +1080,9 @@ export class MinnowDatabase {
|
|
|
1051
1080
|
: { sqlDomain: structuredClone(column.sqlDomain) }),
|
|
1052
1081
|
nullable: column.nullable,
|
|
1053
1082
|
...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
|
|
1083
|
+
...(column.generatedValue === undefined
|
|
1084
|
+
? {}
|
|
1085
|
+
: { generatedValue: column.generatedValue }),
|
|
1054
1086
|
...(column.enumValues === undefined ? {} : { enumValues: [...column.enumValues] }),
|
|
1055
1087
|
})),
|
|
1056
1088
|
...(uniqueKey === undefined ? {} : { uniqueKey }),
|
|
@@ -1121,7 +1153,7 @@ export class MinnowDatabase {
|
|
|
1121
1153
|
}
|
|
1122
1154
|
async #withReadReservation(run) {
|
|
1123
1155
|
if (this.#activeReadReservations >= MAX_DATABASE_ACTIVE_READS) {
|
|
1124
|
-
throw new DatabaseReadBacklogError();
|
|
1156
|
+
throw new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS);
|
|
1125
1157
|
}
|
|
1126
1158
|
this.#activeReadReservations += 1;
|
|
1127
1159
|
try {
|
|
@@ -1132,7 +1164,9 @@ export class MinnowDatabase {
|
|
|
1132
1164
|
}
|
|
1133
1165
|
}
|
|
1134
1166
|
async #queryWithReadReservation(sql, options) {
|
|
1167
|
+
throwIfAborted(options.signal);
|
|
1135
1168
|
await this.#settleExpiredStatementTransaction();
|
|
1169
|
+
throwIfAborted(options.signal);
|
|
1136
1170
|
// An open statement transaction routes through its session, which owns the reservation for
|
|
1137
1171
|
// the actual execution. Avoid charging the same query twice at this public boundary.
|
|
1138
1172
|
return this.#openTransaction === undefined
|
|
@@ -1383,7 +1417,7 @@ export class MinnowDatabase {
|
|
|
1383
1417
|
throw new TypeError(`${tableName} is a view, not a table`);
|
|
1384
1418
|
if (options.ifExists === true)
|
|
1385
1419
|
return false;
|
|
1386
|
-
throw new
|
|
1420
|
+
throw new UnknownTableError(tableName);
|
|
1387
1421
|
}
|
|
1388
1422
|
pinnedTableId ??= table.id;
|
|
1389
1423
|
if (table.id !== pinnedTableId) {
|
|
@@ -1513,7 +1547,7 @@ export class MinnowDatabase {
|
|
|
1513
1547
|
}
|
|
1514
1548
|
if (options.ifExists === true)
|
|
1515
1549
|
return false;
|
|
1516
|
-
throw new
|
|
1550
|
+
throw new UnknownTableError(tableName);
|
|
1517
1551
|
}
|
|
1518
1552
|
pinnedTableId ??= table.id;
|
|
1519
1553
|
if (table.id !== pinnedTableId) {
|
|
@@ -1561,6 +1595,7 @@ export class MinnowDatabase {
|
|
|
1561
1595
|
for (const column of table.columns)
|
|
1562
1596
|
this.#gzipVerdicts.delete(column.id);
|
|
1563
1597
|
this.#autoCompactionBackoff.delete(table.id);
|
|
1598
|
+
this.#autoCompactionHints.delete(table.id);
|
|
1564
1599
|
this.#commitsSinceCompactionCheck.delete(table.id);
|
|
1565
1600
|
this.#idleCompactionTableIds.delete(table.id);
|
|
1566
1601
|
const postingPrefix = `${table.id}/`;
|
|
@@ -1596,12 +1631,14 @@ export class MinnowDatabase {
|
|
|
1596
1631
|
: undefined;
|
|
1597
1632
|
const current = await this.store.getTable(table.id);
|
|
1598
1633
|
if (current === undefined)
|
|
1599
|
-
throw new
|
|
1634
|
+
throw new UnknownTableError(tableName);
|
|
1600
1635
|
const result = await this.#writeBatch(current, batch, "insert", keys, autoIncrement);
|
|
1601
1636
|
return { result, batch, generated, autoIncrement, rowCount };
|
|
1602
1637
|
});
|
|
1603
1638
|
const { result, batch, generated, autoIncrement, rowCount } = completed;
|
|
1604
1639
|
collectAutoIncrementGenerated(batch, generated, autoIncrement);
|
|
1640
|
+
if (result.segmentId === null)
|
|
1641
|
+
throw new Error("An insert did not create a segment");
|
|
1605
1642
|
return {
|
|
1606
1643
|
tableName: result.tableName,
|
|
1607
1644
|
segmentId: result.segmentId,
|
|
@@ -1624,6 +1661,7 @@ export class MinnowDatabase {
|
|
|
1624
1661
|
Object.values(pivoted.columns).find((values) => values.length > 0)?.length ??
|
|
1625
1662
|
0;
|
|
1626
1663
|
normalizeDomainBatch(table, filled.batch);
|
|
1664
|
+
fillStoredGeneratedColumns(table, filled.batch, inputRowCount, filled.generated);
|
|
1627
1665
|
for (const name of filled.generated.keys()) {
|
|
1628
1666
|
const column = table.columns.find((candidate) => candidate.name === name);
|
|
1629
1667
|
const values = filled.batch.columns[name];
|
|
@@ -1655,35 +1693,49 @@ export class MinnowDatabase {
|
|
|
1655
1693
|
async insert(tableName, row) {
|
|
1656
1694
|
return this.insertBatch(tableName, [row]);
|
|
1657
1695
|
}
|
|
1658
|
-
upsertBatch(tableName, input) {
|
|
1659
|
-
return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input));
|
|
1696
|
+
upsertBatch(tableName, input, options = {}) {
|
|
1697
|
+
return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input, options));
|
|
1660
1698
|
}
|
|
1661
|
-
async #upsertBatchReserved(tableName, input) {
|
|
1699
|
+
async #upsertBatchReserved(tableName, input, options) {
|
|
1662
1700
|
const completed = await this.#runWrite(async () => {
|
|
1663
1701
|
const table = await this.#findTable(tableName);
|
|
1664
1702
|
const keyColumn = getUniqueKeyColumn(table);
|
|
1665
1703
|
if (keyColumn === undefined) {
|
|
1666
1704
|
throw new TypeError(`Table needs a unique key before it can be upserted: ${table.name}`);
|
|
1667
1705
|
}
|
|
1668
|
-
const { batch, generated, autoIncrement
|
|
1706
|
+
const { batch, generated, autoIncrement } = await this.#fillDefaults(table, input);
|
|
1669
1707
|
const deferred = autoIncrement !== undefined && autoIncrement.missingIndexes.length > 0;
|
|
1670
1708
|
const keys = deferred ? undefined : batchKeys(table, batch);
|
|
1671
1709
|
const current = await this.store.getTable(table.id);
|
|
1672
1710
|
if (current === undefined)
|
|
1673
|
-
throw new
|
|
1674
|
-
const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement);
|
|
1675
|
-
return { result, batch, generated, autoIncrement
|
|
1711
|
+
throw new UnknownTableError(tableName);
|
|
1712
|
+
const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement, options.conflictWhere);
|
|
1713
|
+
return { result, batch, generated, autoIncrement };
|
|
1676
1714
|
});
|
|
1677
|
-
const { result, batch, generated, autoIncrement
|
|
1715
|
+
const { result, batch, generated, autoIncrement } = completed;
|
|
1678
1716
|
collectAutoIncrementGenerated(batch, generated, autoIncrement);
|
|
1717
|
+
if (result.acceptedRowIndexes.length !== result.requestedRowCount) {
|
|
1718
|
+
for (const [name, values] of generated) {
|
|
1719
|
+
generated.set(name, result.acceptedRowIndexes.map((index) => values[index] ?? null));
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1679
1722
|
return {
|
|
1680
|
-
|
|
1681
|
-
|
|
1723
|
+
tableName: result.tableName,
|
|
1724
|
+
segmentId: result.segmentId,
|
|
1725
|
+
requestedRowCount: result.requestedRowCount,
|
|
1726
|
+
rowCount: result.rowCount,
|
|
1727
|
+
insertedRowCount: result.insertedRowCount,
|
|
1728
|
+
updatedRowCount: result.updatedRowCount,
|
|
1729
|
+
skippedRowCount: result.skippedRowCount,
|
|
1730
|
+
blockCount: result.blockCount,
|
|
1731
|
+
storedBytes: result.storedBytes,
|
|
1732
|
+
version: result.version,
|
|
1733
|
+
metrics: result.metrics,
|
|
1682
1734
|
...(generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }),
|
|
1683
1735
|
};
|
|
1684
1736
|
}
|
|
1685
|
-
async upsert(tableName, row) {
|
|
1686
|
-
return this.upsertBatch(tableName, [row]);
|
|
1737
|
+
async upsert(tableName, row, options = {}) {
|
|
1738
|
+
return this.upsertBatch(tableName, [row], options);
|
|
1687
1739
|
}
|
|
1688
1740
|
updateBatch(tableName, input) {
|
|
1689
1741
|
return this.#withWriteReservation(() => this.#updateBatchReserved(tableName, input));
|
|
@@ -1696,10 +1748,11 @@ export class MinnowDatabase {
|
|
|
1696
1748
|
throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
|
|
1697
1749
|
}
|
|
1698
1750
|
const normalizedInput = normalizeDomainUpdate(table, input);
|
|
1751
|
+
rejectGeneratedUpdateAssignments(table, normalizedInput);
|
|
1699
1752
|
const keys = validateUpdateBatch(table, keyColumn, normalizedInput);
|
|
1700
1753
|
const current = await this.store.getTable(table.id);
|
|
1701
1754
|
if (current === undefined)
|
|
1702
|
-
throw new
|
|
1755
|
+
throw new UnknownTableError(tableName);
|
|
1703
1756
|
const currentKey = getUniqueKeyColumn(current);
|
|
1704
1757
|
if (currentKey?.id !== keyColumn.id) {
|
|
1705
1758
|
throw new TypeError(`Table unique key changed during update: ${tableName}`);
|
|
@@ -1713,6 +1766,10 @@ export class MinnowDatabase {
|
|
|
1713
1766
|
changes: Object.fromEntries(Object.entries(changes).map(([name, value]) => [name, [value]])),
|
|
1714
1767
|
});
|
|
1715
1768
|
}
|
|
1769
|
+
/** Deletes one row by the table's unique key. */
|
|
1770
|
+
async delete(tableName, key) {
|
|
1771
|
+
return this.deleteBatch(tableName, { keys: [key] });
|
|
1772
|
+
}
|
|
1716
1773
|
deleteBatch(tableName, input) {
|
|
1717
1774
|
return this.#withWriteReservation(() => this.#deleteBatchReserved(tableName, input));
|
|
1718
1775
|
}
|
|
@@ -1948,15 +2005,13 @@ export class MinnowDatabase {
|
|
|
1948
2005
|
async #writeUpdateBatch(table, keyColumn, input, keys) {
|
|
1949
2006
|
await this.#assertCompactionCapacity(table);
|
|
1950
2007
|
const started = performance.now();
|
|
1951
|
-
|
|
1952
|
-
Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
|
|
2008
|
+
let logicalBytes;
|
|
1953
2009
|
// Deferred: the record rides the single-shot commit below unless trigger rows stage first.
|
|
1954
2010
|
const transaction = await this.#transactions.beginDeferred({ durableSnapshot: false });
|
|
1955
2011
|
transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
|
|
1956
2012
|
const blockStager = new BoundedWriteBlockStager(transaction);
|
|
1957
2013
|
const segmentId = this.#createId();
|
|
1958
2014
|
const columnBlockIds = {};
|
|
1959
|
-
const changedColumns = Object.keys(input.changes).sort();
|
|
1960
2015
|
let storedBytes = 0;
|
|
1961
2016
|
let blockCount = 0;
|
|
1962
2017
|
let encodeMs = 0;
|
|
@@ -1965,6 +2020,22 @@ export class MinnowDatabase {
|
|
|
1965
2020
|
let retries = 0;
|
|
1966
2021
|
try {
|
|
1967
2022
|
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, keys);
|
|
2023
|
+
const checks = table.checks ?? [];
|
|
2024
|
+
let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
2025
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
2026
|
+
const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
|
|
2027
|
+
changedForeignKey ||
|
|
2028
|
+
tableGeneratedExpressions(table).length > 0 ||
|
|
2029
|
+
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
2030
|
+
readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
|
|
2031
|
+
input = applyStoredGeneratedUpdateChanges(table, input, preImages);
|
|
2032
|
+
validateUpdateBatch(table, keyColumn, input);
|
|
2033
|
+
changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
2034
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
2035
|
+
logicalBytes =
|
|
2036
|
+
estimateValuesBytes(input.keys) +
|
|
2037
|
+
Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
|
|
2038
|
+
const changedColumns = Object.keys(input.changes).sort();
|
|
1968
2039
|
const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
|
|
1969
2040
|
const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : (input.changes[column.name] ?? [])));
|
|
1970
2041
|
const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
|
|
@@ -1998,12 +2069,6 @@ export class MinnowDatabase {
|
|
|
1998
2069
|
}
|
|
1999
2070
|
columnBlockIds[column.id] = blockIds;
|
|
2000
2071
|
}
|
|
2001
|
-
const checks = table.checks ?? [];
|
|
2002
|
-
const changedForeignKey = (table.foreignKeys ?? []).some((key) => foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
2003
|
-
const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
|
|
2004
|
-
changedForeignKey ||
|
|
2005
|
-
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
2006
|
-
readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
|
|
2007
2072
|
stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
|
|
2008
2073
|
const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
|
|
2009
2074
|
if (secondaryDeltas.length > 0) {
|
|
@@ -2076,6 +2141,14 @@ export class MinnowDatabase {
|
|
|
2076
2141
|
blockCount,
|
|
2077
2142
|
storedBytes,
|
|
2078
2143
|
version: manifest.version,
|
|
2144
|
+
...(tableGeneratedExpressions(table).length === 0
|
|
2145
|
+
? {}
|
|
2146
|
+
: {
|
|
2147
|
+
generatedColumns: Object.fromEntries(tableGeneratedExpressions(table).map(({ column }) => [
|
|
2148
|
+
column.name,
|
|
2149
|
+
(input.changes[column.name] ?? []).map((value) => externalSqlDomainValue(value)),
|
|
2150
|
+
])),
|
|
2151
|
+
}),
|
|
2079
2152
|
metrics: createWriteMetrics({
|
|
2080
2153
|
logicalBytes,
|
|
2081
2154
|
storedBytes,
|
|
@@ -2117,12 +2190,13 @@ export class MinnowDatabase {
|
|
|
2117
2190
|
}
|
|
2118
2191
|
throw new Error(`Unique-key lookup returned an inconsistent result: ${table.name}`);
|
|
2119
2192
|
}
|
|
2120
|
-
async #writeBatch(table, input, kind, keys, autoIncrement) {
|
|
2193
|
+
async #writeBatch(table, input, kind, keys, autoIncrement, conflictWhere) {
|
|
2121
2194
|
await this.#assertCompactionCapacity(table);
|
|
2122
2195
|
const started = performance.now();
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2196
|
+
const requestedRowCount = input.columns[table.columns[0]?.name ?? ""]?.length ?? 0;
|
|
2197
|
+
let rowCount = requestedRowCount;
|
|
2198
|
+
let logicalBytes = estimateBatchBytes(input);
|
|
2199
|
+
const { transaction, rowIds: reservedRowIds, autoIncrementValues, } = await this.#transactions.beginWithReservation({ tableId: table.id, count: requestedRowCount }, autoIncrement === undefined
|
|
2126
2200
|
? undefined
|
|
2127
2201
|
: {
|
|
2128
2202
|
tableId: table.id,
|
|
@@ -2133,11 +2207,14 @@ export class MinnowDatabase {
|
|
|
2133
2207
|
transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
|
|
2134
2208
|
const blockStager = new BoundedWriteBlockStager(transaction);
|
|
2135
2209
|
let resolvedKeys = keys;
|
|
2136
|
-
|
|
2210
|
+
let segmentId = null;
|
|
2137
2211
|
const columnBlockIds = {};
|
|
2138
2212
|
let storedBytes = 0;
|
|
2139
2213
|
let blockCount = 0;
|
|
2140
2214
|
let counts;
|
|
2215
|
+
let skippedRowCount = 0;
|
|
2216
|
+
let acceptedRowIndexes = Array.from({ length: requestedRowCount }, (_, index) => index);
|
|
2217
|
+
let upsertFirings;
|
|
2141
2218
|
let encodeMs = 0;
|
|
2142
2219
|
let stageMs = 0;
|
|
2143
2220
|
let commitMs = 0;
|
|
@@ -2157,6 +2234,65 @@ export class MinnowDatabase {
|
|
|
2157
2234
|
}
|
|
2158
2235
|
resolvedKeys = batchKeys(table, input);
|
|
2159
2236
|
}
|
|
2237
|
+
const normalizedConflictWhere = kind === "upsert" && conflictWhere !== undefined
|
|
2238
|
+
? normalizeUpsertConflictWhere(table, conflictWhere)
|
|
2239
|
+
: undefined;
|
|
2240
|
+
const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
|
|
2241
|
+
if (upsertKeyColumn !== undefined) {
|
|
2242
|
+
upsertFirings = await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere, normalizedConflictWhere !== undefined);
|
|
2243
|
+
}
|
|
2244
|
+
if (normalizedConflictWhere !== undefined) {
|
|
2245
|
+
if (upsertFirings === undefined) {
|
|
2246
|
+
throw new Error("Upsert conflict classification is missing");
|
|
2247
|
+
}
|
|
2248
|
+
const classifiedFirings = upsertFirings;
|
|
2249
|
+
acceptedRowIndexes = [...classifiedFirings.inserts, ...classifiedFirings.updates].sort((left, right) => left - right);
|
|
2250
|
+
skippedRowCount = classifiedFirings.skipped.length;
|
|
2251
|
+
input = selectBatchRows(input, acceptedRowIndexes);
|
|
2252
|
+
rowCount = acceptedRowIndexes.length;
|
|
2253
|
+
logicalBytes = estimateBatchBytes(input);
|
|
2254
|
+
resolvedKeys = batchKeys(table, input);
|
|
2255
|
+
counts = {
|
|
2256
|
+
inserted: classifiedFirings.inserts.length,
|
|
2257
|
+
updated: classifiedFirings.updates.length,
|
|
2258
|
+
};
|
|
2259
|
+
const acceptedPosition = new Map(acceptedRowIndexes.map((original, position) => [original, position]));
|
|
2260
|
+
upsertFirings = {
|
|
2261
|
+
inserts: classifiedFirings.inserts.map((index) => acceptedPosition.get(index) ?? -1),
|
|
2262
|
+
updates: classifiedFirings.updates.map((index) => acceptedPosition.get(index) ?? -1),
|
|
2263
|
+
skipped: [],
|
|
2264
|
+
oldImages: acceptedRowIndexes.map((index) => classifiedFirings.oldImages[index]),
|
|
2265
|
+
};
|
|
2266
|
+
if (rowCount === 0) {
|
|
2267
|
+
await transaction.abort();
|
|
2268
|
+
const version = transaction.snapshotVersion;
|
|
2269
|
+
if (version === null)
|
|
2270
|
+
throw new Error("An existing table has no catalog version");
|
|
2271
|
+
return {
|
|
2272
|
+
tableName: table.name,
|
|
2273
|
+
segmentId: null,
|
|
2274
|
+
requestedRowCount,
|
|
2275
|
+
rowCount: 0,
|
|
2276
|
+
insertedRowCount: 0,
|
|
2277
|
+
updatedRowCount: 0,
|
|
2278
|
+
skippedRowCount,
|
|
2279
|
+
acceptedRowIndexes,
|
|
2280
|
+
blockCount: 0,
|
|
2281
|
+
storedBytes: 0,
|
|
2282
|
+
version,
|
|
2283
|
+
metrics: createWriteMetrics({
|
|
2284
|
+
logicalBytes: 0,
|
|
2285
|
+
storedBytes: 0,
|
|
2286
|
+
encodeMs: 0,
|
|
2287
|
+
stageMs: 0,
|
|
2288
|
+
commitMs: 0,
|
|
2289
|
+
totalMs: performance.now() - started,
|
|
2290
|
+
retries: 0,
|
|
2291
|
+
rows: 0,
|
|
2292
|
+
}),
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2160
2296
|
if (resolvedKeys !== undefined) {
|
|
2161
2297
|
transaction.setUniqueKeyChanges({
|
|
2162
2298
|
tableId: table.id,
|
|
@@ -2169,11 +2305,20 @@ export class MinnowDatabase {
|
|
|
2169
2305
|
// generated values are patched also covers an auto-incremented referencing column, and
|
|
2170
2306
|
// registering this batch's key membership first permits valid self-references.
|
|
2171
2307
|
await assertForeignKeys();
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2308
|
+
if (normalizedConflictWhere === undefined) {
|
|
2309
|
+
counts =
|
|
2310
|
+
kind === "insert" && resolvedKeys !== undefined
|
|
2311
|
+
? { inserted: resolvedKeys.size, updated: 0 }
|
|
2312
|
+
: await this.#classifyKeys(table, transaction.snapshotVersion, kind, resolvedKeys);
|
|
2313
|
+
}
|
|
2314
|
+
if (counts === undefined)
|
|
2315
|
+
throw new Error("Write classification is missing");
|
|
2316
|
+
const reserved = reservedRowIds ?? (await this.store.reserveRowIds(table.id, rowCount));
|
|
2317
|
+
const rowIds = {
|
|
2318
|
+
start: reserved.start,
|
|
2319
|
+
endExclusive: reserved.start + BigInt(rowCount),
|
|
2320
|
+
};
|
|
2321
|
+
segmentId = this.#createId();
|
|
2177
2322
|
// Insert batches maintain any active full-text index by tokenizing their own rows into a
|
|
2178
2323
|
// commit delta. Other mutation kinds emit nothing on purpose: the store's stale-writer
|
|
2179
2324
|
// rule then flips the index to "invalid" (keyed histories are unindexable) and the scan
|
|
@@ -2267,10 +2412,6 @@ export class MinnowDatabase {
|
|
|
2267
2412
|
const insertValueAt = (source, column, rowIndex) => (source === "new" ? (input.columns[column]?.[rowIndex] ?? null) : null);
|
|
2268
2413
|
// Upserts fire per-row: INSERT triggers for fresh keys, UPDATE triggers (with the
|
|
2269
2414
|
// replaced row as OLD) for conflicting ones — ON CONFLICT DO UPDATE semantics.
|
|
2270
|
-
const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
|
|
2271
|
-
const upsertFirings = upsertKeyColumn === undefined
|
|
2272
|
-
? undefined
|
|
2273
|
-
: await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount);
|
|
2274
2415
|
stageSecondaryUniqueInsertChanges(transaction, table, input, kind === "upsert" ? upsertFirings?.oldImages : undefined);
|
|
2275
2416
|
if (kind === "insert") {
|
|
2276
2417
|
await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before");
|
|
@@ -2304,12 +2445,15 @@ export class MinnowDatabase {
|
|
|
2304
2445
|
return {
|
|
2305
2446
|
tableName: table.name,
|
|
2306
2447
|
segmentId,
|
|
2448
|
+
requestedRowCount,
|
|
2307
2449
|
rowCount,
|
|
2308
2450
|
blockCount,
|
|
2309
2451
|
storedBytes,
|
|
2310
2452
|
version: manifest.version,
|
|
2311
2453
|
insertedRowCount: counts.inserted,
|
|
2312
2454
|
updatedRowCount: counts.updated,
|
|
2455
|
+
skippedRowCount,
|
|
2456
|
+
acceptedRowIndexes,
|
|
2313
2457
|
metrics: createWriteMetrics({
|
|
2314
2458
|
logicalBytes,
|
|
2315
2459
|
storedBytes,
|
|
@@ -2329,7 +2473,7 @@ export class MinnowDatabase {
|
|
|
2329
2473
|
}
|
|
2330
2474
|
const stagedDerivations = kind === "insert"
|
|
2331
2475
|
? (table.triggers ?? []).some((trigger) => trigger.event === "insert")
|
|
2332
|
-
: upsertFirings !== undefined;
|
|
2476
|
+
: upsertFirings !== undefined || conflictWhere !== undefined;
|
|
2333
2477
|
if (stagedDerivations)
|
|
2334
2478
|
throw new StaleTriggerDerivationsError(error);
|
|
2335
2479
|
retries += 1;
|
|
@@ -2383,6 +2527,7 @@ export class MinnowDatabase {
|
|
|
2383
2527
|
});
|
|
2384
2528
|
return externalizeQueryResult({
|
|
2385
2529
|
columns: columns.map(({ name }) => name),
|
|
2530
|
+
columnDomains: columns.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2386
2531
|
rows,
|
|
2387
2532
|
}).rows;
|
|
2388
2533
|
}
|
|
@@ -2457,12 +2602,14 @@ export class MinnowDatabase {
|
|
|
2457
2602
|
}
|
|
2458
2603
|
async #prepareCompiledPlan(plan, options = {}, probe) {
|
|
2459
2604
|
options = this.#effectiveQueryOptions(options);
|
|
2605
|
+
throwIfAborted(options.signal);
|
|
2460
2606
|
// The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
|
|
2461
2607
|
// directly (no derived materialization) and project each result to the visible aliases,
|
|
2462
2608
|
// so `.search()` costs the same whether or not the caller also selects the score.
|
|
2463
2609
|
const wrapper = transparentProjectionSource(plan);
|
|
2464
2610
|
if (wrapper !== undefined) {
|
|
2465
2611
|
const prepared = await this.#prepareCompiledPlan(wrapper.inner, options, probe);
|
|
2612
|
+
throwIfAborted(options.signal);
|
|
2466
2613
|
return {
|
|
2467
2614
|
sql: prepared.sql,
|
|
2468
2615
|
tables: prepared.tables,
|
|
@@ -2481,7 +2628,9 @@ export class MinnowDatabase {
|
|
|
2481
2628
|
let resolvedPlan = plan;
|
|
2482
2629
|
let ftsStats;
|
|
2483
2630
|
let outputNeedsExternalization = true;
|
|
2631
|
+
let outputColumnDomains = [];
|
|
2484
2632
|
const prepareAtSnapshot = async (snapshot, realTables, visibility) => {
|
|
2633
|
+
throwIfAborted(options.signal);
|
|
2485
2634
|
const typedSchemas = new Map([...realTables.values()].map((table) => [
|
|
2486
2635
|
table.name,
|
|
2487
2636
|
table.columns.map(({ name, type, integer, sqlDomain }) => ({
|
|
@@ -2497,32 +2646,41 @@ export class MinnowDatabase {
|
|
|
2497
2646
|
const expandedPlan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
|
|
2498
2647
|
const resolution = subqueryResolutionSteps(expandedPlan);
|
|
2499
2648
|
for (const step of resolution.steps) {
|
|
2500
|
-
|
|
2649
|
+
throwIfAborted(options.signal);
|
|
2650
|
+
step.substitute(await this.#executeBlockCached(step.block, snapshot, visibility, memory, realTables, typedSchemas, options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows, options.signal));
|
|
2651
|
+
throwIfAborted(options.signal);
|
|
2501
2652
|
}
|
|
2502
2653
|
resolvedPlan = resolution.plan;
|
|
2503
2654
|
// Index-served BM25 statistics, computed against the same catalog snapshot the pruner
|
|
2504
2655
|
// reads, so a pruned scoring scan always carries exact corpus numbers.
|
|
2505
2656
|
ftsStats = await this.#ftsIndexStats(resolvedPlan, realTables, snapshot, visibility);
|
|
2657
|
+
throwIfAborted(options.signal);
|
|
2506
2658
|
columnarTables = await this.#prepareBlockInputs(resolvedPlan, snapshot, visibility, memory, realTables, typedSchemas, undefined,
|
|
2507
2659
|
// memoize: false means "compute this statement's results" — that covers the
|
|
2508
2660
|
// columnar forms of derived and windowed sources too, not just the result memo.
|
|
2509
|
-
options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows);
|
|
2661
|
+
options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows, options.signal);
|
|
2662
|
+
throwIfAborted(options.signal);
|
|
2510
2663
|
outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
|
|
2664
|
+
outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
|
|
2511
2665
|
};
|
|
2512
2666
|
if (options.version !== undefined) {
|
|
2513
2667
|
// Explicit time travel keeps the per-call lease and version-anchored reads.
|
|
2514
2668
|
const realTables = await this.#findRealBlockTables(plan);
|
|
2669
|
+
throwIfAborted(options.signal);
|
|
2515
2670
|
await this.#withLeasedSnapshot(options.version, async (snapshot) => {
|
|
2516
2671
|
const visibility = await this.#blockSegmentVisibility(realTables);
|
|
2672
|
+
throwIfAborted(options.signal);
|
|
2517
2673
|
await prepareAtSnapshot(snapshot, realTables, visibility);
|
|
2518
2674
|
});
|
|
2519
2675
|
}
|
|
2520
2676
|
else {
|
|
2521
2677
|
await this.#withSharedCatalogSnapshot(collectRealTableNames(plan), prepareAtSnapshot, probe);
|
|
2522
2678
|
}
|
|
2679
|
+
throwIfAborted(options.signal);
|
|
2523
2680
|
return createPreparedColumnarQuery(chooseJoinOrder(resolvedPlan, columnarTables), columnarTables, memory, {
|
|
2524
2681
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
2525
2682
|
outputNeedsExternalization,
|
|
2683
|
+
outputColumnDomains,
|
|
2526
2684
|
});
|
|
2527
2685
|
}
|
|
2528
2686
|
catch (error) {
|
|
@@ -2550,7 +2708,7 @@ export class MinnowDatabase {
|
|
|
2550
2708
|
names.forEach((name, index) => {
|
|
2551
2709
|
const table = state.tables[index];
|
|
2552
2710
|
if (table === undefined)
|
|
2553
|
-
throw new
|
|
2711
|
+
throw new UnknownTableError(name);
|
|
2554
2712
|
realTables.set(table.name, table);
|
|
2555
2713
|
});
|
|
2556
2714
|
const segmentsByTable = new Map();
|
|
@@ -2809,38 +2967,50 @@ export class MinnowDatabase {
|
|
|
2809
2967
|
* materialize their referenced columns. Each block resolves its own inputs, so one real table
|
|
2810
2968
|
* projected differently by two blocks never collides.
|
|
2811
2969
|
*/
|
|
2812
|
-
async #prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
|
|
2970
|
+
async #prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
|
|
2971
|
+
throwIfAborted(signal);
|
|
2813
2972
|
const inputs = new Map(extraInputs ?? []);
|
|
2814
2973
|
const sources = [block.base, ...block.joins];
|
|
2815
2974
|
for (const source of sources) {
|
|
2975
|
+
throwIfAborted(signal);
|
|
2816
2976
|
if (inputs.has(source.table))
|
|
2817
2977
|
continue;
|
|
2818
2978
|
if (source.recursive !== undefined) {
|
|
2819
2979
|
const { reference, base, step, all } = source.recursive;
|
|
2820
|
-
const { result: baseResult, schema } = await this.#executeBlockWithSchemaCached(base, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
2980
|
+
const { result: baseResult, schema } = await this.#executeBlockWithSchemaCached(base, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
2821
2981
|
typedSchemas.set(reference, schema);
|
|
2822
2982
|
const state = createRecursiveCteState(baseResult, all);
|
|
2823
2983
|
while (state.frontier.length > 0) {
|
|
2984
|
+
throwIfAborted(signal);
|
|
2824
2985
|
// Each iteration's inputs release with its own context, so only the accumulated rows
|
|
2825
2986
|
// and the final columnar table hold memory across the fixpoint loop.
|
|
2826
2987
|
const iterationMemory = memory.createChild();
|
|
2827
2988
|
try {
|
|
2828
|
-
const frontierTable = derivedColumnarTable(reference, {
|
|
2829
|
-
|
|
2989
|
+
const frontierTable = derivedColumnarTable(reference, {
|
|
2990
|
+
columns: [...state.columns],
|
|
2991
|
+
columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2992
|
+
rows: state.frontier,
|
|
2993
|
+
}, schema);
|
|
2994
|
+
state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows, signal));
|
|
2830
2995
|
}
|
|
2831
2996
|
finally {
|
|
2832
2997
|
iterationMemory.close();
|
|
2833
2998
|
}
|
|
2834
2999
|
}
|
|
2835
3000
|
typedSchemas.set(source.table, schema);
|
|
2836
|
-
inputs.set(source.table, derivedColumnarTable(source.table, {
|
|
3001
|
+
inputs.set(source.table, derivedColumnarTable(source.table, {
|
|
3002
|
+
columns: [...state.columns],
|
|
3003
|
+
columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
3004
|
+
rows: state.rows,
|
|
3005
|
+
}, schema));
|
|
2837
3006
|
continue;
|
|
2838
3007
|
}
|
|
2839
3008
|
if (source.union !== undefined) {
|
|
2840
3009
|
const results = [];
|
|
2841
3010
|
let schema;
|
|
2842
3011
|
for (const member of source.union.blocks) {
|
|
2843
|
-
|
|
3012
|
+
throwIfAborted(signal);
|
|
3013
|
+
const { result, schema: memberSchema } = await this.#executeBlockWithSchemaCached(member, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
2844
3014
|
if (schema === undefined)
|
|
2845
3015
|
schema = memberSchema;
|
|
2846
3016
|
else {
|
|
@@ -2869,6 +3039,7 @@ export class MinnowDatabase {
|
|
|
2869
3039
|
// key plus the window spec, so a warm repeat skips the window pass and the
|
|
2870
3040
|
// rows-to-columnar conversion entirely.
|
|
2871
3041
|
const innerKey = await this.#blockResultCacheKey(source.windowed.block, snapshot, visibility, realTables, cacheResults);
|
|
3042
|
+
throwIfAborted(signal);
|
|
2872
3043
|
const columnarKey = innerKey === undefined
|
|
2873
3044
|
? undefined
|
|
2874
3045
|
: `ctw|${JSON.stringify(source.windowed.windows)}|${innerKey}`;
|
|
@@ -2880,7 +3051,7 @@ export class MinnowDatabase {
|
|
|
2880
3051
|
continue;
|
|
2881
3052
|
}
|
|
2882
3053
|
}
|
|
2883
|
-
const { result: inner, schema: innerSchema } = await this.#executeBlockWithSchemaCached(source.windowed.block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
3054
|
+
const { result: inner, schema: innerSchema } = await this.#executeBlockWithSchemaCached(source.windowed.block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
2884
3055
|
const windowed = applyWindowFunctions(inner, source.windowed.windows, {
|
|
2885
3056
|
copyRows: cacheResults,
|
|
2886
3057
|
});
|
|
@@ -2907,6 +3078,7 @@ export class MinnowDatabase {
|
|
|
2907
3078
|
if (derived === undefined)
|
|
2908
3079
|
continue;
|
|
2909
3080
|
const derivedKey = await this.#blockResultCacheKey(derived, snapshot, visibility, realTables, cacheResults);
|
|
3081
|
+
throwIfAborted(signal);
|
|
2910
3082
|
const columnarKey = derivedKey === undefined ? undefined : `ctd|${derivedKey}`;
|
|
2911
3083
|
if (columnarKey !== undefined) {
|
|
2912
3084
|
const hit = this.#cacheGet(columnarKey);
|
|
@@ -2916,7 +3088,7 @@ export class MinnowDatabase {
|
|
|
2916
3088
|
continue;
|
|
2917
3089
|
}
|
|
2918
3090
|
}
|
|
2919
|
-
const { result, schema } = await this.#executeBlockWithSchemaCached(derived, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
3091
|
+
const { result, schema } = await this.#executeBlockWithSchemaCached(derived, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
2920
3092
|
typedSchemas.set(source.table, schema);
|
|
2921
3093
|
const derivedTable = derivedColumnarTable(source.table, result, schema);
|
|
2922
3094
|
if (columnarKey !== undefined) {
|
|
@@ -2931,6 +3103,7 @@ export class MinnowDatabase {
|
|
|
2931
3103
|
const columns = referencedColumns(block, nameSchemas);
|
|
2932
3104
|
const onlyRealSource = sources.length === 1 && sources[0]?.derived === undefined ? block : undefined;
|
|
2933
3105
|
for (const source of sources) {
|
|
3106
|
+
throwIfAborted(signal);
|
|
2934
3107
|
if (source.derived !== undefined || inputs.has(source.table))
|
|
2935
3108
|
continue;
|
|
2936
3109
|
if (source.table === DUAL_TABLE) {
|
|
@@ -2939,9 +3112,10 @@ export class MinnowDatabase {
|
|
|
2939
3112
|
}
|
|
2940
3113
|
const table = realTables.get(source.table);
|
|
2941
3114
|
if (table === undefined)
|
|
2942
|
-
throw new
|
|
3115
|
+
throw new UnknownTableError(source.table);
|
|
2943
3116
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
2944
3117
|
inputs.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, onlyRealSource));
|
|
3118
|
+
throwIfAborted(signal);
|
|
2945
3119
|
}
|
|
2946
3120
|
return inputs;
|
|
2947
3121
|
}
|
|
@@ -2958,6 +3132,7 @@ export class MinnowDatabase {
|
|
|
2958
3132
|
return this.#queryWithReadReservation(sql, options);
|
|
2959
3133
|
}
|
|
2960
3134
|
async #queryUnreserved(sql, options) {
|
|
3135
|
+
throwIfAborted(options.signal);
|
|
2961
3136
|
const open = this.#openTransaction;
|
|
2962
3137
|
if (open !== undefined) {
|
|
2963
3138
|
// Read-your-writes: inside BEGIN … COMMIT a read sees the pre-scope snapshot plus what
|
|
@@ -2983,6 +3158,7 @@ export class MinnowDatabase {
|
|
|
2983
3158
|
const result = !memoizable
|
|
2984
3159
|
? await this.#queryCompiled(plan, options)
|
|
2985
3160
|
: await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, probe);
|
|
3161
|
+
throwIfAborted(options.signal);
|
|
2986
3162
|
return externalizeQueryResult(result);
|
|
2987
3163
|
}
|
|
2988
3164
|
/**
|
|
@@ -3047,7 +3223,8 @@ export class MinnowDatabase {
|
|
|
3047
3223
|
};
|
|
3048
3224
|
}
|
|
3049
3225
|
async #queryBatches(sql, options, cursor) {
|
|
3050
|
-
options = this.#effectiveQueryOptions(options);
|
|
3226
|
+
options = this.#effectiveQueryOptions({ ...options, signal: cursor.signal });
|
|
3227
|
+
cursor.signal.throwIfAborted();
|
|
3051
3228
|
// A transaction's staged overlay has its own executor. Keep its read-your-writes semantics
|
|
3052
3229
|
// and page the materialized result; native scan batching is for durable snapshots.
|
|
3053
3230
|
if (this.#openTransaction === undefined) {
|
|
@@ -3076,6 +3253,7 @@ export class MinnowDatabase {
|
|
|
3076
3253
|
cursor.signal.throwIfAborted();
|
|
3077
3254
|
await cursor.consume({
|
|
3078
3255
|
columns: [...result.columns],
|
|
3256
|
+
columnDomains: [...result.columnDomains],
|
|
3079
3257
|
rows: result.rows.slice(start, start + cursor.batchRows),
|
|
3080
3258
|
});
|
|
3081
3259
|
}
|
|
@@ -3087,16 +3265,22 @@ export class MinnowDatabase {
|
|
|
3087
3265
|
* and on IndexedDB every probe is a read transaction, a floor under every small query.
|
|
3088
3266
|
*/
|
|
3089
3267
|
async #memoizedQuery(plan, key, options, probe) {
|
|
3268
|
+
throwIfAborted(options.signal);
|
|
3090
3269
|
const before = await probe();
|
|
3270
|
+
throwIfAborted(options.signal);
|
|
3091
3271
|
const cached = this.#cacheGet(`${key}\u0001${String(before.catalogEpoch)}`);
|
|
3092
|
-
if (cached !== undefined)
|
|
3272
|
+
if (cached !== undefined) {
|
|
3273
|
+
options.onStats?.({ peakMemoryBytes: 0 });
|
|
3093
3274
|
return copyQueryResult(cached);
|
|
3275
|
+
}
|
|
3094
3276
|
const result = await this.#queryCompiled(plan, options, before);
|
|
3277
|
+
throwIfAborted(options.signal);
|
|
3095
3278
|
const bytes = queryResultRetainedBytes(result);
|
|
3096
3279
|
if (bytes <= RESULT_MEMO_MAX_BYTES) {
|
|
3097
3280
|
// Cache only when the epoch did not move during execution: the result is then exactly
|
|
3098
3281
|
// that epoch's answer. A moved epoch simply skips the cache — never mis-files.
|
|
3099
3282
|
const after = await probe();
|
|
3283
|
+
throwIfAborted(options.signal);
|
|
3100
3284
|
if (after.catalogEpoch === before.catalogEpoch) {
|
|
3101
3285
|
this.#cachePut(`${key}\u0001${String(before.catalogEpoch)}`, copyQueryResult(result), bytes);
|
|
3102
3286
|
}
|
|
@@ -3276,10 +3460,13 @@ export class MinnowDatabase {
|
|
|
3276
3460
|
*/
|
|
3277
3461
|
async #queryCompiled(plan, options = {}, probe) {
|
|
3278
3462
|
options = this.#effectiveQueryOptions(options);
|
|
3463
|
+
throwIfAborted(options.signal);
|
|
3279
3464
|
// One freshness probe per query: read here unless the caller already has one, and handed
|
|
3280
3465
|
// to the view lookup and the catalog state below, which would otherwise probe again each.
|
|
3281
3466
|
probe ??= await this.store.getCatalogProbe();
|
|
3467
|
+
throwIfAborted(options.signal);
|
|
3282
3468
|
plan = await this.#applyCatalogRewrites(plan, probe);
|
|
3469
|
+
throwIfAborted(options.signal);
|
|
3283
3470
|
const spillPageRows = options.spillPageRows === undefined
|
|
3284
3471
|
? undefined
|
|
3285
3472
|
: positiveWholeNumber(options.spillPageRows, "Query spill page rows");
|
|
@@ -3300,6 +3487,7 @@ export class MinnowDatabase {
|
|
|
3300
3487
|
}
|
|
3301
3488
|
}
|
|
3302
3489
|
const prepared = await this.#prepareCompiledPlan(plan, options, probe);
|
|
3490
|
+
throwIfAborted(options.signal);
|
|
3303
3491
|
// Read the peak before close(): closing releases the context and zeroes what it tracked.
|
|
3304
3492
|
const report = (result) => {
|
|
3305
3493
|
options.onStats?.({ peakMemoryBytes: prepared.memoryUsage.peakBytes });
|
|
@@ -3307,11 +3495,16 @@ export class MinnowDatabase {
|
|
|
3307
3495
|
};
|
|
3308
3496
|
try {
|
|
3309
3497
|
const spill = options.spillToStorage ?? options.executionMemoryBudgetBytes !== undefined;
|
|
3310
|
-
if (!spill)
|
|
3311
|
-
|
|
3498
|
+
if (!spill) {
|
|
3499
|
+
const result = prepared.execute();
|
|
3500
|
+
throwIfAborted(options.signal);
|
|
3501
|
+
return report(result);
|
|
3502
|
+
}
|
|
3312
3503
|
if (options.spillToStorage !== true) {
|
|
3313
3504
|
try {
|
|
3314
|
-
|
|
3505
|
+
const result = prepared.execute();
|
|
3506
|
+
throwIfAborted(options.signal);
|
|
3507
|
+
return report(result);
|
|
3315
3508
|
}
|
|
3316
3509
|
catch (error) {
|
|
3317
3510
|
if (!(error instanceof QueryMemoryBudgetError))
|
|
@@ -3321,12 +3514,33 @@ export class MinnowDatabase {
|
|
|
3321
3514
|
return report(await prepared.executeAsync({
|
|
3322
3515
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
3323
3516
|
spillStore: this.#leasedSpillStore(),
|
|
3517
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
3324
3518
|
}));
|
|
3325
3519
|
}
|
|
3326
3520
|
finally {
|
|
3327
3521
|
prepared.close();
|
|
3328
3522
|
}
|
|
3329
3523
|
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Streams a one-column snapshot projection into bounded pages. DELETE uses this before its
|
|
3526
|
+
* first staged write: selection keeps all ordinary pruning/predicate semantics without
|
|
3527
|
+
* retaining every QueryRow object until the key array is complete. Undefined means the shape
|
|
3528
|
+
* or physical history needs the established row-shaped executor.
|
|
3529
|
+
*/
|
|
3530
|
+
async #queryCompiledFirstColumn(plan, probe) {
|
|
3531
|
+
const options = this.#effectiveQueryOptions({ memoize: false });
|
|
3532
|
+
plan = await this.#applyCatalogRewrites(plan, probe);
|
|
3533
|
+
const values = [];
|
|
3534
|
+
const streamed = await this.#queryStreamed(plan, options, undefined, probe, {
|
|
3535
|
+
batchRows: 2_048,
|
|
3536
|
+
signal: new AbortController().signal,
|
|
3537
|
+
consumeFirstColumn: async (batch) => {
|
|
3538
|
+
values.push(...batch);
|
|
3539
|
+
},
|
|
3540
|
+
consume: async () => undefined,
|
|
3541
|
+
});
|
|
3542
|
+
return streamed === undefined ? undefined : values;
|
|
3543
|
+
}
|
|
3330
3544
|
/**
|
|
3331
3545
|
* Backs the executor's spill pages with durable owner leases: each owner registers a lease
|
|
3332
3546
|
* before its first page write and renews it while pages are read or written, so an abandoned
|
|
@@ -3714,15 +3928,18 @@ export class MinnowDatabase {
|
|
|
3714
3928
|
* one, so a table that cannot be compacted today costs one attempt, not one per query.
|
|
3715
3929
|
*/
|
|
3716
3930
|
#maybeScheduleAutoCompaction(table, segments) {
|
|
3931
|
+
this.#maybeScheduleAutoCompactionFromHint(table, autoCompactionHint(null, segments));
|
|
3932
|
+
}
|
|
3933
|
+
#maybeScheduleAutoCompactionFromHint(table, hint) {
|
|
3717
3934
|
if (this.#closed)
|
|
3718
3935
|
return;
|
|
3719
3936
|
if (!this.#autoCompact)
|
|
3720
3937
|
return;
|
|
3721
3938
|
if (this.#droppingTables.has(table.id))
|
|
3722
3939
|
return;
|
|
3723
|
-
if (!
|
|
3940
|
+
if (!autoCompactionDueHint(hint))
|
|
3724
3941
|
return;
|
|
3725
|
-
if (
|
|
3942
|
+
if (hint.visible < (this.#autoCompactionBackoff.get(table.id) ?? 0))
|
|
3726
3943
|
return;
|
|
3727
3944
|
if (this.#autoCompactionsInFlight.has(table.id)) {
|
|
3728
3945
|
// A final burst can cross the threshold while the prior fold is still planning or
|
|
@@ -3736,14 +3953,14 @@ export class MinnowDatabase {
|
|
|
3736
3953
|
if (folded)
|
|
3737
3954
|
this.#autoCompactionBackoff.delete(table.id);
|
|
3738
3955
|
else {
|
|
3739
|
-
this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2,
|
|
3956
|
+
this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, hint.visible * 2)));
|
|
3740
3957
|
}
|
|
3741
3958
|
})
|
|
3742
3959
|
.catch(() => {
|
|
3743
3960
|
// Back off deterministic failures, but never beyond the maximum L0 prefix a fold can
|
|
3744
3961
|
// consume. A transient conflict near the end of a burst must not strand hundreds of
|
|
3745
3962
|
// segments waiting for a segment count the idle database can never reach.
|
|
3746
|
-
this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2,
|
|
3963
|
+
this.#autoCompactionBackoff.set(table.id, Math.min(AUTO_COMPACT_MAX_LEVEL_ZERO_SEGMENTS, Math.max(2, hint.visible * 2)));
|
|
3747
3964
|
})
|
|
3748
3965
|
.finally(() => {
|
|
3749
3966
|
if (this.#autoCompactionsInFlight.get(table.id) === run) {
|
|
@@ -3763,6 +3980,10 @@ export class MinnowDatabase {
|
|
|
3763
3980
|
// every write and let the commit return the identical CompactionBacklogError at the CAS.
|
|
3764
3981
|
if (!this.#autoCompact)
|
|
3765
3982
|
return;
|
|
3983
|
+
const hintVersion = transaction?.snapshotVersion ?? (await this.store.getCurrentManifestVersion());
|
|
3984
|
+
const hint = this.#autoCompactionHints.get(table.id);
|
|
3985
|
+
if (hint?.version === hintVersion && hint.levelZero < MAX_LEVEL_ZERO_SEGMENTS)
|
|
3986
|
+
return;
|
|
3766
3987
|
let segments = await this.#currentVisibleSegments(table);
|
|
3767
3988
|
let levelZero = 0;
|
|
3768
3989
|
for (const segment of segments) {
|
|
@@ -3836,9 +4057,11 @@ export class MinnowDatabase {
|
|
|
3836
4057
|
const version = await this.store.getCurrentManifestVersion();
|
|
3837
4058
|
segments = await this.#visibleSegmentRecords(table, new Snapshot(this.store, version));
|
|
3838
4059
|
if ((await this.store.getCurrentManifestVersion()) === version) {
|
|
4060
|
+
this.#autoCompactionHints.set(table.id, autoCompactionHint(version, segments));
|
|
3839
4061
|
return segments;
|
|
3840
4062
|
}
|
|
3841
4063
|
}
|
|
4064
|
+
this.#autoCompactionHints.delete(table.id);
|
|
3842
4065
|
return segments;
|
|
3843
4066
|
}
|
|
3844
4067
|
/**
|
|
@@ -3849,6 +4072,7 @@ export class MinnowDatabase {
|
|
|
3849
4072
|
if (this.#closed)
|
|
3850
4073
|
return;
|
|
3851
4074
|
const contribution = transaction?.committedCatalogContribution;
|
|
4075
|
+
this.#advanceAutoCompactionHints(manifest, contribution?.segments);
|
|
3852
4076
|
const initial = contribution?.initialCatalogProbe;
|
|
3853
4077
|
const contributedTableIds = new Set(contribution?.segments.map((segment) => segment.tableId) ?? []);
|
|
3854
4078
|
if (contribution !== undefined &&
|
|
@@ -3942,7 +4166,7 @@ export class MinnowDatabase {
|
|
|
3942
4166
|
this.#armIdleCompactionCheck();
|
|
3943
4167
|
}
|
|
3944
4168
|
/** Counts one physical rewrite version once in this engine, including lost-ack recovery. */
|
|
3945
|
-
#afterCompactionCommit(manifest) {
|
|
4169
|
+
#afterCompactionCommit(manifest, tableId) {
|
|
3946
4170
|
if (this.#accountedCompactionVersions.has(manifest.version))
|
|
3947
4171
|
return;
|
|
3948
4172
|
this.#accountedCompactionVersions.add(manifest.version);
|
|
@@ -3951,8 +4175,49 @@ export class MinnowDatabase {
|
|
|
3951
4175
|
if (oldest !== undefined)
|
|
3952
4176
|
this.#accountedCompactionVersions.delete(oldest);
|
|
3953
4177
|
}
|
|
4178
|
+
// A fold replaces source segments with a different physical layout. Its manifest is
|
|
4179
|
+
// logically unchanged and therefore carries no changed-table ID, so invalidate explicitly;
|
|
4180
|
+
// #runAutoCompaction's stable post-fold probe seeds the replacement counters.
|
|
4181
|
+
this.#autoCompactionHints.delete(tableId);
|
|
3954
4182
|
this.#afterCommit(manifest);
|
|
3955
4183
|
}
|
|
4184
|
+
/** Advances exact local hints, or discards them when a commit's provenance is incomplete. */
|
|
4185
|
+
#advanceAutoCompactionHints(manifest, contributedSegments) {
|
|
4186
|
+
for (const [tableId, hint] of this.#autoCompactionHints) {
|
|
4187
|
+
if (hint.version !== manifest.previousVersion) {
|
|
4188
|
+
// Another database committed since this instance last proved the layout.
|
|
4189
|
+
this.#autoCompactionHints.delete(tableId);
|
|
4190
|
+
continue;
|
|
4191
|
+
}
|
|
4192
|
+
this.#autoCompactionHints.set(tableId, { ...hint, version: manifest.version });
|
|
4193
|
+
}
|
|
4194
|
+
const additionsByTable = new Map();
|
|
4195
|
+
for (const segment of contributedSegments ?? []) {
|
|
4196
|
+
const additions = additionsByTable.get(segment.tableId);
|
|
4197
|
+
if (additions === undefined)
|
|
4198
|
+
additionsByTable.set(segment.tableId, [segment]);
|
|
4199
|
+
else
|
|
4200
|
+
additions.push(segment);
|
|
4201
|
+
}
|
|
4202
|
+
for (const tableId of manifest.changedTableIds) {
|
|
4203
|
+
const hint = this.#autoCompactionHints.get(tableId);
|
|
4204
|
+
if (hint === undefined)
|
|
4205
|
+
continue;
|
|
4206
|
+
const additions = additionsByTable.get(tableId);
|
|
4207
|
+
if (additions === undefined) {
|
|
4208
|
+
// DDL and future write paths without an exact segment contribution fail closed.
|
|
4209
|
+
this.#autoCompactionHints.delete(tableId);
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
const contribution = autoCompactionHint(manifest.version, additions);
|
|
4213
|
+
this.#autoCompactionHints.set(tableId, {
|
|
4214
|
+
version: manifest.version,
|
|
4215
|
+
visible: hint.visible + contribution.visible,
|
|
4216
|
+
levelZero: hint.levelZero + contribution.levelZero,
|
|
4217
|
+
deltas: hint.deltas + contribution.deltas,
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
3956
4221
|
/**
|
|
3957
4222
|
* Debounces the final write-path check for a burst. Sampling every few commits keeps the hot
|
|
3958
4223
|
* path cheap, but the last one through seven commits can be the ones that cross a fold
|
|
@@ -3990,6 +4255,12 @@ export class MinnowDatabase {
|
|
|
3990
4255
|
const table = await this.store.getTable(tableId);
|
|
3991
4256
|
if (table === undefined)
|
|
3992
4257
|
return;
|
|
4258
|
+
const version = await this.store.getCurrentManifestVersion();
|
|
4259
|
+
const hint = this.#autoCompactionHints.get(tableId);
|
|
4260
|
+
if (hint?.version === version) {
|
|
4261
|
+
this.#maybeScheduleAutoCompactionFromHint(table, hint);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
3993
4264
|
this.#maybeScheduleAutoCompaction(table, await this.#currentVisibleSegments(table));
|
|
3994
4265
|
}
|
|
3995
4266
|
catch {
|
|
@@ -4013,7 +4284,7 @@ export class MinnowDatabase {
|
|
|
4013
4284
|
const tables = proof.records;
|
|
4014
4285
|
const table = tables.find((record) => record.name === tableName);
|
|
4015
4286
|
if (table === undefined)
|
|
4016
|
-
throw new
|
|
4287
|
+
throw new UnknownTableError(tableName);
|
|
4017
4288
|
if (table.view !== undefined)
|
|
4018
4289
|
throw new TypeError(`Cannot create a trigger on view ${tableName}`);
|
|
4019
4290
|
pinnedTableId ??= table.id;
|
|
@@ -4046,7 +4317,7 @@ export class MinnowDatabase {
|
|
|
4046
4317
|
}
|
|
4047
4318
|
const target = tables.find((record) => record.name === compiled.table);
|
|
4048
4319
|
if (target === undefined)
|
|
4049
|
-
throw new
|
|
4320
|
+
throw new UnknownTableError(compiled.table);
|
|
4050
4321
|
if (target.view !== undefined) {
|
|
4051
4322
|
throw new TypeError(`Trigger bodies cannot write to views: ${compiled.table}`);
|
|
4052
4323
|
}
|
|
@@ -4124,19 +4395,32 @@ export class MinnowDatabase {
|
|
|
4124
4395
|
* in-batch duplicate key fires as an UPDATE of the earlier occurrence. Returns undefined
|
|
4125
4396
|
* when the table has no insert or update triggers.
|
|
4126
4397
|
*/
|
|
4127
|
-
async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows) {
|
|
4398
|
+
async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere, force = false) {
|
|
4128
4399
|
const fires = (table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
|
|
4129
|
-
if (!fires || rowCount === 0)
|
|
4400
|
+
if ((!fires && !force) || rowCount === 0)
|
|
4130
4401
|
return undefined;
|
|
4131
4402
|
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4132
4403
|
const keyValues = batch.columns[keyColumn.name] ?? [];
|
|
4133
|
-
const
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
const
|
|
4404
|
+
const distinct = new Map();
|
|
4405
|
+
for (const value of keyValues) {
|
|
4406
|
+
if (value !== null)
|
|
4407
|
+
distinct.set(keyToken(keyColumn.type, value), value);
|
|
4408
|
+
}
|
|
4409
|
+
const params = [...distinct.values()];
|
|
4410
|
+
const rows = [];
|
|
4411
|
+
// Each value also contributes a comma token, so the parser's token cap is reached before
|
|
4412
|
+
// its parameter cap. A fixed 1,024-key window stays comfortably inside both limits.
|
|
4413
|
+
const keyWindowSize = Math.min(MAX_SQL_PARAMETERS, 1_024);
|
|
4414
|
+
for (let start = 0; start < params.length; start += keyWindowSize) {
|
|
4415
|
+
const window = params.slice(start, start + keyWindowSize);
|
|
4416
|
+
const placeholders = window.map(() => "?").join(", ");
|
|
4417
|
+
const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
|
|
4418
|
+
const result = readRows === undefined
|
|
4419
|
+
? await this.query(sql, { params: window, memoize: false })
|
|
4420
|
+
: await readRows(sql, window);
|
|
4421
|
+
rows.push(...result.rows);
|
|
4422
|
+
}
|
|
4423
|
+
const byToken = new Map(rows.map((row) => {
|
|
4140
4424
|
const key = row[keyColumn.name];
|
|
4141
4425
|
return [
|
|
4142
4426
|
key === null || key === undefined ? "" : keyToken(keyColumn.type, key),
|
|
@@ -4145,29 +4429,36 @@ export class MinnowDatabase {
|
|
|
4145
4429
|
}));
|
|
4146
4430
|
const inserts = [];
|
|
4147
4431
|
const updates = [];
|
|
4432
|
+
const skipped = [];
|
|
4148
4433
|
const oldImages = [];
|
|
4149
|
-
const
|
|
4434
|
+
const currentByToken = new Map(byToken);
|
|
4150
4435
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
4151
4436
|
const key = keyValues[rowIndex] ?? null;
|
|
4152
4437
|
const token = key === null ? "" : keyToken(keyColumn.type, key);
|
|
4153
|
-
const
|
|
4154
|
-
const old = earlier === undefined
|
|
4155
|
-
? byToken.get(token)
|
|
4156
|
-
: Object.fromEntries(table.columns.map((column) => [
|
|
4157
|
-
column.name,
|
|
4158
|
-
batch.columns[column.name]?.[earlier] ?? null,
|
|
4159
|
-
]));
|
|
4438
|
+
const old = currentByToken.get(token);
|
|
4160
4439
|
if (old === undefined) {
|
|
4161
4440
|
inserts.push(rowIndex);
|
|
4162
4441
|
oldImages.push(undefined);
|
|
4163
4442
|
}
|
|
4164
4443
|
else {
|
|
4165
|
-
updates.push(rowIndex);
|
|
4166
4444
|
oldImages.push(old);
|
|
4445
|
+
const rawLeft = conflictWhere === undefined ? null : (old[conflictWhere.column.name] ?? null);
|
|
4446
|
+
const left = conflictWhere?.column.sqlDomain === undefined
|
|
4447
|
+
? rawLeft
|
|
4448
|
+
: normalizeSqlDomainValue(conflictWhere.column.sqlDomain, rawLeft);
|
|
4449
|
+
if (conflictWhere !== undefined &&
|
|
4450
|
+
!comparisonHolds(conflictWhere.operator, left, conflictWhere.value)) {
|
|
4451
|
+
skipped.push(rowIndex);
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
updates.push(rowIndex);
|
|
4167
4455
|
}
|
|
4168
|
-
|
|
4456
|
+
currentByToken.set(token, Object.fromEntries(table.columns.map((column) => [
|
|
4457
|
+
column.name,
|
|
4458
|
+
batch.columns[column.name]?.[rowIndex] ?? null,
|
|
4459
|
+
])));
|
|
4169
4460
|
}
|
|
4170
|
-
return { inserts, updates, oldImages };
|
|
4461
|
+
return { inserts, updates, skipped, oldImages };
|
|
4171
4462
|
}
|
|
4172
4463
|
/** Fires INSERT and UPDATE triggers for one upsert batch at one timing, per the plan. */
|
|
4173
4464
|
async #stageUpsertTriggerFirings(transaction, table, batch, firings, timing, cascadeBudget = 1) {
|
|
@@ -4250,6 +4541,7 @@ export class MinnowDatabase {
|
|
|
4250
4541
|
const statementNow = this.#now();
|
|
4251
4542
|
const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
|
|
4252
4543
|
normalizeDomainBatch(target, filled.batch);
|
|
4544
|
+
fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
|
|
4253
4545
|
fillCompositePrimaryKey(target, filled.batch, derivedRows.length);
|
|
4254
4546
|
validateBatch(target, filled.batch, filled.autoIncrement?.column.name);
|
|
4255
4547
|
if (filled.autoIncrement !== undefined && filled.autoIncrement.missingIndexes.length > 0) {
|
|
@@ -4522,6 +4814,15 @@ export class MinnowDatabase {
|
|
|
4522
4814
|
? this.#queryCompiled(plan, {}, probe)
|
|
4523
4815
|
: this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
|
|
4524
4816
|
}),
|
|
4817
|
+
queryFirstColumn: (plan) => this.#withReadReservation(async () => {
|
|
4818
|
+
// A staged overlay can add, replace, or remove keys without changing the pinned
|
|
4819
|
+
// manifest. Keep that case on the read-your-writes row executor; before the first
|
|
4820
|
+
// stage, the transaction's exact catalog probe makes the streamed snapshot safe.
|
|
4821
|
+
const probe = transaction.initialCatalogProbe;
|
|
4822
|
+
return transaction.stagedWorkCount === 0 && probe !== undefined
|
|
4823
|
+
? this.#queryCompiledFirstColumn(plan, probe)
|
|
4824
|
+
: undefined;
|
|
4825
|
+
}),
|
|
4525
4826
|
executeStatement: (statement) => {
|
|
4526
4827
|
open();
|
|
4527
4828
|
return guarded(() => this.runStatement(statement, { writer }));
|
|
@@ -4590,21 +4891,26 @@ export class MinnowDatabase {
|
|
|
4590
4891
|
*/
|
|
4591
4892
|
async #sessionQuery(transaction, sql, options = {}) {
|
|
4592
4893
|
options = this.#effectiveQueryOptions(options);
|
|
4894
|
+
throwIfAborted(options.signal);
|
|
4593
4895
|
const bound = bindPlanParameters(this.#compileCached(sql), options.params);
|
|
4594
4896
|
// The same catalog rewrites a read outside a scope gets: a scope that could not see a view
|
|
4595
4897
|
// would make the scope's reads a different language from everyone else's.
|
|
4596
4898
|
const plan = await this.#applyCatalogRewrites(bound);
|
|
4899
|
+
throwIfAborted(options.signal);
|
|
4597
4900
|
return this.#sessionQueryPlan(transaction, plan, options);
|
|
4598
4901
|
}
|
|
4599
4902
|
/** #sessionQuery for an already-bound plan: trigger bodies resolve their reads through it. */
|
|
4600
4903
|
async #sessionQueryPlan(transaction, plan, options = {}) {
|
|
4601
4904
|
options = this.#effectiveQueryOptions(options);
|
|
4905
|
+
throwIfAborted(options.signal);
|
|
4602
4906
|
const names = collectRealTableNames(plan);
|
|
4603
4907
|
const tables = await Promise.all(names.map((name) => this.#findTable(name)));
|
|
4908
|
+
throwIfAborted(options.signal);
|
|
4604
4909
|
const realTables = new Map(tables.map((table) => [table.name, table]));
|
|
4605
4910
|
const pendingIds = new Set(transaction.pendingSegmentIds);
|
|
4606
4911
|
const pendingBlocks = new Set(transaction.pendingBlockIds);
|
|
4607
4912
|
const ourRecord = await this.store.getTransaction(transaction.id);
|
|
4913
|
+
throwIfAborted(options.signal);
|
|
4608
4914
|
// Pinned to the scope's own snapshot, not the current manifest: the documented contract
|
|
4609
4915
|
// is "the pre-scope snapshot plus everything this scope staged", and #assertKeysExist
|
|
4610
4916
|
// already validates against transaction.snapshotVersion — reading the current version
|
|
@@ -4628,6 +4934,7 @@ export class MinnowDatabase {
|
|
|
4628
4934
|
getBlocks: async (blockIds) => {
|
|
4629
4935
|
const blocks = [];
|
|
4630
4936
|
for (let start = 0; start < blockIds.length; start += 64) {
|
|
4937
|
+
throwIfAborted(options.signal);
|
|
4631
4938
|
const window = blockIds.slice(start, start + 64);
|
|
4632
4939
|
const committedIds = [];
|
|
4633
4940
|
const committedIndexes = [];
|
|
@@ -4652,6 +4959,7 @@ export class MinnowDatabase {
|
|
|
4652
4959
|
this.#getBlocksWindowed(committedIds),
|
|
4653
4960
|
Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId))),
|
|
4654
4961
|
]);
|
|
4962
|
+
throwIfAborted(options.signal);
|
|
4655
4963
|
const ordered = new Array(window.length);
|
|
4656
4964
|
committedIndexes.forEach((index, offset) => {
|
|
4657
4965
|
ordered[index] = committed[offset];
|
|
@@ -4670,10 +4978,12 @@ export class MinnowDatabase {
|
|
|
4670
4978
|
const transactionRecords = new Map();
|
|
4671
4979
|
const deferredSegments = transaction.deferredSegments;
|
|
4672
4980
|
for (const table of tables) {
|
|
4981
|
+
throwIfAborted(options.signal);
|
|
4673
4982
|
const segments = [
|
|
4674
4983
|
...(await listTableSegmentsPaged(this.store, table.id)),
|
|
4675
4984
|
...deferredSegments.filter((segment) => segment.tableId === table.id),
|
|
4676
4985
|
];
|
|
4986
|
+
throwIfAborted(options.signal);
|
|
4677
4987
|
// Staged segments sort after every committed one; commitOrdinal keeps their
|
|
4678
4988
|
// staging order (the journal's id list is sorted, so it cannot).
|
|
4679
4989
|
const doctored = segments.map((segment) => pendingIds.has(segment.id)
|
|
@@ -4683,6 +4993,7 @@ export class MinnowDatabase {
|
|
|
4683
4993
|
for (const record of await this.#transactionRecordsForSegments(doctored)) {
|
|
4684
4994
|
transactionRecords.set(record.id, record);
|
|
4685
4995
|
}
|
|
4996
|
+
throwIfAborted(options.signal);
|
|
4686
4997
|
}
|
|
4687
4998
|
if (ourRecord !== undefined) {
|
|
4688
4999
|
transactionRecords.set(transaction.id, {
|
|
@@ -4702,6 +5013,7 @@ export class MinnowDatabase {
|
|
|
4702
5013
|
/** Executes one plan at an explicit snapshot and visibility, covering every plan shape. */
|
|
4703
5014
|
async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
|
|
4704
5015
|
options = this.#effectiveQueryOptions(options);
|
|
5016
|
+
throwIfAborted(options.signal);
|
|
4705
5017
|
plan = expandFtsColumns(plan, (name) => searchableFtsColumns(realTables.get(name)));
|
|
4706
5018
|
const typedSchemas = new Map([...realTables.values()].map((table) => [
|
|
4707
5019
|
table.name,
|
|
@@ -4718,16 +5030,22 @@ export class MinnowDatabase {
|
|
|
4718
5030
|
const spillPageRows = options.spillPageRows === undefined
|
|
4719
5031
|
? undefined
|
|
4720
5032
|
: positiveWholeNumber(options.spillPageRows, "Query spill page rows");
|
|
5033
|
+
const report = (result) => {
|
|
5034
|
+
throwIfAborted(options.signal);
|
|
5035
|
+
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
5036
|
+
return result;
|
|
5037
|
+
};
|
|
4721
5038
|
try {
|
|
4722
5039
|
const resolution = subqueryResolutionSteps(plan);
|
|
4723
5040
|
for (const step of resolution.steps) {
|
|
4724
|
-
|
|
5041
|
+
throwIfAborted(options.signal);
|
|
5042
|
+
step.substitute(await this.#executeBlock(step.block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal));
|
|
4725
5043
|
}
|
|
4726
5044
|
const wrapper = transparentProjectionSource(resolution.plan);
|
|
4727
5045
|
if (wrapper !== undefined) {
|
|
4728
|
-
return projectResultColumns(await this.#executeBlock(wrapper.inner, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows), wrapper.aliases);
|
|
5046
|
+
return report(projectResultColumns(await this.#executeBlock(wrapper.inner, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal), wrapper.aliases));
|
|
4729
5047
|
}
|
|
4730
|
-
return await this.#executeBlock(resolution.plan, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
5048
|
+
return report(await this.#executeBlock(resolution.plan, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal));
|
|
4731
5049
|
}
|
|
4732
5050
|
finally {
|
|
4733
5051
|
memory.close();
|
|
@@ -4812,6 +5130,7 @@ export class MinnowDatabase {
|
|
|
4812
5130
|
throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
|
|
4813
5131
|
}
|
|
4814
5132
|
input = normalizeDomainUpdate(table, input);
|
|
5133
|
+
rejectGeneratedUpdateAssignments(table, input);
|
|
4815
5134
|
const keys = validateUpdateBatch(table, keyColumn, input);
|
|
4816
5135
|
// Read-your-writes membership: keys staged by this scope pass, keys the scope removed
|
|
4817
5136
|
// fail, and everything else checks against the committed snapshot as usual.
|
|
@@ -4826,11 +5145,17 @@ export class MinnowDatabase {
|
|
|
4826
5145
|
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
|
|
4827
5146
|
}
|
|
4828
5147
|
const sessionChecks = table.checks ?? [];
|
|
4829
|
-
|
|
5148
|
+
let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
5149
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
4830
5150
|
const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (preImageSql, params) => this.#sessionQuery(transaction, preImageSql, { params }), sessionChecks.length > 0 ||
|
|
4831
5151
|
changedForeignKey ||
|
|
5152
|
+
tableGeneratedExpressions(table).length > 0 ||
|
|
4832
5153
|
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
4833
5154
|
readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
|
|
5155
|
+
input = applyStoredGeneratedUpdateChanges(table, input, preImages);
|
|
5156
|
+
validateUpdateBatch(table, keyColumn, input);
|
|
5157
|
+
changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
5158
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
4834
5159
|
stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
|
|
4835
5160
|
const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
|
|
4836
5161
|
if (secondaryDeltas.length > 0) {
|
|
@@ -4912,7 +5237,19 @@ export class MinnowDatabase {
|
|
|
4912
5237
|
},
|
|
4913
5238
|
]);
|
|
4914
5239
|
await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
|
|
4915
|
-
return {
|
|
5240
|
+
return {
|
|
5241
|
+
tableName: table.name,
|
|
5242
|
+
segmentId,
|
|
5243
|
+
rowCount: input.keys.length,
|
|
5244
|
+
...(tableGeneratedExpressions(table).length === 0
|
|
5245
|
+
? {}
|
|
5246
|
+
: {
|
|
5247
|
+
generatedColumns: Object.fromEntries(tableGeneratedExpressions(table).map(({ column }) => [
|
|
5248
|
+
column.name,
|
|
5249
|
+
(input.changes[column.name] ?? []).map((value) => externalSqlDomainValue(value)),
|
|
5250
|
+
])),
|
|
5251
|
+
}),
|
|
5252
|
+
};
|
|
4916
5253
|
}
|
|
4917
5254
|
async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
|
|
4918
5255
|
const table = await this.#findTable(tableName);
|
|
@@ -5249,6 +5586,9 @@ export class MinnowDatabase {
|
|
|
5249
5586
|
...(columnDefinition.defaultSpec === undefined
|
|
5250
5587
|
? {}
|
|
5251
5588
|
: { defaultValue: columnDefinition.defaultSpec }),
|
|
5589
|
+
...(columnDefinition.generatedSpec === undefined
|
|
5590
|
+
? {}
|
|
5591
|
+
: { generatedValue: columnDefinition.generatedSpec }),
|
|
5252
5592
|
...(columnDefinition.enumValues === undefined
|
|
5253
5593
|
? {}
|
|
5254
5594
|
: { enumValues: columnDefinition.enumValues }),
|
|
@@ -5386,10 +5726,37 @@ export class MinnowDatabase {
|
|
|
5386
5726
|
}
|
|
5387
5727
|
}
|
|
5388
5728
|
}
|
|
5729
|
+
// Adopting generation on an existing physical column is metadata-only only when every
|
|
5730
|
+
// stored value already equals the declared expression. This supports migrations from an
|
|
5731
|
+
// application-maintained column without silently blessing stale historical rows.
|
|
5732
|
+
for (const step of steps) {
|
|
5733
|
+
if (step.kind !== "alter-generated" || step.generatedValue === null)
|
|
5734
|
+
continue;
|
|
5735
|
+
const target = record.columns.find(({ name }) => name === step.columnName);
|
|
5736
|
+
if (target === undefined)
|
|
5737
|
+
continue;
|
|
5738
|
+
const expression = compileGeneratedColumnExpression(record.name, target.name, step.generatedValue.sql, record.columns);
|
|
5739
|
+
const rows = (await this.query(`SELECT * FROM ${quoteSqlIdentifier(record.name)}`, { memoize: false })).rows;
|
|
5740
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
5741
|
+
const expected = normalizeGeneratedColumnValue(target, evaluateRowExpression(expression, record.name, generatedEvaluationRow(record, row)), rowIndex);
|
|
5742
|
+
const actual = normalizeGeneratedColumnValue(target, row[target.name] ?? null, rowIndex);
|
|
5743
|
+
const equal = expected instanceof Date && actual instanceof Date
|
|
5744
|
+
? dateMilliseconds(expected) === dateMilliseconds(actual)
|
|
5745
|
+
: Object.is(expected, actual);
|
|
5746
|
+
if (!equal) {
|
|
5747
|
+
throw new TypeError(`Generated column cannot be adopted: ${record.name}.${target.name} differs from its expression at row ${String(rowIndex)}`);
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5389
5751
|
// The remaining metadata-only steps share one catalog/manifest CAS. Physical drops ran
|
|
5390
5752
|
// above because segment rewriting and payload retirement have a stronger atomic boundary.
|
|
5391
5753
|
const columns = applyColumnSteps(record, steps, this.#createId);
|
|
5392
|
-
|
|
5754
|
+
const foreignKeyStep = steps.find((step) => step.kind === "alter-foreign-keys");
|
|
5755
|
+
const foreignKeys = foreignKeyStep?.kind === "alter-foreign-keys"
|
|
5756
|
+
? foreignKeyStep.foreignKeys.map((key) => ({ ...key }))
|
|
5757
|
+
: record.foreignKeys;
|
|
5758
|
+
if (JSON.stringify(columns) === JSON.stringify(record.columns) &&
|
|
5759
|
+
JSON.stringify(foreignKeys ?? []) === JSON.stringify(record.foreignKeys ?? [])) {
|
|
5393
5760
|
if (changed)
|
|
5394
5761
|
alteredTables.push(tableName);
|
|
5395
5762
|
continue;
|
|
@@ -5400,6 +5767,7 @@ export class MinnowDatabase {
|
|
|
5400
5767
|
assertDependentViewsKeepSchema(proof.records, record, candidateSchemas);
|
|
5401
5768
|
const updated = await this.store.updateTable(record.id, record.revision, {
|
|
5402
5769
|
columns,
|
|
5770
|
+
...(foreignKeyStep === undefined ? {} : { foreignKeys }),
|
|
5403
5771
|
...(autoIncrementSeed === undefined ? {} : { autoIncrementSeed }),
|
|
5404
5772
|
expectedManifestVersion: { value: proof.manifestVersion },
|
|
5405
5773
|
expectedCatalogEpoch: proof.catalogEpoch,
|
|
@@ -5533,6 +5901,8 @@ export class MinnowDatabase {
|
|
|
5533
5901
|
*/
|
|
5534
5902
|
async #assertForeignKeysPresent(table, rowsByColumn, read, transaction) {
|
|
5535
5903
|
for (const key of table.foreignKeys ?? []) {
|
|
5904
|
+
if (key.enforced === false)
|
|
5905
|
+
continue;
|
|
5536
5906
|
const childNames = foreignKeyColumns(key);
|
|
5537
5907
|
const parentNames = foreignKeyParentColumns(key);
|
|
5538
5908
|
if (childNames.length > 1) {
|
|
@@ -5671,7 +6041,7 @@ export class MinnowDatabase {
|
|
|
5671
6041
|
async #applyReferentialActions(parent, keys, session, cascadeBudget) {
|
|
5672
6042
|
if (keys.length === 0)
|
|
5673
6043
|
return;
|
|
5674
|
-
const children = await this.#childForeignKeys(parent.name);
|
|
6044
|
+
const children = (await this.#childForeignKeys(parent.name)).filter(({ key }) => key.enforced !== false);
|
|
5675
6045
|
if (children.length === 0)
|
|
5676
6046
|
return;
|
|
5677
6047
|
if (cascadeBudget < 0) {
|
|
@@ -5929,6 +6299,7 @@ export class MinnowDatabase {
|
|
|
5929
6299
|
const statementNow = this.#now();
|
|
5930
6300
|
const filledProposed = await fillColumnDefaults(table, proposedBatch, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
|
|
5931
6301
|
normalizeDomainBatch(table, filledProposed.batch);
|
|
6302
|
+
fillStoredGeneratedColumns(table, filledProposed.batch, statement.rows.length, filledProposed.generated);
|
|
5932
6303
|
fillCompositePrimaryKey(table, filledProposed.batch, statement.rows.length);
|
|
5933
6304
|
const proposed = filledProposed.batch;
|
|
5934
6305
|
// UPSERT only handles a uniqueness conflict. Other INSERT-domain failures are still errors,
|
|
@@ -5999,7 +6370,10 @@ export class MinnowDatabase {
|
|
|
5999
6370
|
// and UUID defaults twice and could even change which row conflicts.
|
|
6000
6371
|
const columns = visibleTableColumns(table).map(({ name }) => name);
|
|
6001
6372
|
const deferredAutoIncrement = new Set(filledProposed.autoIncrement?.missingIndexes ?? []);
|
|
6002
|
-
const
|
|
6373
|
+
const generatedColumns = new Set(table.columns.flatMap((column) => column.generatedValue === undefined ? [] : [column.name]));
|
|
6374
|
+
const rows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => generatedColumns.has(name) ||
|
|
6375
|
+
(name === filledProposed.autoIncrement?.column.name &&
|
|
6376
|
+
deferredAutoIncrement.has(rowIndex))
|
|
6003
6377
|
? { default: true }
|
|
6004
6378
|
: (proposed.columns[name]?.[rowIndex] ?? null)));
|
|
6005
6379
|
return this.runStatement({
|
|
@@ -6055,7 +6429,12 @@ export class MinnowDatabase {
|
|
|
6055
6429
|
return value;
|
|
6056
6430
|
});
|
|
6057
6431
|
const selected = await transaction.query(`SELECT * FROM ${quoteSqlIdentifier(table.name)} WHERE ${quoteSqlIdentifier(keyColumn.name)} IN (${keys.map(() => "?").join(", ")})`, { params: keys });
|
|
6058
|
-
const currentByKey = new Map(selected.rows.map((row) =>
|
|
6432
|
+
const currentByKey = new Map(selected.rows.map((row) => {
|
|
6433
|
+
const value = keyColumn.hidden
|
|
6434
|
+
? compositeKeyValue(primaryKeyColumns(table), primaryKeyColumns(table).map(({ name }) => row[name] ?? null))
|
|
6435
|
+
: (row[keyColumn.name] ?? null);
|
|
6436
|
+
return [keyToken(value), row];
|
|
6437
|
+
}));
|
|
6059
6438
|
const updateKeys = [];
|
|
6060
6439
|
const changes = Object.fromEntries(assignments.map(({ column }) => [column, []]));
|
|
6061
6440
|
for (const [position, rowIndex] of conflictingRows.entries()) {
|
|
@@ -6088,6 +6467,8 @@ export class MinnowDatabase {
|
|
|
6088
6467
|
if (freshRows.length > 0) {
|
|
6089
6468
|
const columns = {};
|
|
6090
6469
|
for (const column of table.columns) {
|
|
6470
|
+
if (column.generatedValue !== undefined)
|
|
6471
|
+
continue;
|
|
6091
6472
|
columns[column.name] = freshRows.map((rowIndex) => proposed.columns[column.name]?.[rowIndex] ?? null);
|
|
6092
6473
|
}
|
|
6093
6474
|
await transaction.insertBatch(table.name, { columns });
|
|
@@ -6126,7 +6507,9 @@ export class MinnowDatabase {
|
|
|
6126
6507
|
table: statement.table,
|
|
6127
6508
|
rowCount: outcome.affectedRows.length,
|
|
6128
6509
|
...(version === null || version === undefined ? {} : { version }),
|
|
6129
|
-
...(outcome.returnedRows === undefined
|
|
6510
|
+
...(outcome.returnedRows === undefined || returningColumns === undefined
|
|
6511
|
+
? {}
|
|
6512
|
+
: returningExecuteFields(table, returningColumns, outcome.returnedRows)),
|
|
6130
6513
|
};
|
|
6131
6514
|
}
|
|
6132
6515
|
/** ON CONFLICT DO NOTHING: drops insert rows whose unique key already exists at a snapshot. */
|
|
@@ -6149,10 +6532,13 @@ export class MinnowDatabase {
|
|
|
6149
6532
|
const statementNow = this.#now();
|
|
6150
6533
|
const filled = await fillColumnDefaults(table, proposed, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
|
|
6151
6534
|
normalizeDomainBatch(table, filled.batch);
|
|
6535
|
+
fillStoredGeneratedColumns(table, filled.batch, statement.rows.length, filled.generated);
|
|
6152
6536
|
fillCompositePrimaryKey(table, filled.batch, statement.rows.length);
|
|
6153
6537
|
const columns = visibleTableColumns(table).map(({ name }) => name);
|
|
6154
6538
|
const deferredAutoIncrement = new Set(filled.autoIncrement?.missingIndexes ?? []);
|
|
6155
|
-
const
|
|
6539
|
+
const generatedColumns = new Set(table.columns.flatMap((column) => (column.generatedValue === undefined ? [] : [column.name])));
|
|
6540
|
+
const materializedRows = Array.from({ length: statement.rows.length }, (_, rowIndex) => columns.map((name) => generatedColumns.has(name) ||
|
|
6541
|
+
(name === filled.autoIncrement?.column.name && deferredAutoIncrement.has(rowIndex))
|
|
6156
6542
|
? { default: true }
|
|
6157
6543
|
: (filled.batch.columns[name]?.[rowIndex] ?? null)));
|
|
6158
6544
|
const keys = filled.batch.columns[keyColumn.name] ?? [];
|
|
@@ -6457,7 +6843,11 @@ export class MinnowDatabase {
|
|
|
6457
6843
|
const stageRows = async (rows) => {
|
|
6458
6844
|
if (rows.length === 0)
|
|
6459
6845
|
return;
|
|
6460
|
-
const batchResult = {
|
|
6846
|
+
const batchResult = {
|
|
6847
|
+
columns: tableColumns.map(({ name }) => name),
|
|
6848
|
+
columnDomains: tableColumns.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
6849
|
+
rows: [...rows],
|
|
6850
|
+
};
|
|
6461
6851
|
if (queryResultRetainedBytes(batchResult) > executionMemoryBudgetBytes) {
|
|
6462
6852
|
throw new QueryMemoryBudgetError("CREATE TABLE AS result batch", queryResultRetainedBytes(batchResult), 0, executionMemoryBudgetBytes);
|
|
6463
6853
|
}
|
|
@@ -6592,7 +6982,7 @@ export class MinnowDatabase {
|
|
|
6592
6982
|
}
|
|
6593
6983
|
const record = proof.records.find((candidate) => candidate.name === addColumnStatement.table);
|
|
6594
6984
|
if (record === undefined)
|
|
6595
|
-
throw new
|
|
6985
|
+
throw new UnknownTableError(addColumnStatement.table);
|
|
6596
6986
|
if (record.view !== undefined)
|
|
6597
6987
|
throw new TypeError(`${addColumnStatement.table} is a view, not a table`);
|
|
6598
6988
|
pinnedTableId ??= record.id;
|
|
@@ -6607,6 +6997,9 @@ export class MinnowDatabase {
|
|
|
6607
6997
|
if (record.columns.some(({ name }) => name === added.name)) {
|
|
6608
6998
|
throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
|
|
6609
6999
|
}
|
|
7000
|
+
if (added.generatedValue !== undefined) {
|
|
7001
|
+
throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
|
|
7002
|
+
}
|
|
6610
7003
|
if (added.defaultValue !== undefined) {
|
|
6611
7004
|
validateColumnDefault({
|
|
6612
7005
|
name: added.name,
|
|
@@ -6765,11 +7158,19 @@ export class MinnowDatabase {
|
|
|
6765
7158
|
return this.#mergeConflictingInsertRows(statement, options);
|
|
6766
7159
|
}
|
|
6767
7160
|
if (statement.kind === "insert" && statement.rows.length === 0) {
|
|
7161
|
+
const table = await this.#findTable(statement.table);
|
|
7162
|
+
const returningColumns = options.returning === undefined
|
|
7163
|
+
? undefined
|
|
7164
|
+
: options.returning === "*"
|
|
7165
|
+
? visibleTableColumns(table).map(({ name }) => name)
|
|
7166
|
+
: [...options.returning];
|
|
6768
7167
|
return {
|
|
6769
7168
|
kind: "insert",
|
|
6770
7169
|
table: statement.table,
|
|
6771
7170
|
rowCount: 0,
|
|
6772
|
-
...(
|
|
7171
|
+
...(returningColumns === undefined
|
|
7172
|
+
? {}
|
|
7173
|
+
: returningExecuteFields(table, returningColumns, [])),
|
|
6773
7174
|
};
|
|
6774
7175
|
}
|
|
6775
7176
|
if (statement.kind === "insert") {
|
|
@@ -6831,7 +7232,7 @@ export class MinnowDatabase {
|
|
|
6831
7232
|
...(returningColumns === undefined
|
|
6832
7233
|
? {}
|
|
6833
7234
|
: {
|
|
6834
|
-
|
|
7235
|
+
...returningExecuteFields(table, returningColumns, statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns.map((name) => {
|
|
6835
7236
|
const column = table.columns.find((candidate) => candidate.name === name);
|
|
6836
7237
|
const position = statement.columns.indexOf(name);
|
|
6837
7238
|
const inserted = position < 0 ? undefined : row[position];
|
|
@@ -6841,7 +7242,7 @@ export class MinnowDatabase {
|
|
|
6841
7242
|
? null
|
|
6842
7243
|
: boundInsertValue(inserted));
|
|
6843
7244
|
return [name, executionSqlValueFromInput(column, value)];
|
|
6844
|
-
}))),
|
|
7245
|
+
})))),
|
|
6845
7246
|
}),
|
|
6846
7247
|
};
|
|
6847
7248
|
}
|
|
@@ -6903,6 +7304,32 @@ export class MinnowDatabase {
|
|
|
6903
7304
|
having: [],
|
|
6904
7305
|
orderBy: [],
|
|
6905
7306
|
};
|
|
7307
|
+
if (statement.kind === "delete" && returningColumns === undefined) {
|
|
7308
|
+
const internalWriter = options.writer;
|
|
7309
|
+
if (internalWriter.queryFirstColumn !== undefined) {
|
|
7310
|
+
const selectedKeys = await internalWriter.queryFirstColumn(plan);
|
|
7311
|
+
if (selectedKeys !== undefined) {
|
|
7312
|
+
const keys = selectedKeys;
|
|
7313
|
+
if (keyColumn.type === "string" && keyColumn.sqlDomain === undefined) {
|
|
7314
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
7315
|
+
keys[index] = storedSqlValueFromExecution(keyColumn, keys[index] ?? null);
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
if (keys.some((key) => key === null)) {
|
|
7319
|
+
throw new TypeError(`Unique key values must not be null: ${keyColumn.name}`);
|
|
7320
|
+
}
|
|
7321
|
+
if (keys.length === 0) {
|
|
7322
|
+
return { kind: "delete", table: table.name, rowCount: 0 };
|
|
7323
|
+
}
|
|
7324
|
+
const deleted = await internalWriter.deleteBatch(table.name, { keys });
|
|
7325
|
+
return {
|
|
7326
|
+
kind: "delete",
|
|
7327
|
+
table: table.name,
|
|
7328
|
+
rowCount: deleted.rowCount,
|
|
7329
|
+
};
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
6906
7333
|
let rows;
|
|
6907
7334
|
if (options.writer !== undefined) {
|
|
6908
7335
|
// Inside a scope the rows to touch are the ones the scope can see, which includes what it
|
|
@@ -6930,7 +7357,9 @@ export class MinnowDatabase {
|
|
|
6930
7357
|
kind: "delete",
|
|
6931
7358
|
table: table.name,
|
|
6932
7359
|
rowCount: 0,
|
|
6933
|
-
...(returnedRows === undefined
|
|
7360
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7361
|
+
? {}
|
|
7362
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
6934
7363
|
};
|
|
6935
7364
|
}
|
|
6936
7365
|
const deleted = options.writer === undefined
|
|
@@ -6943,7 +7372,9 @@ export class MinnowDatabase {
|
|
|
6943
7372
|
...(deleted.version === undefined || deleted.version === null
|
|
6944
7373
|
? {}
|
|
6945
7374
|
: { version: deleted.version }),
|
|
6946
|
-
...(returnedRows === undefined
|
|
7375
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7376
|
+
? {}
|
|
7377
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
6947
7378
|
};
|
|
6948
7379
|
}
|
|
6949
7380
|
if (keys.length === 0) {
|
|
@@ -6951,7 +7382,9 @@ export class MinnowDatabase {
|
|
|
6951
7382
|
kind: "update",
|
|
6952
7383
|
table: table.name,
|
|
6953
7384
|
rowCount: 0,
|
|
6954
|
-
...(returningColumns === undefined
|
|
7385
|
+
...(returningColumns === undefined
|
|
7386
|
+
? {}
|
|
7387
|
+
: returningExecuteFields(table, returningColumns, [])),
|
|
6955
7388
|
};
|
|
6956
7389
|
}
|
|
6957
7390
|
const changes = {};
|
|
@@ -6984,7 +7417,9 @@ export class MinnowDatabase {
|
|
|
6984
7417
|
name,
|
|
6985
7418
|
returnedChanges !== undefined && name in returnedChanges
|
|
6986
7419
|
? (returnedChanges[name]?.[index] ?? null)
|
|
6987
|
-
:
|
|
7420
|
+
: updated.generatedColumns?.[name] !== undefined
|
|
7421
|
+
? (updated.generatedColumns[name][index] ?? null)
|
|
7422
|
+
: (row[name] ?? null),
|
|
6988
7423
|
])));
|
|
6989
7424
|
return {
|
|
6990
7425
|
kind: "update",
|
|
@@ -6993,7 +7428,9 @@ export class MinnowDatabase {
|
|
|
6993
7428
|
...(updated.version === undefined || updated.version === null
|
|
6994
7429
|
? {}
|
|
6995
7430
|
: { version: updated.version }),
|
|
6996
|
-
...(returnedRows === undefined
|
|
7431
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7432
|
+
? {}
|
|
7433
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
6997
7434
|
};
|
|
6998
7435
|
}
|
|
6999
7436
|
/**
|
|
@@ -7028,6 +7465,7 @@ export class MinnowDatabase {
|
|
|
7028
7465
|
*/
|
|
7029
7466
|
async #queryStreamed(plan, options, spillPageRows, probe, cursor) {
|
|
7030
7467
|
options = this.#effectiveQueryOptions(options);
|
|
7468
|
+
throwIfAborted(options.signal);
|
|
7031
7469
|
const tableNames = [plan.base.table, ...plan.joins.map((join) => join.table)];
|
|
7032
7470
|
const uniqueTableNames = [...new Set(tableNames)];
|
|
7033
7471
|
if (options.version === undefined) {
|
|
@@ -7057,6 +7495,7 @@ export class MinnowDatabase {
|
|
|
7057
7495
|
});
|
|
7058
7496
|
}
|
|
7059
7497
|
async #queryStreamedAtSnapshot(plan, options, spillPageRows, snapshot, tables, visibility, cursor) {
|
|
7498
|
+
throwIfAborted(options.signal);
|
|
7060
7499
|
// Copy-on-write: expansion clones only full-text plans, leaving the compile cache's copy
|
|
7061
7500
|
// untouched.
|
|
7062
7501
|
plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(tables.find((table) => table.name === tableName)));
|
|
@@ -7077,7 +7516,9 @@ export class MinnowDatabase {
|
|
|
7077
7516
|
const visibleByTable = new Map();
|
|
7078
7517
|
const rowCounts = new Map();
|
|
7079
7518
|
for (const table of tables) {
|
|
7519
|
+
throwIfAborted(options.signal);
|
|
7080
7520
|
const segments = await this.#visibleSegmentRecords(table, snapshot, visibility);
|
|
7521
|
+
throwIfAborted(options.signal);
|
|
7081
7522
|
visibleByTable.set(table.name, segments);
|
|
7082
7523
|
if (segments.every((segment) => {
|
|
7083
7524
|
const kind = segment.kind;
|
|
@@ -7092,7 +7533,7 @@ export class MinnowDatabase {
|
|
|
7092
7533
|
plan = chooseJoinOrder(plan, rowCounts);
|
|
7093
7534
|
const baseTable = tables.find((table) => table.name === plan.base.table);
|
|
7094
7535
|
if (baseTable === undefined)
|
|
7095
|
-
throw new
|
|
7536
|
+
throw new UnknownTableError(plan.base.table);
|
|
7096
7537
|
const requestedBaseColumns = columns.get(baseTable.name) ?? [];
|
|
7097
7538
|
const projectedBaseColumns = requestedBaseColumns.length === 0 ? [] : resolveReadColumns(baseTable, requestedBaseColumns);
|
|
7098
7539
|
if (secondaryIndexOrderForPlan(plan, baseTable, projectedBaseColumns, false) !== undefined) {
|
|
@@ -7111,6 +7552,7 @@ export class MinnowDatabase {
|
|
|
7111
7552
|
record.name,
|
|
7112
7553
|
record.id === freshBaseTable.id ? freshBaseTable : record,
|
|
7113
7554
|
])), snapshot, visibility);
|
|
7555
|
+
throwIfAborted(options.signal);
|
|
7114
7556
|
if (planContainsFts(plan, "bm25") && ftsStats === undefined)
|
|
7115
7557
|
return undefined;
|
|
7116
7558
|
// The streamed scan honors the same index pruning as the materialized path; a budgeted
|
|
@@ -7119,11 +7561,14 @@ export class MinnowDatabase {
|
|
|
7119
7561
|
(await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility));
|
|
7120
7562
|
this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
|
|
7121
7563
|
const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
|
|
7564
|
+
throwIfAborted(options.signal);
|
|
7122
7565
|
const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
|
|
7566
|
+
throwIfAborted(options.signal);
|
|
7123
7567
|
const baseSegments = indexed.segments;
|
|
7124
7568
|
// Zone-map elimination composes after index pruning: whole row groups whose statistics
|
|
7125
7569
|
// reject the plan's predicates never stream at all.
|
|
7126
7570
|
const zonePruned = await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot);
|
|
7571
|
+
throwIfAborted(options.signal);
|
|
7127
7572
|
const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
|
|
7128
7573
|
if (baseView === undefined)
|
|
7129
7574
|
return undefined;
|
|
@@ -7145,26 +7590,31 @@ export class MinnowDatabase {
|
|
|
7145
7590
|
const budget = options.executionMemoryBudgetBytes;
|
|
7146
7591
|
const estimate = estimatedColumnarBytes(buildSegments, projectedBuildColumns);
|
|
7147
7592
|
if (buildView !== undefined && estimate > budget / 4) {
|
|
7148
|
-
return this.#runPartitionedJoin(plan, budget, estimate, baseView, buildView, buildTable.name, partitionedShape.buildKeyName);
|
|
7593
|
+
return this.#runPartitionedJoin(plan, budget, estimate, baseView, buildView, buildTable.name, partitionedShape.buildKeyName, options.signal);
|
|
7149
7594
|
}
|
|
7150
7595
|
}
|
|
7151
7596
|
}
|
|
7152
7597
|
}
|
|
7153
7598
|
const runStreamedExecution = async (attempt) => {
|
|
7599
|
+
throwIfAborted(options.signal);
|
|
7154
7600
|
const memory = new QueryMemoryContext(options.executionMemoryBudgetBytes);
|
|
7155
7601
|
let prepared;
|
|
7156
7602
|
try {
|
|
7157
7603
|
const streamed = await baseView.create(memory);
|
|
7604
|
+
throwIfAborted(options.signal);
|
|
7158
7605
|
const inputTables = new Map([[baseTable.name, streamed.table]]);
|
|
7159
7606
|
for (const table of tables) {
|
|
7607
|
+
throwIfAborted(options.signal);
|
|
7160
7608
|
if (table.name === baseTable.name)
|
|
7161
7609
|
continue;
|
|
7162
7610
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
7163
7611
|
inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility));
|
|
7612
|
+
throwIfAborted(options.signal);
|
|
7164
7613
|
}
|
|
7165
7614
|
prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
|
|
7166
7615
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
7167
7616
|
outputNeedsExternalization: queryResultNeedsExternalization(plan, typedSchemas),
|
|
7617
|
+
outputColumnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7168
7618
|
});
|
|
7169
7619
|
if (cursor !== undefined) {
|
|
7170
7620
|
const emission = { happened: false };
|
|
@@ -7172,20 +7622,37 @@ export class MinnowDatabase {
|
|
|
7172
7622
|
batchRows: cursor.batchRows,
|
|
7173
7623
|
signal: cursor.signal,
|
|
7174
7624
|
loadScanWindow: streamed.load,
|
|
7625
|
+
...(cursor.consumeFirstColumn === undefined
|
|
7626
|
+
? {}
|
|
7627
|
+
: {
|
|
7628
|
+
consumeFirstColumn: async (values) => {
|
|
7629
|
+
emission.happened = true;
|
|
7630
|
+
await cursor.consumeFirstColumn?.(values);
|
|
7631
|
+
},
|
|
7632
|
+
}),
|
|
7175
7633
|
}, async (batch) => {
|
|
7176
7634
|
emission.happened = true;
|
|
7177
7635
|
await cursor.consume(externalizeQueryResult(batch));
|
|
7178
7636
|
});
|
|
7179
7637
|
if (!emission.happened) {
|
|
7180
|
-
await cursor.consume({
|
|
7638
|
+
await cursor.consume({
|
|
7639
|
+
columns: [...columns],
|
|
7640
|
+
columnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7641
|
+
rows: [],
|
|
7642
|
+
});
|
|
7181
7643
|
}
|
|
7182
7644
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7183
|
-
return {
|
|
7645
|
+
return {
|
|
7646
|
+
columns: [...columns],
|
|
7647
|
+
columnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7648
|
+
rows: [],
|
|
7649
|
+
};
|
|
7184
7650
|
}
|
|
7185
7651
|
if (!attempt.spill) {
|
|
7186
7652
|
try {
|
|
7187
7653
|
const streamedResult = await prepared.executeAsync({
|
|
7188
7654
|
loadScanWindow: streamed.load,
|
|
7655
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7189
7656
|
});
|
|
7190
7657
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7191
7658
|
return streamedResult;
|
|
@@ -7200,6 +7667,7 @@ export class MinnowDatabase {
|
|
|
7200
7667
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
7201
7668
|
spillStore: this.#leasedSpillStore(),
|
|
7202
7669
|
loadScanWindow: streamed.load,
|
|
7670
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7203
7671
|
});
|
|
7204
7672
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7205
7673
|
return spilledResult;
|
|
@@ -7449,7 +7917,8 @@ export class MinnowDatabase {
|
|
|
7449
7917
|
* like any unordered query. The rescans trade time for bounded memory; the decoded-block
|
|
7450
7918
|
* cache keeps repeat decodes cheap.
|
|
7451
7919
|
*/
|
|
7452
|
-
async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName) {
|
|
7920
|
+
async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
|
|
7921
|
+
throwIfAborted(signal);
|
|
7453
7922
|
// One partition should fit in an eighth of the budget: the resident partition briefly
|
|
7454
7923
|
// exists twice (boxed rows, then their vectors) and shares the budget with both streamed
|
|
7455
7924
|
// windows and the accumulating result.
|
|
@@ -7462,6 +7931,7 @@ export class MinnowDatabase {
|
|
|
7462
7931
|
try {
|
|
7463
7932
|
const combined = [];
|
|
7464
7933
|
for (let partition = 0; partition < partitions; partition += 1) {
|
|
7934
|
+
throwIfAborted(signal);
|
|
7465
7935
|
if (combined.length >= wantedRows)
|
|
7466
7936
|
break;
|
|
7467
7937
|
const passMemory = root.createChild();
|
|
@@ -7472,6 +7942,7 @@ export class MinnowDatabase {
|
|
|
7472
7942
|
const collectMemory = passMemory.createChild();
|
|
7473
7943
|
const buildScanMemory = collectMemory.createChild();
|
|
7474
7944
|
const build = await buildView.create(buildScanMemory);
|
|
7945
|
+
throwIfAborted(signal);
|
|
7475
7946
|
let buildRows = [];
|
|
7476
7947
|
const buildColumns = [...build.table.columns.entries()];
|
|
7477
7948
|
// Small steps keep the collection windows a minor budget term next to the resident
|
|
@@ -7479,8 +7950,10 @@ export class MinnowDatabase {
|
|
|
7479
7950
|
// consumed length clamps to the returned resident end.
|
|
7480
7951
|
const step = 1_024;
|
|
7481
7952
|
for (let start = 0; start < build.table.rowCount;) {
|
|
7953
|
+
throwIfAborted(signal);
|
|
7482
7954
|
let length = Math.min(step, build.table.rowCount - start);
|
|
7483
7955
|
const residentEnd = await build.load(start, length);
|
|
7956
|
+
throwIfAborted(signal);
|
|
7484
7957
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
7485
7958
|
length = Math.min(length, residentEnd - start);
|
|
7486
7959
|
}
|
|
@@ -7511,13 +7984,17 @@ export class MinnowDatabase {
|
|
|
7511
7984
|
const buildInput = columnarTableFromRows(buildTableName, buildRows, undefined, false);
|
|
7512
7985
|
buildRows = [];
|
|
7513
7986
|
const streamedBase = await baseView.create(passMemory);
|
|
7987
|
+
throwIfAborted(signal);
|
|
7514
7988
|
const inputTables = new Map([
|
|
7515
7989
|
[buildTableName, buildInput],
|
|
7516
7990
|
[streamedBase.table.name, streamedBase.table],
|
|
7517
7991
|
]);
|
|
7518
7992
|
prepared = createPreparedColumnarQuery(strippedPlan, inputTables, passMemory);
|
|
7519
7993
|
collectMemory.close();
|
|
7520
|
-
const result = await prepared.executeAsync({
|
|
7994
|
+
const result = await prepared.executeAsync({
|
|
7995
|
+
loadScanWindow: streamedBase.load,
|
|
7996
|
+
...(signal === undefined ? {} : { signal }),
|
|
7997
|
+
});
|
|
7521
7998
|
for (const row of result.rows) {
|
|
7522
7999
|
if (combined.length >= wantedRows)
|
|
7523
8000
|
break;
|
|
@@ -7534,7 +8011,8 @@ export class MinnowDatabase {
|
|
|
7534
8011
|
const rows = startRow > 0 || limit !== undefined
|
|
7535
8012
|
? combined.slice(startRow, limit === undefined ? undefined : startRow + limit)
|
|
7536
8013
|
: combined;
|
|
7537
|
-
|
|
8014
|
+
const columns = plan.select.map((item) => item.alias);
|
|
8015
|
+
return { columns, columnDomains: unknownColumnDomains(columns), rows };
|
|
7538
8016
|
}
|
|
7539
8017
|
finally {
|
|
7540
8018
|
root.close();
|
|
@@ -8080,17 +8558,24 @@ export class MinnowDatabase {
|
|
|
8080
8558
|
};
|
|
8081
8559
|
}
|
|
8082
8560
|
/** Prepares one block's inputs and executes it, returning the caller-owned result. */
|
|
8083
|
-
async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
|
|
8561
|
+
async #executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
|
|
8562
|
+
throwIfAborted(signal);
|
|
8084
8563
|
const ftsStats = await this.#ftsIndexStats(block, realTables, snapshot, visibility);
|
|
8085
|
-
|
|
8564
|
+
throwIfAborted(signal);
|
|
8565
|
+
const inputs = await this.#prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
8566
|
+
throwIfAborted(signal);
|
|
8086
8567
|
const prepared = createPreparedColumnarQuery(block, inputs, memory.createChild(), ftsStats === undefined ? {} : { ftsStats });
|
|
8087
8568
|
try {
|
|
8088
8569
|
if (!allowSpill || memory.usage.budgetBytes === Number.MAX_SAFE_INTEGER) {
|
|
8089
|
-
|
|
8570
|
+
const result = prepared.execute();
|
|
8571
|
+
throwIfAborted(signal);
|
|
8572
|
+
return result;
|
|
8090
8573
|
}
|
|
8091
8574
|
if (!forceSpill) {
|
|
8092
8575
|
try {
|
|
8093
|
-
|
|
8576
|
+
const result = prepared.execute();
|
|
8577
|
+
throwIfAborted(signal);
|
|
8578
|
+
return result;
|
|
8094
8579
|
}
|
|
8095
8580
|
catch (error) {
|
|
8096
8581
|
if (!(error instanceof QueryMemoryBudgetError))
|
|
@@ -8100,6 +8585,7 @@ export class MinnowDatabase {
|
|
|
8100
8585
|
return await prepared.executeAsync({
|
|
8101
8586
|
spillStore: this.#leasedSpillStore(),
|
|
8102
8587
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
8588
|
+
...(signal === undefined ? {} : { signal }),
|
|
8103
8589
|
});
|
|
8104
8590
|
}
|
|
8105
8591
|
finally {
|
|
@@ -8113,14 +8599,17 @@ export class MinnowDatabase {
|
|
|
8113
8599
|
* real tables it references, so an identical block over identical table states reuses
|
|
8114
8600
|
* the previous result instead of re-reading and re-executing.
|
|
8115
8601
|
*/
|
|
8116
|
-
async #executeBlockCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
|
|
8602
|
+
async #executeBlockCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
|
|
8603
|
+
throwIfAborted(signal);
|
|
8117
8604
|
const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
|
|
8605
|
+
throwIfAborted(signal);
|
|
8118
8606
|
if (key !== undefined) {
|
|
8119
8607
|
const cached = this.#cacheGet(key);
|
|
8120
8608
|
if (cached !== undefined)
|
|
8121
8609
|
return cached;
|
|
8122
8610
|
}
|
|
8123
|
-
const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
8611
|
+
const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
8612
|
+
throwIfAborted(signal);
|
|
8124
8613
|
if (key !== undefined)
|
|
8125
8614
|
this.#cachePut(key, result, queryResultRetainedBytes(result));
|
|
8126
8615
|
return result;
|
|
@@ -8131,15 +8620,18 @@ export class MinnowDatabase {
|
|
|
8131
8620
|
* synthetic sources in typedSchemas, and a later cache hit skips that registration, so
|
|
8132
8621
|
* re-inferring the schema at hit time would find those names missing.
|
|
8133
8622
|
*/
|
|
8134
|
-
async #executeBlockWithSchemaCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows) {
|
|
8623
|
+
async #executeBlockWithSchemaCached(block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults = true, allowSpill = true, forceSpill = false, spillPageRows, signal) {
|
|
8624
|
+
throwIfAborted(signal);
|
|
8135
8625
|
const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
|
|
8626
|
+
throwIfAborted(signal);
|
|
8136
8627
|
const schemaKey = key === undefined ? undefined : `s${key}`;
|
|
8137
8628
|
if (schemaKey !== undefined) {
|
|
8138
8629
|
const cached = this.#cacheGet(schemaKey);
|
|
8139
8630
|
if (cached !== undefined)
|
|
8140
8631
|
return cached;
|
|
8141
8632
|
}
|
|
8142
|
-
const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows);
|
|
8633
|
+
const result = await this.#executeBlock(block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
8634
|
+
throwIfAborted(signal);
|
|
8143
8635
|
const schema = inferBlockSchema(block, typedSchemas);
|
|
8144
8636
|
const entry = { result, schema };
|
|
8145
8637
|
if (schemaKey !== undefined) {
|
|
@@ -10512,7 +11004,7 @@ export class MinnowDatabase {
|
|
|
10512
11004
|
if (recoveredManifest === undefined) {
|
|
10513
11005
|
throw new Error(`Compaction manifest is missing: ${String(linkedTransaction.committedVersion)}`);
|
|
10514
11006
|
}
|
|
10515
|
-
this.#afterCompactionCommit(recoveredManifest);
|
|
11007
|
+
this.#afterCompactionCommit(recoveredManifest, table.id);
|
|
10516
11008
|
return this.#publishedCompactionProgress(table, job);
|
|
10517
11009
|
}
|
|
10518
11010
|
const rewritePlan = job.rewritePlan;
|
|
@@ -10698,7 +11190,7 @@ export class MinnowDatabase {
|
|
|
10698
11190
|
let publicationConflict;
|
|
10699
11191
|
try {
|
|
10700
11192
|
manifest = await transaction.commit();
|
|
10701
|
-
this.#afterCompactionCommit(manifest);
|
|
11193
|
+
this.#afterCompactionCommit(manifest, table.id);
|
|
10702
11194
|
break;
|
|
10703
11195
|
}
|
|
10704
11196
|
catch (error) {
|
|
@@ -11531,7 +12023,7 @@ export class MinnowDatabase {
|
|
|
11531
12023
|
}
|
|
11532
12024
|
const table = tables.find((candidate) => candidate.name === tableName);
|
|
11533
12025
|
if (table === undefined || table.view !== undefined) {
|
|
11534
|
-
throw new
|
|
12026
|
+
throw new UnknownTableError(tableName);
|
|
11535
12027
|
}
|
|
11536
12028
|
if (indexColumns.length === 0)
|
|
11537
12029
|
throw new TypeError("An index needs at least one column");
|
|
@@ -13739,7 +14231,7 @@ export class MinnowDatabase {
|
|
|
13739
14231
|
validateName(name, "Table");
|
|
13740
14232
|
const table = await this.store.getTableByName(name);
|
|
13741
14233
|
if (table === undefined)
|
|
13742
|
-
throw new
|
|
14234
|
+
throw new UnknownTableError(name);
|
|
13743
14235
|
// Reads resolve a view into its query before reaching here, so a view arriving at this
|
|
13744
14236
|
// point is a write, a DDL statement, or a path that forgot to rewrite: all of them errors.
|
|
13745
14237
|
if (table.view !== undefined)
|
|
@@ -13939,13 +14431,27 @@ function assertTriggerBodyTargetSchema(compiled, target) {
|
|
|
13939
14431
|
if (column !== undefined &&
|
|
13940
14432
|
isDefaultInsertValue(value) &&
|
|
13941
14433
|
!column.nullable &&
|
|
13942
|
-
column.defaultValue === undefined
|
|
14434
|
+
column.defaultValue === undefined &&
|
|
14435
|
+
column.generatedValue === undefined) {
|
|
13943
14436
|
throw new TypeError(`Trigger body INSERT uses DEFAULT for a non-nullable column without a default: ${column.name}`);
|
|
13944
14437
|
}
|
|
13945
14438
|
}
|
|
13946
14439
|
}
|
|
14440
|
+
for (const [index, column] of insertColumns.entries()) {
|
|
14441
|
+
if (column.generatedValue === undefined)
|
|
14442
|
+
continue;
|
|
14443
|
+
for (const row of compiled.rows) {
|
|
14444
|
+
const value = row[index];
|
|
14445
|
+
if (value === undefined || !isDefaultInsertValue(value)) {
|
|
14446
|
+
throw new TypeError(`Trigger body INSERT assigns generated column: ${column.name}`);
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
13947
14450
|
for (const column of columns) {
|
|
13948
|
-
if (!provided.has(column.name) &&
|
|
14451
|
+
if (!provided.has(column.name) &&
|
|
14452
|
+
!column.nullable &&
|
|
14453
|
+
column.defaultValue === undefined &&
|
|
14454
|
+
column.generatedValue === undefined) {
|
|
13949
14455
|
throw new TypeError(`Trigger body INSERT omits a non-nullable column without a default: ${column.name}`);
|
|
13950
14456
|
}
|
|
13951
14457
|
}
|
|
@@ -13967,6 +14473,9 @@ function assertTriggerBodyTargetSchema(compiled, target) {
|
|
|
13967
14473
|
if (assignedColumn === undefined) {
|
|
13968
14474
|
throw new TypeError(`Trigger body UPDATE assignment column does not exist: ${assignment.column}`);
|
|
13969
14475
|
}
|
|
14476
|
+
if (assignedColumn.generatedValue !== undefined) {
|
|
14477
|
+
throw new TypeError(`Trigger body UPDATE assigns generated column: ${assignment.column}`);
|
|
14478
|
+
}
|
|
13970
14479
|
if (assignedColumn.id === target.uniqueKeyColumnId ||
|
|
13971
14480
|
(target.primaryKeyColumnIds ?? []).includes(assignedColumn.id)) {
|
|
13972
14481
|
throw new TypeError(`Trigger body UPDATE cannot update a key column: ${assignment.column}`);
|
|
@@ -14116,6 +14625,7 @@ function visibleSegmentManifestVersion(value, label) {
|
|
|
14116
14625
|
*/
|
|
14117
14626
|
const compiledChecks = new Map();
|
|
14118
14627
|
const COMPILED_CHECK_CACHE_LIMIT = 64;
|
|
14628
|
+
const compiledGeneratedColumns = new Map();
|
|
14119
14629
|
function tableChecks(table) {
|
|
14120
14630
|
const key = `${table.id}/${String(table.revision)}`;
|
|
14121
14631
|
const cached = compiledChecks.get(key);
|
|
@@ -14138,6 +14648,84 @@ function tableChecks(table) {
|
|
|
14138
14648
|
compiledChecks.set(key, compiled);
|
|
14139
14649
|
return compiled;
|
|
14140
14650
|
}
|
|
14651
|
+
function tableGeneratedExpressions(table) {
|
|
14652
|
+
const key = `${table.id}/${String(table.revision)}`;
|
|
14653
|
+
const cached = compiledGeneratedColumns.get(key);
|
|
14654
|
+
if (cached !== undefined)
|
|
14655
|
+
return cached;
|
|
14656
|
+
const compiled = table.columns.flatMap((column) => column.generatedValue === undefined
|
|
14657
|
+
? []
|
|
14658
|
+
: [
|
|
14659
|
+
{
|
|
14660
|
+
column,
|
|
14661
|
+
expression: compileGeneratedColumnExpression(table.name, column.name, column.generatedValue.sql, table.columns),
|
|
14662
|
+
},
|
|
14663
|
+
]);
|
|
14664
|
+
if (compiledGeneratedColumns.size >= COMPILED_CHECK_CACHE_LIMIT) {
|
|
14665
|
+
const oldest = compiledGeneratedColumns.keys().next().value;
|
|
14666
|
+
if (oldest !== undefined)
|
|
14667
|
+
compiledGeneratedColumns.delete(oldest);
|
|
14668
|
+
}
|
|
14669
|
+
compiledGeneratedColumns.set(key, compiled);
|
|
14670
|
+
return compiled;
|
|
14671
|
+
}
|
|
14672
|
+
function normalizeGeneratedColumnValue(column, value, rowIndex) {
|
|
14673
|
+
const stored = column.sqlDomain === undefined
|
|
14674
|
+
? storedSqlValueFromExecution(column, value)
|
|
14675
|
+
: normalizeSqlDomainValue(column.sqlDomain, value);
|
|
14676
|
+
validateValue(column, stored, rowIndex);
|
|
14677
|
+
return stored;
|
|
14678
|
+
}
|
|
14679
|
+
/** Computes every stored generated column after defaults/domains have produced the base row. */
|
|
14680
|
+
function fillStoredGeneratedColumns(table, batch, rowCount, generated) {
|
|
14681
|
+
for (const { column, expression } of tableGeneratedExpressions(table)) {
|
|
14682
|
+
const values = new Array(rowCount);
|
|
14683
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
14684
|
+
const row = batchRowAt(table, batch, rowIndex);
|
|
14685
|
+
const value = evaluateRowExpression(expression, table.name, row);
|
|
14686
|
+
values[rowIndex] = normalizeGeneratedColumnValue(column, value, rowIndex);
|
|
14687
|
+
}
|
|
14688
|
+
batch.columns[column.name] = values;
|
|
14689
|
+
generated?.set(column.name, values.map((value) => externalSqlDomainValue(value)));
|
|
14690
|
+
}
|
|
14691
|
+
}
|
|
14692
|
+
function rejectGeneratedUpdateAssignments(table, input) {
|
|
14693
|
+
for (const column of table.columns) {
|
|
14694
|
+
if (column.generatedValue !== undefined && input.changes[column.name] !== undefined) {
|
|
14695
|
+
throw new TypeError(`Generated column cannot be assigned: ${column.name}`);
|
|
14696
|
+
}
|
|
14697
|
+
}
|
|
14698
|
+
}
|
|
14699
|
+
function generatedEvaluationRow(table, source) {
|
|
14700
|
+
const row = {};
|
|
14701
|
+
for (const column of table.columns) {
|
|
14702
|
+
const value = source[column.name] ?? null;
|
|
14703
|
+
row[column.name] =
|
|
14704
|
+
column.sqlDomain === undefined
|
|
14705
|
+
? executionSqlValueFromStorage(column, value)
|
|
14706
|
+
: normalizeSqlDomainValue(column.sqlDomain, value);
|
|
14707
|
+
}
|
|
14708
|
+
return row;
|
|
14709
|
+
}
|
|
14710
|
+
function applyStoredGeneratedUpdateChanges(table, input, preImages) {
|
|
14711
|
+
const generated = tableGeneratedExpressions(table);
|
|
14712
|
+
if (generated.length === 0)
|
|
14713
|
+
return input;
|
|
14714
|
+
const changes = { ...input.changes };
|
|
14715
|
+
for (const { column, expression } of generated) {
|
|
14716
|
+
changes[column.name] = input.keys.map((_, rowIndex) => {
|
|
14717
|
+
const old = preImages[rowIndex];
|
|
14718
|
+
if (old === undefined)
|
|
14719
|
+
throw new Error(`Generated-column pre-image is missing: ${table.name}`);
|
|
14720
|
+
const row = generatedEvaluationRow(table, old);
|
|
14721
|
+
for (const [name, values] of Object.entries(input.changes)) {
|
|
14722
|
+
row[name] = values[rowIndex] ?? null;
|
|
14723
|
+
}
|
|
14724
|
+
return normalizeGeneratedColumnValue(column, evaluateRowExpression(expression, table.name, row), rowIndex);
|
|
14725
|
+
});
|
|
14726
|
+
}
|
|
14727
|
+
return { keys: input.keys, changes };
|
|
14728
|
+
}
|
|
14141
14729
|
/**
|
|
14142
14730
|
* Applies a table's CHECK constraints to one row (E141-06). A constraint fails only when it
|
|
14143
14731
|
* evaluates to false: SQL's three-valued logic lets an unknown pass, which is why a NULL column
|
|
@@ -14995,32 +15583,72 @@ function compositeKeyValue(columns, values) {
|
|
|
14995
15583
|
/** Canonicalizes logical PostgreSQL domains before validation and primitive block encoding. */
|
|
14996
15584
|
function normalizeDomainBatch(table, input) {
|
|
14997
15585
|
for (const column of table.columns) {
|
|
14998
|
-
const domain = column.sqlDomain;
|
|
14999
|
-
if (domain === undefined)
|
|
15000
|
-
continue;
|
|
15001
15586
|
const values = input.columns[column.name];
|
|
15002
15587
|
if (values === undefined)
|
|
15003
15588
|
continue;
|
|
15004
|
-
|
|
15589
|
+
if (column.sqlDomain !== undefined || column.type === "datetime") {
|
|
15590
|
+
input.columns[column.name] = values.map((value) => normalizeColumnLogicalValue(column, value));
|
|
15591
|
+
}
|
|
15005
15592
|
}
|
|
15006
15593
|
}
|
|
15594
|
+
function normalizeColumnLogicalValue(column, value) {
|
|
15595
|
+
if (column.sqlDomain !== undefined)
|
|
15596
|
+
return normalizeSqlDomainValue(column.sqlDomain, value);
|
|
15597
|
+
if (column.type === "datetime" && isDateDomainValue(value)) {
|
|
15598
|
+
const external = externalSqlDomainValue(value);
|
|
15599
|
+
if (typeof external !== "string")
|
|
15600
|
+
throw new TypeError("Invalid DATE value");
|
|
15601
|
+
return new Date(`${external}T00:00:00.000Z`);
|
|
15602
|
+
}
|
|
15603
|
+
return value;
|
|
15604
|
+
}
|
|
15605
|
+
function selectBatchRows(input, indexes) {
|
|
15606
|
+
return {
|
|
15607
|
+
columns: Object.fromEntries(Object.entries(input.columns).map(([name, values]) => [
|
|
15608
|
+
name,
|
|
15609
|
+
indexes.map((index) => values[index] ?? null),
|
|
15610
|
+
])),
|
|
15611
|
+
...(input.omitted === undefined
|
|
15612
|
+
? {}
|
|
15613
|
+
: {
|
|
15614
|
+
omitted: Object.fromEntries(Object.entries(input.omitted).map(([name, values]) => [
|
|
15615
|
+
name,
|
|
15616
|
+
indexes.map((index) => values[index] ?? false),
|
|
15617
|
+
])),
|
|
15618
|
+
}),
|
|
15619
|
+
rowCount: indexes.length,
|
|
15620
|
+
};
|
|
15621
|
+
}
|
|
15622
|
+
function normalizeUpsertConflictWhere(table, predicate) {
|
|
15623
|
+
const operators = ["=", "!=", "<>", ">", ">=", "<", "<="];
|
|
15624
|
+
if (!operators.includes(predicate.operator)) {
|
|
15625
|
+
throw new TypeError(`Unsupported upsert conflict operator: ${predicate.operator}`);
|
|
15626
|
+
}
|
|
15627
|
+
const column = visibleTableColumns(table).find(({ name }) => name === predicate.column);
|
|
15628
|
+
if (column === undefined) {
|
|
15629
|
+
throw new TypeError(`Unknown upsert conflict column: ${table.name}.${predicate.column}`);
|
|
15630
|
+
}
|
|
15631
|
+
const value = normalizeColumnLogicalValue(column, predicate.value);
|
|
15632
|
+
if (value !== null)
|
|
15633
|
+
validateValue({ ...column, nullable: true }, value, 0);
|
|
15634
|
+
return { column, operator: predicate.operator, value };
|
|
15635
|
+
}
|
|
15007
15636
|
function normalizeDomainUpdate(table, input) {
|
|
15008
|
-
if (!table.columns.some((column) => column.sqlDomain !== undefined))
|
|
15637
|
+
if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime"))
|
|
15009
15638
|
return input;
|
|
15010
15639
|
let changed = false;
|
|
15011
15640
|
const changes = { ...input.changes };
|
|
15012
15641
|
for (const [name, values] of Object.entries(input.changes)) {
|
|
15013
|
-
const
|
|
15014
|
-
if (
|
|
15642
|
+
const column = table.columns.find((candidate) => candidate.name === name);
|
|
15643
|
+
if (column === undefined || (column.sqlDomain === undefined && column.type !== "datetime"))
|
|
15015
15644
|
continue;
|
|
15016
15645
|
changed = true;
|
|
15017
|
-
changes[name] = values.map((value) =>
|
|
15646
|
+
changes[name] = values.map((value) => normalizeColumnLogicalValue(column, value));
|
|
15018
15647
|
}
|
|
15019
15648
|
const key = getUniqueKeyColumn(table);
|
|
15020
|
-
const
|
|
15021
|
-
const keys = keyDomain === undefined
|
|
15649
|
+
const keys = key === undefined || (key.sqlDomain === undefined && key.type !== "datetime")
|
|
15022
15650
|
? input.keys
|
|
15023
|
-
: input.keys.map((value) =>
|
|
15651
|
+
: input.keys.map((value) => normalizeColumnLogicalValue(key, value));
|
|
15024
15652
|
return changed || keys !== input.keys ? { keys, changes } : input;
|
|
15025
15653
|
}
|
|
15026
15654
|
/** Coerces literals and bound parameters opposite a domain column before planning/index probes. */
|
|
@@ -15217,7 +15845,7 @@ function tryKeyToken(type, value) {
|
|
|
15217
15845
|
* (level one and above) are the folded state, not fragmentation, and do not count — a large
|
|
15218
15846
|
* keyed table is many partitions by design.
|
|
15219
15847
|
*/
|
|
15220
|
-
function
|
|
15848
|
+
function autoCompactionHint(version, segments) {
|
|
15221
15849
|
let levelZero = 0;
|
|
15222
15850
|
let deltas = 0;
|
|
15223
15851
|
for (const segment of segments) {
|
|
@@ -15227,7 +15855,18 @@ function autoCompactionDue(segments) {
|
|
|
15227
15855
|
if (kind !== "insert" && kind !== "base")
|
|
15228
15856
|
deltas += 1;
|
|
15229
15857
|
}
|
|
15230
|
-
return
|
|
15858
|
+
return {
|
|
15859
|
+
version,
|
|
15860
|
+
visible: segments.length,
|
|
15861
|
+
levelZero,
|
|
15862
|
+
deltas,
|
|
15863
|
+
};
|
|
15864
|
+
}
|
|
15865
|
+
function autoCompactionDueHint(hint) {
|
|
15866
|
+
return hint.levelZero >= AUTO_COMPACT_SCAN_SEGMENTS || hint.deltas >= AUTO_COMPACT_DELTA_SEGMENTS;
|
|
15867
|
+
}
|
|
15868
|
+
function autoCompactionDue(segments) {
|
|
15869
|
+
return autoCompactionDueHint(autoCompactionHint(null, segments));
|
|
15231
15870
|
}
|
|
15232
15871
|
/** A macrotask boundary, so background work lets queued queries and writes run between steps. */
|
|
15233
15872
|
function yieldToEventLoop() {
|
|
@@ -16233,6 +16872,17 @@ function columnVectorKeyToken(type, vector, row) {
|
|
|
16233
16872
|
}
|
|
16234
16873
|
throw new Error("Column vector type mismatch");
|
|
16235
16874
|
}
|
|
16875
|
+
function normalizedDatetimeLiteral(value) {
|
|
16876
|
+
if (value instanceof Date)
|
|
16877
|
+
return Number.isFinite(dateMilliseconds(value)) ? value : undefined;
|
|
16878
|
+
if (!isDateDomainValue(value))
|
|
16879
|
+
return undefined;
|
|
16880
|
+
const external = externalSqlDomainValue(value);
|
|
16881
|
+
if (typeof external !== "string")
|
|
16882
|
+
return undefined;
|
|
16883
|
+
const date = new Date(`${external}T00:00:00.000Z`);
|
|
16884
|
+
return Number.isFinite(dateMilliseconds(date)) ? date : undefined;
|
|
16885
|
+
}
|
|
16236
16886
|
function zonePredicates(plan, table) {
|
|
16237
16887
|
if (plan.joins.length > 0 || plan.base.table !== table.name)
|
|
16238
16888
|
return [];
|
|
@@ -16251,10 +16901,11 @@ function zonePredicates(plan, table) {
|
|
|
16251
16901
|
const literalValue = (column, expression) => {
|
|
16252
16902
|
if (expression.kind !== "literal")
|
|
16253
16903
|
return undefined;
|
|
16904
|
+
const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(expression.value) : undefined;
|
|
16254
16905
|
const value = column.type === "datetime"
|
|
16255
|
-
?
|
|
16256
|
-
?
|
|
16257
|
-
:
|
|
16906
|
+
? datetime === undefined
|
|
16907
|
+
? undefined
|
|
16908
|
+
: dateMilliseconds(datetime)
|
|
16258
16909
|
: typeof expression.value === "number"
|
|
16259
16910
|
? expression.value
|
|
16260
16911
|
: undefined;
|
|
@@ -16297,10 +16948,11 @@ function zonePredicates(plan, table) {
|
|
|
16297
16948
|
const column = leftColumn ?? rightColumn;
|
|
16298
16949
|
if (column === undefined || literal.kind !== "literal")
|
|
16299
16950
|
continue;
|
|
16951
|
+
const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(literal.value) : undefined;
|
|
16300
16952
|
const value = column.type === "datetime"
|
|
16301
|
-
?
|
|
16302
|
-
?
|
|
16303
|
-
:
|
|
16953
|
+
? datetime === undefined
|
|
16954
|
+
? undefined
|
|
16955
|
+
: dateMilliseconds(datetime)
|
|
16304
16956
|
: typeof literal.value === "number"
|
|
16305
16957
|
? literal.value
|
|
16306
16958
|
: undefined;
|
|
@@ -16330,9 +16982,16 @@ function secondaryIndexPredicates(plan, table) {
|
|
|
16330
16982
|
const literalValue = (column, expression) => {
|
|
16331
16983
|
if (expression.kind !== "literal" || expression.value === null)
|
|
16332
16984
|
return undefined;
|
|
16985
|
+
let value = expression.value;
|
|
16986
|
+
if (column.type === "datetime" && isDateDomainValue(value)) {
|
|
16987
|
+
const datetime = normalizedDatetimeLiteral(value);
|
|
16988
|
+
if (datetime === undefined)
|
|
16989
|
+
return undefined;
|
|
16990
|
+
value = datetime;
|
|
16991
|
+
}
|
|
16333
16992
|
try {
|
|
16334
|
-
secondaryIndexTerm(column.type,
|
|
16335
|
-
return
|
|
16993
|
+
secondaryIndexTerm(column.type, value);
|
|
16994
|
+
return value;
|
|
16336
16995
|
}
|
|
16337
16996
|
catch {
|
|
16338
16997
|
return undefined;
|