@orkestrel/database 0.0.12 → 0.0.14

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
@@ -1,11 +1,15 @@
1
1
  # @orkestrel/database
2
2
 
3
- A typed database abstraction for the `@orkestrel` line one public
4
- `Database` over internal table/query engines and pluggable storage drivers at
5
- the seams. Consumers use `TableInterface` and `QueryInterface`. Built to sit beside
6
- `@orkestrel/contract` (validation) and `@orkestrel/emitter` (observable
7
- lifecycle), reusing both directly. `TableInterface.cursor()` exposes the
8
- `CursorInterface` contract for serial bulk mutation.
3
+ > One typed database API for keyed rows, fluent queries, cursors, and
4
+ > whole-store transactions, running unchanged over an in-memory map, a JSON
5
+ > file, SQLite, or IndexedDB.
6
+
7
+ Declare your tables with the `createDatabase` function, hold each
8
+ `TableInterface` it hands back, and reach rows by key or through the fluent
9
+ `query()` builder. `TableInterface.cursor()` opens the `CursorInterface`
10
+ contract for serial bulk mutation. Built on `@orkestrel/contract` for
11
+ validation and `@orkestrel/emitter` for the observation surface, reusing both
12
+ directly. Part of the `@orkestrel` line.
9
13
 
10
14
  ## Install
11
15
 
@@ -23,10 +27,10 @@ npm install @orkestrel/database
23
27
 
24
28
  ## Status
25
29
 
26
- Pre-release (`0.0.7`): the core engine and the memory, JSON file, SQLite,
30
+ Pre-release: the core engine and the memory, JSON file, SQLite,
27
31
  and IndexedDB drivers are all implemented and tested, but the public API is
28
32
  still unstable and may change without notice. See
29
- [guides/src/database.md](./guides/src/database.md) for the full documented
33
+ [guides/database.md](./guides/database.md) for the full documented
30
34
  surface.
31
35
 
32
36
  ## Package
@@ -1,37 +1,34 @@
1
- import { ColumnStorage } from '@orkestrel/database';
2
- import { Condition } from '@orkestrel/database';
1
+ import type { ColumnStorage } from '@orkestrel/database';
2
+ import type { Condition } from '@orkestrel/database';
3
3
  import { DatabaseError } from '@orkestrel/database';
4
- import { DriverInterface } from '@orkestrel/database';
5
- import { DriverInterface as DriverInterface_2 } from '@orkestrel/database';
6
- import { DriverMetadata } from '@orkestrel/database';
7
- import { IndexedDBError } from '@orkestrel/indexeddb';
8
- import { Key } from '@orkestrel/database';
9
- import { MigrationInput } from '@orkestrel/database';
10
- import { OperationOptions } from '@orkestrel/database';
11
- import { QueryInput } from '@orkestrel/database';
12
- import { QueryInput as QueryInput_2 } from '@orkestrel/database';
13
- import { Row } from '@orkestrel/database';
14
- import { StoreDefinition } from '@orkestrel/indexeddb';
15
- import { TableSchema } from '@orkestrel/database';
16
- import { TableSchema as TableSchema_2 } from '@orkestrel/database';
4
+ import type { DriverInterface } from '@orkestrel/database';
5
+ import type { DriverMetadata } from '@orkestrel/database';
6
+ import type { IndexedDBError } from '@orkestrel/indexeddb';
7
+ import type { Key } from '@orkestrel/database';
8
+ import type { MigrationInput } from '@orkestrel/database';
9
+ import type { OperationOptions } from '@orkestrel/database';
10
+ import type { QueryInput } from '@orkestrel/database';
11
+ import type { Row } from '@orkestrel/database';
12
+ import type { StoreDefinition } from '@orkestrel/indexeddb';
13
+ import type { TableSchema } from '@orkestrel/database';
17
14
 
