@orkestrel/database 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,10 +23,10 @@ npm install @orkestrel/database
23
23
 
24
24
  ## Status
25
25
 
26
- Pre-release (`0.0.7`): the core engine and the memory, JSON file, SQLite,
26
+ Pre-release: the core engine and the memory, JSON file, SQLite,
27
27
  and IndexedDB drivers are all implemented and tested, but the public API is
28
28
  still unstable and may change without notice. See
29
- [guides/src/database.md](./guides/src/database.md) for the full documented
29
+ [guides/database.md](./guides/database.md) for the full documented
30
30
  surface.
31
31
 
32
32
  ## Package
@@ -16,9 +16,9 @@ import { TableSchema } from '@orkestrel/database';
16
16
  import { TableSchema as TableSchema_2 } from '@orkestrel/database';
17
17
 
18
18
  /**
19
- * The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one
20
- * of the six exact key comparisons over scalar operands; otherwise
21
- * `undefined`.
19
+ * Translates one {@link Condition} to the `IDBKeyRange` it maps to, when its
20
+ * operator is one of the exact key comparisons over scalar operands; otherwise
21
+ * returns `undefined`.
22
22
  *
23
23
  * @remarks
24
24
  * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
@@ -29,7 +29,7 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
29
29
  * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
30
30
  * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
31
31
  * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
32
- * guard (`typeof` string/number) rejects a non-scalar value (e.g. an array, a
32
+ * guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
33
33
  * boolean) that is not a usable key. `between` additionally guards against a
34
34
  * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
35
35
  * `DataError` `DOMException` for a lower bound above the upper bound, so a
@@ -47,7 +47,7 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
47
47
  export declare function conditionToRange(condition: Condition): IDBKeyRange | undefined;
48
48
 
49
49
  /**
50
- * Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
50
+ * Creates a persistent IndexedDB {@link DriverInterface} for the core database layer.
51
51
  *
52
52
  * @remarks
53
53
  * Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
@@ -77,7 +77,7 @@ export declare function conditionToRange(condition: Condition): IDBKeyRange | un
77
77
  export declare function createIndexedDBDriver(name: string): DriverInterface_2;
78
78
 
79
79
  /**
80
- * Derive an IndexedDB index name for a declared column group — a bare column
80
+ * Derives an IndexedDB index name for a declared column group — a bare column
81
81
  * name for a single-column index, a deterministic collision-free encoding for a
82
82
  * compound one.
83
83
  *
@@ -106,11 +106,23 @@ export declare function createIndexedDBDriver(name: string): DriverInterface_2;
106
106
  */
107
107
  export declare function deriveIndexedDBIndexName(columns: readonly string[]): string;
108
108
 
109
- export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
109
+ /**
110
+ * Lists the declared {@link ColumnStorage}s that are valid, orderable IndexedDB keys.
111
+ *
112
+ * @remarks
113
+ * `text` / `integer` / `real` occupy IndexedDB's string / number key space, so a
114
+ * column declared with one of them can back a store or index range read.
115
+ * `boolean` / `json` / `blob` are not valid `IDBValidKey`s and a range over one
116
+ * would silently miss rows, so `selectPlan` never pushes a condition down on
117
+ * them and the core engine answers the read instead. A frozen array, matching
118
+ * `EXACT_COLUMN_STORAGE` in `src/server`: a consumer holding it reads the
119
+ * membership with `includes` and cannot change the driver's pushdown behavior.
120
+ */
121
+ export declare const INDEXABLE_STORAGE: readonly ColumnStorage[];
110
122
 
