@minnowdb/core 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +5 -1
  5. package/dist/engine/catalog.js +5 -1
  6. package/dist/engine/client.d.ts +34 -6
  7. package/dist/engine/client.js +87 -19
  8. package/dist/engine/database.d.ts +43 -20
  9. package/dist/engine/database.js +823 -164
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +19 -0
  12. package/dist/engine/errors.js +31 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +12 -13
  16. package/dist/engine/optimizer.js +546 -39
  17. package/dist/engine/query-cache.js +1 -0
  18. package/dist/engine/query.d.ts +16 -278
  19. package/dist/engine/query.js +260 -74
  20. package/dist/engine/result-wire.d.ts +2 -0
  21. package/dist/engine/result-wire.js +21 -5
  22. package/dist/engine/schema-wire.d.ts +14 -1
  23. package/dist/engine/schema-wire.js +7 -1
  24. package/dist/engine/schema.d.ts +83 -32
  25. package/dist/engine/schema.js +180 -14
  26. package/dist/engine/sql-domains.d.ts +11 -0
  27. package/dist/engine/sql-domains.js +65 -1
  28. package/dist/engine/sql-json.js +22 -3
  29. package/dist/engine/sql-semantics.js +21 -3
  30. package/dist/engine/vector.d.ts +2 -2
  31. package/dist/engine/vector.js +328 -79
  32. package/dist/engine/worker-host.js +119 -44
  33. package/dist/plan/index.d.ts +5 -4
  34. package/dist/plan/index.js +3 -3
  35. package/dist/plan/model.d.ts +218 -0
  36. package/dist/plan/model.js +1 -0
  37. package/dist/storage/indexeddb.js +4 -12
  38. package/dist/storage/toolkit/record-core.js +7 -22
  39. package/dist/storage/types.d.ts +26 -8
  40. package/dist/storage/types.js +85 -0
  41. package/dist/transactions/index.d.ts +5 -3
  42. package/dist/transactions/index.js +58 -8
  43. package/dist/worker-protocol/index.d.ts +6 -1
  44. package/dist/worker-protocol/index.js +5 -2
  45. package/package.json +1 -1
  46. package/postgres-feature-profile.json +5 -0
  47. package/sql-feature-matrix.json +75 -19
package/README.md CHANGED
@@ -9,15 +9,15 @@ npm install @minnowdb/core
9
9
 
10
10
  - Direct SQL through `MinnowDatabase.query()` and `execute()`.
11
11
  - Joins, CTEs, window functions, grouping sets, upserts, `RETURNING`, triggers, exact decimals,
12
- JSON/JSONB, arrays, enums, sequences, and savepoints.
12
+ nested JSON/JSONB, stored generated columns, zoneless DATE, arrays, enums, sequences, and savepoints.
13
13
  - Compressed column storage, secondary indexes, full-text search, and snapshot reads.
14
14
  - Atomic writes across tabs through IndexedDB or OPFS, strict durability by default, and explicit
15
15
  origin-eviction persistence policy.
16
16
  - A ready-made worker client with the same everyday database API.
17
- - TypeScript schema declarations and metadata-only migrations, including SQL domains and
18
- composite primary/foreign keys.
19
- - Batch writes, pull-driven query cursors, live queries, snapshots, compaction, and configurable
20
- query memory.
17
+ - TypeScript schema declarations and metadata-only migrations, including SQL domains,
18
+ composite primary/foreign keys, and informational relationships.
19
+ - Guarded columnar upserts, result-column SQL domains, typed catalog errors, and batch writes.
20
+ - Pull-driven query cursors, live queries, snapshots, compaction, and configurable query memory.
21
21
 