18
15
  /**
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`.
16
+ * Translates one {@link Condition} to the `IDBKeyRange` it maps to, when its
17
+ * operator is one of the exact key comparisons over scalar operands; otherwise
18
+ * returns `undefined`.
22
19
  *
23
20
  * @remarks
24
21
  * Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
25
22
  * translate to a key range that a typed (string/number) column can back with an
26
23
  * IndexedDB store/index read — see {@link selectPlan} for the caveats that
27
- * decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the
24
+ * decide which of `below`/`to` may drive a secondary-index read versus the
28
25
  * primary store only (a column-type / absent-row concern, not a range-shape
29
26
  * one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
30
27
  * the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
31
28
  * `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
29
+ * guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
33
30
  * boolean) that is not a usable key. `between` additionally guards against a
34
- * REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
31
+ * reversed pair (`first > second`): native `IDBKeyRange.bound` throws a raw
35
32
  * `DataError` `DOMException` for a lower bound above the upper bound, so a
36
33
  * reversed pair returns `undefined` here (falls back to a full scan, which the
37
34
  * engine then correctly resolves to an empty result) rather than letting a
@@ -47,7 +44,7 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
47
44
  export declare function conditionToRange(condition: Condition): IDBKeyRange | undefined;
48
45
 
49
46
  /**
50
- * Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
47
+ * Creates a persistent IndexedDB {@link DriverInterface} for a browser database name.
51
48
  *
52
49
  * @remarks
53
50
  * Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
@@ -74,23 +71,23 @@ export declare function conditionToRange(condition: Condition): IDBKeyRange | un
74
71
  * await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB
75
72
  * ```
76
73
  */
77
- export declare function createIndexedDBDriver(name: string): DriverInterface_2;
74
+ export declare function createIndexedDBDriver(name: string): DriverInterface;
78
75
 
79
76
  /**
80
- * Derive an IndexedDB index name for a declared column group — a bare column
77
+ * Derives an IndexedDB index name for a declared column group — a bare column
81
78
  * name for a single-column index, a deterministic collision-free encoding for a
82
79
  * compound one.
83
80
  *
84
81
  * @remarks
85
82
  * Naming a compound index by joining its columns with `_` (`['a', 'b'] →
86
- * 'a_b'`) collides with a single-column index over a column LITERALLY named
83
+ * 'a_b'`) collides with a single-column index over a column literally named
87
84
  * `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
88
85
  * which either throws a native `ConstraintError` from a duplicate
89
86
  * `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
90
- * lookup match the wrong index. A single-column index keeps the BARE column
87
+ * lookup match the wrong index. A single-column index keeps the bare column
91
88
  * name — {@link selectPlan} matches `available.includes(condition.column)` by
92
89
  * that exact name, so a single-column index must stay named after its column
93
- * verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED
90
+ * verbatim. A compound index instead encodes each column as a length-prefixed
94
91
  * segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
95
92
  * and cannot be reconstructed by any other column list — including one
96
93
  * containing a column that happens to look like an encoded segment.
@@ -106,11 +103,23 @@ export declare function createIndexedDBDriver(name: string): DriverInterface_2;
106
103
  */
107
104
  export declare function deriveIndexedDBIndexName(columns: readonly string[]): string;
108
105
 
109
- export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
106
+ /**
107
+ * Lists the declared {@link ColumnStorage}s that are valid, orderable IndexedDB keys.
108
+ *
109
+ * @remarks
110
+ * `text` / `integer` / `real` occupy IndexedDB's string / number key space, so a
111
+ * column declared with one of them can back a store or index range read.
112
+ * `boolean` / `json` / `blob` are not valid `IDBValidKey`s and a range over one
113
+ * would silently miss rows, so `selectPlan` never pushes a condition down on
114
+ * them and the core engine answers the read instead. A frozen array, matching
115
+ * `EXACT_COLUMN_STORAGE` in `src/server`: a consumer holding it reads the
116
+ * membership with `includes` and cannot change the driver's pushdown behavior.
117
+ */
118
+ export declare const INDEXABLE_STORAGE: readonly ColumnStorage[];
110
119
 