111
123
  /**
112
- * The IndexedDB {@link DriverInterface} — the persistent browser backend, built on
113
- * the published `@orkestrel/indexeddb` wrapper.
124
+ * Implements the {@link DriverInterface} over IndexedDB — the persistent browser backend,
125
+ * built on the published `@orkestrel/indexeddb` wrapper.
114
126
  *
115
127
  * @remarks
116
128
  * A thin adapter: it implements the storage primitives the core database layer
@@ -126,12 +138,12 @@ export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
126
138
  * wrapper's native `getAll` / `getAllKeys`, and `snapshot` rolls back through one
127
139
  * atomic wrapper transaction.
128
140
  *
129
- * It also implements the optional native `records` / `stream` hooks
130
- * (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
141
+ * It also implements the optional native `records` / `stream` hooks:
142
+ * `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
131
143
  * key-range pushdown over the primary key or a single-column secondary index,
132
144
  * fetching a candidate **superset** that the core engine (`applyQuery` /
133
145
  * `matchesQuery`) then refines — so a native read is byte-identical to a full
134
- * scan, just cheaper. Pushdown is conservative: only the exact-comparison
146
+ * scan, only cheaper. Pushdown is conservative: only the exact-comparison
135
147
  * operators over orderable columns narrow to a range; everything else falls back
136
148
  * to a full scan + the engine.
137
149
  *
@@ -145,7 +157,7 @@ export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
145
157
  * transaction (`onupgradeneeded`), so `migrate` closes the current connection
146
158
  * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
147
159
  * plan's steps — dropping stores, adding/removing indexes on the raw
148
- * `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk
160
+ * `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
149
161
  * (the one step needing to touch existing data; `column.add` is a no-op — this
150
162
  * driver stores whatever a row carries, so there is nothing to backfill). A
151
163
  * step referencing an unknown table is validated BEFORE the reconnect, so a
@@ -154,11 +166,12 @@ export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
154
166
  * @remarks
155
167
  * This unit deliberately OMITS `aggregate` / `transaction`. There is no native
156
168
  * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
157
- * `records` covers it. `transaction` is impossible here: the wrapper auto-commits
158
- * an `IDBTransaction` when control yields outside its request chain, so arbitrary
159
- * callback awaits cannot remain inside one native transaction. Every atomic
160
- * multi-operation sequence in this driver
161
- * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.
169
+ * `records` covers it. `transaction` is impossible here: the wrapper
170
+ * auto-commits an `IDBTransaction` when control yields outside its request
171
+ * chain, so arbitrary callback awaits cannot remain inside one native
172
+ * transaction. Every atomic multi-operation sequence in this driver
173
+ * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
174
+ * scope.
162
175
  */
