@minnowdb/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/engine/catalog.d.ts +2 -0
- package/dist/engine/catalog.js +4 -1
- package/dist/engine/client.d.ts +3 -3
- package/dist/engine/client.js +6 -5
- package/dist/engine/database.d.ts +20 -6
- package/dist/engine/database.js +318 -95
- package/dist/engine/errors.d.ts +6 -0
- package/dist/engine/errors.js +9 -0
- package/dist/engine/live.js +10 -1
- package/dist/engine/optimizer.js +6 -1
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +13 -2
- package/dist/engine/query.js +183 -43
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +7 -0
- package/dist/engine/schema-wire.js +3 -1
- package/dist/engine/schema.d.ts +17 -0
- package/dist/engine/schema.js +42 -7
- package/dist/engine/sql-domains.d.ts +3 -0
- package/dist/engine/sql-domains.js +40 -1
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.js +28 -41
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/opfs/leader.js +14 -2
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +19 -8
- package/dist/storage/types.js +69 -0
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +6 -0
package/dist/engine/database.js
CHANGED
|
@@ -2,23 +2,23 @@ import { toColumnarBatch, } from "./batch.js";
|
|
|
2
2
|
import { ArtifactCache } from "./artifact-cache.js";
|
|
3
3
|
import { BufferedTableWriter } from "./buffered-writer.js";
|
|
4
4
|
export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_ADDS, } from "./buffered-writer.js";
|
|
5
|
-
import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
|
|
5
|
+
import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
|
|
6
6
|
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
7
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
8
8
|
import { estimateCompactionRowsPerOutput, planAlignedWriteBlockRanges as writeBlockRanges, } from "./write-block-planner.js";
|
|
9
|
-
import { MAX_CACHEABLE_TEXT_CHARACTERS } from "./cache-limits.js";
|
|
9
|
+
import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_PARAMETERS } from "./cache-limits.js";
|
|
10
10
|
import { fillColumnDefaults, patchAutoIncrementValues, } from "./defaults.js";
|
|
11
11
|
import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize as ftsTokenize, } from "./fts.js";
|
|
12
12
|
import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, } from "../storage/types.js";
|
|
13
13
|
import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
|
|
14
14
|
import { Snapshot, TransactionManager, } from "../transactions/index.js";
|
|
15
|
-
import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, compileStatement, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
|
|
15
|
+
import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
|
|
16
16
|
import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
|
|
17
17
|
import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
|
|
18
18
|
import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
|
|
19
19
|
import { chooseJoinOrder, renderPlan } from "./optimizer.js";
|
|
20
20
|
import { encodeSqlEqualityValue } from "./sql-semantics.js";
|
|
21
|
-
import { externalSqlDomainValue, externalSqlTextValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
21
|
+
import { externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
22
22
|
import { toCatalog } from "./catalog.js";
|
|
23
23
|
import { applyColumnSteps, assertColumnDroppable, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
|
|
24
24
|
import { columnarTableFromRows, createColumnarTable, vectorValue, } from "./vector.js";
|
|
@@ -272,7 +272,7 @@ function migrationResult(steps) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
export const MAX_GARBAGE_COLLECTION_RETAIN_RECENT_VERSIONS = 1_024;
|
|
275
|
-
export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
|
|
275
|
+
export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
|
|
276
276
|
/**
|
|
277
277
|
* How deep a delete's referential actions may cascade. A chain of CASCADE keys can reach further
|
|
278
278
|
* tables; the bound stops a cycle from recursing, the same way the trigger chain is bounded.
|
|
@@ -297,7 +297,11 @@ function externalizeExecuteResult(result) {
|
|
|
297
297
|
const columns = [...new Set(result.returnedRows.flatMap((row) => Object.keys(row)))];
|
|
298
298
|
return {
|
|
299
299
|
...result,
|
|
300
|
-
returnedRows: externalizeQueryResult({
|
|
300
|
+
returnedRows: externalizeQueryResult({
|
|
301
|
+
columns,
|
|
302
|
+
columnDomains: unknownColumnDomains(columns),
|
|
303
|
+
rows: result.returnedRows,
|
|
304
|
+
}).rows,
|
|
301
305
|
};
|
|
302
306
|
}
|
|
303
307
|
/** Rejects an INSERT value that is still an unbound `?`/`$n` slot. */
|
|
@@ -894,6 +898,9 @@ export class MinnowDatabase {
|
|
|
894
898
|
const checkNames = (input.checks ?? []).map((check) => claimConstraintName(check.name));
|
|
895
899
|
const uniqueConstraintNames = (input.uniqueConstraints ?? []).map((constraint) => claimConstraintName(constraint.name));
|
|
896
900
|
const foreignKeys = await Promise.all((input.foreignKeys ?? []).map(async (key, keyIndex) => {
|
|
901
|
+
if (key.enforced === false && key.onDelete !== "restrict") {
|
|
902
|
+
throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
|
|
903
|
+
}
|
|
897
904
|
const childNames = key.columns ?? [key.column];
|
|
898
905
|
const children = childNames.map((columnName) => {
|
|
899
906
|
const column = columns.find((candidate) => candidate.name === columnName);
|
|
@@ -959,6 +966,7 @@ export class MinnowDatabase {
|
|
|
959
966
|
parentTable: key.parentTable,
|
|
960
967
|
parentColumns: parentNames,
|
|
961
968
|
onDelete: key.onDelete,
|
|
969
|
+
...(key.enforced === false ? { enforced: false } : {}),
|
|
962
970
|
};
|
|
963
971
|
}));
|
|
964
972
|
const checks = (input.checks ?? []).map((check, checkIndex) => {
|
|
@@ -1383,7 +1391,7 @@ export class MinnowDatabase {
|
|
|
1383
1391
|
throw new TypeError(`${tableName} is a view, not a table`);
|
|
1384
1392
|
if (options.ifExists === true)
|
|
1385
1393
|
return false;
|
|
1386
|
-
throw new
|
|
1394
|
+
throw new UnknownTableError(tableName);
|
|
1387
1395
|
}
|
|
1388
1396
|
pinnedTableId ??= table.id;
|
|
1389
1397
|
if (table.id !== pinnedTableId) {
|
|
@@ -1513,7 +1521,7 @@ export class MinnowDatabase {
|
|
|
1513
1521
|
}
|
|
1514
1522
|
if (options.ifExists === true)
|
|
1515
1523
|
return false;
|
|
1516
|
-
throw new
|
|
1524
|
+
throw new UnknownTableError(tableName);
|
|
1517
1525
|
}
|
|
1518
1526
|
pinnedTableId ??= table.id;
|
|
1519
1527
|
if (table.id !== pinnedTableId) {
|
|
@@ -1596,12 +1604,14 @@ export class MinnowDatabase {
|
|
|
1596
1604
|
: undefined;
|
|
1597
1605
|
const current = await this.store.getTable(table.id);
|
|
1598
1606
|
if (current === undefined)
|
|
1599
|
-
throw new
|
|
1607
|
+
throw new UnknownTableError(tableName);
|
|
1600
1608
|
const result = await this.#writeBatch(current, batch, "insert", keys, autoIncrement);
|
|
1601
1609
|
return { result, batch, generated, autoIncrement, rowCount };
|
|
1602
1610
|
});
|
|
1603
1611
|
const { result, batch, generated, autoIncrement, rowCount } = completed;
|
|
1604
1612
|
collectAutoIncrementGenerated(batch, generated, autoIncrement);
|
|
1613
|
+
if (result.segmentId === null)
|
|
1614
|
+
throw new Error("An insert did not create a segment");
|
|
1605
1615
|
return {
|
|
1606
1616
|
tableName: result.tableName,
|
|
1607
1617
|
segmentId: result.segmentId,
|
|
@@ -1655,35 +1665,49 @@ export class MinnowDatabase {
|
|
|
1655
1665
|
async insert(tableName, row) {
|
|
1656
1666
|
return this.insertBatch(tableName, [row]);
|
|
1657
1667
|
}
|
|
1658
|
-
upsertBatch(tableName, input) {
|
|
1659
|
-
return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input));
|
|
1668
|
+
upsertBatch(tableName, input, options = {}) {
|
|
1669
|
+
return this.#withWriteReservation(() => this.#upsertBatchReserved(tableName, input, options));
|
|
1660
1670
|
}
|
|
1661
|
-
async #upsertBatchReserved(tableName, input) {
|
|
1671
|
+
async #upsertBatchReserved(tableName, input, options) {
|
|
1662
1672
|
const completed = await this.#runWrite(async () => {
|
|
1663
1673
|
const table = await this.#findTable(tableName);
|
|
1664
1674
|
const keyColumn = getUniqueKeyColumn(table);
|
|
1665
1675
|
if (keyColumn === undefined) {
|
|
1666
1676
|
throw new TypeError(`Table needs a unique key before it can be upserted: ${table.name}`);
|
|
1667
1677
|
}
|
|
1668
|
-
const { batch, generated, autoIncrement
|
|
1678
|
+
const { batch, generated, autoIncrement } = await this.#fillDefaults(table, input);
|
|
1669
1679
|
const deferred = autoIncrement !== undefined && autoIncrement.missingIndexes.length > 0;
|
|
1670
1680
|
const keys = deferred ? undefined : batchKeys(table, batch);
|
|
1671
1681
|
const current = await this.store.getTable(table.id);
|
|
1672
1682
|
if (current === undefined)
|
|
1673
|
-
throw new
|
|
1674
|
-
const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement);
|
|
1675
|
-
return { result, batch, generated, autoIncrement
|
|
1683
|
+
throw new UnknownTableError(tableName);
|
|
1684
|
+
const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement, options.conflictWhere);
|
|
1685
|
+
return { result, batch, generated, autoIncrement };
|
|
1676
1686
|
});
|
|
1677
|
-
const { result, batch, generated, autoIncrement
|
|
1687
|
+
const { result, batch, generated, autoIncrement } = completed;
|
|
1678
1688
|
collectAutoIncrementGenerated(batch, generated, autoIncrement);
|
|
1689
|
+
if (result.acceptedRowIndexes.length !== result.requestedRowCount) {
|
|
1690
|
+
for (const [name, values] of generated) {
|
|
1691
|
+
generated.set(name, result.acceptedRowIndexes.map((index) => values[index] ?? null));
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1679
1694
|
return {
|
|
1680
|
-
|
|
1681
|
-
|
|
1695
|
+
tableName: result.tableName,
|
|
1696
|
+
segmentId: result.segmentId,
|
|
1697
|
+
requestedRowCount: result.requestedRowCount,
|
|
1698
|
+
rowCount: result.rowCount,
|
|
1699
|
+
insertedRowCount: result.insertedRowCount,
|
|
1700
|
+
updatedRowCount: result.updatedRowCount,
|
|
1701
|
+
skippedRowCount: result.skippedRowCount,
|
|
1702
|
+
blockCount: result.blockCount,
|
|
1703
|
+
storedBytes: result.storedBytes,
|
|
1704
|
+
version: result.version,
|
|
1705
|
+
metrics: result.metrics,
|
|
1682
1706
|
...(generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }),
|
|
1683
1707
|
};
|
|
1684
1708
|
}
|
|
1685
|
-
async upsert(tableName, row) {
|
|
1686
|
-
return this.upsertBatch(tableName, [row]);
|
|
1709
|
+
async upsert(tableName, row, options = {}) {
|
|
1710
|
+
return this.upsertBatch(tableName, [row], options);
|
|
1687
1711
|
}
|
|
1688
1712
|
updateBatch(tableName, input) {
|
|
1689
1713
|
return this.#withWriteReservation(() => this.#updateBatchReserved(tableName, input));
|
|
@@ -1699,7 +1723,7 @@ export class MinnowDatabase {
|
|
|
1699
1723
|
const keys = validateUpdateBatch(table, keyColumn, normalizedInput);
|
|
1700
1724
|
const current = await this.store.getTable(table.id);
|
|
1701
1725
|
if (current === undefined)
|
|
1702
|
-
throw new
|
|
1726
|
+
throw new UnknownTableError(tableName);
|
|
1703
1727
|
const currentKey = getUniqueKeyColumn(current);
|
|
1704
1728
|
if (currentKey?.id !== keyColumn.id) {
|
|
1705
1729
|
throw new TypeError(`Table unique key changed during update: ${tableName}`);
|
|
@@ -1999,7 +2023,8 @@ export class MinnowDatabase {
|
|
|
1999
2023
|
columnBlockIds[column.id] = blockIds;
|
|
2000
2024
|
}
|
|
2001
2025
|
const checks = table.checks ?? [];
|
|
2002
|
-
const changedForeignKey = (table.foreignKeys ?? []).some((key) =>
|
|
2026
|
+
const changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
2027
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
2003
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 ||
|
|
2004
2029
|
changedForeignKey ||
|
|
2005
2030
|
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
@@ -2117,12 +2142,13 @@ export class MinnowDatabase {
|
|
|
2117
2142
|
}
|
|
2118
2143
|
throw new Error(`Unique-key lookup returned an inconsistent result: ${table.name}`);
|
|
2119
2144
|
}
|
|
2120
|
-
async #writeBatch(table, input, kind, keys, autoIncrement) {
|
|
2145
|
+
async #writeBatch(table, input, kind, keys, autoIncrement, conflictWhere) {
|
|
2121
2146
|
await this.#assertCompactionCapacity(table);
|
|
2122
2147
|
const started = performance.now();
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2148
|
+
const requestedRowCount = input.columns[table.columns[0]?.name ?? ""]?.length ?? 0;
|
|
2149
|
+
let rowCount = requestedRowCount;
|
|
2150
|
+
let logicalBytes = estimateBatchBytes(input);
|
|
2151
|
+
const { transaction, rowIds: reservedRowIds, autoIncrementValues, } = await this.#transactions.beginWithReservation({ tableId: table.id, count: requestedRowCount }, autoIncrement === undefined
|
|
2126
2152
|
? undefined
|
|
2127
2153
|
: {
|
|
2128
2154
|
tableId: table.id,
|
|
@@ -2133,11 +2159,14 @@ export class MinnowDatabase {
|
|
|
2133
2159
|
transaction.limitLevelZeroSegments(table.id, MAX_LEVEL_ZERO_SEGMENTS);
|
|
2134
2160
|
const blockStager = new BoundedWriteBlockStager(transaction);
|
|
2135
2161
|
let resolvedKeys = keys;
|
|
2136
|
-
|
|
2162
|
+
let segmentId = null;
|
|
2137
2163
|
const columnBlockIds = {};
|
|
2138
2164
|
let storedBytes = 0;
|
|
2139
2165
|
let blockCount = 0;
|
|
2140
2166
|
let counts;
|
|
2167
|
+
let skippedRowCount = 0;
|
|
2168
|
+
let acceptedRowIndexes = Array.from({ length: requestedRowCount }, (_, index) => index);
|
|
2169
|
+
let upsertFirings;
|
|
2141
2170
|
let encodeMs = 0;
|
|
2142
2171
|
let stageMs = 0;
|
|
2143
2172
|
let commitMs = 0;
|
|
@@ -2157,6 +2186,65 @@ export class MinnowDatabase {
|
|
|
2157
2186
|
}
|
|
2158
2187
|
resolvedKeys = batchKeys(table, input);
|
|
2159
2188
|
}
|
|
2189
|
+
const normalizedConflictWhere = kind === "upsert" && conflictWhere !== undefined
|
|
2190
|
+
? normalizeUpsertConflictWhere(table, conflictWhere)
|
|
2191
|
+
: undefined;
|
|
2192
|
+
const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
|
|
2193
|
+
if (upsertKeyColumn !== undefined) {
|
|
2194
|
+
upsertFirings = await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount, (sql, params) => this.#sessionQuery(transaction, sql, { params }), normalizedConflictWhere, normalizedConflictWhere !== undefined);
|
|
2195
|
+
}
|
|
2196
|
+
if (normalizedConflictWhere !== undefined) {
|
|
2197
|
+
if (upsertFirings === undefined) {
|
|
2198
|
+
throw new Error("Upsert conflict classification is missing");
|
|
2199
|
+
}
|
|
2200
|
+
const classifiedFirings = upsertFirings;
|
|
2201
|
+
acceptedRowIndexes = [...classifiedFirings.inserts, ...classifiedFirings.updates].sort((left, right) => left - right);
|
|
2202
|
+
skippedRowCount = classifiedFirings.skipped.length;
|
|
2203
|
+
input = selectBatchRows(input, acceptedRowIndexes);
|
|
2204
|
+
rowCount = acceptedRowIndexes.length;
|
|
2205
|
+
logicalBytes = estimateBatchBytes(input);
|
|
2206
|
+
resolvedKeys = batchKeys(table, input);
|
|
2207
|
+
counts = {
|
|
2208
|
+
inserted: classifiedFirings.inserts.length,
|
|
2209
|
+
updated: classifiedFirings.updates.length,
|
|
2210
|
+
};
|
|
2211
|
+
const acceptedPosition = new Map(acceptedRowIndexes.map((original, position) => [original, position]));
|
|
2212
|
+
upsertFirings = {
|
|
2213
|
+
inserts: classifiedFirings.inserts.map((index) => acceptedPosition.get(index) ?? -1),
|
|
2214
|
+
updates: classifiedFirings.updates.map((index) => acceptedPosition.get(index) ?? -1),
|
|
2215
|
+
skipped: [],
|
|
2216
|
+
oldImages: acceptedRowIndexes.map((index) => classifiedFirings.oldImages[index]),
|
|
2217
|
+
};
|
|
2218
|
+
if (rowCount === 0) {
|
|
2219
|
+
await transaction.abort();
|
|
2220
|
+
const version = transaction.snapshotVersion;
|
|
2221
|
+
if (version === null)
|
|
2222
|
+
throw new Error("An existing table has no catalog version");
|
|
2223
|
+
return {
|
|
2224
|
+
tableName: table.name,
|
|
2225
|
+
segmentId: null,
|
|
2226
|
+
requestedRowCount,
|
|
2227
|
+
rowCount: 0,
|
|
2228
|
+
insertedRowCount: 0,
|
|
2229
|
+
updatedRowCount: 0,
|
|
2230
|
+
skippedRowCount,
|
|
2231
|
+
acceptedRowIndexes,
|
|
2232
|
+
blockCount: 0,
|
|
2233
|
+
storedBytes: 0,
|
|
2234
|
+
version,
|
|
2235
|
+
metrics: createWriteMetrics({
|
|
2236
|
+
logicalBytes: 0,
|
|
2237
|
+
storedBytes: 0,
|
|
2238
|
+
encodeMs: 0,
|
|
2239
|
+
stageMs: 0,
|
|
2240
|
+
commitMs: 0,
|
|
2241
|
+
totalMs: performance.now() - started,
|
|
2242
|
+
retries: 0,
|
|
2243
|
+
rows: 0,
|
|
2244
|
+
}),
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2160
2248
|
if (resolvedKeys !== undefined) {
|
|
2161
2249
|
transaction.setUniqueKeyChanges({
|
|
2162
2250
|
tableId: table.id,
|
|
@@ -2169,11 +2257,20 @@ export class MinnowDatabase {
|
|
|
2169
2257
|
// generated values are patched also covers an auto-incremented referencing column, and
|
|
2170
2258
|
// registering this batch's key membership first permits valid self-references.
|
|
2171
2259
|
await assertForeignKeys();
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2260
|
+
if (normalizedConflictWhere === undefined) {
|
|
2261
|
+
counts =
|
|
2262
|
+
kind === "insert" && resolvedKeys !== undefined
|
|
2263
|
+
? { inserted: resolvedKeys.size, updated: 0 }
|
|
2264
|
+
: await this.#classifyKeys(table, transaction.snapshotVersion, kind, resolvedKeys);
|
|
2265
|
+
}
|
|
2266
|
+
if (counts === undefined)
|
|
2267
|
+
throw new Error("Write classification is missing");
|
|
2268
|
+
const reserved = reservedRowIds ?? (await this.store.reserveRowIds(table.id, rowCount));
|
|
2269
|
+
const rowIds = {
|
|
2270
|
+
start: reserved.start,
|
|
2271
|
+
endExclusive: reserved.start + BigInt(rowCount),
|
|
2272
|
+
};
|
|
2273
|
+
segmentId = this.#createId();
|
|
2177
2274
|
// Insert batches maintain any active full-text index by tokenizing their own rows into a
|
|
2178
2275
|
// commit delta. Other mutation kinds emit nothing on purpose: the store's stale-writer
|
|
2179
2276
|
// rule then flips the index to "invalid" (keyed histories are unindexable) and the scan
|
|
@@ -2267,10 +2364,6 @@ export class MinnowDatabase {
|
|
|
2267
2364
|
const insertValueAt = (source, column, rowIndex) => (source === "new" ? (input.columns[column]?.[rowIndex] ?? null) : null);
|
|
2268
2365
|
// Upserts fire per-row: INSERT triggers for fresh keys, UPDATE triggers (with the
|
|
2269
2366
|
// replaced row as OLD) for conflicting ones — ON CONFLICT DO UPDATE semantics.
|
|
2270
|
-
const upsertKeyColumn = kind === "upsert" ? getUniqueKeyColumn(table) : undefined;
|
|
2271
|
-
const upsertFirings = upsertKeyColumn === undefined
|
|
2272
|
-
? undefined
|
|
2273
|
-
: await this.#upsertTriggerFirings(table, upsertKeyColumn, input, rowCount);
|
|
2274
2367
|
stageSecondaryUniqueInsertChanges(transaction, table, input, kind === "upsert" ? upsertFirings?.oldImages : undefined);
|
|
2275
2368
|
if (kind === "insert") {
|
|
2276
2369
|
await this.#stageTriggerDerivedInserts(transaction, table, "insert", rowCount, insertValueAt, "before");
|
|
@@ -2304,12 +2397,15 @@ export class MinnowDatabase {
|
|
|
2304
2397
|
return {
|
|
2305
2398
|
tableName: table.name,
|
|
2306
2399
|
segmentId,
|
|
2400
|
+
requestedRowCount,
|
|
2307
2401
|
rowCount,
|
|
2308
2402
|
blockCount,
|
|
2309
2403
|
storedBytes,
|
|
2310
2404
|
version: manifest.version,
|
|
2311
2405
|
insertedRowCount: counts.inserted,
|
|
2312
2406
|
updatedRowCount: counts.updated,
|
|
2407
|
+
skippedRowCount,
|
|
2408
|
+
acceptedRowIndexes,
|
|
2313
2409
|
metrics: createWriteMetrics({
|
|
2314
2410
|
logicalBytes,
|
|
2315
2411
|
storedBytes,
|
|
@@ -2329,7 +2425,7 @@ export class MinnowDatabase {
|
|
|
2329
2425
|
}
|
|
2330
2426
|
const stagedDerivations = kind === "insert"
|
|
2331
2427
|
? (table.triggers ?? []).some((trigger) => trigger.event === "insert")
|
|
2332
|
-
: upsertFirings !== undefined;
|
|
2428
|
+
: upsertFirings !== undefined || conflictWhere !== undefined;
|
|
2333
2429
|
if (stagedDerivations)
|
|
2334
2430
|
throw new StaleTriggerDerivationsError(error);
|
|
2335
2431
|
retries += 1;
|
|
@@ -2383,6 +2479,7 @@ export class MinnowDatabase {
|
|
|
2383
2479
|
});
|
|
2384
2480
|
return externalizeQueryResult({
|
|
2385
2481
|
columns: columns.map(({ name }) => name),
|
|
2482
|
+
columnDomains: columns.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2386
2483
|
rows,
|
|
2387
2484
|
}).rows;
|
|
2388
2485
|
}
|
|
@@ -2481,6 +2578,7 @@ export class MinnowDatabase {
|
|
|
2481
2578
|
let resolvedPlan = plan;
|
|
2482
2579
|
let ftsStats;
|
|
2483
2580
|
let outputNeedsExternalization = true;
|
|
2581
|
+
let outputColumnDomains = [];
|
|
2484
2582
|
const prepareAtSnapshot = async (snapshot, realTables, visibility) => {
|
|
2485
2583
|
const typedSchemas = new Map([...realTables.values()].map((table) => [
|
|
2486
2584
|
table.name,
|
|
@@ -2508,6 +2606,7 @@ export class MinnowDatabase {
|
|
|
2508
2606
|
// columnar forms of derived and windowed sources too, not just the result memo.
|
|
2509
2607
|
options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows);
|
|
2510
2608
|
outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
|
|
2609
|
+
outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
|
|
2511
2610
|
};
|
|
2512
2611
|
if (options.version !== undefined) {
|
|
2513
2612
|
// Explicit time travel keeps the per-call lease and version-anchored reads.
|
|
@@ -2523,6 +2622,7 @@ export class MinnowDatabase {
|
|
|
2523
2622
|
return createPreparedColumnarQuery(chooseJoinOrder(resolvedPlan, columnarTables), columnarTables, memory, {
|
|
2524
2623
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
2525
2624
|
outputNeedsExternalization,
|
|
2625
|
+
outputColumnDomains,
|
|
2526
2626
|
});
|
|
2527
2627
|
}
|
|
2528
2628
|
catch (error) {
|
|
@@ -2550,7 +2650,7 @@ export class MinnowDatabase {
|
|
|
2550
2650
|
names.forEach((name, index) => {
|
|
2551
2651
|
const table = state.tables[index];
|
|
2552
2652
|
if (table === undefined)
|
|
2553
|
-
throw new
|
|
2653
|
+
throw new UnknownTableError(name);
|
|
2554
2654
|
realTables.set(table.name, table);
|
|
2555
2655
|
});
|
|
2556
2656
|
const segmentsByTable = new Map();
|
|
@@ -2825,7 +2925,11 @@ export class MinnowDatabase {
|
|
|
2825
2925
|
// and the final columnar table hold memory across the fixpoint loop.
|
|
2826
2926
|
const iterationMemory = memory.createChild();
|
|
2827
2927
|
try {
|
|
2828
|
-
const frontierTable = derivedColumnarTable(reference, {
|
|
2928
|
+
const frontierTable = derivedColumnarTable(reference, {
|
|
2929
|
+
columns: [...state.columns],
|
|
2930
|
+
columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2931
|
+
rows: state.frontier,
|
|
2932
|
+
}, schema);
|
|
2829
2933
|
state.absorb(await this.#executeBlock(step, snapshot, visibility, iterationMemory, realTables, typedSchemas, new Map([[reference, frontierTable]]), cacheResults, allowSpill, forceSpill, spillPageRows));
|
|
2830
2934
|
}
|
|
2831
2935
|
finally {
|
|
@@ -2833,7 +2937,11 @@ export class MinnowDatabase {
|
|
|
2833
2937
|
}
|
|
2834
2938
|
}
|
|
2835
2939
|
typedSchemas.set(source.table, schema);
|
|
2836
|
-
inputs.set(source.table, derivedColumnarTable(source.table, {
|
|
2940
|
+
inputs.set(source.table, derivedColumnarTable(source.table, {
|
|
2941
|
+
columns: [...state.columns],
|
|
2942
|
+
columnDomains: schema.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
2943
|
+
rows: state.rows,
|
|
2944
|
+
}, schema));
|
|
2837
2945
|
continue;
|
|
2838
2946
|
}
|
|
2839
2947
|
if (source.union !== undefined) {
|
|
@@ -2939,7 +3047,7 @@ export class MinnowDatabase {
|
|
|
2939
3047
|
}
|
|
2940
3048
|
const table = realTables.get(source.table);
|
|
2941
3049
|
if (table === undefined)
|
|
2942
|
-
throw new
|
|
3050
|
+
throw new UnknownTableError(source.table);
|
|
2943
3051
|
const requestedColumns = columns.get(table.name) ?? [];
|
|
2944
3052
|
inputs.set(table.name, await this.#materializeColumnarTableAtSnapshot(table, snapshot, requestedColumns.length === 0 ? [] : resolveReadColumns(table, requestedColumns), visibility, onlyRealSource));
|
|
2945
3053
|
}
|
|
@@ -3076,6 +3184,7 @@ export class MinnowDatabase {
|
|
|
3076
3184
|
cursor.signal.throwIfAborted();
|
|
3077
3185
|
await cursor.consume({
|
|
3078
3186
|
columns: [...result.columns],
|
|
3187
|
+
columnDomains: [...result.columnDomains],
|
|
3079
3188
|
rows: result.rows.slice(start, start + cursor.batchRows),
|
|
3080
3189
|
});
|
|
3081
3190
|
}
|
|
@@ -4013,7 +4122,7 @@ export class MinnowDatabase {
|
|
|
4013
4122
|
const tables = proof.records;
|
|
4014
4123
|
const table = tables.find((record) => record.name === tableName);
|
|
4015
4124
|
if (table === undefined)
|
|
4016
|
-
throw new
|
|
4125
|
+
throw new UnknownTableError(tableName);
|
|
4017
4126
|
if (table.view !== undefined)
|
|
4018
4127
|
throw new TypeError(`Cannot create a trigger on view ${tableName}`);
|
|
4019
4128
|
pinnedTableId ??= table.id;
|
|
@@ -4046,7 +4155,7 @@ export class MinnowDatabase {
|
|
|
4046
4155
|
}
|
|
4047
4156
|
const target = tables.find((record) => record.name === compiled.table);
|
|
4048
4157
|
if (target === undefined)
|
|
4049
|
-
throw new
|
|
4158
|
+
throw new UnknownTableError(compiled.table);
|
|
4050
4159
|
if (target.view !== undefined) {
|
|
4051
4160
|
throw new TypeError(`Trigger bodies cannot write to views: ${compiled.table}`);
|
|
4052
4161
|
}
|
|
@@ -4124,19 +4233,32 @@ export class MinnowDatabase {
|
|
|
4124
4233
|
* in-batch duplicate key fires as an UPDATE of the earlier occurrence. Returns undefined
|
|
4125
4234
|
* when the table has no insert or update triggers.
|
|
4126
4235
|
*/
|
|
4127
|
-
async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows) {
|
|
4236
|
+
async #upsertTriggerFirings(table, keyColumn, batch, rowCount, readRows, conflictWhere, force = false) {
|
|
4128
4237
|
const fires = (table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
|
|
4129
|
-
if (!fires || rowCount === 0)
|
|
4238
|
+
if ((!fires && !force) || rowCount === 0)
|
|
4130
4239
|
return undefined;
|
|
4131
4240
|
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4132
4241
|
const keyValues = batch.columns[keyColumn.name] ?? [];
|
|
4133
|
-
const
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
const
|
|
4242
|
+
const distinct = new Map();
|
|
4243
|
+
for (const value of keyValues) {
|
|
4244
|
+
if (value !== null)
|
|
4245
|
+
distinct.set(keyToken(keyColumn.type, value), value);
|
|
4246
|
+
}
|
|
4247
|
+
const params = [...distinct.values()];
|
|
4248
|
+
const rows = [];
|
|
4249
|
+
// Each value also contributes a comma token, so the parser's token cap is reached before
|
|
4250
|
+
// its parameter cap. A fixed 1,024-key window stays comfortably inside both limits.
|
|
4251
|
+
const keyWindowSize = Math.min(MAX_SQL_PARAMETERS, 1_024);
|
|
4252
|
+
for (let start = 0; start < params.length; start += keyWindowSize) {
|
|
4253
|
+
const window = params.slice(start, start + keyWindowSize);
|
|
4254
|
+
const placeholders = window.map(() => "?").join(", ");
|
|
4255
|
+
const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
|
|
4256
|
+
const result = readRows === undefined
|
|
4257
|
+
? await this.query(sql, { params: window, memoize: false })
|
|
4258
|
+
: await readRows(sql, window);
|
|
4259
|
+
rows.push(...result.rows);
|
|
4260
|
+
}
|
|
4261
|
+
const byToken = new Map(rows.map((row) => {
|
|
4140
4262
|
const key = row[keyColumn.name];
|
|
4141
4263
|
return [
|
|
4142
4264
|
key === null || key === undefined ? "" : keyToken(keyColumn.type, key),
|
|
@@ -4145,29 +4267,36 @@ export class MinnowDatabase {
|
|
|
4145
4267
|
}));
|
|
4146
4268
|
const inserts = [];
|
|
4147
4269
|
const updates = [];
|
|
4270
|
+
const skipped = [];
|
|
4148
4271
|
const oldImages = [];
|
|
4149
|
-
const
|
|
4272
|
+
const currentByToken = new Map(byToken);
|
|
4150
4273
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
4151
4274
|
const key = keyValues[rowIndex] ?? null;
|
|
4152
4275
|
const token = key === null ? "" : keyToken(keyColumn.type, key);
|
|
4153
|
-
const
|
|
4154
|
-
const old = earlier === undefined
|
|
4155
|
-
? byToken.get(token)
|
|
4156
|
-
: Object.fromEntries(table.columns.map((column) => [
|
|
4157
|
-
column.name,
|
|
4158
|
-
batch.columns[column.name]?.[earlier] ?? null,
|
|
4159
|
-
]));
|
|
4276
|
+
const old = currentByToken.get(token);
|
|
4160
4277
|
if (old === undefined) {
|
|
4161
4278
|
inserts.push(rowIndex);
|
|
4162
4279
|
oldImages.push(undefined);
|
|
4163
4280
|
}
|
|
4164
4281
|
else {
|
|
4165
|
-
updates.push(rowIndex);
|
|
4166
4282
|
oldImages.push(old);
|
|
4283
|
+
const rawLeft = conflictWhere === undefined ? null : (old[conflictWhere.column.name] ?? null);
|
|
4284
|
+
const left = conflictWhere?.column.sqlDomain === undefined
|
|
4285
|
+
? rawLeft
|
|
4286
|
+
: normalizeSqlDomainValue(conflictWhere.column.sqlDomain, rawLeft);
|
|
4287
|
+
if (conflictWhere !== undefined &&
|
|
4288
|
+
!comparisonHolds(conflictWhere.operator, left, conflictWhere.value)) {
|
|
4289
|
+
skipped.push(rowIndex);
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
updates.push(rowIndex);
|
|
4167
4293
|
}
|
|
4168
|
-
|
|
4294
|
+
currentByToken.set(token, Object.fromEntries(table.columns.map((column) => [
|
|
4295
|
+
column.name,
|
|
4296
|
+
batch.columns[column.name]?.[rowIndex] ?? null,
|
|
4297
|
+
])));
|
|
4169
4298
|
}
|
|
4170
|
-
return { inserts, updates, oldImages };
|
|
4299
|
+
return { inserts, updates, skipped, oldImages };
|
|
4171
4300
|
}
|
|
4172
4301
|
/** Fires INSERT and UPDATE triggers for one upsert batch at one timing, per the plan. */
|
|
4173
4302
|
async #stageUpsertTriggerFirings(transaction, table, batch, firings, timing, cascadeBudget = 1) {
|
|
@@ -4826,7 +4955,8 @@ export class MinnowDatabase {
|
|
|
4826
4955
|
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
|
|
4827
4956
|
}
|
|
4828
4957
|
const sessionChecks = table.checks ?? [];
|
|
4829
|
-
const changedForeignKey = (table.foreignKeys ?? []).some((key) =>
|
|
4958
|
+
const changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false &&
|
|
4959
|
+
foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
|
|
4830
4960
|
const preImages = await this.#triggerPreImages(table, keyColumn, input.keys.filter((key) => key !== null), "update", (preImageSql, params) => this.#sessionQuery(transaction, preImageSql, { params }), sessionChecks.length > 0 ||
|
|
4831
4961
|
changedForeignKey ||
|
|
4832
4962
|
secondaryIndexUpdateNeedsPreImages(table, input) ||
|
|
@@ -5389,7 +5519,12 @@ export class MinnowDatabase {
|
|
|
5389
5519
|
// The remaining metadata-only steps share one catalog/manifest CAS. Physical drops ran
|
|
5390
5520
|
// above because segment rewriting and payload retirement have a stronger atomic boundary.
|
|
5391
5521
|
const columns = applyColumnSteps(record, steps, this.#createId);
|
|
5392
|
-
|
|
5522
|
+
const foreignKeyStep = steps.find((step) => step.kind === "alter-foreign-keys");
|
|
5523
|
+
const foreignKeys = foreignKeyStep?.kind === "alter-foreign-keys"
|
|
5524
|
+
? foreignKeyStep.foreignKeys.map((key) => ({ ...key }))
|
|
5525
|
+
: record.foreignKeys;
|
|
5526
|
+
if (JSON.stringify(columns) === JSON.stringify(record.columns) &&
|
|
5527
|
+
JSON.stringify(foreignKeys ?? []) === JSON.stringify(record.foreignKeys ?? [])) {
|
|
5393
5528
|
if (changed)
|
|
5394
5529
|
alteredTables.push(tableName);
|
|
5395
5530
|
continue;
|
|
@@ -5400,6 +5535,7 @@ export class MinnowDatabase {
|
|
|
5400
5535
|
assertDependentViewsKeepSchema(proof.records, record, candidateSchemas);
|
|
5401
5536
|
const updated = await this.store.updateTable(record.id, record.revision, {
|
|
5402
5537
|
columns,
|
|
5538
|
+
...(foreignKeyStep === undefined ? {} : { foreignKeys }),
|
|
5403
5539
|
...(autoIncrementSeed === undefined ? {} : { autoIncrementSeed }),
|
|
5404
5540
|
expectedManifestVersion: { value: proof.manifestVersion },
|
|
5405
5541
|
expectedCatalogEpoch: proof.catalogEpoch,
|
|
@@ -5533,6 +5669,8 @@ export class MinnowDatabase {
|
|
|
5533
5669
|
*/
|
|
5534
5670
|
async #assertForeignKeysPresent(table, rowsByColumn, read, transaction) {
|
|
5535
5671
|
for (const key of table.foreignKeys ?? []) {
|
|
5672
|
+
if (key.enforced === false)
|
|
5673
|
+
continue;
|
|
5536
5674
|
const childNames = foreignKeyColumns(key);
|
|
5537
5675
|
const parentNames = foreignKeyParentColumns(key);
|
|
5538
5676
|
if (childNames.length > 1) {
|
|
@@ -5671,7 +5809,7 @@ export class MinnowDatabase {
|
|
|
5671
5809
|
async #applyReferentialActions(parent, keys, session, cascadeBudget) {
|
|
5672
5810
|
if (keys.length === 0)
|
|
5673
5811
|
return;
|
|
5674
|
-
const children = await this.#childForeignKeys(parent.name);
|
|
5812
|
+
const children = (await this.#childForeignKeys(parent.name)).filter(({ key }) => key.enforced !== false);
|
|
5675
5813
|
if (children.length === 0)
|
|
5676
5814
|
return;
|
|
5677
5815
|
if (cascadeBudget < 0) {
|
|
@@ -6055,7 +6193,12 @@ export class MinnowDatabase {
|
|
|
6055
6193
|
return value;
|
|
6056
6194
|
});
|
|
6057
6195
|
const selected = await transaction.query(`SELECT * FROM ${quoteSqlIdentifier(table.name)} WHERE ${quoteSqlIdentifier(keyColumn.name)} IN (${keys.map(() => "?").join(", ")})`, { params: keys });
|
|
6058
|
-
const currentByKey = new Map(selected.rows.map((row) =>
|
|
6196
|
+
const currentByKey = new Map(selected.rows.map((row) => {
|
|
6197
|
+
const value = keyColumn.hidden
|
|
6198
|
+
? compositeKeyValue(primaryKeyColumns(table), primaryKeyColumns(table).map(({ name }) => row[name] ?? null))
|
|
6199
|
+
: (row[keyColumn.name] ?? null);
|
|
6200
|
+
return [keyToken(value), row];
|
|
6201
|
+
}));
|
|
6059
6202
|
const updateKeys = [];
|
|
6060
6203
|
const changes = Object.fromEntries(assignments.map(({ column }) => [column, []]));
|
|
6061
6204
|
for (const [position, rowIndex] of conflictingRows.entries()) {
|
|
@@ -6457,7 +6600,11 @@ export class MinnowDatabase {
|
|
|
6457
6600
|
const stageRows = async (rows) => {
|
|
6458
6601
|
if (rows.length === 0)
|
|
6459
6602
|
return;
|
|
6460
|
-
const batchResult = {
|
|
6603
|
+
const batchResult = {
|
|
6604
|
+
columns: tableColumns.map(({ name }) => name),
|
|
6605
|
+
columnDomains: tableColumns.map(({ sqlDomain }) => sqlDomain ?? null),
|
|
6606
|
+
rows: [...rows],
|
|
6607
|
+
};
|
|
6461
6608
|
if (queryResultRetainedBytes(batchResult) > executionMemoryBudgetBytes) {
|
|
6462
6609
|
throw new QueryMemoryBudgetError("CREATE TABLE AS result batch", queryResultRetainedBytes(batchResult), 0, executionMemoryBudgetBytes);
|
|
6463
6610
|
}
|
|
@@ -6592,7 +6739,7 @@ export class MinnowDatabase {
|
|
|
6592
6739
|
}
|
|
6593
6740
|
const record = proof.records.find((candidate) => candidate.name === addColumnStatement.table);
|
|
6594
6741
|
if (record === undefined)
|
|
6595
|
-
throw new
|
|
6742
|
+
throw new UnknownTableError(addColumnStatement.table);
|
|
6596
6743
|
if (record.view !== undefined)
|
|
6597
6744
|
throw new TypeError(`${addColumnStatement.table} is a view, not a table`);
|
|
6598
6745
|
pinnedTableId ??= record.id;
|
|
@@ -7092,7 +7239,7 @@ export class MinnowDatabase {
|
|
|
7092
7239
|
plan = chooseJoinOrder(plan, rowCounts);
|
|
7093
7240
|
const baseTable = tables.find((table) => table.name === plan.base.table);
|
|
7094
7241
|
if (baseTable === undefined)
|
|
7095
|
-
throw new
|
|
7242
|
+
throw new UnknownTableError(plan.base.table);
|
|
7096
7243
|
const requestedBaseColumns = columns.get(baseTable.name) ?? [];
|
|
7097
7244
|
const projectedBaseColumns = requestedBaseColumns.length === 0 ? [] : resolveReadColumns(baseTable, requestedBaseColumns);
|
|
7098
7245
|
if (secondaryIndexOrderForPlan(plan, baseTable, projectedBaseColumns, false) !== undefined) {
|
|
@@ -7165,6 +7312,7 @@ export class MinnowDatabase {
|
|
|
7165
7312
|
prepared = createPreparedColumnarQuery(plan, inputTables, memory, {
|
|
7166
7313
|
...(ftsStats === undefined ? {} : { ftsStats }),
|
|
7167
7314
|
outputNeedsExternalization: queryResultNeedsExternalization(plan, typedSchemas),
|
|
7315
|
+
outputColumnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7168
7316
|
});
|
|
7169
7317
|
if (cursor !== undefined) {
|
|
7170
7318
|
const emission = { happened: false };
|
|
@@ -7177,10 +7325,18 @@ export class MinnowDatabase {
|
|
|
7177
7325
|
await cursor.consume(externalizeQueryResult(batch));
|
|
7178
7326
|
});
|
|
7179
7327
|
if (!emission.happened) {
|
|
7180
|
-
await cursor.consume({
|
|
7328
|
+
await cursor.consume({
|
|
7329
|
+
columns: [...columns],
|
|
7330
|
+
columnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7331
|
+
rows: [],
|
|
7332
|
+
});
|
|
7181
7333
|
}
|
|
7182
7334
|
options.onStats?.({ peakMemoryBytes: memory.usage.peakBytes });
|
|
7183
|
-
return {
|
|
7335
|
+
return {
|
|
7336
|
+
columns: [...columns],
|
|
7337
|
+
columnDomains: inferResultColumnDomains(plan, typedSchemas),
|
|
7338
|
+
rows: [],
|
|
7339
|
+
};
|
|
7184
7340
|
}
|
|
7185
7341
|
if (!attempt.spill) {
|
|
7186
7342
|
try {
|
|
@@ -7534,7 +7690,8 @@ export class MinnowDatabase {
|
|
|
7534
7690
|
const rows = startRow > 0 || limit !== undefined
|
|
7535
7691
|
? combined.slice(startRow, limit === undefined ? undefined : startRow + limit)
|
|
7536
7692
|
: combined;
|
|
7537
|
-
|
|
7693
|
+
const columns = plan.select.map((item) => item.alias);
|
|
7694
|
+
return { columns, columnDomains: unknownColumnDomains(columns), rows };
|
|
7538
7695
|
}
|
|
7539
7696
|
finally {
|
|
7540
7697
|
root.close();
|
|
@@ -8395,6 +8552,7 @@ export class MinnowDatabase {
|
|
|
8395
8552
|
let writtenBytes = 0;
|
|
8396
8553
|
let batchBytes = 0;
|
|
8397
8554
|
let metadataBatchBytes = 0;
|
|
8555
|
+
let batchBlockCount = 0;
|
|
8398
8556
|
const batch = [];
|
|
8399
8557
|
const flush = async () => {
|
|
8400
8558
|
if (batch.length === 0 || identity === undefined)
|
|
@@ -8410,6 +8568,7 @@ export class MinnowDatabase {
|
|
|
8410
8568
|
});
|
|
8411
8569
|
batchBytes = 0;
|
|
8412
8570
|
metadataBatchBytes = 0;
|
|
8571
|
+
batchBlockCount = 0;
|
|
8413
8572
|
writtenBytes = session.stagedBytes;
|
|
8414
8573
|
options.onProgress?.({ phase: "blocks", writtenBytes, totalBytes });
|
|
8415
8574
|
};
|
|
@@ -8439,6 +8598,7 @@ export class MinnowDatabase {
|
|
|
8439
8598
|
if (batch.length > 0 &&
|
|
8440
8599
|
(batch.length >= MAX_SNAPSHOT_FRAME_BATCH_ITEMS ||
|
|
8441
8600
|
batchBytes + entry.frame.payload.byteLength > MAX_SNAPSHOT_FRAME_BATCH_BYTES ||
|
|
8601
|
+
(entry.frame.kind === "block" && batchBlockCount > 0) ||
|
|
8442
8602
|
(entry.frame.kind !== "block" &&
|
|
8443
8603
|
metadataBatchBytes + entry.frame.payload.byteLength >
|
|
8444
8604
|
MAX_SNAPSHOT_METADATA_BATCH_BYTES))) {
|
|
@@ -8449,6 +8609,9 @@ export class MinnowDatabase {
|
|
|
8449
8609
|
if (entry.frame.kind !== "block") {
|
|
8450
8610
|
metadataBatchBytes = safeWholeNumberSum([metadataBatchBytes, entry.frame.payload.byteLength], "Snapshot import metadata batch bytes");
|
|
8451
8611
|
}
|
|
8612
|
+
else {
|
|
8613
|
+
batchBlockCount += 1;
|
|
8614
|
+
}
|
|
8452
8615
|
continue;
|
|
8453
8616
|
}
|
|
8454
8617
|
if (identity === undefined)
|
|
@@ -11525,7 +11688,7 @@ export class MinnowDatabase {
|
|
|
11525
11688
|
}
|
|
11526
11689
|
const table = tables.find((candidate) => candidate.name === tableName);
|
|
11527
11690
|
if (table === undefined || table.view !== undefined) {
|
|
11528
|
-
throw new
|
|
11691
|
+
throw new UnknownTableError(tableName);
|
|
11529
11692
|
}
|
|
11530
11693
|
if (indexColumns.length === 0)
|
|
11531
11694
|
throw new TypeError("An index needs at least one column");
|
|
@@ -13733,7 +13896,7 @@ export class MinnowDatabase {
|
|
|
13733
13896
|
validateName(name, "Table");
|
|
13734
13897
|
const table = await this.store.getTableByName(name);
|
|
13735
13898
|
if (table === undefined)
|
|
13736
|
-
throw new
|
|
13899
|
+
throw new UnknownTableError(name);
|
|
13737
13900
|
// Reads resolve a view into its query before reaching here, so a view arriving at this
|
|
13738
13901
|
// point is a write, a DDL statement, or a path that forgot to rewrite: all of them errors.
|
|
13739
13902
|
if (table.view !== undefined)
|
|
@@ -14989,32 +15152,72 @@ function compositeKeyValue(columns, values) {
|
|
|
14989
15152
|
/** Canonicalizes logical PostgreSQL domains before validation and primitive block encoding. */
|
|
14990
15153
|
function normalizeDomainBatch(table, input) {
|
|
14991
15154
|
for (const column of table.columns) {
|
|
14992
|
-
const domain = column.sqlDomain;
|
|
14993
|
-
if (domain === undefined)
|
|
14994
|
-
continue;
|
|
14995
15155
|
const values = input.columns[column.name];
|
|
14996
15156
|
if (values === undefined)
|
|
14997
15157
|
continue;
|
|
14998
|
-
|
|
15158
|
+
if (column.sqlDomain !== undefined || column.type === "datetime") {
|
|
15159
|
+
input.columns[column.name] = values.map((value) => normalizeColumnLogicalValue(column, value));
|
|
15160
|
+
}
|
|
15161
|
+
}
|
|
15162
|
+
}
|
|
15163
|
+
function normalizeColumnLogicalValue(column, value) {
|
|
15164
|
+
if (column.sqlDomain !== undefined)
|
|
15165
|
+
return normalizeSqlDomainValue(column.sqlDomain, value);
|
|
15166
|
+
if (column.type === "datetime" && isDateDomainValue(value)) {
|
|
15167
|
+
const external = externalSqlDomainValue(value);
|
|
15168
|
+
if (typeof external !== "string")
|
|
15169
|
+
throw new TypeError("Invalid DATE value");
|
|
15170
|
+
return new Date(`${external}T00:00:00.000Z`);
|
|
14999
15171
|
}
|
|
15172
|
+
return value;
|
|
15173
|
+
}
|
|
15174
|
+
function selectBatchRows(input, indexes) {
|
|
15175
|
+
return {
|
|
15176
|
+
columns: Object.fromEntries(Object.entries(input.columns).map(([name, values]) => [
|
|
15177
|
+
name,
|
|
15178
|
+
indexes.map((index) => values[index] ?? null),
|
|
15179
|
+
])),
|
|
15180
|
+
...(input.omitted === undefined
|
|
15181
|
+
? {}
|
|
15182
|
+
: {
|
|
15183
|
+
omitted: Object.fromEntries(Object.entries(input.omitted).map(([name, values]) => [
|
|
15184
|
+
name,
|
|
15185
|
+
indexes.map((index) => values[index] ?? false),
|
|
15186
|
+
])),
|
|
15187
|
+
}),
|
|
15188
|
+
rowCount: indexes.length,
|
|
15189
|
+
};
|
|
15190
|
+
}
|
|
15191
|
+
function normalizeUpsertConflictWhere(table, predicate) {
|
|
15192
|
+
const operators = ["=", "!=", "<>", ">", ">=", "<", "<="];
|
|
15193
|
+
if (!operators.includes(predicate.operator)) {
|
|
15194
|
+
throw new TypeError(`Unsupported upsert conflict operator: ${predicate.operator}`);
|
|
15195
|
+
}
|
|
15196
|
+
const column = visibleTableColumns(table).find(({ name }) => name === predicate.column);
|
|
15197
|
+
if (column === undefined) {
|
|
15198
|
+
throw new TypeError(`Unknown upsert conflict column: ${table.name}.${predicate.column}`);
|
|
15199
|
+
}
|
|
15200
|
+
const value = normalizeColumnLogicalValue(column, predicate.value);
|
|
15201
|
+
if (value !== null)
|
|
15202
|
+
validateValue({ ...column, nullable: true }, value, 0);
|
|
15203
|
+
return { column, operator: predicate.operator, value };
|
|
15000
15204
|
}
|
|
15001
15205
|
function normalizeDomainUpdate(table, input) {
|
|
15002
|
-
if (!table.columns.some((column) => column.sqlDomain !== undefined))
|
|
15206
|
+
if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime"))
|
|
15003
15207
|
return input;
|
|
15004
15208
|
let changed = false;
|
|
15005
15209
|
const changes = { ...input.changes };
|
|
15006
15210
|
for (const [name, values] of Object.entries(input.changes)) {
|
|
15007
|
-
const
|
|
15008
|
-
if (
|
|
15211
|
+
const column = table.columns.find((candidate) => candidate.name === name);
|
|
15212
|
+
if (column === undefined || (column.sqlDomain === undefined && column.type !== "datetime"))
|
|
15009
15213
|
continue;
|
|
15010
15214
|
changed = true;
|
|
15011
|
-
changes[name] = values.map((value) =>
|
|
15215
|
+
changes[name] = values.map((value) => normalizeColumnLogicalValue(column, value));
|
|
15012
15216
|
}
|
|
15013
15217
|
const key = getUniqueKeyColumn(table);
|
|
15014
|
-
const
|
|
15015
|
-
const keys = keyDomain === undefined
|
|
15218
|
+
const keys = key === undefined || (key.sqlDomain === undefined && key.type !== "datetime")
|
|
15016
15219
|
? input.keys
|
|
15017
|
-
: input.keys.map((value) =>
|
|
15220
|
+
: input.keys.map((value) => normalizeColumnLogicalValue(key, value));
|
|
15018
15221
|
return changed || keys !== input.keys ? { keys, changes } : input;
|
|
15019
15222
|
}
|
|
15020
15223
|
/** Coerces literals and bound parameters opposite a domain column before planning/index probes. */
|
|
@@ -16227,6 +16430,17 @@ function columnVectorKeyToken(type, vector, row) {
|
|
|
16227
16430
|
}
|
|
16228
16431
|
throw new Error("Column vector type mismatch");
|
|
16229
16432
|
}
|
|
16433
|
+
function normalizedDatetimeLiteral(value) {
|
|
16434
|
+
if (value instanceof Date)
|
|
16435
|
+
return Number.isFinite(dateMilliseconds(value)) ? value : undefined;
|
|
16436
|
+
if (!isDateDomainValue(value))
|
|
16437
|
+
return undefined;
|
|
16438
|
+
const external = externalSqlDomainValue(value);
|
|
16439
|
+
if (typeof external !== "string")
|
|
16440
|
+
return undefined;
|
|
16441
|
+
const date = new Date(`${external}T00:00:00.000Z`);
|
|
16442
|
+
return Number.isFinite(dateMilliseconds(date)) ? date : undefined;
|
|
16443
|
+
}
|
|
16230
16444
|
function zonePredicates(plan, table) {
|
|
16231
16445
|
if (plan.joins.length > 0 || plan.base.table !== table.name)
|
|
16232
16446
|
return [];
|
|
@@ -16245,10 +16459,11 @@ function zonePredicates(plan, table) {
|
|
|
16245
16459
|
const literalValue = (column, expression) => {
|
|
16246
16460
|
if (expression.kind !== "literal")
|
|
16247
16461
|
return undefined;
|
|
16462
|
+
const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(expression.value) : undefined;
|
|
16248
16463
|
const value = column.type === "datetime"
|
|
16249
|
-
?
|
|
16250
|
-
?
|
|
16251
|
-
:
|
|
16464
|
+
? datetime === undefined
|
|
16465
|
+
? undefined
|
|
16466
|
+
: dateMilliseconds(datetime)
|
|
16252
16467
|
: typeof expression.value === "number"
|
|
16253
16468
|
? expression.value
|
|
16254
16469
|
: undefined;
|
|
@@ -16291,10 +16506,11 @@ function zonePredicates(plan, table) {
|
|
|
16291
16506
|
const column = leftColumn ?? rightColumn;
|
|
16292
16507
|
if (column === undefined || literal.kind !== "literal")
|
|
16293
16508
|
continue;
|
|
16509
|
+
const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(literal.value) : undefined;
|
|
16294
16510
|
const value = column.type === "datetime"
|
|
16295
|
-
?
|
|
16296
|
-
?
|
|
16297
|
-
:
|
|
16511
|
+
? datetime === undefined
|
|
16512
|
+
? undefined
|
|
16513
|
+
: dateMilliseconds(datetime)
|
|
16298
16514
|
: typeof literal.value === "number"
|
|
16299
16515
|
? literal.value
|
|
16300
16516
|
: undefined;
|
|
@@ -16324,9 +16540,16 @@ function secondaryIndexPredicates(plan, table) {
|
|
|
16324
16540
|
const literalValue = (column, expression) => {
|
|
16325
16541
|
if (expression.kind !== "literal" || expression.value === null)
|
|
16326
16542
|
return undefined;
|
|
16543
|
+
let value = expression.value;
|
|
16544
|
+
if (column.type === "datetime" && isDateDomainValue(value)) {
|
|
16545
|
+
const datetime = normalizedDatetimeLiteral(value);
|
|
16546
|
+
if (datetime === undefined)
|
|
16547
|
+
return undefined;
|
|
16548
|
+
value = datetime;
|
|
16549
|
+
}
|
|
16327
16550
|
try {
|
|
16328
|
-
secondaryIndexTerm(column.type,
|
|
16329
|
-
return
|
|
16551
|
+
secondaryIndexTerm(column.type, value);
|
|
16552
|
+
return value;
|
|
16330
16553
|
}
|
|
16331
16554
|
catch {
|
|
16332
16555
|
return undefined;
|