111
120
  /**
112
- * The IndexedDB {@link DriverInterface} — the persistent browser backend, built on
113
- * the published `@orkestrel/indexeddb` wrapper.
121
+ * Implements the {@link DriverInterface} over IndexedDB — the persistent browser backend,
122
+ * built on the published `@orkestrel/indexeddb` wrapper.
114
123
  *
115
124
  * @remarks
116
125
  * A thin adapter: it implements the storage primitives the core database layer
@@ -126,12 +135,12 @@ export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
126
135
  * wrapper's native `getAll` / `getAllKeys`, and `snapshot` rolls back through one
127
136
  * atomic wrapper transaction.
128
137
  *
129
- * It also implements the optional native `records` / `stream` hooks
130
- * (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
138
+ * It also implements the optional native `records` / `stream` hooks:
139
+ * `selectPlan` ({@link selectPlan}) turns the {@link QueryInput} into a
131
140
  * key-range pushdown over the primary key or a single-column secondary index,
132
141
  * fetching a candidate **superset** that the core engine (`applyQuery` /
133
142
  * `matchesQuery`) then refines — so a native read is byte-identical to a full
134
- * scan, just cheaper. Pushdown is conservative: only the exact-comparison
143
+ * scan, only cheaper. Pushdown is conservative: only the exact-comparison
135
144
  * operators over orderable columns narrow to a range; everything else falls back
136
145
  * to a full scan + the engine.
137
146
  *
@@ -143,27 +152,28 @@ export declare const INDEXABLE_STORAGE: ReadonlySet<ColumnStorage>;
143
152
  * {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
144
153
  * store, creating/dropping an index) is legal only inside a versionchange
145
154
  * transaction (`onupgradeneeded`), so `migrate` closes the current connection
146
- * and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
155
+ * and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
147
156
  * plan's steps — dropping stores, adding/removing indexes on the raw
148
- * `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk
157
+ * `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
149
158
  * (the one step needing to touch existing data; `column.add` is a no-op — this
150
159
  * driver stores whatever a row carries, so there is nothing to backfill). A
151
- * step referencing an unknown table is validated BEFORE the reconnect, so a
160
+ * step referencing an unknown table is validated before the reconnect, so a
152
161
  * `MIGRATION` `DatabaseError` never wastes a version bump.
153
162
  *
154
163
  * @remarks
155
- * This unit deliberately OMITS `aggregate` / `transaction`. There is no native
164
+ * This unit deliberately omits `aggregate` / `transaction`. There is no native
156
165
  * `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.
166
+ * `records` covers it. `transaction` is impossible here: the wrapper
167
+ * auto-commits an `IDBTransaction` when control yields outside its request
168
+ * chain, so arbitrary callback awaits cannot remain inside one native
169
+ * transaction. Every atomic multi-operation sequence in this driver
170
+ * (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
171
+ * scope.
162
172
  */