163
176
  export declare class IndexedDBDriver implements DriverInterface {
164
177
  #private;
@@ -176,7 +189,7 @@ export declare class IndexedDBDriver implements DriverInterface {
176
189
  stream(table: string, input: QueryInput_2): AsyncIterable<Row>;
177
190
  snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
178
191
  /**
179
- * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
192
+ * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
180
193
  * never been stamped.
181
194
  *
182
195
  * @remarks
@@ -189,13 +202,13 @@ export declare class IndexedDBDriver implements DriverInterface {
189
202
  */
190
203
  metadata(): Promise<DriverMetadata | undefined>;
191
204
  /**
192
- * Persist an owned metadata snapshot for a later `metadata()` to return.
205
+ * Persists an owned metadata snapshot for a later `metadata()` to return.
193
206
  *
194
207
  * @param metadata - The {@link DriverMetadata} to persist
195
208
  */
196
209
  stamp(metadata: DriverMetadata): Promise<void>;
197
210
  /**
198
- * Apply a {@link Migration} plan by reconnecting at a bumped version and
211
+ * Applies a {@link Migration} plan by reconnecting at a bumped version and
199
212
  * running the plan's steps inside the wrapper's `upgrade` hook.
200
213
  *
201
214
  * @remarks
@@ -220,7 +233,7 @@ export declare class IndexedDBDriver implements DriverInterface {
220
233
  }
221
234
 
222
235
  /**
223
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
236
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
224
237
  * — the default mapping used everywhere except inside `migrate()`.
225
238
  *
226
239
  * @remarks
@@ -246,7 +259,7 @@ export declare class IndexedDBDriver implements DriverInterface {
246
259
  export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
247
260
 
248
261
  /**
249
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
262
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
250
263
  * for use INSIDE `migrate()` — the one context where `UPGRADE` means the
251
264
  * migration itself failed, not a generic driver fault.
252
265
  *
@@ -263,10 +276,19 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
263
276
  */
264
277
  export declare function mapMigrationError(error: IndexedDBError): DatabaseError;
265
278
 
279
+ /**
280
+ * Names the reserved out-of-line store the {@link IndexedDBDriver} stamps its
281
+ * {@link DriverMetadata} into.
282
+ *
283
+ * @remarks
284
+ * Backs the driver's `metadata` / `stamp` hooks. A user table declared with this
285
+ * exact name collides with the driver's own bookkeeping, so a caller must avoid
286
+ * it; the collision is caught at `open`.
287
+ */
266
288
  export declare const METADATA_STORE = "__metadata__";
267
289
 
268
290
  /**
269
- * A pushdown plan — an optional index and optional `IDBKeyRange` used to narrow
291
+ * Represents a pushdown plan — an optional index and optional `IDBKeyRange` used to narrow
270
292
  * a read. An omitted `index` selects the primary store; an omitted `range`
271
293
  * performs a full scan. The plan is always a superset of the matching rows;
272
294
  * the core engine refines it to the exact result. An empty plan (`{}`) is a
@@ -278,7 +300,7 @@ export declare interface QueryPlan {
278
300
  }
279
301
 
280
302
  /**
281
- * Project a table schema into the IndexedDB wrapper's store definition.
303
+ * Projects a table schema into the IndexedDB wrapper's store definition.
282
304
  *
283
305
  * @param schema - Portable table schema
284
306
  * @returns Store definition with declared indexes
@@ -286,7 +308,7 @@ export declare interface QueryPlan {
286
308
  export declare function schemaToStore(schema: TableSchema): StoreDefinition;
287
309
 
288
310
  /**
289
- * Plan an IndexedDB read for a {@link QueryInput} — pick the index (or the primary
311
+ * Plans an IndexedDB read for a {@link QueryInput} — picks the index (or the primary
290
312
  * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
291
313
  *
292
314
  * @remarks
@@ -1,18 +1,39 @@
1
- import { DatabaseError, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, compareValues, equalsValue, extractKey, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
2
- import { createIndexedDBDatabase, isIndexedDBError, rangeAboveKey, rangeBelowKey, rangeBetweenKeys, rangeExactKey, rangeFromKey, rangeToKey } from "@orkestrel/indexeddb";
1
+ import { DatabaseError, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, compareValues, equalsValue, extractKey, findColumn, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
2
+ import { createIndexedDBDatabase, isIndexedDBError, rangeAboveKey, rangeBelowKey, rangeFromKey, rangeToKey } from "@orkestrel/indexeddb";
3
3
  //#region src/browser/constants.ts
4
- var INDEXABLE_STORAGE = /* @__PURE__ */ new Set([
4
+ /**
5
+ * Lists the declared {@link ColumnStorage}s that are valid, orderable IndexedDB keys.
6
+ *
7
+ * @remarks
8
+ * `text` / `integer` / `real` occupy IndexedDB's string / number key space, so a
9
+ * column declared with one of them can back a store or index range read.
10
+ * `boolean` / `json` / `blob` are not valid `IDBValidKey`s and a range over one
11
+ * would silently miss rows, so `selectPlan` never pushes a condition down on
12
+ * them and the core engine answers the read instead. A frozen array, matching
13
+ * `EXACT_COLUMN_STORAGE` in `src/server`: a consumer holding it reads the
14
+ * membership with `includes` and cannot change the driver's pushdown behavior.
15
+ */
16
+ var INDEXABLE_STORAGE = Object.freeze([
5
17
  "text",
6
18
  "integer",
7
19
  "real"
8
20
  ]);
21
+ /**
22
+ * Names the reserved out-of-line store the {@link IndexedDBDriver} stamps its
23
+ * {@link DriverMetadata} into.
24
+ *
25
+ * @remarks
26
+ * Backs the driver's `metadata` / `stamp` hooks. A user table declared with this
27
+ * exact name collides with the driver's own bookkeeping, so a caller must avoid
28
+ * it; the collision is caught at `open`.
29
+ */
9
30
  var METADATA_STORE = "__metadata__";
10
31
  //#endregion
11
32
  //#region src/browser/helpers.ts
12
33
  /**
13
- * The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one
14
- * of the six exact key comparisons over scalar operands; otherwise
15
- * `undefined`.
34
+ * Translates one {@link Condition} to the `IDBKeyRange` it maps to, when its
35
+ * operator is one of the exact key comparisons over scalar operands; otherwise
36
+ * returns `undefined`.
16
37
  *
17
38
  * @remarks
18
39
  * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
@@ -23,7 +44,7 @@ var METADATA_STORE = "__metadata__";
23
44
  * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
24
45
  * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
25
46
  * `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
26
- * guard (`typeof` string/number) rejects a non-scalar value (e.g. an array, a
47
+ * guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
27
48
  * boolean) that is not a usable key. `between` additionally guards against a
28
49
  * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
29
50
  * `DataError` `DOMException` for a lower bound above the upper bound, so a
@@ -42,12 +63,12 @@ function conditionToRange(condition) {
42
63
  const first = condition.values[0];
43
64
  const second = condition.values[1];
44
65
  switch (condition.operator) {
45
- case "equals": return isKey(first) ? rangeExactKey(first) : void 0;
66
+ case "equals": return isKey(first) ? IDBKeyRange.only(first) : void 0;
46
67
  case "above": return isKey(first) ? rangeAboveKey(first) : void 0;
47
68
  case "below": return isKey(first) ? rangeBelowKey(first) : void 0;
48
69
  case "from": return isKey(first) ? rangeFromKey(first) : void 0;
49
70
  case "to": return isKey(first) ? rangeToKey(first) : void 0;
50
- case "between": return isKey(first) && isKey(second) && compareValues(first, second) <= 0 ? rangeBetweenKeys(first, second) : void 0;
71
+ case "between": return isKey(first) && isKey(second) && compareValues(first, second) <= 0 ? IDBKeyRange.bound(first, second) : void 0;
51
72
  case "not":
52
73
  case "like":
53
74
  case "glob":
@@ -60,7 +81,7 @@ function conditionToRange(condition) {
60
81
  }
61
82
  }
62
83
  /**
63
- * Plan an IndexedDB read for a {@link QueryInput} — pick the index (or the primary
84
+ * Plans an IndexedDB read for a {@link QueryInput} — picks the index (or the primary
64
85
  * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
65
86
  *
66
87
  * @remarks
@@ -129,8 +150,8 @@ function selectPlan(input, schema, available) {
129
150
  if (conditions.slice(1).some((condition) => condition.connector === "or")) return {};
130
151
  for (const condition of conditions) {
131
152
  if (typeof condition.column !== "string") continue;
132
- const column = schema.columns.find((candidate) => candidate.name === condition.column);
133
- if (column === void 0 || !INDEXABLE_STORAGE.has(column.storage)) continue;
153
+ const column = findColumn(condition.column, schema);
154
+ if (column === void 0 || !INDEXABLE_STORAGE.includes(column.storage)) continue;
134
155
  const range = conditionToRange(condition);
135
156
  if (range === void 0) continue;
136
157
  if (condition.column === schema.primary) return { range };
@@ -143,7 +164,7 @@ function selectPlan(input, schema, available) {
143
164
  return {};
144
165
  }
145
166
  /**
146
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
167
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
147
168
  * — the default mapping used everywhere except inside `migrate()`.
148
169
  *
149
170
  * @remarks
@@ -187,7 +208,7 @@ function mapIndexedDBError(error) {
187
208
  }
188
209
  }
189
210
  /**
190
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
211
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
191
212
  * for use INSIDE `migrate()` — the one context where `UPGRADE` means the
192
213
  * migration itself failed, not a generic driver fault.
193
214
  *
@@ -207,7 +228,7 @@ function mapMigrationError(error) {
207
228
  return mapIndexedDBError(error);
208
229
  }
209
230
  /**
210
- * Derive an IndexedDB index name for a declared column group — a bare column
231
+ * Derives an IndexedDB index name for a declared column group — a bare column
211
232
  * name for a single-column index, a deterministic collision-free encoding for a
212
233
  * compound one.
213
234
  *
@@ -240,7 +261,7 @@ function deriveIndexedDBIndexName(columns) {
240
261
  return `${columns.length}#${columns.map((part) => `${part.length}:${part}`).join("")}`;
241
262
  }
242
263
  /**
243
- * Project a table schema into the IndexedDB wrapper's store definition.
264
+ * Projects a table schema into the IndexedDB wrapper's store definition.
244
265
  *
245
266
  * @param schema - Portable table schema
246
267
  * @returns Store definition with declared indexes
@@ -257,8 +278,8 @@ function schemaToStore(schema) {
257
278
  //#endregion
258
279
  //#region src/browser/drivers/IndexedDBDriver.ts
259
280
  /**
260
- * The IndexedDB {@link DriverInterface} — the persistent browser backend, built on
261
- * the published `@orkestrel/indexeddb` wrapper.
281
+ * Implements the {@link DriverInterface} over IndexedDB — the persistent browser backend,
282
+ * built on the published `@orkestrel/indexeddb` wrapper.
262
283
  *
263
284
  * @remarks
264
285
  * A thin adapter: it implements the storage primitives the core database layer
@@ -274,12 +295,12 @@ function schemaToStore(schema) {
274
295
  * wrapper's native `getAll` / `getAllKeys`, and `snapshot` rolls back through one
275
296
  * atomic wrapper transaction.
276
297
  *
277
- * It also implements the optional native `records` / `stream` hooks
278
- * (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
298
+ * It also implements the optional native `records` / `stream` hooks:
299
+ * `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
279
300
  * key-range pushdown over the primary key or a single-column secondary index,
280
301
  * fetching a candidate **superset** that the core engine (`applyQuery` /
281
302
  * `matchesQuery`) then refines — so a native read is byte-identical to a full
282
- * scan, just cheaper. Pushdown is conservative: only the exact-comparison
303
+ * scan, only cheaper. Pushdown is conservative: only the exact-comparison
283
304
  * operators over orderable columns narrow to a range; everything else falls back
284
305
  * to a full scan + the engine.
285
306
  *
@@ -293,7 +314,7 @@ function schemaToStore(schema) {
293
314
  * transaction (`onupgradeneeded`), so `migrate` closes the current connection
294
315
  * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
295
316
  * plan's steps — dropping stores, adding/removing indexes on the raw
296
- * `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk
317
+ * `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
297
318
  * (the one step needing to touch existing data; `column.add` is a no-op — this
298
319
  * driver stores whatever a row carries, so there is nothing to backfill). A
299
320
  * step referencing an unknown table is validated BEFORE the reconnect, so a
@@ -302,11 +323,12 @@ function schemaToStore(schema) {
302
323
  * @remarks
303
324
  * This unit deliberately OMITS `aggregate` / `transaction`. There is no native
304
325
  * `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
305
- * `records` covers it. `transaction` is impossible here: the wrapper auto-commits
306
- * an `IDBTransaction` when control yields outside its request chain, so arbitrary
307
- * callback awaits cannot remain inside one native transaction. Every atomic
308
- * multi-operation sequence in this driver
309
- * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.
326
+ * `records` covers it. `transaction` is impossible here: the wrapper
327
+ * auto-commits an `IDBTransaction` when control yields outside its request
328
+ * chain, so arbitrary callback awaits cannot remain inside one native
329
+ * transaction. Every atomic multi-operation sequence in this driver
330
+ * (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
331
+ * scope.
310
332
  */
311
333
  var IndexedDBDriver = class {
312
334
  #name;
@@ -434,30 +456,6 @@ var IndexedDBDriver = class {
434
456
  validatePage(input);
435
457
  return this.#stream(table, input);
436
458
  }
437
- async *#stream(table, input) {
438
- try {
439
- const schema = this.#table(table);
440
- const store = this.#store(table);
441
- const plan = selectPlan(input, schema, store.indexes);
442
- const conditions = input.conditions ?? [];
443
- const offset = input.offset ?? 0;
444
- const limit = input.limit;
445
- let skipped = 0;
446
- let yielded = 0;
447
- for (const row of await this.#candidates(store, schema, plan)) {
448
- if (limit !== void 0 && yielded >= limit) break;
449
- if (!matchesQuery(row, conditions)) continue;
450
- if (skipped < offset) {
451
- skipped += 1;
452
- continue;
453
- }
454
- yielded += 1;
455
- yield row;
456
- }
457
- } catch (error) {
458
- throw this.#wrap(error);
459
- }
460
- }
461
459
  async snapshot(tables) {
462
460
  try {
463
461
  const database = this.#require();
@@ -529,7 +527,7 @@ var IndexedDBDriver = class {
529
527
  }
530
528
  }
531
529
  /**
532
- * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
530
+ * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
533
531
  * never been stamped.
534
532
  *
535
533
  * @remarks
@@ -548,7 +546,7 @@ var IndexedDBDriver = class {
548
546
  }
549
547
  }
550
548
  /**
551
- * Persist an owned metadata snapshot for a later `metadata()` to return.
549
+ * Persists an owned metadata snapshot for a later `metadata()` to return.
552
550
  *
553
551
  * @param metadata - The {@link DriverMetadata} to persist
554
552
  */
@@ -565,7 +563,7 @@ var IndexedDBDriver = class {
565
563
  }
566
564
  }
567
565
  /**
568
- * Apply a {@link Migration} plan by reconnecting at a bumped version and
566
+ * Applies a {@link Migration} plan by reconnecting at a bumped version and
569
567
  * running the plan's steps inside the wrapper's `upgrade` hook.
570
568
  *
571
569
  * @remarks
@@ -647,6 +645,30 @@ var IndexedDBDriver = class {
647
645
  throw cause;
648
646
  }
649
647
  }
648
+ async *#stream(table, input) {
649
+ try {
650
+ const schema = this.#table(table);
651
+ const store = this.#store(table);
652
+ const plan = selectPlan(input, schema, store.indexes);
653
+ const conditions = input.conditions ?? [];
654
+ const offset = input.offset ?? 0;
655
+ const limit = input.limit;
656
+ let skipped = 0;
657
+ let yielded = 0;
658
+ for (const row of await this.#candidates(store, schema, plan)) {
659
+ if (limit !== void 0 && yielded >= limit) break;
660
+ if (!matchesQuery(row, conditions)) continue;
661
+ if (skipped < offset) {
662
+ skipped += 1;
663
+ continue;
664
+ }
665
+ yielded += 1;
666
+ yield row;
667
+ }
668
+ } catch (error) {
669
+ throw this.#wrap(error);
670
+ }
671
+ }
650
672
  async #mutate(table, options, scope) {
651
673
  const signal = options?.signal;
652
674
  checkAbort(signal);
@@ -759,31 +781,33 @@ var IndexedDBDriver = class {
759
781
  return schema;
760
782
  }
761
783
  async #upgrade(input, added, context) {
762
- for (const name of added) if (name !== "__metadata__" && context.stores.includes(name)) context.drop(name);
784
+ for (const name of added) if (name !== "__metadata__" && context.stores.names.includes(name)) context.stores.drop(name);
763
785
  for (const step of input.plan.steps) switch (step.operation) {
764
786
  case "table.add":
765
- context.create(step.table.name, schemaToStore(step.table));
787
+ context.stores.create(step.table.name, schemaToStore(step.table));
766
788
  break;
767
789
  case "table.remove":
768
- context.drop(step.table);
790
+ context.stores.drop(step.table);
769
791
  break;
770
792
  case "index.add": {
771
793
  const name = deriveIndexedDBIndexName(step.index);
772
794
  const [column] = step.index;
773
795
  const path = step.index.length === 1 && column !== void 0 ? column : [...step.index];
774
- context.index(step.table, {
796
+ context.indexes.create(step.table, {
775
797
  name,
776
798
  path
777
799
  });
778
800
  break;
779
801
  }
780
802
  case "index.remove":
781
- context.deindex(step.table, deriveIndexedDBIndexName(step.index));
803
+ context.indexes.drop(step.table, deriveIndexedDBIndexName(step.index));
782
804
  break;
783
805
  case "column.remove": {
784
- let cursor = await context.store(step.table).cursor();
806
+ let cursor = await context.stores.store(step.table).cursor();
785
807
  while (cursor !== null) {
786
- const [migrated] = migrateRows([cursor.value], [step]);
808
+ const row = cursor.value;
809
+ if (row === void 0) throw new DatabaseError("MIGRATION", "migrate: stored value is not a record", { table: step.table });
810
+ const [migrated] = migrateRows([row], [step]);
787
811
  if (migrated === void 0) throw new DatabaseError("MIGRATION", "migrate: transformed row is missing", { table: step.table });
788
812
  await cursor.update(migrated);
789
813
  cursor = await cursor.continue();
@@ -791,7 +815,7 @@ var IndexedDBDriver = class {
791
815
  break;
792
816
  }
793
817
  }
794
- if (input.metadata !== void 0) await context.store(METADATA_STORE).set({
818
+ if (input.metadata !== void 0) await context.stores.store(METADATA_STORE).set({
795
819
  version: input.metadata.version,
796
820
  schema: input.metadata.schema
797
821
  }, "metadata");
@@ -800,7 +824,7 @@ var IndexedDBDriver = class {
800
824
  //#endregion
801
825
  //#region src/browser/factories.ts
802
826
  /**
803
- * Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
827
+ * Creates a persistent IndexedDB {@link DriverInterface} for the core database layer.
804
828
  *
805
829
  * @remarks
806
830
  * Pass it to `createDatabase` from `@orkestrel/database` to run the typed database