@minnowdb/core 0.5.0 → 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 +1 -1
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +3 -1
- package/dist/engine/catalog.js +1 -0
- package/dist/engine/client.d.ts +32 -4
- package/dist/engine/client.js +82 -15
- package/dist/engine/database.d.ts +23 -14
- package/dist/engine/database.js +516 -74
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +13 -0
- package/dist/engine/errors.js +22 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +2 -12
- package/dist/engine/optimizer.js +540 -38
- package/dist/engine/query.d.ts +5 -278
- package/dist/engine/query.js +78 -32
- package/dist/engine/schema-wire.d.ts +7 -1
- package/dist/engine/schema-wire.js +4 -0
- package/dist/engine/schema.d.ts +67 -33
- package/dist/engine/schema.js +138 -7
- package/dist/engine/sql-domains.d.ts +8 -0
- package/dist/engine/sql-domains.js +25 -0
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +301 -39
- 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/types.d.ts +7 -0
- package/dist/storage/types.js +16 -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/sql-feature-matrix.json +69 -19
package/dist/engine/database.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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, UnknownTableError, 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";
|
|
@@ -20,7 +22,7 @@ import { chooseJoinOrder, renderPlan } from "./optimizer.js";
|
|
|
20
22
|
import { encodeSqlEqualityValue } from "./sql-semantics.js";
|
|
21
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. */
|
|
@@ -291,17 +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
304
|
returnedRows: externalizeQueryResult({
|
|
301
305
|
columns,
|
|
302
|
-
columnDomains
|
|
306
|
+
columnDomains,
|
|
303
307
|
rows: result.returnedRows,
|
|
304
308
|
}).rows,
|
|
309
|
+
returnedColumns: [...columns],
|
|
310
|
+
returnedColumnDomains: structuredClone(columnDomains),
|
|
305
311
|
};
|
|
306
312
|
}
|
|
307
313
|
/** Rejects an INSERT value that is still an unbound `?`/`$n` slot. */
|
|
@@ -571,6 +577,8 @@ export class MinnowDatabase {
|
|
|
571
577
|
#droppingTables = new Set();
|
|
572
578
|
/** Per table: the visible segment count a failed auto-compaction must see before retrying. */
|
|
573
579
|
#autoCompactionBackoff = new Map();
|
|
580
|
+
/** Exact current-layout counters; local commits advance them without rescanning history. */
|
|
581
|
+
#autoCompactionHints = new Map();
|
|
574
582
|
/** Data commits per table since its last write-path auto-compaction check. */
|
|
575
583
|
#commitsSinceCompactionCheck = new Map();
|
|
576
584
|
/** The compaction step in flight per table, so steps on one table run one at a time. */
|
|
@@ -749,6 +757,7 @@ export class MinnowDatabase {
|
|
|
749
757
|
this.#autoCompactionsRequested.clear();
|
|
750
758
|
this.#idleCompactionTableIds.clear();
|
|
751
759
|
this.#autoCompactionBackoff.clear();
|
|
760
|
+
this.#autoCompactionHints.clear();
|
|
752
761
|
this.#commitsSinceCompactionCheck.clear();
|
|
753
762
|
this.#artifactCache.clear();
|
|
754
763
|
}
|
|
@@ -813,6 +822,9 @@ export class MinnowDatabase {
|
|
|
813
822
|
...(sqlDomain === undefined ? {} : { sqlDomain: structuredClone(sqlDomain) }),
|
|
814
823
|
nullable: column.nullable ?? false,
|
|
815
824
|
...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
|
|
825
|
+
...(column.generatedValue === undefined
|
|
826
|
+
? {}
|
|
827
|
+
: { generatedValue: structuredClone(column.generatedValue) }),
|
|
816
828
|
...(column.enumValues === undefined
|
|
817
829
|
? {}
|
|
818
830
|
: { enumValues: validateEnumValues(column.enumValues, columnName) }),
|
|
@@ -871,6 +883,15 @@ export class MinnowDatabase {
|
|
|
871
883
|
throw new TypeError(`Unique key cannot be nullable: ${uniqueKeyColumn.name}`);
|
|
872
884
|
}
|
|
873
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
|
+
}
|
|
874
895
|
if (column.defaultValue !== undefined) {
|
|
875
896
|
validateColumnDefault({ ...column, isUniqueKey: column === uniqueKeyColumn }, column.defaultValue);
|
|
876
897
|
if (column.defaultValue.kind === "literal" && column.sqlDomain !== undefined) {
|
|
@@ -1059,6 +1080,9 @@ export class MinnowDatabase {
|
|
|
1059
1080
|
: { sqlDomain: structuredClone(column.sqlDomain) }),
|
|
1060
1081
|
nullable: column.nullable,
|
|
1061
1082
|
...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
|
|
1083
|
+
...(column.generatedValue === undefined
|
|
1084
|
+
? {}
|
|
1085
|
+
: { generatedValue: column.generatedValue }),
|
|
1062
1086
|
...(column.enumValues === undefined ? {} : { enumValues: [...column.enumValues] }),
|
|
1063
1087
|
})),
|
|
1064
1088
|
...(uniqueKey === undefined ? {} : { uniqueKey }),
|
|
@@ -1129,7 +1153,7 @@ export class MinnowDatabase {
|
|
|
1129
1153
|
}
|
|
1130
1154
|
async #withReadReservation(run) {
|
|
1131
1155
|
if (this.#activeReadReservations >= MAX_DATABASE_ACTIVE_READS) {
|
|
1132
|
-
throw new DatabaseReadBacklogError();
|
|
1156
|
+
throw new DatabaseReadBacklogError(MAX_DATABASE_ACTIVE_READS);
|
|
1133
1157
|
}
|
|
1134
1158
|
this.#activeReadReservations += 1;
|
|
1135
1159
|
try {
|
|
@@ -1140,7 +1164,9 @@ export class MinnowDatabase {
|
|
|
1140
1164
|
}
|
|
1141
1165
|
}
|
|
1142
1166
|
async #queryWithReadReservation(sql, options) {
|
|
1167
|
+
throwIfAborted(options.signal);
|
|
1143
1168
|
await this.#settleExpiredStatementTransaction();
|
|
1169
|
+
throwIfAborted(options.signal);
|
|
1144
1170
|
// An open statement transaction routes through its session, which owns the reservation for
|
|
1145
1171
|
// the actual execution. Avoid charging the same query twice at this public boundary.
|
|
1146
1172
|
return this.#openTransaction === undefined
|
|
@@ -1569,6 +1595,7 @@ export class MinnowDatabase {
|
|
|
1569
1595
|
for (const column of table.columns)
|
|
1570
1596
|
this.#gzipVerdicts.delete(column.id);
|
|
1571
1597
|
this.#autoCompactionBackoff.delete(table.id);
|
|
1598
|
+
this.#autoCompactionHints.delete(table.id);
|
|
1572
1599
|
this.#commitsSinceCompactionCheck.delete(table.id);
|
|
1573
1600
|
this.#idleCompactionTableIds.delete(table.id);
|
|
1574
1601
|
const postingPrefix = `${table.id}/`;
|
|
@@ -1634,6 +1661,7 @@ export class MinnowDatabase {
|
|
|
1634
1661
|
Object.values(pivoted.columns).find((values) => values.length > 0)?.length ??
|
|
1635
1662
|
0;
|
|
1636
1663
|
normalizeDomainBatch(table, filled.batch);
|
|
1664
|
+
fillStoredGeneratedColumns(table, filled.batch, inputRowCount, filled.generated);
|
|
1637
1665
|
for (const name of filled.generated.keys()) {
|
|
1638
1666
|
const column = table.columns.find((candidate) => candidate.name === name);
|
|
1639
1667
|
const values = filled.batch.columns[name];
|
|
@@ -1720,6 +1748,7 @@ export class MinnowDatabase {
|
|
|
1720
1748
|
throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
|
|
1721
1749
|
}
|
|
1722
1750
|
const normalizedInput = normalizeDomainUpdate(table, input);
|
|
1751
|
+
rejectGeneratedUpdateAssignments(table, normalizedInput);
|
|
1723
1752
|
const keys = validateUpdateBatch(table, keyColumn, normalizedInput);
|
|
1724
1753
|
const current = await this.store.getTable(table.id);
|
|
1725
1754
|
if (current === undefined)
|
|
@@ -1737,6 +1766,10 @@ export class MinnowDatabase {
|
|
|
1737
1766
|
changes: Object.fromEntries(Object.entries(changes).map(([name, value]) => [name, [value]])),
|
|
1738
1767
|
});
|
|
1739
1768
|
}
|
|
1769
|
+
/** Deletes one row by the table's unique key. */
|
|
1770
|
+
async delete(tableName, key) {
|
|
1771
|
+
return this.deleteBatch(tableName, { keys: [key] });
|
|
1772
|
+
}
|
|
1740
1773
|
deleteBatch(tableName, input) {
|
|
1741
1774
|
return this.#withWriteReservation(() => this.#deleteBatchReserved(tableName, input));
|
|
1742
1775
|
}
|
|
@@ -1972,15 +2005,13 @@ export class MinnowDatabase {
|
|
|
1972
2005
|
async #writeUpdateBatch(table, keyColumn, input, keys) {
|
|
1973
2006
|
await this.#assertCompactionCapacity(table);
|
|
1974
2007
|
const started = performance.now();
|
|
1975
|
-
|
|
1976
|
-
Object.values(input.changes).reduce((total, values) => total + estimateValuesBytes(values), 0);
|
|
2008
|
+
let logicalBytes;
|
|
1977
2009
|
// Deferred: the record rides the single-shot commit below unless trigger rows stage first.
|
|
1978
2010
|
const transaction = await this.#transactions.beginDeferred({ durableSnapshot: false });
|
|
1979
2011
|
transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
|
|
1980
2012
|
const blockStager = new BoundedWriteBlockStager(transaction);
|
|
1981
2013
|
const segmentId = this.#createId();
|
|
1982
2014
|
const columnBlockIds = {};
|
|
1983
|
-
const changedColumns = Object.keys(input.changes).sort();
|
|
1984
2015
|
let storedBytes = 0;
|
|
1985
2016
|
let blockCount = 0;
|
|
1986
2017
|
let encodeMs = 0;
|
|
@@ -1989,6 +2020,22 @@ export class MinnowDatabase {
|
|
|
1989
2020
|
let retries = 0;
|
|
1990
2021
|
try {
|
|
1991
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();
|
|
1992
2039
|
const columns = [keyColumn, ...changedColumns.map((name) => findColumn(table, name))];
|
|
1993
2040
|
const plannedColumns = columns.map((column) => writeColumnValues(column.type, column.id === keyColumn.id ? input.keys : (input.changes[column.name] ?? [])));
|
|
1994
2041
|
const ranges = writeBlockRanges(plannedColumns, input.keys.length, this.#rowsPerBlock, this.#targetBlockBytes);
|
|
@@ -2022,13 +2069,6 @@ export class MinnowDatabase {
|
|
|
2022
2069
|
}
|
|
2023
2070
|
columnBlockIds[column.id] = blockIds;
|
|
2024
2071
|
}
|
|
2025
|
-
const checks = table.checks ?? [];
|
|
2026
|
-
const changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
2027
|
-
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
2028
|
-
const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (sql, params) => this.#sessionQuery(transaction, sql, { params }), checks.length > 0 ||
|
|
2029
|
-
changedForeignKey ||
|
|
2030
|
-
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
2031
|
-
readyUniqueSecondaryIndexes(table).some(({ columns }) => columns.some((column) => input.changes[column.name] !== undefined)));
|
|
2032
2072
|
stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
|
|
2033
2073
|
const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
|
|
2034
2074
|
if (secondaryDeltas.length > 0) {
|
|
@@ -2101,6 +2141,14 @@ export class MinnowDatabase {
|
|
|
2101
2141
|
blockCount,
|
|
2102
2142
|
storedBytes,
|
|
2103
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
|
+
}),
|
|
2104
2152
|
metrics: createWriteMetrics({
|
|
2105
2153
|
logicalBytes,
|
|
2106
2154
|
storedBytes,
|
|
@@ -2554,12 +2602,14 @@ export class MinnowDatabase {
|
|
|
2554
2602
|
}
|
|
2555
2603
|
async #prepareCompiledPlan(plan, options = {}, probe) {
|
|
2556
2604
|
options = this.#effectiveQueryOptions(options);
|
|
2605
|
+
throwIfAborted(options.signal);
|
|
2557
2606
|
// The ORDER-BY-expression desugar's wrapper is projection-only: prepare the inner block
|
|
2558
2607
|
// directly (no derived materialization) and project each result to the visible aliases,
|
|
2559
2608
|
// so `.search()` costs the same whether or not the caller also selects the score.
|
|
2560
2609
|
const wrapper = transparentProjectionSource(plan);
|
|
2561
2610
|
if (wrapper !== undefined) {
|
|
2562
2611
|
const prepared = await this.#prepareCompiledPlan(wrapper.inner, options, probe);
|
|
2612
|
+
throwIfAborted(options.signal);
|
|
2563
2613
|
return {
|
|
2564
2614
|
sql: prepared.sql,
|
|
2565
2615
|
tables: prepared.tables,
|
|
@@ -2580,6 +2630,7 @@ export class MinnowDatabase {
|
|
|
2580
2630
|
let outputNeedsExternalization = true;
|
|
2581
2631
|
let outputColumnDomains = [];
|
|
2582
2632
|
const prepareAtSnapshot = async (snapshot, realTables, visibility) => {
|
|
2633
|
+
throwIfAborted(options.signal);
|
|
2583
2634
|
const typedSchemas = new Map([...realTables.values()].map((table) => [
|
|
2584
2635
|
table.name,
|
|
2585
2636
|
table.columns.map(({ name, type, integer, sqlDomain }) => ({
|
|
@@ -2595,30 +2646,37 @@ export class MinnowDatabase {
|
|
|
2595
2646
|
const expandedPlan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(realTables.get(tableName)));
|
|
2596
2647
|
const resolution = subqueryResolutionSteps(expandedPlan);
|
|
2597
2648
|
for (const step of resolution.steps) {
|
|
2598
|
-
|
|
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);
|
|
2599
2652
|
}
|
|
2600
2653
|
resolvedPlan = resolution.plan;
|
|
2601
2654
|
// Index-served BM25 statistics, computed against the same catalog snapshot the pruner
|
|
2602
2655
|
// reads, so a pruned scoring scan always carries exact corpus numbers.
|
|
2603
2656
|
ftsStats = await this.#ftsIndexStats(resolvedPlan, realTables, snapshot, visibility);
|
|
2657
|
+
throwIfAborted(options.signal);
|
|
2604
2658
|
columnarTables = await this.#prepareBlockInputs(resolvedPlan, snapshot, visibility, memory, realTables, typedSchemas, undefined,
|
|
2605
2659
|
// memoize: false means "compute this statement's results" — that covers the
|
|
2606
2660
|
// columnar forms of derived and windowed sources too, not just the result memo.
|
|
2607
|
-
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);
|
|
2608
2663
|
outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
|
|
2609
2664
|
outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
|
|
2610
2665
|
};
|
|
2611
2666
|
if (options.version !== undefined) {
|
|
2612
2667
|
// Explicit time travel keeps the per-call lease and version-anchored reads.
|
|
2613
2668
|
const realTables = await this.#findRealBlockTables(plan);
|
|
2669
|
+
throwIfAborted(options.signal);
|
|
2614
2670
|
await this.#withLeasedSnapshot(options.version, async (snapshot) => {
|
|
2615
2671
|
const visibility = await this.#blockSegmentVisibility(realTables);
|
|
2672
|
+
throwIfAborted(options.signal);
|
|
2616
2673
|
await prepareAtSnapshot(snapshot, realTables, visibility);
|
|
2617
2674
|
});
|
|
2618
2675
|
}
|
|
2619
2676
|
else {
|
|
2620
2677
|
await this.#withSharedCatalogSnapshot(collectRealTableNames(plan), prepareAtSnapshot, probe);
|
|
2621
2678
|
}
|
|
2679
|
+
throwIfAborted(options.signal);
|
|
2622
2680
|
return createPreparedColumnarQuery(chooseJoinOrder(resolvedPlan, columnarTables), columnarTables, memory, {
|
|
2623
2681
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
2624
2682
|
outputNeedsExternalization,
|
|
@@ -2909,18 +2967,21 @@ export class MinnowDatabase {
|
|
|
2909
2967
|
* materialize their referenced columns. Each block resolves its own inputs, so one real table
|
|
2910
2968
|
* projected differently by two blocks never collides.
|
|
2911
2969
|
*/
|
|
2912
|
-
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);
|
|
2913
2972
|
const inputs = new Map(extraInputs ?? []);
|
|
2914
2973
|
const sources = [block.base, ...block.joins];
|
|
2915
2974
|
for (const source of sources) {
|
|
2975
|
+
throwIfAborted(signal);
|
|
2916
2976
|
if (inputs.has(source.table))
|
|
2917
2977
|
continue;
|
|
2918
2978
|
if (source.recursive !== undefined) {
|
|
2919
2979
|
const { reference, base, step, all } = source.recursive;
|
|
2920
|
-
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);
|
|
2921
2981
|
typedSchemas.set(reference, schema);
|
|
2922
2982
|
const state = createRecursiveCteState(baseResult, all);
|
|
2923
2983
|
while (state.frontier.length > 0) {
|
|
2984
|
+
throwIfAborted(signal);
|
|
2924
2985
|
// Each iteration's inputs release with its own context, so only the accumulated rows
|
|
2925
2986
|
// and the final columnar table hold memory across the fixpoint loop.
|
|
2926
2987
|
const iterationMemory = memory.createChild();
|
|
@@ -2930,7 +2991,7 @@ export class MinnowDatabase {
|
|
|
2930
2991
|
columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2931
2992
|
rows: state.frontier,
|
|
2932
2993
|
}, schema);
|
|
2933
|
-
state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows));
|
|
2994
|
+
state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows, signal));
|
|
2934
2995
|
}
|
|
2935
2996
|
finally {
|
|
2936
2997
|
iterationMemory.close();
|
|
@@ -2948,7 +3009,8 @@ export class MinnowDatabase {
|
|
|
2948
3009
|
const results = [];
|
|
2949
3010
|
let schema;
|
|
2950
3011
|
for (const member of source.union.blocks) {
|
|
2951
|
-
|
|
3012
|
+
throwIfAborted(signal);
|
|
3013
|
+
const { result, schema: memberSchema } = await this.#executeBlockWithSchemaCached(member, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
2952
3014
|
if (schema === undefined)
|
|
2953
3015
|
schema = memberSchema;
|
|
2954
3016
|
else {
|
|
@@ -2977,6 +3039,7 @@ export class MinnowDatabase {
|
|
|
2977
3039
|
// key plus the window spec, so a warm repeat skips the window pass and the
|
|
2978
3040
|
// rows-to-columnar conversion entirely.
|
|
2979
3041
|
const innerKey = await this.#blockResultCacheKey(source.windowed.block, snapshot, visibility, realTables, cacheResults);
|
|
3042
|
+
throwIfAborted(signal);
|
|
2980
3043
|
const columnarKey = innerKey === undefined
|
|
2981
3044
|
? undefined
|
|
2982
3045
|
: `ctw|${JSON.stringify(source.windowed.windows)}|${innerKey}`;
|
|
@@ -2988,7 +3051,7 @@ export class MinnowDatabase {
|
|
|
2988
3051
|
continue;
|
|
2989
3052
|
}
|
|
2990
3053
|
}
|
|
2991
|
-
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);
|
|
2992
3055
|
const windowed = applyWindowFunctions(inner, source.windowed.windows, {
|
|
2993
3056
|
copyRows: cacheResults,
|
|
2994
3057
|
});
|
|
@@ -3015,6 +3078,7 @@ export class MinnowDatabase {
|
|
|
3015
3078
|
if (derived === undefined)
|
|
3016
3079
|
continue;
|
|
3017
3080
|
const derivedKey = await this.#blockResultCacheKey(derived, snapshot, visibility, realTables, cacheResults);
|
|
3081
|
+
throwIfAborted(signal);
|
|
3018
3082
|
const columnarKey = derivedKey === undefined ? undefined : `ctd|${derivedKey}`;
|
|
3019
3083
|
if (columnarKey !== undefined) {
|
|
3020
3084
|
const hit = this.#cacheGet(columnarKey);
|
|
@@ -3024,7 +3088,7 @@ export class MinnowDatabase {
|
|
|
3024
3088
|
continue;
|
|
3025
3089
|
}
|
|
3026
3090
|
}
|
|
3027
|
-
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);
|
|
3028
3092
|
typedSchemas.set(source.table, schema);
|
|
3029
3093
|
const derivedTable = derivedColumnarTable(source.table, result, schema);
|
|
3030
3094
|
if (columnarKey !== undefined) {
|
|
@@ -3039,6 +3103,7 @@ export class MinnowDatabase {
|
|
|
3039
3103
|
const columns = referencedColumns(block, nameSchemas);
|
|
3040
3104
|
const onlyRealSource = sources.length === 1 && sources[0]?.derived === undefined ? block : undefined;
|
|
3041
3105
|
for (const source of sources) {
|
|
3106
|
+
throwIfAborted(signal);
|
|
3042
3107
|
if (source.derived !== undefined || inputs.has(source.table))
|
|
3043
3108
|
continue;
|
|
3044
3109
|
if (source.table === DUAL_TABLE) {
|
|
@@ -3050,6 +3115,7 @@ export class MinnowDatabase {
|
|
|
3050
3115
|
throw new UnknownTableError(source.table);
|
|
3051
3116
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
3052
3117
|
inputs.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, onlyRealSource));
|
|
3118
|
+
throwIfAborted(signal);
|
|
3053
3119
|
}
|
|
3054
3120
|
return inputs;
|
|
3055
3121
|
}
|
|
@@ -3066,6 +3132,7 @@ export class MinnowDatabase {
|
|
|
3066
3132
|
return this.#queryWithReadReservation(sql, options);
|
|
3067
3133
|
}
|
|
3068
3134
|
async #queryUnreserved(sql, options) {
|
|
3135
|
+
throwIfAborted(options.signal);
|
|
3069
3136
|
const open = this.#openTransaction;
|
|
3070
3137
|
if (open !== undefined) {
|
|
3071
3138
|
// Read-your-writes: inside BEGIN … COMMIT a read sees the pre-scope snapshot plus what
|
|
@@ -3091,6 +3158,7 @@ export class MinnowDatabase {
|
|
|
3091
3158
|
const result = !memoizable
|
|
3092
3159
|
? await this.#queryCompiled(plan, options)
|
|
3093
3160
|
: await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, probe);
|
|
3161
|
+
throwIfAborted(options.signal);
|
|
3094
3162
|
return externalizeQueryResult(result);
|
|
3095
3163
|
}
|
|
3096
3164
|
/**
|
|
@@ -3155,7 +3223,8 @@ export class MinnowDatabase {
|
|
|
3155
3223
|
};
|
|
3156
3224
|
}
|
|
3157
3225
|
async #queryBatches(sql, options, cursor) {
|
|
3158
|
-
options = this.#effectiveQueryOptions(options);
|
|
3226
|
+
options = this.#effectiveQueryOptions({ ...options, signal: cursor.signal });
|
|
3227
|
+
cursor.signal.throwIfAborted();
|
|
3159
3228
|
// A transaction's staged overlay has its own executor. Keep its read-your-writes semantics
|
|
3160
3229
|
// and page the materialized result; native scan batching is for durable snapshots.
|
|
3161
3230
|
if (this.#openTransaction === undefined) {
|
|
@@ -3196,16 +3265,22 @@ export class MinnowDatabase {
|
|
|
3196
3265
|
* and on IndexedDB every probe is a read transaction, a floor under every small query.
|
|
3197
3266
|
*/
|
|
3198
3267
|
async #memoizedQuery(plan, key, options, probe) {
|
|
3268
|
+
throwIfAborted(options.signal);
|
|
3199
3269
|
const before = await probe();
|
|
3270
|
+
throwIfAborted(options.signal);
|
|
3200
3271
|
const cached = this.#cacheGet(`${key}\u0001${String(before.catalogEpoch)}`);
|
|
3201
|
-
if (cached !== undefined)
|
|
3272
|
+
if (cached !== undefined) {
|
|
3273
|
+
options.onStats?.({ peakMemoryBytes: 0 });
|
|
3202
3274
|
return copyQueryResult(cached);
|
|
3275
|
+
}
|
|
3203
3276
|
const result = await this.#queryCompiled(plan, options, before);
|
|
3277
|
+
throwIfAborted(options.signal);
|
|
3204
3278
|
const bytes = queryResultRetainedBytes(result);
|
|
3205
3279
|
if (bytes <= RESULT_MEMO_MAX_BYTES) {
|
|
3206
3280
|
// Cache only when the epoch did not move during execution: the result is then exactly
|
|
3207
3281
|
// that epoch's answer. A moved epoch simply skips the cache — never mis-files.
|
|
3208
3282
|
const after = await probe();
|
|
3283
|
+
throwIfAborted(options.signal);
|
|
3209
3284
|
if (after.catalogEpoch === before.catalogEpoch) {
|
|
3210
3285
|
this.#cachePut(`${key}\u0001${String(before.catalogEpoch)}`, copyQueryResult(result), bytes);
|
|
3211
3286
|
}
|
|
@@ -3385,10 +3460,13 @@ export class MinnowDatabase {
|
|
|
3385
3460
|
*/
|
|
3386
3461
|
async #queryCompiled(plan, options = {}, probe) {
|
|
3387
3462
|
options = this.#effectiveQueryOptions(options);
|
|
3463
|
+
throwIfAborted(options.signal);
|
|
3388
3464
|
// One freshness probe per query: read here unless the caller already has one, and handed
|
|
3389
3465
|
// to the view lookup and the catalog state below, which would otherwise probe again each.
|
|
3390
3466
|
probe ??= await this.store.getCatalogProbe();
|
|
3467
|
+
throwIfAborted(options.signal);
|
|
3391
3468
|
plan = await this.#applyCatalogRewrites(plan, probe);
|
|
3469
|
+
throwIfAborted(options.signal);
|
|
3392
3470
|
const spillPageRows = options.spillPageRows === undefined
|
|
3393
3471
|
? undefined
|
|
3394
3472
|
: positiveWholeNumber(options.spillPageRows, "Query spill page rows");
|
|
@@ -3409,6 +3487,7 @@ export class MinnowDatabase {
|
|
|
3409
3487
|
}
|
|
3410
3488
|
}
|
|
3411
3489
|
const prepared = await this.#prepareCompiledPlan(plan, options, probe);
|
|
3490
|
+
throwIfAborted(options.signal);
|
|
3412
3491
|
// Read the peak before close(): closing releases the context and zeroes what it tracked.
|
|
3413
3492
|
const report = (result) => {
|
|
3414
3493
|
options.onStats?.({ peakMemoryBytes: prepared.memoryUsage.peakBytes });
|
|
@@ -3416,11 +3495,16 @@ export class MinnowDatabase {
|
|
|
3416
3495
|
};
|
|
3417
3496
|
try {
|
|
3418
3497
|
const spill = options.spillToStorage ?? options.executionMemoryBudgetBytes !== undefined;
|
|
3419
|
-
if (!spill)
|
|
3420
|
-
|
|
3498
|
+
if (!spill) {
|
|
3499
|
+
const result = prepared.execute();
|
|
3500
|
+
throwIfAborted(options.signal);
|
|
3501
|
+
return report(result);
|
|
3502
|
+
}
|
|
3421
3503
|
if (options.spillToStorage !== true) {
|
|
3422
3504
|
try {
|
|
3423
|
-
|
|
3505
|
+
const result = prepared.execute();
|
|
3506
|
+
throwIfAborted(options.signal);
|
|
3507
|
+
return report(result);
|
|
3424
3508
|
}
|
|
3425
3509
|
catch (error) {
|
|
3426
3510
|
if (!(error instanceof QueryMemoryBudgetError))
|
|
@@ -3430,12 +3514,33 @@ export class MinnowDatabase {
|
|
|
3430
3514
|
return report(await prepared.executeAsync({
|
|
3431
3515
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
3432
3516
|
spillStore: this.#leasedSpillStore(),
|
|
3517
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
3433
3518
|
}));
|
|
3434
3519
|
}
|
|
3435
3520
|
finally {
|
|
3436
3521
|
prepared.close();
|
|
3437
3522
|
}
|
|
3438
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
|
+
}
|
|
3439
3544
|
/**
|
|
3440
3545
|
* Backs the executor's spill pages with durable owner leases: each owner registers a lease
|
|
3441
3546
|
* before its first page write and renews it while pages are read or written, so an abandoned
|
|
@@ -3823,15 +3928,18 @@ export class MinnowDatabase {
|
|
|
3823
3928
|
* one, so a table that cannot be compacted today costs one attempt, not one per query.
|
|
3824
3929
|
*/
|
|
3825
3930
|
#maybeScheduleAutoCompaction(table, segments) {
|
|
3931
|
+
this.#maybeScheduleAutoCompactionFromHint(table, autoCompactionHint(null, segments));
|
|
3932
|
+
}
|
|
3933
|
+
#maybeScheduleAutoCompactionFromHint(table, hint) {
|
|
3826
3934
|
if (this.#closed)
|
|
3827
3935
|
return;
|
|
3828
3936
|
if (!this.#autoCompact)
|
|
3829
3937
|
return;
|
|
3830
3938
|
if (this.#droppingTables.has(table.id))
|
|
3831
3939
|
return;
|
|
3832
|
-
if (!
|
|
3940
|
+
if (!autoCompactionDueHint(hint))
|
|
3833
3941
|
return;
|
|
3834
|
-
if (
|
|
3942
|
+
if (hint.visible < (this.#autoCompactionBackoff.get(table.id) ?? 0))
|
|
3835
3943
|
return;
|
|
3836
3944
|
if (this.#autoCompactionsInFlight.has(table.id)) {
|
|
3837
3945
|
// A final burst can cross the threshold while the prior fold is still planning or
|
|
@@ -3845,14 +3953,14 @@ export class MinnowDatabase {
|
|
|
3845
3953
|
if (folded)
|
|
3846
3954
|
this.#autoCompactionBackoff.delete(table.id);
|
|
3847
3955
|
else {
|
|
3848
|
-
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)));
|
|
3849
3957
|
}
|
|
3850
3958
|
})
|
|
3851
3959
|
.catch(() => {
|
|
3852
3960
|
// Back off deterministic failures, but never beyond the maximum L0 prefix a fold can
|
|
3853
3961
|
// consume. A transient conflict near the end of a burst must not strand hundreds of
|
|
3854
3962
|
// segments waiting for a segment count the idle database can never reach.
|
|
3855
|
-
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)));
|
|
3856
3964
|
})
|
|
3857
3965
|
.finally(() => {
|
|
3858
3966
|
if (this.#autoCompactionsInFlight.get(table.id) === run) {
|
|
@@ -3872,6 +3980,10 @@ export class MinnowDatabase {
|
|
|
3872
3980
|
// every write and let the commit return the identical CompactionBacklogError at the CAS.
|
|
3873
3981
|
if (!this.#autoCompact)
|
|
3874
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;
|
|
3875
3987
|
let segments = await this.#currentVisibleSegments(table);
|
|
3876
3988
|
let levelZero = 0;
|
|
3877
3989
|
for (const segment of segments) {
|
|
@@ -3945,9 +4057,11 @@ export class MinnowDatabase {
|
|
|
3945
4057
|
const version = await this.store.getCurrentManifestVersion();
|
|
3946
4058
|
segments = await this.#visibleSegmentRecords(table, new Snapshot(this.store, version));
|
|
3947
4059
|
if ((await this.store.getCurrentManifestVersion()) === version) {
|
|
4060
|
+
this.#autoCompactionHints.set(table.id, autoCompactionHint(version, segments));
|
|
3948
4061
|
return segments;
|
|
3949
4062
|
}
|
|
3950
4063
|
}
|
|
4064
|
+
this.#autoCompactionHints.delete(table.id);
|
|
3951
4065
|
return segments;
|
|
3952
4066
|
}
|
|
3953
4067
|
/**
|
|
@@ -3958,6 +4072,7 @@ export class MinnowDatabase {
|
|
|
3958
4072
|
if (this.#closed)
|
|
3959
4073
|
return;
|
|
3960
4074
|
const contribution = transaction?.committedCatalogContribution;
|
|
4075
|
+
this.#advanceAutoCompactionHints(manifest, contribution?.segments);
|
|
3961
4076
|
const initial = contribution?.initialCatalogProbe;
|
|
3962
4077
|
const contributedTableIds = new Set(contribution?.segments.map((segment) => segment.tableId) ?? []);
|
|
3963
4078
|
if (contribution !== undefined &&
|
|
@@ -4051,7 +4166,7 @@ export class MinnowDatabase {
|
|
|
4051
4166
|
this.#armIdleCompactionCheck();
|
|
4052
4167
|
}
|
|
4053
4168
|
/** Counts one physical rewrite version once in this engine, including lost-ack recovery. */
|
|
4054
|
-
#afterCompactionCommit(manifest) {
|
|
4169
|
+
#afterCompactionCommit(manifest, tableId) {
|
|
4055
4170
|
if (this.#accountedCompactionVersions.has(manifest.version))
|
|
4056
4171
|
return;
|
|
4057
4172
|
this.#accountedCompactionVersions.add(manifest.version);
|
|
@@ -4060,8 +4175,49 @@ export class MinnowDatabase {
|
|
|
4060
4175
|
if (oldest !== undefined)
|
|
4061
4176
|
this.#accountedCompactionVersions.delete(oldest);
|
|
4062
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);
|
|
4063
4182
|
this.#afterCommit(manifest);
|
|
4064
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
|
+
}
|
|
4065
4221
|
/**
|
|
4066
4222
|
* Debounces the final write-path check for a burst. Sampling every few commits keeps the hot
|
|
4067
4223
|
* path cheap, but the last one through seven commits can be the ones that cross a fold
|
|
@@ -4099,6 +4255,12 @@ export class MinnowDatabase {
|
|
|
4099
4255
|
const table = await this.store.getTable(tableId);
|
|
4100
4256
|
if (table === undefined)
|
|
4101
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
|
+
}
|
|
4102
4264
|
this.#maybeScheduleAutoCompaction(table, await this.#currentVisibleSegments(table));
|
|
4103
4265
|
}
|
|
4104
4266
|
catch {
|
|
@@ -4379,6 +4541,7 @@ export class MinnowDatabase {
|
|
|
4379
4541
|
const statementNow = this.#now();
|
|
4380
4542
|
const filled = await fillColumnDefaults(target, input, (sql) => this.#evaluateDefaultExpression(sql, statementNow), derivedRows.length);
|
|
4381
4543
|
normalizeDomainBatch(target, filled.batch);
|
|
4544
|
+
fillStoredGeneratedColumns(target, filled.batch, derivedRows.length, filled.generated);
|
|
4382
4545
|
fillCompositePrimaryKey(target, filled.batch, derivedRows.length);
|
|
4383
4546
|
validateBatch(target, filled.batch, filled.autoIncrement?.column.name);
|
|
4384
4547
|
if (filled.autoIncrement !== undefined && filled.autoIncrement.missingIndexes.length > 0) {
|
|
@@ -4651,6 +4814,15 @@ export class MinnowDatabase {
|
|
|
4651
4814
|
? this.#queryCompiled(plan, {}, probe)
|
|
4652
4815
|
: this.#sessionQueryPlan(transaction, await this.#applyCatalogRewrites(plan));
|
|
4653
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
|
+
}),
|
|
4654
4826
|
executeStatement: (statement) => {
|
|
4655
4827
|
open();
|
|
4656
4828
|
return guarded(() => this.runStatement(statement, { writer }));
|
|
@@ -4719,21 +4891,26 @@ export class MinnowDatabase {
|
|
|
4719
4891
|
*/
|
|
4720
4892
|
async #sessionQuery(transaction, sql, options = {}) {
|
|
4721
4893
|
options = this.#effectiveQueryOptions(options);
|
|
4894
|
+
throwIfAborted(options.signal);
|
|
4722
4895
|
const bound = bindPlanParameters(this.#compileCached(sql), options.params);
|
|
4723
4896
|
// The same catalog rewrites a read outside a scope gets: a scope that could not see a view
|
|
4724
4897
|
// would make the scope's reads a different language from everyone else's.
|
|
4725
4898
|
const plan = await this.#applyCatalogRewrites(bound);
|
|
4899
|
+
throwIfAborted(options.signal);
|
|
4726
4900
|
return this.#sessionQueryPlan(transaction, plan, options);
|
|
4727
4901
|
}
|
|
4728
4902
|
/** #sessionQuery for an already-bound plan: trigger bodies resolve their reads through it. */
|
|
4729
4903
|
async #sessionQueryPlan(transaction, plan, options = {}) {
|
|
4730
4904
|
options = this.#effectiveQueryOptions(options);
|
|
4905
|
+
throwIfAborted(options.signal);
|
|
4731
4906
|
const names = collectRealTableNames(plan);
|
|
4732
4907
|
const tables = await Promise.all(names.map((name) => this.#findTable(name)));
|
|
4908
|
+
throwIfAborted(options.signal);
|
|
4733
4909
|
const realTables = new Map(tables.map((table) => [table.name, table]));
|
|
4734
4910
|
const pendingIds = new Set(transaction.pendingSegmentIds);
|
|
4735
4911
|
const pendingBlocks = new Set(transaction.pendingBlockIds);
|
|
4736
4912
|
const ourRecord = await this.store.getTransaction(transaction.id);
|
|
4913
|
+
throwIfAborted(options.signal);
|
|
4737
4914
|
// Pinned to the scope's own snapshot, not the current manifest: the documented contract
|
|
4738
4915
|
// is "the pre-scope snapshot plus everything this scope staged", and #assertKeysExist
|
|
4739
4916
|
// already validates against transaction.snapshotVersion — reading the current version
|
|
@@ -4757,6 +4934,7 @@ export class MinnowDatabase {
|
|
|
4757
4934
|
getBlocks: async (blockIds) => {
|
|
4758
4935
|
const blocks = [];
|
|
4759
4936
|
for (let start = 0; start < blockIds.length; start += 64) {
|
|
4937
|
+
throwIfAborted(options.signal);
|
|
4760
4938
|
const window = blockIds.slice(start, start + 64);
|
|
4761
4939
|
const committedIds = [];
|
|
4762
4940
|
const committedIndexes = [];
|
|
@@ -4781,6 +4959,7 @@ export class MinnowDatabase {
|
|
|
4781
4959
|
this.#getBlocksWindowed(committedIds),
|
|
4782
4960
|
Promise.all(stagedIds.map((blockId) => transaction.getBlock(blockId))),
|
|
4783
4961
|
]);
|
|
4962
|
+
throwIfAborted(options.signal);
|
|
4784
4963
|
const ordered = new Array(window.length);
|
|
4785
4964
|
committedIndexes.forEach((index, offset) => {
|
|
4786
4965
|
ordered[index] = committed[offset];
|
|
@@ -4799,10 +4978,12 @@ export class MinnowDatabase {
|
|
|
4799
4978
|
const transactionRecords = new Map();
|
|
4800
4979
|
const deferredSegments = transaction.deferredSegments;
|
|
4801
4980
|
for (const table of tables) {
|
|
4981
|
+
throwIfAborted(options.signal);
|
|
4802
4982
|
const segments = [
|
|
4803
4983
|
...(await listTableSegmentsPaged(this.store, table.id)),
|
|
4804
4984
|
...deferredSegments.filter((segment) => segment.tableId === table.id),
|
|
4805
4985
|
];
|
|
4986
|
+
throwIfAborted(options.signal);
|
|
4806
4987
|
// Staged segments sort after every committed one; commitOrdinal keeps their
|
|
4807
4988
|
// staging order (the journal's id list is sorted, so it cannot).
|
|
4808
4989
|
const doctored = segments.map((segment) => pendingIds.has(segment.id)
|
|
@@ -4812,6 +4993,7 @@ export class MinnowDatabase {
|
|
|
4812
4993
|
for (const record of await this.#transactionRecordsForSegments(doctored)) {
|
|
4813
4994
|
transactionRecords.set(record.id, record);
|
|
4814
4995
|
}
|
|
4996
|
+
throwIfAborted(options.signal);
|
|
4815
4997
|
}
|
|
4816
4998
|
if (ourRecord !== undefined) {
|
|
4817
4999
|
transactionRecords.set(transaction.id, {
|
|
@@ -4831,6 +5013,7 @@ export class MinnowDatabase {
|
|
|
4831
5013
|
/** Executes one plan at an explicit snapshot and visibility, covering every plan shape. */
|
|
4832
5014
|
async #queryAtVisibility(plan, snapshot, visibility, realTables, cacheResults = true, options = {}) {
|
|
4833
5015
|
options = this.#effectiveQueryOptions(options);
|
|
5016
|
+
throwIfAborted(options.signal);
|
|
4834
5017
|
plan = expandFtsColumns(plan, (name) => searchableFtsColumns(realTables.get(name)));
|
|
4835
5018
|
const typedSchemas = new Map([...realTables.values()].map((table) => [
|
|
4836
5019
|
table.name,
|
|
@@ -4847,16 +5030,22 @@ export class MinnowDatabase {
|
|
|
4847
5030
|
const spillPageRows = options.spillPageRows === undefined
|
|
4848
5031
|
? undefined
|
|
4849
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
|
+
};
|
|
4850
5038
|
try {
|
|
4851
5039
|
const resolution = subqueryResolutionSteps(plan);
|
|
4852
5040
|
for (const step of resolution.steps) {
|
|
4853
|
-
|
|
5041
|
+
throwIfAborted(options.signal);
|
|
5042
|
+
step.substitute(await this.#executeBlock(step.block, snapshot, visibility, memory, realTables, typedSchemas, undefined, cacheResults, allowSpill, forceSpill, spillPageRows, options.signal));
|
|
4854
5043
|
}
|
|
4855
5044
|
const wrapper = transparentProjectionSource(resolution.plan);
|
|
4856
5045
|
if (wrapper !== undefined) {
|
|
4857
|
-
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));
|
|
4858
5047
|
}
|
|
4859
|
-
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));
|
|
4860
5049
|
}
|
|
4861
5050
|
finally {
|
|
4862
5051
|
memory.close();
|
|
@@ -4941,6 +5130,7 @@ export class MinnowDatabase {
|
|
|
4941
5130
|
throw new TypeError(`Table needs a unique key before rows can be updated: ${table.name}`);
|
|
4942
5131
|
}
|
|
4943
5132
|
input = normalizeDomainUpdate(table, input);
|
|
5133
|
+
rejectGeneratedUpdateAssignments(table, input);
|
|
4944
5134
|
const keys = validateUpdateBatch(table, keyColumn, input);
|
|
4945
5135
|
// Read-your-writes membership: keys staged by this scope pass, keys the scope removed
|
|
4946
5136
|
// fail, and everything else checks against the committed snapshot as usual.
|
|
@@ -4955,12 +5145,17 @@ export class MinnowDatabase {
|
|
|
4955
5145
|
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
|
|
4956
5146
|
}
|
|
4957
5147
|
const sessionChecks = table.checks ?? [];
|
|
4958
|
-
|
|
5148
|
+
let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
4959
5149
|
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
4960
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 ||
|
|
4961
5151
|
changedForeignKey ||
|
|
5152
|
+
tableGeneratedExpressions(table).length > 0 ||
|
|
4962
5153
|
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
4963
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));
|
|
4964
5159
|
stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
|
|
4965
5160
|
const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
|
|
4966
5161
|
if (secondaryDeltas.length > 0) {
|
|
@@ -5042,7 +5237,19 @@ export class MinnowDatabase {
|
|
|
5042
5237
|
},
|
|
5043
5238
|
]);
|
|
5044
5239
|
await this.#stageTriggerDerivedInserts(transaction, table, "update", input.keys.length, sessionUpdateValueAt, "after", cascadeBudget);
|
|
5045
|
-
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
|
+
};
|
|
5046
5253
|
}
|
|
5047
5254
|
async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
|
|
5048
5255
|
const table = await this.#findTable(tableName);
|
|
@@ -5379,6 +5586,9 @@ export class MinnowDatabase {
|
|
|
5379
5586
|
...(columnDefinition.defaultSpec === undefined
|
|
5380
5587
|
? {}
|
|
5381
5588
|
: { defaultValue: columnDefinition.defaultSpec }),
|
|
5589
|
+
...(columnDefinition.generatedSpec === undefined
|
|
5590
|
+
? {}
|
|
5591
|
+
: { generatedValue: columnDefinition.generatedSpec }),
|
|
5382
5592
|
...(columnDefinition.enumValues === undefined
|
|
5383
5593
|
? {}
|
|
5384
5594
|
: { enumValues: columnDefinition.enumValues }),
|
|
@@ -5516,6 +5726,28 @@ export class MinnowDatabase {
|
|
|
5516
5726
|
}
|
|
5517
5727
|
}
|
|
5518
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
|
+
}
|
|
5519
5751
|
// The remaining metadata-only steps share one catalog/manifest CAS. Physical drops ran
|
|
5520
5752
|
// above because segment rewriting and payload retirement have a stronger atomic boundary.
|
|
5521
5753
|
const columns = applyColumnSteps(record, steps, this.#createId);
|
|
@@ -6067,6 +6299,7 @@ export class MinnowDatabase {
|
|
|
6067
6299
|
const statementNow = this.#now();
|
|
6068
6300
|
const filledProposed = await fillColumnDefaults(table, proposedBatch, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
|
|
6069
6301
|
normalizeDomainBatch(table, filledProposed.batch);
|
|
6302
|
+
fillStoredGeneratedColumns(table, filledProposed.batch, statement.rows.length, filledProposed.generated);
|
|
6070
6303
|
fillCompositePrimaryKey(table, filledProposed.batch, statement.rows.length);
|
|
6071
6304
|
const proposed = filledProposed.batch;
|
|
6072
6305
|
// UPSERT only handles a uniqueness conflict. Other INSERT-domain failures are still errors,
|
|
@@ -6137,7 +6370,10 @@ export class MinnowDatabase {
|
|
|
6137
6370
|
// and UUID defaults twice and could even change which row conflicts.
|
|
6138
6371
|
const columns = visibleTableColumns(table).map(({ name }) => name);
|
|
6139
6372
|
const deferredAutoIncrement = new Set(filledProposed.autoIncrement?.missingIndexes ?? []);
|
|
6140
|
-
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))
|
|
6141
6377
|
? { default: true }
|
|
6142
6378
|
: (proposed.columns[name]?.[rowIndex] ?? null)));
|
|
6143
6379
|
return this.runStatement({
|
|
@@ -6231,6 +6467,8 @@ export class MinnowDatabase {
|
|
|
6231
6467
|
if (freshRows.length > 0) {
|
|
6232
6468
|
const columns = {};
|
|
6233
6469
|
for (const column of table.columns) {
|
|
6470
|
+
if (column.generatedValue !== undefined)
|
|
6471
|
+
continue;
|
|
6234
6472
|
columns[column.name] = freshRows.map((rowIndex) => proposed.columns[column.name]?.[rowIndex] ?? null);
|
|
6235
6473
|
}
|
|
6236
6474
|
await transaction.insertBatch(table.name, { columns });
|
|
@@ -6269,7 +6507,9 @@ export class MinnowDatabase {
|
|
|
6269
6507
|
table: statement.table,
|
|
6270
6508
|
rowCount: outcome.affectedRows.length,
|
|
6271
6509
|
...(version === null || version === undefined ? {} : { version }),
|
|
6272
|
-
...(outcome.returnedRows === undefined
|
|
6510
|
+
...(outcome.returnedRows === undefined || returningColumns === undefined
|
|
6511
|
+
? {}
|
|
6512
|
+
: returningExecuteFields(table, returningColumns, outcome.returnedRows)),
|
|
6273
6513
|
};
|
|
6274
6514
|
}
|
|
6275
6515
|
/** ON CONFLICT DO NOTHING: drops insert rows whose unique key already exists at a snapshot. */
|
|
@@ -6292,10 +6532,13 @@ export class MinnowDatabase {
|
|
|
6292
6532
|
const statementNow = this.#now();
|
|
6293
6533
|
const filled = await fillColumnDefaults(table, proposed, (sql) => this.#evaluateDefaultExpression(sql, statementNow), statement.rows.length);
|
|
6294
6534
|
normalizeDomainBatch(table, filled.batch);
|
|
6535
|
+
fillStoredGeneratedColumns(table, filled.batch, statement.rows.length, filled.generated);
|
|
6295
6536
|
fillCompositePrimaryKey(table, filled.batch, statement.rows.length);
|
|
6296
6537
|
const columns = visibleTableColumns(table).map(({ name }) => name);
|
|
6297
6538
|
const deferredAutoIncrement = new Set(filled.autoIncrement?.missingIndexes ?? []);
|
|
6298
|
-
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))
|
|
6299
6542
|
? { default: true }
|
|
6300
6543
|
: (filled.batch.columns[name]?.[rowIndex] ?? null)));
|
|
6301
6544
|
const keys = filled.batch.columns[keyColumn.name] ?? [];
|
|
@@ -6754,6 +6997,9 @@ export class MinnowDatabase {
|
|
|
6754
6997
|
if (record.columns.some(({ name }) => name === added.name)) {
|
|
6755
6998
|
throw new TypeError(`Column already exists: ${addColumnStatement.table}.${added.name}`);
|
|
6756
6999
|
}
|
|
7000
|
+
if (added.generatedValue !== undefined) {
|
|
7001
|
+
throw new TypeError("ALTER TABLE cannot add a generated column without rewriting existing rows");
|
|
7002
|
+
}
|
|
6757
7003
|
if (added.defaultValue !== undefined) {
|
|
6758
7004
|
validateColumnDefault({
|
|
6759
7005
|
name: added.name,
|
|
@@ -6912,11 +7158,19 @@ export class MinnowDatabase {
|
|
|
6912
7158
|
return this.#mergeConflictingInsertRows(statement, options);
|
|
6913
7159
|
}
|
|
6914
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];
|
|
6915
7167
|
return {
|
|
6916
7168
|
kind: "insert",
|
|
6917
7169
|
table: statement.table,
|
|
6918
7170
|
rowCount: 0,
|
|
6919
|
-
...(
|
|
7171
|
+
...(returningColumns === undefined
|
|
7172
|
+
? {}
|
|
7173
|
+
: returningExecuteFields(table, returningColumns, [])),
|
|
6920
7174
|
};
|
|
6921
7175
|
}
|
|
6922
7176
|
if (statement.kind === "insert") {
|
|
@@ -6978,7 +7232,7 @@ export class MinnowDatabase {
|
|
|
6978
7232
|
...(returningColumns === undefined
|
|
6979
7233
|
? {}
|
|
6980
7234
|
: {
|
|
6981
|
-
|
|
7235
|
+
...returningExecuteFields(table, returningColumns, statement.rows.map((row, rowIndex) => Object.fromEntries(returningColumns.map((name) => {
|
|
6982
7236
|
const column = table.columns.find((candidate) => candidate.name === name);
|
|
6983
7237
|
const position = statement.columns.indexOf(name);
|
|
6984
7238
|
const inserted = position < 0 ? undefined : row[position];
|
|
@@ -6988,7 +7242,7 @@ export class MinnowDatabase {
|
|
|
6988
7242
|
? null
|
|
6989
7243
|
: boundInsertValue(inserted));
|
|
6990
7244
|
return [name, executionSqlValueFromInput(column, value)];
|
|
6991
|
-
}))),
|
|
7245
|
+
})))),
|
|
6992
7246
|
}),
|
|
6993
7247
|
};
|
|
6994
7248
|
}
|
|
@@ -7050,6 +7304,32 @@ export class MinnowDatabase {
|
|
|
7050
7304
|
having: [],
|
|
7051
7305
|
orderBy: [],
|
|
7052
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
|
+
}
|
|
7053
7333
|
let rows;
|
|
7054
7334
|
if (options.writer !== undefined) {
|
|
7055
7335
|
// Inside a scope the rows to touch are the ones the scope can see, which includes what it
|
|
@@ -7077,7 +7357,9 @@ export class MinnowDatabase {
|
|
|
7077
7357
|
kind: "delete",
|
|
7078
7358
|
table: table.name,
|
|
7079
7359
|
rowCount: 0,
|
|
7080
|
-
...(returnedRows === undefined
|
|
7360
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7361
|
+
? {}
|
|
7362
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
7081
7363
|
};
|
|
7082
7364
|
}
|
|
7083
7365
|
const deleted = options.writer === undefined
|
|
@@ -7090,7 +7372,9 @@ export class MinnowDatabase {
|
|
|
7090
7372
|
...(deleted.version === undefined || deleted.version === null
|
|
7091
7373
|
? {}
|
|
7092
7374
|
: { version: deleted.version }),
|
|
7093
|
-
...(returnedRows === undefined
|
|
7375
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7376
|
+
? {}
|
|
7377
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
7094
7378
|
};
|
|
7095
7379
|
}
|
|
7096
7380
|
if (keys.length === 0) {
|
|
@@ -7098,7 +7382,9 @@ export class MinnowDatabase {
|
|
|
7098
7382
|
kind: "update",
|
|
7099
7383
|
table: table.name,
|
|
7100
7384
|
rowCount: 0,
|
|
7101
|
-
...(returningColumns === undefined
|
|
7385
|
+
...(returningColumns === undefined
|
|
7386
|
+
? {}
|
|
7387
|
+
: returningExecuteFields(table, returningColumns, [])),
|
|
7102
7388
|
};
|
|
7103
7389
|
}
|
|
7104
7390
|
const changes = {};
|
|
@@ -7131,7 +7417,9 @@ export class MinnowDatabase {
|
|
|
7131
7417
|
name,
|
|
7132
7418
|
returnedChanges !== undefined && name in returnedChanges
|
|
7133
7419
|
? (returnedChanges[name]?.[index] ?? null)
|
|
7134
|
-
:
|
|
7420
|
+
: updated.generatedColumns?.[name] !== undefined
|
|
7421
|
+
? (updated.generatedColumns[name][index] ?? null)
|
|
7422
|
+
: (row[name] ?? null),
|
|
7135
7423
|
])));
|
|
7136
7424
|
return {
|
|
7137
7425
|
kind: "update",
|
|
@@ -7140,7 +7428,9 @@ export class MinnowDatabase {
|
|
|
7140
7428
|
...(updated.version === undefined || updated.version === null
|
|
7141
7429
|
? {}
|
|
7142
7430
|
: { version: updated.version }),
|
|
7143
|
-
...(returnedRows === undefined
|
|
7431
|
+
...(returnedRows === undefined || returningColumns === undefined
|
|
7432
|
+
? {}
|
|
7433
|
+
: returningExecuteFields(table, returningColumns, returnedRows)),
|
|
7144
7434
|
};
|
|
7145
7435
|
}
|
|
7146
7436
|
/**
|
|
@@ -7175,6 +7465,7 @@ export class MinnowDatabase {
|
|
|
7175
7465
|
*/
|
|
7176
7466
|
async #queryStreamed(plan, options, spillPageRows, probe, cursor) {
|
|
7177
7467
|
options = this.#effectiveQueryOptions(options);
|
|
7468
|
+
throwIfAborted(options.signal);
|
|
7178
7469
|
const tableNames = [plan.base.table, ...plan.joins.map((join) => join.table)];
|
|
7179
7470
|
const uniqueTableNames = [...new Set(tableNames)];
|
|
7180
7471
|
if (options.version === undefined) {
|
|
@@ -7204,6 +7495,7 @@ export class MinnowDatabase {
|
|
|
7204
7495
|
});
|
|
7205
7496
|
}
|
|
7206
7497
|
async #queryStreamedAtSnapshot(plan, options, spillPageRows, snapshot, tables, visibility, cursor) {
|
|
7498
|
+
throwIfAborted(options.signal);
|
|
7207
7499
|
// Copy-on-write: expansion clones only full-text plans, leaving the compile cache's copy
|
|
7208
7500
|
// untouched.
|
|
7209
7501
|
plan = expandFtsColumns(plan, (tableName) => searchableFtsColumns(tables.find((table) => table.name === tableName)));
|
|
@@ -7224,7 +7516,9 @@ export class MinnowDatabase {
|
|
|
7224
7516
|
const visibleByTable = new Map();
|
|
7225
7517
|
const rowCounts = new Map();
|
|
7226
7518
|
for (const table of tables) {
|
|
7519
|
+
throwIfAborted(options.signal);
|
|
7227
7520
|
const segments = await this.#visibleSegmentRecords(table, snapshot, visibility);
|
|
7521
|
+
throwIfAborted(options.signal);
|
|
7228
7522
|
visibleByTable.set(table.name, segments);
|
|
7229
7523
|
if (segments.every((segment) => {
|
|
7230
7524
|
const kind = segment.kind;
|
|
@@ -7258,6 +7552,7 @@ export class MinnowDatabase {
|
|
|
7258
7552
|
record.name,
|
|
7259
7553
|
record.id === freshBaseTable.id ? freshBaseTable : record,
|
|
7260
7554
|
])), snapshot, visibility);
|
|
7555
|
+
throwIfAborted(options.signal);
|
|
7261
7556
|
if (planContainsFts(plan, "bm25") && ftsStats === undefined)
|
|
7262
7557
|
return undefined;
|
|
7263
7558
|
// The streamed scan honors the same index pruning as the materialized path; a budgeted
|
|
@@ -7266,11 +7561,14 @@ export class MinnowDatabase {
|
|
|
7266
7561
|
(await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility));
|
|
7267
7562
|
this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
|
|
7268
7563
|
const ftsSegments = await this.#ftsPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot);
|
|
7564
|
+
throwIfAborted(options.signal);
|
|
7269
7565
|
const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
|
|
7566
|
+
throwIfAborted(options.signal);
|
|
7270
7567
|
const baseSegments = indexed.segments;
|
|
7271
7568
|
// Zone-map elimination composes after index pruning: whole row groups whose statistics
|
|
7272
7569
|
// reject the plan's predicates never stream at all.
|
|
7273
7570
|
const zonePruned = await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot);
|
|
7571
|
+
throwIfAborted(options.signal);
|
|
7274
7572
|
const baseView = this.#streamedViewFactory(baseTable, projectedBaseColumns, zonePruned?.segments ?? baseSegments, snapshot, zonePruned?.storedBlocks, zonePruned !== undefined || indexed.pruned);
|
|
7275
7573
|
if (baseView === undefined)
|
|
7276
7574
|
return undefined;
|
|
@@ -7292,22 +7590,26 @@ export class MinnowDatabase {
|
|
|
7292
7590
|
const budget = options.executionMemoryBudgetBytes;
|
|
7293
7591
|
const estimate = estimatedColumnarBytes(buildSegments, projectedBuildColumns);
|
|
7294
7592
|
if (buildView !== undefined && estimate > budget / 4) {
|
|
7295
|
-
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);
|
|
7296
7594
|
}
|
|
7297
7595
|
}
|
|
7298
7596
|
}
|
|
7299
7597
|
}
|
|
7300
7598
|
const runStreamedExecution = async (attempt) => {
|
|
7599
|
+
throwIfAborted(options.signal);
|
|
7301
7600
|
const memory = new QueryMemoryContext(options.executionMemoryBudgetBytes);
|
|
7302
7601
|
let prepared;
|
|
7303
7602
|
try {
|
|
7304
7603
|
const streamed = await baseView.create(memory);
|
|
7604
|
+
throwIfAborted(options.signal);
|
|
7305
7605
|
const inputTables = new Map([[baseTable.name, streamed.table]]);
|
|
7306
7606
|
for (const table of tables) {
|
|
7607
|
+
throwIfAborted(options.signal);
|
|
7307
7608
|
if (table.name === baseTable.name)
|
|
7308
7609
|
continue;
|
|
7309
7610
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
7310
7611
|
inputTables.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility));
|
|
7612
|
+
throwIfAborted(options.signal);
|
|
7311
7613
|
}
|
|
7312
7614
|
prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
|
|
7313
7615
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
@@ -7320,6 +7622,14 @@ export class MinnowDatabase {
|
|
|
7320
7622
|
batchRows: cursor.batchRows,
|
|
7321
7623
|
signal: cursor.signal,
|
|
7322
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
|
+
}),
|
|
7323
7633
|
}, async (batch) => {
|
|
7324
7634
|
emission.happened = true;
|
|
7325
7635
|
await cursor.consume(externalizeQueryResult(batch));
|
|
@@ -7342,6 +7652,7 @@ export class MinnowDatabase {
|
|
|
7342
7652
|
try {
|
|
7343
7653
|
const streamedResult = await prepared.executeAsync({
|
|
7344
7654
|
loadScanWindow: streamed.load,
|
|
7655
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7345
7656
|
});
|
|
7346
7657
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7347
7658
|
return streamedResult;
|
|
@@ -7356,6 +7667,7 @@ export class MinnowDatabase {
|
|
|
7356
7667
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
7357
7668
|
spillStore: this.#leasedSpillStore(),
|
|
7358
7669
|
loadScanWindow: streamed.load,
|
|
7670
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
7359
7671
|
});
|
|
7360
7672
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7361
7673
|
return spilledResult;
|
|
@@ -7605,7 +7917,8 @@ export class MinnowDatabase {
|
|
|
7605
7917
|
* like any unordered query. The rescans trade time for bounded memory; the decoded-block
|
|
7606
7918
|
* cache keeps repeat decodes cheap.
|
|
7607
7919
|
*/
|
|
7608
|
-
async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName) {
|
|
7920
|
+
async #runPartitionedJoin(plan, budgetBytes, estimatedBuildBytes, baseView, buildView, buildTableName, buildKeyName, signal) {
|
|
7921
|
+
throwIfAborted(signal);
|
|
7609
7922
|
// One partition should fit in an eighth of the budget: the resident partition briefly
|
|
7610
7923
|
// exists twice (boxed rows, then their vectors) and shares the budget with both streamed
|
|
7611
7924
|
// windows and the accumulating result.
|
|
@@ -7618,6 +7931,7 @@ export class MinnowDatabase {
|
|
|
7618
7931
|
try {
|
|
7619
7932
|
const combined = [];
|
|
7620
7933
|
for (let partition = 0; partition < partitions; partition += 1) {
|
|
7934
|
+
throwIfAborted(signal);
|
|
7621
7935
|
if (combined.length >= wantedRows)
|
|
7622
7936
|
break;
|
|
7623
7937
|
const passMemory = root.createChild();
|
|
@@ -7628,6 +7942,7 @@ export class MinnowDatabase {
|
|
|
7628
7942
|
const collectMemory = passMemory.createChild();
|
|
7629
7943
|
const buildScanMemory = collectMemory.createChild();
|
|
7630
7944
|
const build = await buildView.create(buildScanMemory);
|
|
7945
|
+
throwIfAborted(signal);
|
|
7631
7946
|
let buildRows = [];
|
|
7632
7947
|
const buildColumns = [...build.table.columns.entries()];
|
|
7633
7948
|
// Small steps keep the collection windows a minor budget term next to the resident
|
|
@@ -7635,8 +7950,10 @@ export class MinnowDatabase {
|
|
|
7635
7950
|
// consumed length clamps to the returned resident end.
|
|
7636
7951
|
const step = 1_024;
|
|
7637
7952
|
for (let start = 0; start < build.table.rowCount;) {
|
|
7953
|
+
throwIfAborted(signal);
|
|
7638
7954
|
let length = Math.min(step, build.table.rowCount - start);
|
|
7639
7955
|
const residentEnd = await build.load(start, length);
|
|
7956
|
+
throwIfAborted(signal);
|
|
7640
7957
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
7641
7958
|
length = Math.min(length, residentEnd - start);
|
|
7642
7959
|
}
|
|
@@ -7667,13 +7984,17 @@ export class MinnowDatabase {
|
|
|
7667
7984
|
const buildInput = columnarTableFromRows(buildTableName, buildRows, undefined, false);
|
|
7668
7985
|
buildRows = [];
|
|
7669
7986
|
const streamedBase = await baseView.create(passMemory);
|
|
7987
|
+
throwIfAborted(signal);
|
|
7670
7988
|
const inputTables = new Map([
|
|
7671
7989
|
[buildTableName, buildInput],
|
|
7672
7990
|
[streamedBase.table.name, streamedBase.table],
|
|
7673
7991
|
]);
|
|
7674
7992
|
prepared = createPreparedColumnarQuery(strippedPlan, inputTables, passMemory);
|
|
7675
7993
|
collectMemory.close();
|
|
7676
|
-
const result = await prepared.executeAsync({
|
|
7994
|
+
const result = await prepared.executeAsync({
|
|
7995
|
+
loadScanWindow: streamedBase.load,
|
|
7996
|
+
...(signal === undefined ? {} : { signal }),
|
|
7997
|
+
});
|
|
7677
7998
|
for (const row of result.rows) {
|
|
7678
7999
|
if (combined.length >= wantedRows)
|
|
7679
8000
|
break;
|
|
@@ -8237,17 +8558,24 @@ export class MinnowDatabase {
|
|
|
8237
8558
|
};
|
|
8238
8559
|
}
|
|
8239
8560
|
/** Prepares one block's inputs and executes it, returning the caller-owned result. */
|
|
8240
|
-
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);
|
|
8241
8563
|
const ftsStats = await this.#ftsIndexStats(block, realTables, snapshot, visibility);
|
|
8242
|
-
|
|
8564
|
+
throwIfAborted(signal);
|
|
8565
|
+
const inputs = await this.#prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
|
|
8566
|
+
throwIfAborted(signal);
|
|
8243
8567
|
const prepared = createPreparedColumnarQuery(block, inputs, memory.createChild(), ftsStats === undefined ? {} : { ftsStats });
|
|
8244
8568
|
try {
|
|
8245
8569
|
if (!allowSpill || memory.usage.budgetBytes === Number.MAX_SAFE_INTEGER) {
|
|
8246
|
-
|
|
8570
|
+
const result = prepared.execute();
|
|
8571
|
+
throwIfAborted(signal);
|
|
8572
|
+
return result;
|
|
8247
8573
|
}
|
|
8248
8574
|
if (!forceSpill) {
|
|
8249
8575
|
try {
|
|
8250
|
-
|
|
8576
|
+
const result = prepared.execute();
|
|
8577
|
+
throwIfAborted(signal);
|
|
8578
|
+
return result;
|
|
8251
8579
|
}
|
|
8252
8580
|
catch (error) {
|
|
8253
8581
|
if (!(error instanceof QueryMemoryBudgetError))
|
|
@@ -8257,6 +8585,7 @@ export class MinnowDatabase {
|
|
|
8257
8585
|
return await prepared.executeAsync({
|
|
8258
8586
|
spillStore: this.#leasedSpillStore(),
|
|
8259
8587
|
...(spillPageRows === undefined ? {} : { spillPageRows }),
|
|
8588
|
+
...(signal === undefined ? {} : { signal }),
|
|
8260
8589
|
});
|
|
8261
8590
|
}
|
|
8262
8591
|
finally {
|
|
@@ -8270,14 +8599,17 @@ export class MinnowDatabase {
|
|
|
8270
8599
|
* real tables it references, so an identical block over identical table states reuses
|
|
8271
8600
|
* the previous result instead of re-reading and re-executing.
|
|
8272
8601
|
*/
|
|
8273
|
-
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);
|
|
8274
8604
|
const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
|
|
8605
|
+
throwIfAborted(signal);
|
|
8275
8606
|
if (key !== undefined) {
|
|
8276
8607
|
const cached = this.#cacheGet(key);
|
|
8277
8608
|
if (cached !== undefined)
|
|
8278
8609
|
return cached;
|
|
8279
8610
|
}
|
|
8280
|
-
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);
|
|
8281
8613
|
if (key !== undefined)
|
|
8282
8614
|
this.#cachePut(key, result, queryResultRetainedBytes(result));
|
|
8283
8615
|
return result;
|
|
@@ -8288,15 +8620,18 @@ export class MinnowDatabase {
|
|
|
8288
8620
|
* synthetic sources in typedSchemas, and a later cache hit skips that registration, so
|
|
8289
8621
|
* re-inferring the schema at hit time would find those names missing.
|
|
8290
8622
|
*/
|
|
8291
|
-
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);
|
|
8292
8625
|
const key = await this.#blockResultCacheKey(block, snapshot, visibility, realTables, cacheResults);
|
|
8626
|
+
throwIfAborted(signal);
|
|
8293
8627
|
const schemaKey = key === undefined ? undefined : `s${key}`;
|
|
8294
8628
|
if (schemaKey !== undefined) {
|
|
8295
8629
|
const cached = this.#cacheGet(schemaKey);
|
|
8296
8630
|
if (cached !== undefined)
|
|
8297
8631
|
return cached;
|
|
8298
8632
|
}
|
|
8299
|
-
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);
|
|
8300
8635
|
const schema = inferBlockSchema(block, typedSchemas);
|
|
8301
8636
|
const entry = { result, schema };
|
|
8302
8637
|
if (schemaKey !== undefined) {
|
|
@@ -10669,7 +11004,7 @@ export class MinnowDatabase {
|
|
|
10669
11004
|
if (recoveredManifest === undefined) {
|
|
10670
11005
|
throw new Error(`Compaction manifest is missing: ${String(linkedTransaction.committedVersion)}`);
|
|
10671
11006
|
}
|
|
10672
|
-
this.#afterCompactionCommit(recoveredManifest);
|
|
11007
|
+
this.#afterCompactionCommit(recoveredManifest, table.id);
|
|
10673
11008
|
return this.#publishedCompactionProgress(table, job);
|
|
10674
11009
|
}
|
|
10675
11010
|
const rewritePlan = job.rewritePlan;
|
|
@@ -10855,7 +11190,7 @@ export class MinnowDatabase {
|
|
|
10855
11190
|
let publicationConflict;
|
|
10856
11191
|
try {
|
|
10857
11192
|
manifest = await transaction.commit();
|
|
10858
|
-
this.#afterCompactionCommit(manifest);
|
|
11193
|
+
this.#afterCompactionCommit(manifest, table.id);
|
|
10859
11194
|
break;
|
|
10860
11195
|
}
|
|
10861
11196
|
catch (error) {
|
|
@@ -14096,13 +14431,27 @@ function assertTriggerBodyTargetSchema(compiled, target) {
|
|
|
14096
14431
|
if (column !== undefined &&
|
|
14097
14432
|
isDefaultInsertValue(value) &&
|
|
14098
14433
|
!column.nullable &&
|
|
14099
|
-
column.defaultValue === undefined
|
|
14434
|
+
column.defaultValue === undefined &&
|
|
14435
|
+
column.generatedValue === undefined) {
|
|
14100
14436
|
throw new TypeError(`Trigger body INSERT uses DEFAULT for a non-nullable column without a default: ${column.name}`);
|
|
14101
14437
|
}
|
|
14102
14438
|
}
|
|
14103
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
|
+
}
|
|
14104
14450
|
for (const column of columns) {
|
|
14105
|
-
if (!provided.has(column.name) &&
|
|
14451
|
+
if (!provided.has(column.name) &&
|
|
14452
|
+
!column.nullable &&
|
|
14453
|
+
column.defaultValue === undefined &&
|
|
14454
|
+
column.generatedValue === undefined) {
|
|
14106
14455
|
throw new TypeError(`Trigger body INSERT omits a non-nullable column without a default: ${column.name}`);
|
|
14107
14456
|
}
|
|
14108
14457
|
}
|
|
@@ -14124,6 +14473,9 @@ function assertTriggerBodyTargetSchema(compiled, target) {
|
|
|
14124
14473
|
if (assignedColumn === undefined) {
|
|
14125
14474
|
throw new TypeError(`Trigger body UPDATE assignment column does not exist: ${assignment.column}`);
|
|
14126
14475
|
}
|
|
14476
|
+
if (assignedColumn.generatedValue !== undefined) {
|
|
14477
|
+
throw new TypeError(`Trigger body UPDATE assigns generated column: ${assignment.column}`);
|
|
14478
|
+
}
|
|
14127
14479
|
if (assignedColumn.id === target.uniqueKeyColumnId ||
|
|
14128
14480
|
(target.primaryKeyColumnIds ?? []).includes(assignedColumn.id)) {
|
|
14129
14481
|
throw new TypeError(`Trigger body UPDATE cannot update a key column: ${assignment.column}`);
|
|
@@ -14273,6 +14625,7 @@ function visibleSegmentManifestVersion(value, label) {
|
|
|
14273
14625
|
*/
|
|
14274
14626
|
const compiledChecks = new Map();
|
|
14275
14627
|
const COMPILED_CHECK_CACHE_LIMIT = 64;
|
|
14628
|
+
const compiledGeneratedColumns = new Map();
|
|
14276
14629
|
function tableChecks(table) {
|
|
14277
14630
|
const key = `${table.id}/${String(table.revision)}`;
|
|
14278
14631
|
const cached = compiledChecks.get(key);
|
|
@@ -14295,6 +14648,84 @@ function tableChecks(table) {
|
|
|
14295
14648
|
compiledChecks.set(key, compiled);
|
|
14296
14649
|
return compiled;
|
|
14297
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
|
+
}
|
|
14298
14729
|
/**
|
|
14299
14730
|
* Applies a table's CHECK constraints to one row (E141-06). A constraint fails only when it
|
|
14300
14731
|
* evaluates to false: SQL's three-valued logic lets an unknown pass, which is why a NULL column
|
|
@@ -15414,7 +15845,7 @@ function tryKeyToken(type, value) {
|
|
|
15414
15845
|
* (level one and above) are the folded state, not fragmentation, and do not count — a large
|
|
15415
15846
|
* keyed table is many partitions by design.
|
|
15416
15847
|
*/
|
|
15417
|
-
function
|
|
15848
|
+
function autoCompactionHint(version, segments) {
|
|
15418
15849
|
let levelZero = 0;
|
|
15419
15850
|
let deltas = 0;
|
|
15420
15851
|
for (const segment of segments) {
|
|
@@ -15424,7 +15855,18 @@ function autoCompactionDue(segments) {
|
|
|
15424
15855
|
if (kind !== "insert" && kind !== "base")
|
|
15425
15856
|
deltas += 1;
|
|
15426
15857
|
}
|
|
15427
|
-
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));
|
|
15428
15870
|
}
|
|
15429
15871
|
/** A macrotask boundary, so background work lets queued queries and writes run between steps. */
|
|
15430
15872
|
function yieldToEventLoop() {
|