163
173
  export declare class IndexedDBDriver implements DriverInterface {
164
174
  #private;
165
175
  constructor(name: string);
166
- open(schema: readonly TableSchema_2[]): Promise<void>;
176
+ open(schema: readonly TableSchema[]): Promise<void>;
167
177
  close(): Promise<void>;
168
178
  read(table: string, key: Key): Promise<Row | undefined>;
169
179
  write(table: string, key: Key, row: Row, options?: OperationOptions): Promise<void>;
@@ -172,11 +182,11 @@ export declare class IndexedDBDriver implements DriverInterface {
172
182
  keys(table: string): Promise<readonly Key[]>;
173
183
  scan(table: string): AsyncIterable<Row>;
174
184
  clear(table: string): Promise<void>;
175
- records(table: string, input: QueryInput_2): Promise<readonly Row[]>;
176
- stream(table: string, input: QueryInput_2): AsyncIterable<Row>;
185
+ records(table: string, input: QueryInput): Promise<readonly Row[]>;
186
+ stream(table: string, input: QueryInput): AsyncIterable<Row>;
177
187
  snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
178
188
  /**
179
- * Return the persisted {@link DriverMetadata}, or `undefined` when the store has
189
+ * Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
180
190
  * never been stamped.
181
191
  *
182
192
  * @remarks
@@ -189,22 +199,22 @@ export declare class IndexedDBDriver implements DriverInterface {
189
199
  */
190
200
  metadata(): Promise<DriverMetadata | undefined>;
191
201
  /**
192
- * Persist an owned metadata snapshot for a later `metadata()` to return.
202
+ * Persists an owned metadata snapshot for a later `metadata()` to return.
193
203
  *
194
204
  * @param metadata - The {@link DriverMetadata} to persist
195
205
  */
196
206
  stamp(metadata: DriverMetadata): Promise<void>;
197
207
  /**
198
- * Apply a {@link Migration} plan by reconnecting at a bumped version and
208
+ * Applies a {@link Migration} plan by reconnecting at a bumped version and
199
209
  * running the plan's steps inside the wrapper's `upgrade` hook.
200
210
  *
201
211
  * @remarks
202
212
  * IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
203
- * the current connection and opens a FRESH one at `version + 1`, declaring
204
- * every currently-known store (plus {@link METADATA_STORE}) so nothing is lost,
205
- * and applying `table.remove` / `index.add` / `index.remove` /
213
+ * the current connection and opens a fresh one at `version + 1`, declaring
214
+ * every store known at that point (plus {@link METADATA_STORE}) so nothing is
215
+ * lost, and applying `table.remove` / `index.add` / `index.remove` /
206
216
  * `column.remove` inside `upgrade`. Every step's `table` is validated against
207
- * the driver's own `#schema` BEFORE the reconnect — an unknown-table step
217
+ * the driver's own `#schema` before the reconnect — an unknown-table step
208
218
  * throws `DatabaseError` `MIGRATION` without ever bumping the version.
209
219
  * `table.add` / `column.add` need no upgrade-time action: `table.add` is
210
220
  * created by the wrapper's built-in create-missing-stores pass (its
@@ -220,7 +230,7 @@ export declare class IndexedDBDriver implements DriverInterface {
220
230
  }
221
231
 
222
232
  /**
223
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
233
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
224
234
  * — the default mapping used everywhere except inside `migrate()`.
225
235
  *
226
236
  * @remarks
@@ -246,8 +256,8 @@ export declare class IndexedDBDriver implements DriverInterface {
246
256
  export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
247
257
 
248
258
  /**
249
- * Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
250
- * for use INSIDE `migrate()` — the one context where `UPGRADE` means the
259
+ * Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
260
+ * for use inside `migrate()` — the one context where `UPGRADE` means the
251
261
  * migration itself failed, not a generic driver fault.
252
262
  *
253
263
  * @remarks
@@ -263,14 +273,24 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
263
273
  */
264
274
  export declare function mapMigrationError(error: IndexedDBError): DatabaseError;
265
275
 
276
+ /**
277
+ * Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
278
+ * stamps its {@link DriverMetadata} into.
279
+ *
280
+ * @remarks
281
+ * Backs the driver's `metadata` / `stamp` hooks. A user table named `__metadata__`
282
+ * collides with the driver's own bookkeeping, so a caller must avoid it; the
283
+ * collision is caught at `open`.
284
+ */
266
285
  export declare const METADATA_STORE = "__metadata__";
267
286
 
268
287
  /**
269
- * A pushdown plan — an optional index and optional `IDBKeyRange` used to narrow
288
+ * Represents a pushdown plan — an optional index and optional `IDBKeyRange` used to narrow
270
289
  * a read. An omitted `index` selects the primary store; an omitted `range`
271
290
  * performs a full scan. The plan is always a superset of the matching rows;
272
291
  * the core engine refines it to the exact result. An empty plan (`{}`) is a
273
- * primary-store full scan.
292
+ * primary-store full scan. The `@orkestrel/database/browser` entry point exports
293
+ * this type.
274
294
  */
275
295
  export declare interface QueryPlan {
276
296
  readonly index?: string;
@@ -278,7 +298,12 @@ export declare interface QueryPlan {
278
298
  }
279
299
 
280
300
  /**
281
- * Project a table schema into the IndexedDB wrapper's store definition.
301
+ * Projects a table schema into the IndexedDB wrapper's store definition.
302
+ *
303
+ * @remarks
304
+ * The definition is what an ordered versionchange migration creates the store
305
+ * from, carrying each declared index under the name {@link deriveIndexedDBIndexName}
306
+ * derives for its column group.
282
307
  *
283
308
  * @param schema - Portable table schema
284
309
  * @returns Store definition with declared indexes
@@ -286,11 +311,11 @@ export declare interface QueryPlan {
286
311
  export declare function schemaToStore(schema: TableSchema): StoreDefinition;
287
312
 
288
313
  /**
289
- * Plan an IndexedDB read for a {@link QueryInput} — pick the index (or the primary
314
+ * Plans an IndexedDB read for a {@link QueryInput} — picks the index (or the primary
290
315
  * store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
291
316
  *
292
317
  * @remarks
293
- * Pushdown is sound ONLY when every condition is `and`-joined: the engine folds
318
+ * Pushdown is sound only when every condition is `and`-joined: the engine folds
294
319
  * conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
295
320
  * each — narrowing on any one is then a valid superset. A single `or` breaks that
296
321
  * (a row can match through a later condition the range would exclude), so any `or`
@@ -304,21 +329,21 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
304
329
  * type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
305
330
  * non-scalar operand cannot push and is skipped.
306
331
  *
307
- * **`below`/`to` may drive a SECONDARY-index range only when the column has NO
332
+ * **`below`/`to` may drive a secondary-index range only when the column has no
308
333
  * absent/null rows to lose — which this planner cannot verify from the schema
309
- * alone, so it restricts them to the PRIMARY store, where that is always true.**
310
- * The engine's total order (`compareValues`, see `@src/core`) ranks
311
- * `undefined` (absent) and `null` BELOW every number/string, so
312
- * `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent
313
- * or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose
334
+ * alone, so it restricts them to the primary store, where that is always true.**
335
+ * The engine's total order (`compareValues`, see `@orkestrel/database`) ranks
336
+ * `undefined` (absent) and `null` below every number/string, so
337
+ * `matchesCondition('below' | 'to', …)` is true for a row whose field is absent
338
+ * or `null` — but a secondary IndexedDB index has no entry for a row whose
314
339
  * indexed field is absent/`null`, so a `below`/`to` range read against that
315
- * index would SILENTLY DROP those rows (they can never be over-fetched, only
340
+ * index would silently drop those rows (they can never be over-fetched, only
316
341
  * missed — the one shape of lossiness this planner must never produce). The
317
- * table's PRIMARY key is exempt: a row's primary-key value is always present
342
+ * table's primary key is exempt: a row's primary-key value is always present
318
343
  * and never `null` (it is the row's identity, enforced at write time), so a
319
344
  * `below`/`to` range against the primary store can never exclude an
320
345
  * absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
321
- * `between` stay index-eligible on ANY orderable column, primary or secondary:
346
+ * `between` stay index-eligible on any orderable column, primary or secondary:
322
347
  * each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
323
348
  * `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
324
349
  * in the total order, so an absent/null-valued row can never satisfy them — the
@@ -332,7 +357,7 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
332
357
  * metadata.
333
358
  *
334
359
  * When no condition qualifies the plan is a full scan (`{}`) and the engine
335
- * does everything. The plan is always a SUPERSET of the
360
+ * does everything. The plan is always a superset of the
336
361
  * matching rows — the only correctness contract — so the driver may safely run
337
362
  * the exact engine over it.
338
363
  *