@minnowdb/core 0.7.7 → 0.7.10

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.
@@ -1,5 +1,8 @@
1
1
  import { type ColumnDefault, type SqlDomain, type TableColumnRecord, type TableRecord } from "../storage/types.js";
2
+ import type { ComparisonOperator } from "../plan/model.js";
3
+ import type { BatchRow, BatchValue, InsertBatchInput } from "./batch.js";
2
4
  import { type Catalog, type CatalogColumn, type CatalogTable } from "./catalog.js";
5
+ import type { DatabaseRow, DeleteBatchInput, UpdateBatchInput, UpsertOptions } from "./database.js";
3
6
  import { type Expression, type QueryValue } from "./query.js";
4
7
  /**
5
8
  * Typed schema DSL and catalog migration planning. Column builders carry compile-time value and
@@ -313,8 +316,8 @@ export declare function schema<TTables extends readonly AnyTable[], TViews exten
313
316
  }): SchemaDefinition<TTables, TViews>;
314
317
  type ColumnTypeMetadata<TColumn> = TColumn extends {
315
318
  readonly "~types"?: infer TMetadata extends {
316
- readonly select: unknown;
317
- readonly input: unknown;
319
+ readonly select: SchemaValue;
320
+ readonly input: SchemaValue;
318
321
  };
319
322
  } ? NonNullable<TMetadata> : never;
320
323
  type ColumnValue<TColumn extends AnyColumn> = TColumn["isNullable"] extends true ? ColumnTypeMetadata<TColumn>["select"] | null : ColumnTypeMetadata<TColumn>["select"];
@@ -359,6 +362,86 @@ export type PrimaryKeyKeys<TTable extends AnyTable> = {
359
362
  export type InferUpdateChanges<TTable extends AnyTable> = {
360
363
  [K in keyof TTable["columns"] as K extends PrimaryKeyKeys<TTable> | GeneratedKeys<TTable> ? never : K]?: ColumnInputValue<TTable["columns"][K]> | undefined;
361
364
  };
365
+ /** The bound every schema type parameter carries. */
366
+ export type AnySchema = SchemaDefinition<readonly AnyTable[]>;
367
+ /**
368
+ * The schema type parameter's default: no declaration, so the batch API addresses tables by
369
+ * plain string. It is `any` rather than `AnySchema` so that a database declared against a schema
370
+ * remains assignable to the undeclared class — `MinnowDatabase<typeof mine>` is a
371
+ * `MinnowDatabase` — which no ordinary type argument can express once table names are checked.
372
+ * Every type below resolves to its erased branch for it, so nothing else about it is `any`.
373
+ */
374
+ export type UntypedSchema = any;
375
+ type SchemaTable<TSchema extends AnySchema> = TSchema["tables"][number];
376
+ /** True when the schema carries no literal table names — the untyped default. */
377
+ type ErasedSchema<TSchema extends AnySchema> = string extends SchemaTable<TSchema>["name"] ? true : false;
378
+ /** The declared table names; `string` for an untyped database. */
379
+ export type TableName<TSchema extends AnySchema> = SchemaTable<TSchema>["name"];
380
+ /** The declaration behind one table name. */
381
+ export type TableOf<TSchema extends AnySchema, TName extends TableName<TSchema>> = Extract<SchemaTable<TSchema>, {
382
+ readonly name: TName;
383
+ }>;
384
+ /** A declared table's column names; `string` for an untyped database. */
385
+ export type BatchColumnName<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? string : keyof TableOf<TSchema, TName>["columns"] & string;
386
+ /** The columnar form of `InferInsertRow`: one vector per column, optional where the row is. */
387
+ export interface ColumnarInsertBatch<TTable extends AnyTable> {
388
+ readonly columns: {
389
+ readonly [K in RequiredInsertKeys<TTable>]: ReadonlyArray<ColumnInputValue<TTable["columns"][K]>>;
390
+ } & {
391
+ readonly [K in OptionalInsertKeys<TTable>]?: ReadonlyArray<ColumnInputValue<TTable["columns"][K]>>;
392
+ };
393
+ /** Per row, whether the column was omitted (takes its default) rather than set to NULL. */
394
+ readonly omitted?: Partial<Readonly<Record<OptionalInsertKeys<TTable>, readonly boolean[]>>>;
395
+ readonly rowCount?: number;
396
+ }
397
+ /** What `insertBatch` and `upsertBatch` accept for one table: typed rows, or a typed columnar batch. */
398
+ export type BatchInsertInput<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? InsertBatchInput : ReadonlyArray<InferInsertRow<TableOf<TSchema, TName>>> | ColumnarInsertBatch<TableOf<TSchema, TName>>;
399
+ /** One row for `insert`, `upsert`, or a buffered writer's `add`. */
400
+ export type BatchInsertRow<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? BatchRow : InferInsertRow<TableOf<TSchema, TName>>;
401
+ /** What `readTable` returns per row. */
402
+ export type BatchReadRow<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? DatabaseRow : InferRow<TableOf<TSchema, TName>>;
403
+ /**
404
+ * The key a batch write addresses a row by: the `.unique()` column or a one-column table-level
405
+ * primary key. A composite primary key is addressed through its hidden locator, which the batch
406
+ * API does not expose, so those tables get `never` here and are written through SQL instead.
407
+ */
408
+ type BatchKey<TTable extends AnyTable> = TTable["primaryKey"] extends readonly [
409
+ unknown,
410
+ unknown,
411
+ ...unknown[]
412
+ ] ? never : TTable["primaryKey"] extends readonly [infer TOnly extends keyof TTable["columns"]] ? NonNullable<ColumnValue<TTable["columns"][TOnly]>> : NonNullable<ScalarUniqueKeyValue<TTable>>;
413
+ /** One key value for `update` and `delete`. */
414
+ export type BatchKeyValue<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? Exclude<BatchValue, null> : BatchKey<TableOf<TSchema, TName>>;
415
+ /** The changes object `update` takes: every non-key, non-generated column, each optional. */
416
+ export type BatchUpdateChanges<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? Readonly<Record<string, BatchValue>> : InferUpdateChanges<TableOf<TSchema, TName>>;
417
+ /** What `updateBatch` takes: typed keys and one vector per changed column. */
418
+ export type BatchUpdateInput<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? UpdateBatchInput : {
419
+ readonly keys: ReadonlyArray<BatchKey<TableOf<TSchema, TName>>>;
420
+ readonly changes: {
421
+ readonly [K in keyof InferUpdateChanges<TableOf<TSchema, TName>>]?: ReadonlyArray<Exclude<InferUpdateChanges<TableOf<TSchema, TName>>[K], undefined>>;
422
+ };
423
+ };
424
+ /** What `deleteBatch` takes. */
425
+ export type BatchDeleteInput<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? DeleteBatchInput : {
426
+ readonly keys: ReadonlyArray<BatchKey<TableOf<TSchema, TName>>>;
427
+ };
428
+ /** A `conflictWhere` guard whose `value` follows the column it names. */
429
+ export type BatchConflictWhere<TTable extends AnyTable> = {
430
+ [K in keyof TTable["columns"] & string]: {
431
+ readonly column: K;
432
+ readonly operator: ComparisonOperator;
433
+ readonly value: ColumnInputValue<TTable["columns"][K]>;
434
+ };
435
+ }[keyof TTable["columns"] & string];
436
+ /** `upsertBatch`/`upsert` options, with the guard typed against the table. */
437
+ export type BatchUpsertOptions<TSchema extends AnySchema, TName extends TableName<TSchema>> = ErasedSchema<TSchema> extends true ? UpsertOptions : {
438
+ readonly conflictWhere?: BatchConflictWhere<TableOf<TSchema, TName>>;
439
+ };
440
+ /** `readTable` options; naming `columns` narrows the returned rows to those columns. */
441
+ export interface BatchReadOptions<TSchema extends AnySchema, TName extends TableName<TSchema>, TColumns extends ReadonlyArray<BatchColumnName<TSchema, TName>> = ReadonlyArray<BatchColumnName<TSchema, TName>>> {
442
+ readonly version?: number;
443
+ readonly columns?: TColumns;
444
+ }
362
445
  export type MigrationStep = {
363
446
  kind: "create-table";
364
447
  table: AnyTable;
@@ -1,7 +1,10 @@
1
- import type { LiveQueryInput, LiveQueryObserveOptions } from "./live.js";
1
+ import type { LiveQueryInput, LiveQueryObserveOptions, LiveQuerySubscribeOptions } from "./live.js";
2
+ import type { QueryResult } from "./query.js";
2
3
  /** The structural live-query surface shared by MinnowDatabase and its worker client. */
3
4
  export interface LiveQueryBackend {
4
5
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscriptionLike>;
6
+ /** Result delivery, used when the source can decode the engine's result itself. */
7
+ subscribe?(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscriptionLike>;
5
8
  refresh(): Promise<void>;
6
9
  close(): void | Promise<void>;
7
10
  }
@@ -22,6 +25,14 @@ export interface LiveQueryDriver {
22
25
  export interface LiveQuerySource<out TRow> {
23
26
  readonly query: LiveQueryInput;
24
27
  execute(signal?: AbortSignal): Promise<readonly TRow[]>;
28
+ /**
29
+ * Turns a result the engine delivered into the adapter's rows. With it, the query subscribes
30
+ * for results rather than invalidations: the engine executes or patches the statement where
31
+ * the data is, compares, and hands over a changed result once — over a worker channel, as
32
+ * one columnar transfer — and `execute` is never called after the statement is registered.
33
+ * Without it, an invalidation is followed by `execute`, which the engine's memo serves.
34
+ */
35
+ decode?(result: QueryResult): readonly TRow[] | Promise<readonly TRow[]>;
25
36
  }
26
37
  export type LiveSnapshot<TRow> = {
27
38
  readonly status: "loading";
@@ -1,6 +1,26 @@
1
1
  import { sameLiveValue } from "./live-equal.js";
2
- function immutableRows(rows) {
3
- return Object.freeze([...rows]);
2
+ function reconcileRows(previous, next, retained) {
3
+ const rows = new Array(next.length);
4
+ let changed = previous.length !== next.length;
5
+ const provenance = retained?.length === next.length ? retained : void 0;
6
+ for (let index = 0; index < next.length; index += 1) {
7
+ const row = next[index];
8
+ const was = provenance?.[index] ?? -1;
9
+ if (was >= 0 && was < previous.length) {
10
+ rows[index] = previous[was];
11
+ if (was !== index)
12
+ changed = true;
13
+ continue;
14
+ }
15
+ const before = previous[index];
16
+ if (index < previous.length && sameLiveValue(before, row))
17
+ rows[index] = before;
18
+ else {
19
+ rows[index] = row;
20
+ changed = true;
21
+ }
22
+ }
23
+ return changed ? Object.freeze(rows) : void 0;
4
24
  }
5
25
  class LiveQuery {
6
26
  #listeners = /* @__PURE__ */ new Set();
@@ -11,6 +31,9 @@ class LiveQuery {
11
31
  #subscription;
12
32
  #observationGeneration = 0;
13
33
  #queued;
34
+ #lastDelivered;
35
+ #deliveriesReceived = 0;
36
+ #rowsFromDelivery = 0;
14
37
  #execution;
15
38
  #executionAbort;
16
39
  #invalidationSequence = 0;
@@ -52,10 +75,24 @@ class LiveQuery {
52
75
  }
53
76
  if (this.#invalidationSequence === sequence) {
54
77
  const version = this.#snapshot.status === "loading" ? null : this.#snapshot.version;
55
- this.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
78
+ if (this.#decodes()) {
79
+ const last = this.#lastDelivered;
80
+ if (this.#snapshot.status === "error" && last !== void 0)
81
+ this.#schedule(last);
82
+ } else
83
+ this.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
56
84
  }
57
85
  await this.#waitForIdle();
58
86
  }
87
+ #decodes() {
88
+ return this.#source.decode !== void 0 && this.#backend.subscribe !== void 0;
89
+ }
90
+ async #decodeDelivered(result) {
91
+ const source = this.#source;
92
+ if (source.decode === void 0)
93
+ throw new TypeError("Live query source lost its decoder");
94
+ return source.decode(result);
95
+ }
59
96
  close() {
60
97
  if (this.#closed)
61
98
  return;
@@ -75,7 +112,36 @@ class LiveQuery {
75
112
  if (this.#subscription !== void 0 || this.#closed)
76
113
  return;
77
114
  const generation = this.#observationGeneration += 1;
115
+ if (this.#source.decode !== void 0 && this.#backend.subscribe !== void 0) {
116
+ const subscription2 = this.#backend.subscribe(this.#source.query, {
117
+ onChange: (result, delivery) => {
118
+ if (generation !== this.#observationGeneration || this.#closed)
119
+ return;
120
+ this.#deliveriesReceived += 1;
121
+ this.#schedule({ result, delivery, sequence: this.#deliveriesReceived });
122
+ },
123
+ onError: (error) => {
124
+ if (generation !== this.#observationGeneration || this.#closed)
125
+ return;
126
+ this.#setError(error, this.#currentVersion());
127
+ },
128
+ onComplete: () => {
129
+ if (generation !== this.#observationGeneration)
130
+ return;
131
+ this.#subscription = void 0;
132
+ }
133
+ });
134
+ this.#subscription = subscription2;
135
+ subscription2.catch((error) => {
136
+ if (generation !== this.#observationGeneration || this.#closed)
137
+ return;
138
+ this.#subscription = void 0;
139
+ this.#setError(error, this.#currentVersion());
140
+ });
141
+ return;
142
+ }
78
143
  const subscription = this.#backend.observe(this.#source.query, {
144
+ suppressUnchanged: true,
79
145
  onInvalidate: (invalidation) => {
80
146
  if (generation !== this.#observationGeneration || this.#closed)
81
147
  return;
@@ -109,9 +175,9 @@ class LiveQuery {
109
175
  void subscription.then((handle) => handle.close()).catch(() => void 0);
110
176
  }
111
177
  }
112
- #schedule(invalidation) {
178
+ #schedule(work) {
113
179
  this.#invalidationSequence += 1;
114
- this.#queued = invalidation;
180
+ this.#queued = work;
115
181
  if (this.#execution !== void 0)
116
182
  return;
117
183
  const execution = this.#drain();
@@ -125,24 +191,35 @@ class LiveQuery {
125
191
  }
126
192
  async #drain() {
127
193
  while (this.#queued !== void 0 && !this.#closed) {
128
- const invalidation = this.#queued;
194
+ const work = this.#queued;
129
195
  this.#queued = void 0;
130
196
  const abort = new AbortController();
131
197
  this.#executionAbort = abort;
198
+ const invalidation = "result" in work ? work.delivery : work;
132
199
  try {
133
- const rows = immutableRows(await this.#source.execute(abort.signal));
200
+ let executed;
201
+ if ("result" in work) {
202
+ this.#lastDelivered = work;
203
+ executed = await this.#decodeDelivered(work.result);
204
+ this.#lastDelivered = void 0;
205
+ } else
206
+ executed = await this.#source.execute(abort.signal);
134
207
  if (this.#executionWasCancelled(abort))
135
208
  continue;
136
209
  if (this.#hasQueuedInvalidation())
137
210
  continue;
138
211
  const previous = this.#snapshot.rows;
139
- if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion && sameLiveValue(previous, rows)) {
140
- continue;
141
- }
142
- if (sameLiveValue(previous, rows) && this.#snapshot.status !== "loading") {
212
+ const provenance = "result" in work && this.#rowsFromDelivery === work.sequence - 1 ? work.delivery.retained : void 0;
213
+ const rows = reconcileRows(previous, executed, provenance);
214
+ if ("result" in work)
215
+ this.#rowsFromDelivery = work.sequence;
216
+ if (rows === void 0) {
217
+ if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion) {
218
+ continue;
219
+ }
143
220
  this.#snapshot = {
144
221
  status: "ready",
145
- rows: previous,
222
+ rows: this.#snapshot.status === "loading" ? Object.freeze([...previous]) : previous,
146
223
  version: invalidation.manifestVersion
147
224
  };
148
225
  } else {
@@ -379,7 +379,8 @@ class DatabaseRpcServer {
379
379
  try {
380
380
  set = this.database.liveQueries({
381
381
  ...channelName === void 0 ? {} : { channelName },
382
- ...pollIntervalMs === void 0 ? {} : { pollIntervalMs }
382
+ ...pollIntervalMs === void 0 ? {} : { pollIntervalMs },
383
+ sharedResults: true
383
384
  });
384
385
  this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: /* @__PURE__ */ new Set() });
385
386
  } catch (error) {
@@ -667,11 +668,15 @@ class DatabaseRpcServer {
667
668
  let subscription;
668
669
  try {
669
670
  subscription = await handle.set.subscribe(query, {
670
- onChange: (result) => {
671
+ onChange: (result, delivery) => {
671
672
  const encoded = encodeQueryResult(result);
672
- this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
673
- transfer: encoded.transfer
674
- });
673
+ const retained = delivery.retained?.slice();
674
+ if (retained !== void 0)
675
+ encoded.transfer.push(retained.buffer);
676
+ this.scope.postMessage(rpcEvent(subscriptionId, "change", {
677
+ result: encoded.payload,
678
+ delivery: retained === void 0 ? delivery : { ...delivery, retained }
679
+ }), { transfer: encoded.transfer });
675
680
  },
676
681
  onError: (error) => {
677
682
  this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
@@ -700,9 +705,11 @@ class DatabaseRpcServer {
700
705
  case "observe": {
701
706
  const subscriptionId = this.#claimHandleId(args[0]);
702
707
  const query = args[1];
708
+ const { suppressUnchanged } = args[2] ?? {};
703
709
  let subscription;
704
710
  try {
705
711
  subscription = await handle.set.observe(query, {
712
+ ...suppressUnchanged === true ? { suppressUnchanged: true } : {},
706
713
  onInvalidate: (invalidation) => {
707
714
  this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
708
715
  },
@@ -1,4 +1,4 @@
1
- export declare const protocolVersion: 3;
1
+ export declare const protocolVersion: 6;
2
2
  /** Outstanding request/response pairs retained by either side of one database RPC connection. */
3
3
  export declare const MAX_DATABASE_RPC_IN_FLIGHT = 256;
4
4
  export type RpcRequest = {
@@ -1,4 +1,4 @@
1
- const protocolVersion = 3;
1
+ const protocolVersion = 6;
2
2
  const MAX_DATABASE_RPC_IN_FLIGHT = 256;
3
3
  function serializeError(error) {
4
4
  if (!(error instanceof Error)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.7.7",
3
+ "version": "0.7.10",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",