@minnowdb/core 0.4.1 → 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.
@@ -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({ columns, rows: result.returnedRows }).rows,
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 Error(`Table not found: ${tableName}`);
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 Error(`Table not found: ${tableName}`);
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 TypeError(`Unknown table: ${tableName}`);
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, rowCount } = await this.#fillDefaults(table, input);
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 TypeError(`Unknown table: ${tableName}`);
1674
- const result = await this.#writeBatch(current, batch, "upsert", keys, autoIncrement);
1675
- return { result, batch, generated, autoIncrement, rowCount };
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, rowCount } = completed;
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
- ...result,
1681
- rowCount,
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 TypeError(`Unknown table: ${tableName}`);
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) => foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
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 rowCount = input.columns[table.columns[0]?.name ?? ""]?.length ?? 0;
2124
- const logicalBytes = estimateBatchBytes(input);
2125
- const { transaction, rowIds: reservedRowIds, autoIncrementValues, } = await this.#transactions.beginWithReservation({ tableId: table.id, count: rowCount }, autoIncrement === undefined
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
- const segmentId = this.#createId();
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
- counts =
2173
- kind === "insert" && resolvedKeys !== undefined
2174
- ? { inserted: resolvedKeys.size, updated: 0 }
2175
- : await this.#classifyKeys(table, transaction.snapshotVersion, kind, resolvedKeys);
2176
- const rowIds = reservedRowIds ?? (await this.store.reserveRowIds(table.id, rowCount));
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 Error(`Table not found: ${name}`);
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, { columns: [...state.columns], rows: state.frontier }, schema);
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, { columns: [...state.columns], rows: state.rows }, schema));
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 TypeError(`Unknown table: ${source.table}`);
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 TypeError(`Table not found: ${tableName}`);
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 TypeError(`Table not found: ${compiled.table}`);
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 params = keyValues.filter((value) => value !== null);
4134
- const placeholders = params.map(() => "?").join(", ");
4135
- const sql = `SELECT * FROM ${quote(table.name)} WHERE ${quote(keyColumn.name)} IN (${placeholders})`;
4136
- const result = readRows === undefined
4137
- ? await this.query(sql, { params, memoize: false })
4138
- : await readRows(sql, params);
4139
- const byToken = new Map(result.rows.map((row) => {
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 seenInBatch = new Map();
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 earlier = seenInBatch.get(token);
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
- seenInBatch.set(token, rowIndex);
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) => foreignKeyColumns(key).some((column) => input.changes[column] !== undefined));
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
- if (JSON.stringify(columns) === JSON.stringify(record.columns)) {
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) => [keyToken(row[keyColumn.name] ?? null), 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 = { columns: tableColumns.map(({ name }) => name), rows: [...rows] };
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 TypeError(`Unknown table: ${addColumnStatement.table}`);
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 TypeError(`Unknown table: ${plan.base.table}`);
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({ columns: [...columns], rows: [] });
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 { columns: [...columns], rows: [] };
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
- return { columns: plan.select.map((item) => item.alias), rows };
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();
@@ -11531,7 +11688,7 @@ export class MinnowDatabase {
11531
11688
  }
11532
11689
  const table = tables.find((candidate) => candidate.name === tableName);
11533
11690
  if (table === undefined || table.view !== undefined) {
11534
- throw new TypeError(`Unknown table: ${tableName}`);
11691
+ throw new UnknownTableError(tableName);
11535
11692
  }
11536
11693
  if (indexColumns.length === 0)
11537
11694
  throw new TypeError("An index needs at least one column");
@@ -13739,7 +13896,7 @@ export class MinnowDatabase {
13739
13896
  validateName(name, "Table");
13740
13897
  const table = await this.store.getTableByName(name);
13741
13898
  if (table === undefined)
13742
- throw new Error(`Table not found: ${name}`);
13899
+ throw new UnknownTableError(name);
13743
13900
  // Reads resolve a view into its query before reaching here, so a view arriving at this
13744
13901
  // point is a write, a DDL statement, or a path that forgot to rewrite: all of them errors.
13745
13902
  if (table.view !== undefined)
@@ -14995,32 +15152,72 @@ function compositeKeyValue(columns, values) {
14995
15152
  /** Canonicalizes logical PostgreSQL domains before validation and primitive block encoding. */
14996
15153
  function normalizeDomainBatch(table, input) {
14997
15154
  for (const column of table.columns) {
14998
- const domain = column.sqlDomain;
14999
- if (domain === undefined)
15000
- continue;
15001
15155
  const values = input.columns[column.name];
15002
15156
  if (values === undefined)
15003
15157
  continue;
15004
- input.columns[column.name] = values.map((value) => normalizeSqlDomainValue(domain, value));
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`);
15005
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 };
15006
15204
  }
15007
15205
  function normalizeDomainUpdate(table, input) {
15008
- if (!table.columns.some((column) => column.sqlDomain !== undefined))
15206
+ if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime"))
15009
15207
  return input;
15010
15208
  let changed = false;
15011
15209
  const changes = { ...input.changes };
15012
15210
  for (const [name, values] of Object.entries(input.changes)) {
15013
- const domain = table.columns.find((column) => column.name === name)?.sqlDomain;
15014
- if (domain === undefined)
15211
+ const column = table.columns.find((candidate) => candidate.name === name);
15212
+ if (column === undefined || (column.sqlDomain === undefined && column.type !== "datetime"))
15015
15213
  continue;
15016
15214
  changed = true;
15017
- changes[name] = values.map((value) => normalizeSqlDomainValue(domain, value));
15215
+ changes[name] = values.map((value) => normalizeColumnLogicalValue(column, value));
15018
15216
  }
15019
15217
  const key = getUniqueKeyColumn(table);
15020
- const keyDomain = key?.sqlDomain;
15021
- const keys = keyDomain === undefined
15218
+ const keys = key === undefined || (key.sqlDomain === undefined && key.type !== "datetime")
15022
15219
  ? input.keys
15023
- : input.keys.map((value) => normalizeSqlDomainValue(keyDomain, value));
15220
+ : input.keys.map((value) => normalizeColumnLogicalValue(key, value));
15024
15221
  return changed || keys !== input.keys ? { keys, changes } : input;
15025
15222
  }
15026
15223
  /** Coerces literals and bound parameters opposite a domain column before planning/index probes. */
@@ -16233,6 +16430,17 @@ function columnVectorKeyToken(type, vector, row) {
16233
16430
  }
16234
16431
  throw new Error("Column vector type mismatch");
16235
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
+ }
16236
16444
  function zonePredicates(plan, table) {
16237
16445
  if (plan.joins.length > 0 || plan.base.table !== table.name)
16238
16446
  return [];
@@ -16251,10 +16459,11 @@ function zonePredicates(plan, table) {
16251
16459
  const literalValue = (column, expression) => {
16252
16460
  if (expression.kind !== "literal")
16253
16461
  return undefined;
16462
+ const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(expression.value) : undefined;
16254
16463
  const value = column.type === "datetime"
16255
- ? expression.value instanceof Date
16256
- ? dateMilliseconds(expression.value)
16257
- : undefined
16464
+ ? datetime === undefined
16465
+ ? undefined
16466
+ : dateMilliseconds(datetime)
16258
16467
  : typeof expression.value === "number"
16259
16468
  ? expression.value
16260
16469
  : undefined;
@@ -16297,10 +16506,11 @@ function zonePredicates(plan, table) {
16297
16506
  const column = leftColumn ?? rightColumn;
16298
16507
  if (column === undefined || literal.kind !== "literal")
16299
16508
  continue;
16509
+ const datetime = column.type === "datetime" ? normalizedDatetimeLiteral(literal.value) : undefined;
16300
16510
  const value = column.type === "datetime"
16301
- ? literal.value instanceof Date
16302
- ? dateMilliseconds(literal.value)
16303
- : undefined
16511
+ ? datetime === undefined
16512
+ ? undefined
16513
+ : dateMilliseconds(datetime)
16304
16514
  : typeof literal.value === "number"
16305
16515
  ? literal.value
16306
16516
  : undefined;
@@ -16330,9 +16540,16 @@ function secondaryIndexPredicates(plan, table) {
16330
16540
  const literalValue = (column, expression) => {
16331
16541
  if (expression.kind !== "literal" || expression.value === null)
16332
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
+ }
16333
16550
  try {
16334
- secondaryIndexTerm(column.type, expression.value);
16335
- return expression.value;
16551
+ secondaryIndexTerm(column.type, value);
16552
+ return value;
16336
16553
  }
16337
16554
  catch {
16338
16555
  return undefined;