22
22
  Use [the PostgreSQL compatibility page](https://minnowdb.com/docs/sql/feature-matrix/) for the
23
23
  exact SQL surface. Use [the documentation](https://minnowdb.com/docs/) for installation, storage,
@@ -0,0 +1,2 @@
1
+ /** One compact check shared by every bounded query-execution path. */
2
+ export declare function throwIfAborted(signal: AbortSignal | undefined): void;
@@ -0,0 +1,4 @@
1
+ /** One compact check shared by every bounded query-execution path. */
2
+ export function throwIfAborted(signal) {
3
+ signal?.throwIfAborted();
4
+ }
@@ -1,4 +1,4 @@
1
- import { type ColumnDefault, type SecondaryIndexState, type SimpleDataType, type SqlDomain, type TableRecord } from "../storage/types.js";
1
+ import { type ColumnDefault, type ColumnGenerated, type SecondaryIndexState, type SimpleDataType, type SqlDomain, type TableRecord } from "../storage/types.js";
2
2
  /**
3
3
  * The published shape of the catalog: everything a schema tool needs to diff a live database
4
4
  * against a desired schema, without reading engine internals.
@@ -20,6 +20,8 @@ export interface CatalogColumn {
20
20
  readonly nullable: boolean;
21
21
  /** Filled for omitted or SQL DEFAULT insert slots; never applied at read time. */
22
22
  readonly defaultValue?: ColumnDefault;
23
+ /** Stored expression maintained by the engine; callers cannot assign this column. */
24
+ readonly generatedValue?: ColumnGenerated;
23
25
  /** String columns only: the closed set of values writes may draw from. */
24
26
  readonly enumValues?: readonly string[];
25
27
  /** Derived from the default spec, because a planner should not have to decode one. */
@@ -33,6 +35,8 @@ export interface CatalogForeignKey {
33
35
  readonly parentTable: string;
34
36
  readonly parentColumns: readonly string[];
35
37
  readonly onDelete: "restrict" | "cascade" | "set null";
38
+ /** Whether writes and parent deletes enforce this relationship. */
39
+ readonly enforced: boolean;
36
40
  }
37
41
  export interface CatalogCheck {
38
42
  readonly name: string;
@@ -9,6 +9,7 @@ function toCatalogColumn(column) {
9
9
  ...(column.sqlDomain === undefined ? {} : { sqlDomain: structuredClone(column.sqlDomain) }),
10
10
  nullable: column.nullable,
11
11
  ...(column.defaultValue === undefined ? {} : { defaultValue: column.defaultValue }),
12
+ ...(column.generatedValue === undefined ? {} : { generatedValue: column.generatedValue }),
12
13
  ...(column.enumValues === undefined ? {} : { enumValues: [...column.enumValues] }),
13
14
  ...(column.backfill === undefined
14
15
  ? {}
@@ -45,7 +46,10 @@ export function toCatalog(records) {
45
46
  ...(record.primaryKeyColumnIds === undefined
46
47
  ? {}
47
48
  : { primaryKeyColumnIds: [...record.primaryKeyColumnIds] }),
48
- foreignKeys: (record.foreignKeys ?? []).map((key) => ({ ...key })),
49
+ foreignKeys: (record.foreignKeys ?? []).map((key) => ({
50
+ ...key,
51
+ enforced: key.enforced !== false,
52
+ })),
49
53
  checks: (record.checks ?? []).map((check) => ({ ...check })),
50
54
  ...(record.secondaryIndexes === undefined
51
55
  ? {}
@@ -1,7 +1,7 @@
1
1
  import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
2
2
  import { type BatchRow, type InsertBatchInput } from "./batch.js";
3
3
  import type { Catalog } from "./catalog.js";
4
- import type { BatchValue, BufferPoolStats, StagedWriteResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, DatabaseRow, MigrateOptions, DeleteBatchInput, DeleteBatchResult, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, ReadTableOptions, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchInput, UpdateBatchResult, UpsertBatchResult, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
4
+ import type { BatchValue, BufferPoolStats, StagedWriteResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, DatabaseRow, MigrateOptions, DeleteBatchInput, DeleteBatchResult, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryExecutionStats, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, ReadTableOptions, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchInput, UpdateBatchResult, UpsertBatchResult, UpsertOptions, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
5
5
  import type { LiveQueryInput, LiveQueryInvalidation, LiveQueryObserveOptions, LiveQueryStats, LiveQuerySubscribeOptions } from "./live.js";
6
6
  import type { CompiledQuery, CompiledStatement, QueryResult, QueryValue } from "./query.js";
7
7
  import type { AnyTable, SchemaDefinition } from "./schema.js";
@@ -65,6 +65,11 @@ interface EventRoute {
65
65
  onComplete?: () => void;
66
66
  /** Payload shape is the handle's own; only snapshot loads emit these today. */
67
67
  onProgress?: (progress: unknown) => void;
68
+ onStats?: (stats: QueryExecutionStats) => void;
69
+ }
70
+ interface RpcCallControls {
71
+ signal?: AbortSignal | undefined;
72
+ onStats?: ((stats: QueryExecutionStats) => void) | undefined;
68
73
  }
69
74
  export declare class MinnowDatabaseClient {
70
75
  #private;
@@ -72,14 +77,37 @@ export declare class MinnowDatabaseClient {
72
77
  /** Resolves once the worker has opened the store and constructed the database. */
73
78
  ready(): Promise<void>;
74
79
  createTable(input: CreateTableInput): Promise<void>;
80
+ createView(name: string, sql: string, options?: {
81
+ orReplace?: boolean;
82
+ managed?: boolean;
83
+ }): Promise<void>;
84
+ dropView(name: string, options?: {
85
+ ifExists?: boolean;
86
+ }): Promise<boolean>;
87
+ dropColumn(tableName: string, columnName: string, options?: {
88
+ ifExists?: boolean;
89
+ }): Promise<boolean>;
90
+ dropTable(tableName: string, options?: {
91
+ ifExists?: boolean;
92
+ }): Promise<boolean>;
93
+ createIndex(indexName: string, tableName: string, requestedColumns: string | ReadonlyArray<{
94
+ name: string;
95
+ direction: "asc" | "desc";
96
+ }>, options?: {
97
+ unique?: boolean;
98
+ }): Promise<void>;
99
+ dropIndex(indexName: string, options?: {
100
+ ifExists?: boolean;
101
+ }): Promise<boolean>;
102
+ buildFtsIndex(tableName: string, columnName: string): Promise<void>;
75
103
  /** The published catalog; see `MinnowDatabase.introspect()`. */
76
104
  introspect(): Promise<Catalog>;
77
105
  listTables(): Promise<TableDefinition[]>;
78
106
  migrate(definition: SchemaDefinition<readonly AnyTable[]>, options?: MigrateOptions): Promise<ClientMigrationResult>;
79
107
  insertBatch(tableName: string, input: InsertBatchInput): Promise<InsertBatchResult>;
80
108
  insert(tableName: string, row: BatchRow): Promise<InsertBatchResult>;
81
- upsertBatch(tableName: string, input: InsertBatchInput): Promise<UpsertBatchResult>;
82
- upsert(tableName: string, row: BatchRow): Promise<UpsertBatchResult>;
109
+ upsertBatch(tableName: string, input: InsertBatchInput, options?: UpsertOptions): Promise<UpsertBatchResult>;
110
+ upsert(tableName: string, row: BatchRow, options?: UpsertOptions): Promise<UpsertBatchResult>;
83
111
  updateBatch(tableName: string, input: UpdateBatchInput): Promise<UpdateBatchResult>;
84
112
  update(tableName: string, key: Exclude<BatchValue, null>, changes: Readonly<Record<string, BatchValue>>): Promise<UpdateBatchResult>;
85
113
  deleteBatch(tableName: string, input: DeleteBatchInput): Promise<DeleteBatchResult>;
@@ -166,6 +194,8 @@ export declare class MinnowDatabaseClient {
166
194
  /** @internal */
167
195
  _invoke(handleId: string, method: string, args: unknown[]): Promise<unknown>;
168
196
  /** @internal */
197
+ _invokeControlled(handleId: string, method: string, args: unknown[], controls: RpcCallControls): Promise<unknown>;
198
+ /** @internal */
169
199
  _routeEvents(handleId: string, route: EventRoute): void;
170
200
  /** @internal */
171
201
  _unrouteEvents(handleId: string): void;
@@ -177,9 +207,7 @@ export declare class MinnowDatabaseClient {
177
207
  /** The scope handed to the client `write()`; mirrors the in-worker WriteSession. */
178
208
  export interface ClientWriteSession {
179
209
  /** Read-your-writes: observes the pre-scope snapshot plus everything staged so far. */
180
- query(sql: string, options?: {
181
- params?: QueryValue[];
182
- }): Promise<QueryResult>;
210
+ query(sql: string, options?: QueryOptions): Promise<QueryResult>;
183
211
  execute(sql: string, params?: readonly QueryValue[]): Promise<ExecuteResult>;
184
212
  insertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
185
213
  upsertBatch(tableName: string, input: InsertBatchInput): Promise<StagedWriteResult>;
@@ -2,7 +2,7 @@ import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConfl
2
2
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
3
3
  import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion, } from "../worker-protocol/index.js";
4
4
  import { toColumnarBatch } from "./batch.js";
5
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
5
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
6
6
  import { QueryMemoryBudgetError } from "./memory.js";
7
7
  import { decodeQueryResult } from "./result-wire.js";
8
8
  import { serializeSchema } from "./schema-wire.js";
@@ -30,11 +30,14 @@ function throwIfClientSnapshotAborted(signal) {
30
30
  const errorRegistry = new Map([
31
31
  UniqueConstraintError,
32
32
  MissingKeyError,
33
+ UnknownTableError,
33
34
  CompactionBacklogError,
34
35
  CompactionMemoryBudgetError,
35
36
  CompactionWriteAmplificationError,
36
37
  CompactionJobCancelledError,
37
38
  MaintenanceBacklogError,
39
+ DatabaseReadBacklogError,
40
+ LiveQueryLimitError,
38
41
  SqlCompileError,
39
42
  QueryMemoryBudgetError,
40
43
  VisibleSegmentCursorStaleError,
@@ -141,6 +144,27 @@ export class MinnowDatabaseClient {
141
144
  async createTable(input) {
142
145
  await this.#call("createTable", [input]);
143
146
  }
147
+ async createView(name, sql, options = {}) {
148
+ await this.#call("createView", [name, sql, options]);
149
+ }
150
+ async dropView(name, options = {}) {
151
+ return (await this.#call("dropView", [name, options]));
152
+ }
153
+ async dropColumn(tableName, columnName, options = {}) {
154
+ return (await this.#call("dropColumn", [tableName, columnName, options]));
155
+ }
156
+ async dropTable(tableName, options = {}) {
157
+ return (await this.#call("dropTable", [tableName, options]));
158
+ }
159
+ async createIndex(indexName, tableName, requestedColumns, options = {}) {
160
+ await this.#call("createIndex", [indexName, tableName, requestedColumns, options]);
161
+ }
162
+ async dropIndex(indexName, options = {}) {
163
+ return (await this.#call("dropIndex", [indexName, options]));
164
+ }
165
+ async buildFtsIndex(tableName, columnName) {
166
+ await this.#call("buildFtsIndex", [tableName, columnName]);
167
+ }
144
168
  /** The published catalog; see `MinnowDatabase.introspect()`. */
145
169
  async introspect() {
146
170
  return (await this.#call("introspect", []));
@@ -163,12 +187,12 @@ export class MinnowDatabaseClient {
163
187
  async insert(tableName, row) {
164
188
  return (await this.#call("insert", [tableName, row]));
165
189
  }
166
- async upsertBatch(tableName, input) {
190
+ async upsertBatch(tableName, input, options = {}) {
167
191
  const batch = toColumnarBatch(input);
168
- return (await this.#call("upsertBatch", [tableName, batch]));
192
+ return (await this.#call("upsertBatch", [tableName, batch, options]));
169
193
  }
170
- async upsert(tableName, row) {
171
- return (await this.#call("upsert", [tableName, row]));
194
+ async upsert(tableName, row, options = {}) {
195
+ return (await this.#call("upsert", [tableName, row, options]));
172
196
  }
173
197
  async updateBatch(tableName, input) {
174
198
  return (await this.#call("updateBatch", [tableName, input]));
@@ -200,17 +224,17 @@ export class MinnowDatabaseClient {
200
224
  * datetimes) and are rebuilt into row objects here; see `result-wire.ts`.
201
225
  */
202
226
  async query(sql, options) {
203
- return decodeQueryResult(await this.#call("query", options === undefined ? [sql] : [sql, options]));
227
+ const { signal, onStats, ...wireOptions } = options ?? {};
228
+ return decodeQueryResult(await this.#call("query", [sql, wireOptions, onStats !== undefined], { signal, onStats }));
204
229
  }
205
230
  /** Pulls one transferred columnar page at a time, preserving worker backpressure. */
206
231
  queryCursor(sql, options = {}) {
207
232
  const call = this.#call.bind(this);
208
233
  const invoke = this._invoke.bind(this);
234
+ const routeEvents = this._routeEvents.bind(this);
235
+ const unrouteEvents = this._unrouteEvents.bind(this);
209
236
  const handleId = crypto.randomUUID();
210
237
  const { signal, onStats, ...wireOptions } = options;
211
- if (onStats !== undefined) {
212
- throw new TypeError("Query cursor onStats callbacks are not available across a worker");
213
- }
214
238
  async function* batches() {
215
239
  let opened = false;
216
240
  let closing;
@@ -224,9 +248,11 @@ export class MinnowDatabaseClient {
224
248
  void close();
225
249
  };
226
250
  signal?.addEventListener("abort", onAbort, { once: true });
251
+ if (onStats !== undefined)
252
+ routeEvents(handleId, { onStats });
227
253
  try {
228
254
  signal?.throwIfAborted();
229
- await call("queryCursorOpen", [handleId, sql, wireOptions]);
255
+ await call("queryCursorOpen", [handleId, sql, wireOptions, onStats !== undefined]);
230
256
  opened = true;
231
257
  if (signal?.aborted === true) {
232
258
  await close();
@@ -242,6 +268,7 @@ export class MinnowDatabaseClient {
242
268
  finally {
243
269
  signal?.removeEventListener("abort", onAbort);
244
270
  await close();
271
+ unrouteEvents(handleId);
245
272
  }
246
273
  }
247
274
  return batches();
@@ -290,7 +317,10 @@ export class MinnowDatabaseClient {
290
317
  const opened = (await this.#call("writeOpen", []));
291
318
  const stage = (op, tableName, input) => this._invoke(opened.handleId, "stage", [op, tableName, input]);
292
319
  const session = {
293
- query: async (sql, options) => decodeQueryResult(await this._invoke(opened.handleId, "query", [sql, options])),
320
+ query: async (sql, options = {}) => {
321
+ const { signal, onStats, ...wireOptions } = options;
322
+ return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== undefined], { signal, onStats }));
323
+ },
294
324
  execute: (sql, params) => this._invoke(opened.handleId, "execute", params === undefined ? [sql] : [sql, params]),
295
325
  insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
296
326
  upsertBatch: (tableName, input) => stage("upsertBatch", tableName, input),
@@ -501,6 +531,12 @@ export class MinnowDatabaseClient {
501
531
  return this.#post("rpc-call", handleId, method, args);
502
532
  }
503
533
  /** @internal */
534
+ async _invokeControlled(handleId, method, args, controls) {
535
+ if (this.#closed)
536
+ throw new Error("Database client is closed");
537
+ return this.#post("rpc-call", handleId, method, args, undefined, false, controls);
538
+ }
539
+ /** @internal */
504
540
  _routeEvents(handleId, route) {
505
541
  this.#events.set(handleId, route);
506
542
  }
@@ -508,23 +544,49 @@ export class MinnowDatabaseClient {
508
544
  _unrouteEvents(handleId) {
509
545
  this.#events.delete(handleId);
510
546
  }
511
- async #call(method, args) {
547
+ async #call(method, args, controls = {}) {
512
548
  if (this.#closed)
513
549
  throw new Error("Database client is closed");
514
- return this.#post("rpc-call", null, method, args);
550
+ return this.#post("rpc-call", null, method, args, undefined, false, controls);
515
551
  }
516
- async #post(kind, handleId, method, args, transfer, bypassLimit = kind === "rpc-init" || method === "dispose") {
552
+ async #post(kind, handleId, method, args, transfer, bypassLimit = kind === "rpc-init" || method === "dispose", controls = {}) {
517
553
  if (this.#fatal !== undefined)
518
554
  throw this.#fatal;
555
+ controls.signal?.throwIfAborted();
519
556
  if (!bypassLimit && this.#pending.size >= MAX_DATABASE_RPC_IN_FLIGHT) {
520
557
  throw new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`);
521
558
  }
522
559
  const requestId = crypto.randomUUID();
523
560
  return new Promise((resolve, reject) => {
524
- this.#pending.set(requestId, { resolve, reject });
525
- this.#transport.postMessage(kind === "rpc-init"
526
- ? { version: protocolVersion, requestId, kind, payload: args[0] }
527
- : { version: protocolVersion, requestId, kind, handleId, method, args }, transfer === undefined ? undefined : { transfer });
561
+ const onAbort = () => {
562
+ try {
563
+ this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
564
+ }
565
+ catch {
566
+ // The original RPC still owns completion. A transport failure also reaches #fail via
567
+ // its error event; throwing from an AbortSignal listener would only be unhandled noise.
568
+ }
569
+ };
570
+ const cleanup = () => {
571
+ controls.signal?.removeEventListener("abort", onAbort);
572
+ if (controls.onStats !== undefined)
573
+ this.#events.delete(requestId);
574
+ };
575
+ controls.signal?.addEventListener("abort", onAbort, { once: true });
576
+ if (controls.onStats !== undefined) {
577
+ this.#events.set(requestId, { onStats: controls.onStats });
578
+ }
579
+ this.#pending.set(requestId, { resolve, reject, cleanup });
580
+ try {
581
+ this.#transport.postMessage(kind === "rpc-init"
582
+ ? { version: protocolVersion, requestId, kind, payload: args[0] }
583
+ : { version: protocolVersion, requestId, kind, handleId, method, args }, transfer === undefined ? undefined : { transfer });
584
+ }
585
+ catch (error) {
586
+ this.#pending.delete(requestId);
587
+ cleanup();
588
+ reject(error instanceof Error ? error : new Error("Database request failed", { cause: error }));
589
+ }
528
590
  });
529
591
  }
530
592
  #receive(message) {
@@ -545,6 +607,9 @@ export class MinnowDatabaseClient {
545
607
  }
546
608
  else if (response.event === "progress")
547
609
  route.onProgress?.(response.payload);
610
+ else if (response.event === "stats") {
611
+ route.onStats?.(response.payload);
612
+ }
548
613
  else if (response.event === "complete") {
549
614
  this.#events.delete(response.handleId);
550
615
  route.onComplete?.();
@@ -555,6 +620,7 @@ export class MinnowDatabaseClient {
555
620
  if (pending === undefined)
556
621
  return;
557
622
  this.#pending.delete(response.requestId);
623
+ pending.cleanup?.();
558
624
  if (response.kind === "rpc-result")
559
625
  pending.resolve(response.result);
560
626
  else
@@ -565,8 +631,10 @@ export class MinnowDatabaseClient {
565
631
  const pending = [...this.#pending.values()];
566
632
  this.#pending.clear();
567
633
  this.#events.clear();
568
- for (const call of pending)
634
+ for (const call of pending) {
635
+ call.cleanup?.();
569
636
  call.reject(error);
637
+ }
570
638
  }
571
639
  }
572
640
  /**
@@ -1,11 +1,12 @@
1
1
  import { type BatchRow, type BatchValue, type InsertBatchInput } from "./batch.js";
2
2
  import { BufferedTableWriter, type BufferedWriterOptions } from "./buffered-writer.js";
3
3
  export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_ADDS, type BufferedFlushResult, type BufferedWriterOptions, type LifecycleDocumentTarget, type LifecycleFlushOptions, type LifecycleFlushRequester, type LifecyclePageTarget, } from "./buffered-writer.js";
4
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
4
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
5
+ export { DatabaseReadBacklogError } from "./errors.js";
5
6
  import { type Compression } from "../block-format/index.js";
6
- import { type BlockStore, type ColumnDefault, CompactionBacklogError, type CompactionJobRecord, type CompactionJobState, type GarbageCollectionJobRecord, type GarbageCollectionJobState, type SimpleDataType, type SqlDomain, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult, TableInUseError } from "../storage/types.js";
7
+ import { type BlockStore, type ColumnDefault, type ColumnGenerated, CompactionBacklogError, type CompactionJobRecord, type CompactionJobState, type GarbageCollectionJobRecord, type GarbageCollectionJobState, type SimpleDataType, type SqlDomain, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult, TableInUseError } from "../storage/types.js";
7
8
  import type { SnapshotExportProgress, SnapshotLoadProgress } from "../storage/snapshot.js";
8
- import { type CompiledQuery, type CompiledStatement, type ForeignKeyDefinition, type QueryResult, type QueryRow, type QueryValue, type UniqueConstraintDefinition } from "./query.js";
9
+ import { type ComparisonOperator, type CompiledQuery, type CompiledStatement, type ForeignKeyDefinition, type QueryResult, type QueryRow, type QueryValue, type UniqueConstraintDefinition } from "./query.js";
9
10
  import { LiveQuerySet, type LiveQuerySetOptions } from "./live.js";
10
11
  import { type Catalog } from "./catalog.js";
11
12
  import { type AnyTable, type MigrationStep, type SchemaDefinition } from "./schema.js";
@@ -16,11 +17,6 @@ export declare const MAX_TRANSACTION_SAVEPOINT_BYTES: number;
16
17
  export declare const MAX_DATABASE_PENDING_WRITES = 64;
17
18
  /** Concurrent direct reads retained by one database instance. Reads still execute in parallel. */
18
19
  export declare const MAX_DATABASE_ACTIVE_READS = 256;
19
- export declare class DatabaseReadBacklogError extends Error {
20
- readonly limit: number;
21
- readonly name = "DatabaseReadBacklogError";
22
- constructor(limit?: number);
23
- }
24
20
  export interface ColumnDefinition {
25
21
  name: string;
26
22
  type: SimpleDataType;
@@ -31,6 +27,8 @@ export interface ColumnDefinition {
31
27
  nullable?: boolean;
32
28
  /** Fills omitted or SQL DEFAULT slots at insert time; never applied at read time. */
33
29
  defaultValue?: ColumnDefault;
30
+ /** Stored expression over sibling columns; callers cannot assign this column. */
31
+ generatedValue?: ColumnGenerated;
34
32
  /** String columns only: the closed set of values writes must draw from. */
35
33
  enumValues?: readonly string[];
36
34
  /** What rows written before this column existed read as, instead of NULL. */
@@ -91,9 +89,23 @@ export interface InsertBatchResult {
91
89
  */
92
90
  generatedColumns?: Record<string, BatchValue[]>;
93
91
  }
94
- export interface UpsertBatchResult extends InsertBatchResult {
92
+ export interface UpsertBatchResult extends Omit<InsertBatchResult, "segmentId"> {
93
+ segmentId: string | null;
94
+ /** Input rows considered by the statement, including conflicts rejected by `conflictWhere`. */
95
+ requestedRowCount: number;
95
96
  insertedRowCount: number;
96
97
  updatedRowCount: number;
98
+ skippedRowCount: number;
99
+ }
100
+ export interface UpsertConflictWhere {
101
+ /** Column of the existing conflicting row to test. */
102
+ column: string;
103
+ operator: ComparisonOperator;
104
+ value: BatchValue;
105
+ }
106
+ export interface UpsertOptions {
107
+ /** SQL `ON CONFLICT DO UPDATE ... WHERE` semantics over the existing target row. */
108
+ conflictWhere?: UpsertConflictWhere;
97
109
  }
98
110
  export interface UpdateBatchInput {
99
111
  /** Stable ordinary arrays; do not mutate or expose changing accessors until the write settles. */
@@ -111,6 +123,8 @@ export interface UpdateBatchResult {
111
123
  storedBytes: number;
112
124
  version: number;
113
125
  metrics: WriteMetrics;
126
+ /** Recomputed generated-column vectors in input key order. */
127
+ generatedColumns?: Record<string, BatchValue[]>;
114
128
  }
115
129
  export interface WriteMetrics {
116
130
  logicalBytes: number;
@@ -216,6 +230,11 @@ export interface MaintenanceStatus {
216
230
  } | null;
217
231
  }
218
232
  export interface QueryOptions {
233
+ /**
234
+ * Stops a read between bounded execution or storage batches. An abort never returns a partial
235
+ * result and releases any reader lease and temporary spill owner before the promise rejects.
236
+ */
237
+ readonly signal?: AbortSignal;
219
238
  /**
220
239
  * Called once with what this execution cost, before the result is returned. Additive and
221
240
  * optional: the engine can report its own memory because it reserves before it allocates,
@@ -254,8 +273,6 @@ export interface QueryOptions {
254
273
  export interface QueryCursorOptions extends QueryOptions {
255
274
  /** Maximum rows in one yielded result batch. Defaults to the vector scan batch size. */
256
275
  readonly batchRows?: number;
257
- /** Cancels the scan and releases its snapshot lease. */
258
- readonly signal?: AbortSignal;
259
276
  }
260
277
  export interface DeleteBatchInput {
261
278
  /** Stable ordinary array; do not mutate or expose changing accessors until the write settles. */
@@ -415,7 +432,7 @@ export interface TableDefinition {
415
432
  uniqueKey?: string;
416
433
  }
417
434
  export type DatabaseRow = Record<string, Exclude<BatchValue, null> | null>;
418
- export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
435
+ export { CompactionBacklogError, CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, TableInUseError, UniqueConstraintError, VisibleSegmentCursorStaleError, };
419
436
  export interface MinnowDatabaseOptions {
420
437
  /**
421
438
  * Block codec for newly written blocks; defaults to "gzip", which is also what compaction
@@ -570,26 +587,30 @@ export type ExecuteResult = {
570
587
  } | {
571
588
  kind: "drop-trigger";
572
589
  name: string;
573
- } | {
590
+ } | ({
574
591
  kind: "insert";
575
592
  table: string;
576
593
  rowCount: number;
577
594
  /** Absent when the statement affected no rows (an INSERT ... SELECT of an empty set). */
578
595
  version?: number;
579
- returnedRows?: QueryRow[];
580
- } | {
596
+ } & ReturningExecuteFields) | ({
581
597
  kind: "update";
582
598
  table: string;
583
599
  rowCount: number;
584
600
  version?: number;
585
- returnedRows?: QueryRow[];
586
- } | {
601
+ } & ReturningExecuteFields) | ({
587
602
  kind: "delete";
588
603
  table: string;
589
604
  rowCount: number;
590
605
  version?: number | null;
606
+ } & ReturningExecuteFields);
607
+ interface ReturningExecuteFields {
591
608
  returnedRows?: QueryRow[];
592
- };
609
+ /** RETURNING projection order; present with returnedRows, including for an empty result. */
610
+ returnedColumns?: string[];
611
+ /** Logical domains aligned with returnedColumns. */
612
+ returnedColumnDomains?: Array<SqlDomain | null>;
613
+ }
593
614
  export interface RunStatementOptions {
594
615
  /**
595
616
  * Projects the affected rows back: column names, or "*" for every table column. Inserts echo
@@ -722,10 +743,12 @@ export declare class MinnowDatabase {
722
743
  }): Promise<boolean>;
723
744
  insertBatch(tableName: string, input: InsertBatchInput): Promise<InsertBatchResult>;
724
745
  insert(tableName: string, row: BatchRow): Promise<InsertBatchResult>;
725
- upsertBatch(tableName: string, input: InsertBatchInput): Promise<UpsertBatchResult>;
726
- upsert(tableName: string, row: BatchRow): Promise<UpsertBatchResult>;
746
+ upsertBatch(tableName: string, input: InsertBatchInput, options?: UpsertOptions): Promise<UpsertBatchResult>;
747
+ upsert(tableName: string, row: BatchRow, options?: UpsertOptions): Promise<UpsertBatchResult>;
727
748
  updateBatch(tableName: string, input: UpdateBatchInput): Promise<UpdateBatchResult>;
728
749
  update(tableName: string, key: Exclude<BatchValue, null>, changes: Readonly<Record<string, BatchValue>>): Promise<UpdateBatchResult>;
750
+ /** Deletes one row by the table's unique key. */
751
+ delete(tableName: string, key: Exclude<BatchValue, null>): Promise<DeleteBatchResult>;
729
752
  deleteBatch(tableName: string, input: DeleteBatchInput): Promise<DeleteBatchResult>;
730
753
  bufferedWriter(tableName: string, options?: BufferedWriterOptions): BufferedTableWriter;
731
754
  readTable(tableName: string, versionOrOptions?: number | ReadTableOptions): Promise<DatabaseRow[]>;