@orkestrel/database 0.0.13 → 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 +10 -6
- package/dist/src/browser/index.d.ts +54 -51
- package/dist/src/browser/index.js +35 -30
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +109 -42
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +274 -61
- package/dist/src/core/index.d.ts +274 -61
- package/dist/src/core/index.js +109 -42
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +54 -34
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +93 -76
- package/dist/src/server/index.d.ts +93 -76
- package/dist/src/server/index.js +54 -34
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -16
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# @orkestrel/database
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`
|
|
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
|
|
|
@@ -1,19 +1,16 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
16
|
* Translates one {@link Condition} to the `IDBKeyRange` it maps to, when its
|
|
@@ -24,14 +21,14 @@ import { TableSchema as TableSchema_2 } from '@orkestrel/database';
|
|
|
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
|
|
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
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
|
-
*
|
|
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
|
-
* Creates a persistent IndexedDB {@link DriverInterface} for
|
|
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,7 +71,7 @@ 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):
|
|
74
|
+
export declare function createIndexedDBDriver(name: string): DriverInterface;
|
|
78
75
|
|
|
79
76
|
/**
|
|
80
77
|
* Derives an IndexedDB index name for a declared column group — a bare column
|
|
@@ -83,14 +80,14 @@ export declare function createIndexedDBDriver(name: string): DriverInterface_2;
|
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -155,28 +152,28 @@ export declare const INDEXABLE_STORAGE: readonly ColumnStorage[];
|
|
|
155
152
|
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
156
153
|
* store, creating/dropping an index) is legal only inside a versionchange
|
|
157
154
|
* transaction (`onupgradeneeded`), so `migrate` closes the current connection
|
|
158
|
-
* and opens a
|
|
155
|
+
* and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
|
|
159
156
|
* plan's steps — dropping stores, adding/removing indexes on the raw
|
|
160
157
|
* `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
|
|
161
158
|
* (the one step needing to touch existing data; `column.add` is a no-op — this
|
|
162
159
|
* driver stores whatever a row carries, so there is nothing to backfill). A
|
|
163
|
-
* step referencing an unknown table is validated
|
|
160
|
+
* step referencing an unknown table is validated before the reconnect, so a
|
|
164
161
|
* `MIGRATION` `DatabaseError` never wastes a version bump.
|
|
165
162
|
*
|
|
166
163
|
* @remarks
|
|
167
|
-
* This unit deliberately
|
|
164
|
+
* This unit deliberately omits `aggregate` / `transaction`. There is no native
|
|
168
165
|
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
169
166
|
* `records` covers it. `transaction` is impossible here: the wrapper
|
|
170
167
|
* auto-commits an `IDBTransaction` when control yields outside its request
|
|
171
168
|
* chain, so arbitrary callback awaits cannot remain inside one native
|
|
172
169
|
* transaction. Every atomic multi-operation sequence in this driver
|
|
173
|
-
* (`snapshot`'s rollback) instead runs entirely inside
|
|
170
|
+
* (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
|
|
174
171
|
* scope.
|
|
175
172
|
*/
|
|
176
173
|
export declare class IndexedDBDriver implements DriverInterface {
|
|
177
174
|
#private;
|
|
178
175
|
constructor(name: string);
|
|
179
|
-
open(schema: readonly
|
|
176
|
+
open(schema: readonly TableSchema[]): Promise<void>;
|
|
180
177
|
close(): Promise<void>;
|
|
181
178
|
read(table: string, key: Key): Promise<Row | undefined>;
|
|
182
179
|
write(table: string, key: Key, row: Row, options?: OperationOptions): Promise<void>;
|
|
@@ -185,8 +182,8 @@ export declare class IndexedDBDriver implements DriverInterface {
|
|
|
185
182
|
keys(table: string): Promise<readonly Key[]>;
|
|
186
183
|
scan(table: string): AsyncIterable<Row>;
|
|
187
184
|
clear(table: string): Promise<void>;
|
|
188
|
-
records(table: string, input:
|
|
189
|
-
stream(table: string, input:
|
|
185
|
+
records(table: string, input: QueryInput): Promise<readonly Row[]>;
|
|
186
|
+
stream(table: string, input: QueryInput): AsyncIterable<Row>;
|
|
190
187
|
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
191
188
|
/**
|
|
192
189
|
* Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
@@ -213,11 +210,11 @@ export declare class IndexedDBDriver implements DriverInterface {
|
|
|
213
210
|
*
|
|
214
211
|
* @remarks
|
|
215
212
|
* IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
|
|
216
|
-
* the current connection and opens a
|
|
217
|
-
* every
|
|
218
|
-
* 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` /
|
|
219
216
|
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
220
|
-
* the driver's own `#schema`
|
|
217
|
+
* the driver's own `#schema` before the reconnect — an unknown-table step
|
|
221
218
|
* throws `DatabaseError` `MIGRATION` without ever bumping the version.
|
|
222
219
|
* `table.add` / `column.add` need no upgrade-time action: `table.add` is
|
|
223
220
|
* created by the wrapper's built-in create-missing-stores pass (its
|
|
@@ -260,7 +257,7 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
|
|
|
260
257
|
|
|
261
258
|
/**
|
|
262
259
|
* Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
263
|
-
* for use
|
|
260
|
+
* for use inside `migrate()` — the one context where `UPGRADE` means the
|
|
264
261
|
* migration itself failed, not a generic driver fault.
|
|
265
262
|
*
|
|
266
263
|
* @remarks
|
|
@@ -277,13 +274,13 @@ export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
|
|
|
277
274
|
export declare function mapMigrationError(error: IndexedDBError): DatabaseError;
|
|
278
275
|
|
|
279
276
|
/**
|
|
280
|
-
* Names the reserved out-of-line store the {@link IndexedDBDriver}
|
|
281
|
-
* {@link DriverMetadata} into.
|
|
277
|
+
* Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
|
|
278
|
+
* stamps its {@link DriverMetadata} into.
|
|
282
279
|
*
|
|
283
280
|
* @remarks
|
|
284
|
-
* Backs the driver's `metadata` / `stamp` hooks. A user table
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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`.
|
|
287
284
|
*/
|
|
288
285
|
export declare const METADATA_STORE = "__metadata__";
|
|
289
286
|
|
|
@@ -292,7 +289,8 @@ export declare const METADATA_STORE = "__metadata__";
|
|
|
292
289
|
* a read. An omitted `index` selects the primary store; an omitted `range`
|
|
293
290
|
* performs a full scan. The plan is always a superset of the matching rows;
|
|
294
291
|
* the core engine refines it to the exact result. An empty plan (`{}`) is a
|
|
295
|
-
* primary-store full scan.
|
|
292
|
+
* primary-store full scan. The `@orkestrel/database/browser` entry point exports
|
|
293
|
+
* this type.
|
|
296
294
|
*/
|
|
297
295
|
export declare interface QueryPlan {
|
|
298
296
|
readonly index?: string;
|
|
@@ -302,6 +300,11 @@ export declare interface QueryPlan {
|
|
|
302
300
|
/**
|
|
303
301
|
* Projects a table schema into the IndexedDB wrapper's store definition.
|
|
304
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.
|
|
307
|
+
*
|
|
305
308
|
* @param schema - Portable table schema
|
|
306
309
|
* @returns Store definition with declared indexes
|
|
307
310
|
*/
|
|
@@ -312,7 +315,7 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
|
|
|
312
315
|
* store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
|
|
313
316
|
*
|
|
314
317
|
* @remarks
|
|
315
|
-
* Pushdown is sound
|
|
318
|
+
* Pushdown is sound only when every condition is `and`-joined: the engine folds
|
|
316
319
|
* conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
|
|
317
320
|
* each — narrowing on any one is then a valid superset. A single `or` breaks that
|
|
318
321
|
* (a row can match through a later condition the range would exclude), so any `or`
|
|
@@ -326,21 +329,21 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
|
|
|
326
329
|
* type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
|
|
327
330
|
* non-scalar operand cannot push and is skipped.
|
|
328
331
|
*
|
|
329
|
-
* **`below`/`to` may drive a
|
|
332
|
+
* **`below`/`to` may drive a secondary-index range only when the column has no
|
|
330
333
|
* absent/null rows to lose — which this planner cannot verify from the schema
|
|
331
|
-
* alone, so it restricts them to the
|
|
332
|
-
* The engine's total order (`compareValues`, see `@
|
|
333
|
-
* `undefined` (absent) and `null`
|
|
334
|
-
* `matchesCondition('below' | 'to', …)` is
|
|
335
|
-
* or `null` — but a secondary IndexedDB index has
|
|
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
|
|
336
339
|
* indexed field is absent/`null`, so a `below`/`to` range read against that
|
|
337
|
-
* index would
|
|
340
|
+
* index would silently drop those rows (they can never be over-fetched, only
|
|
338
341
|
* missed — the one shape of lossiness this planner must never produce). The
|
|
339
|
-
* table's
|
|
342
|
+
* table's primary key is exempt: a row's primary-key value is always present
|
|
340
343
|
* and never `null` (it is the row's identity, enforced at write time), so a
|
|
341
344
|
* `below`/`to` range against the primary store can never exclude an
|
|
342
345
|
* absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
|
|
343
|
-
* `between` stay index-eligible on
|
|
346
|
+
* `between` stay index-eligible on any orderable column, primary or secondary:
|
|
344
347
|
* each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
|
|
345
348
|
* `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
|
|
346
349
|
* in the total order, so an absent/null-valued row can never satisfy them — the
|
|
@@ -354,7 +357,7 @@ export declare function schemaToStore(schema: TableSchema): StoreDefinition;
|
|
|
354
357
|
* metadata.
|
|
355
358
|
*
|
|
356
359
|
* When no condition qualifies the plan is a full scan (`{}`) and the engine
|
|
357
|
-
* does everything. The plan is always a
|
|
360
|
+
* does everything. The plan is always a superset of the
|
|
358
361
|
* matching rows — the only correctness contract — so the driver may safely run
|
|
359
362
|
* the exact engine over it.
|
|
360
363
|
*
|
|
@@ -19,13 +19,13 @@ var INDEXABLE_STORAGE = Object.freeze([
|
|
|
19
19
|
"real"
|
|
20
20
|
]);
|
|
21
21
|
/**
|
|
22
|
-
* Names the reserved out-of-line store the {@link IndexedDBDriver}
|
|
23
|
-
* {@link DriverMetadata} into.
|
|
22
|
+
* Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
|
|
23
|
+
* stamps its {@link DriverMetadata} into.
|
|
24
24
|
*
|
|
25
25
|
* @remarks
|
|
26
|
-
* Backs the driver's `metadata` / `stamp` hooks. A user table
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Backs the driver's `metadata` / `stamp` hooks. A user table named `__metadata__`
|
|
27
|
+
* collides with the driver's own bookkeeping, so a caller must avoid it; the
|
|
28
|
+
* collision is caught at `open`.
|
|
29
29
|
*/
|
|
30
30
|
var METADATA_STORE = "__metadata__";
|
|
31
31
|
//#endregion
|
|
@@ -39,14 +39,14 @@ var METADATA_STORE = "__metadata__";
|
|
|
39
39
|
* Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
|
|
40
40
|
* translate to a key range that a typed (string/number) column can back with an
|
|
41
41
|
* IndexedDB store/index read — see {@link selectPlan} for the caveats that
|
|
42
|
-
* decide
|
|
42
|
+
* decide which of `below`/`to` may drive a secondary-index read versus the
|
|
43
43
|
* primary store only (a column-type / absent-row concern, not a range-shape
|
|
44
44
|
* one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
|
|
45
45
|
* the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
|
|
46
46
|
* `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
|
|
47
47
|
* guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
|
|
48
48
|
* boolean) that is not a usable key. `between` additionally guards against a
|
|
49
|
-
*
|
|
49
|
+
* reversed pair (`first > second`): native `IDBKeyRange.bound` throws a raw
|
|
50
50
|
* `DataError` `DOMException` for a lower bound above the upper bound, so a
|
|
51
51
|
* reversed pair returns `undefined` here (falls back to a full scan, which the
|
|
52
52
|
* engine then correctly resolves to an empty result) rather than letting a
|
|
@@ -85,7 +85,7 @@ function conditionToRange(condition) {
|
|
|
85
85
|
* store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
|
|
86
86
|
*
|
|
87
87
|
* @remarks
|
|
88
|
-
* Pushdown is sound
|
|
88
|
+
* Pushdown is sound only when every condition is `and`-joined: the engine folds
|
|
89
89
|
* conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
|
|
90
90
|
* each — narrowing on any one is then a valid superset. A single `or` breaks that
|
|
91
91
|
* (a row can match through a later condition the range would exclude), so any `or`
|
|
@@ -99,21 +99,21 @@ function conditionToRange(condition) {
|
|
|
99
99
|
* type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
|
|
100
100
|
* non-scalar operand cannot push and is skipped.
|
|
101
101
|
*
|
|
102
|
-
* **`below`/`to` may drive a
|
|
102
|
+
* **`below`/`to` may drive a secondary-index range only when the column has no
|
|
103
103
|
* absent/null rows to lose — which this planner cannot verify from the schema
|
|
104
|
-
* alone, so it restricts them to the
|
|
104
|
+
* alone, so it restricts them to the primary store, where that is always true.**
|
|
105
105
|
* The engine's total order (`compareValues`, see `@src/core`) ranks
|
|
106
|
-
* `undefined` (absent) and `null`
|
|
107
|
-
* `matchesCondition('below' | 'to', …)` is
|
|
108
|
-
* or `null` — but a secondary IndexedDB index has
|
|
106
|
+
* `undefined` (absent) and `null` below every number/string, so
|
|
107
|
+
* `matchesCondition('below' | 'to', …)` is true for a row whose field is absent
|
|
108
|
+
* or `null` — but a secondary IndexedDB index has no entry for a row whose
|
|
109
109
|
* indexed field is absent/`null`, so a `below`/`to` range read against that
|
|
110
|
-
* index would
|
|
110
|
+
* index would silently drop those rows (they can never be over-fetched, only
|
|
111
111
|
* missed — the one shape of lossiness this planner must never produce). The
|
|
112
|
-
* table's
|
|
112
|
+
* table's primary key is exempt: a row's primary-key value is always present
|
|
113
113
|
* and never `null` (it is the row's identity, enforced at write time), so a
|
|
114
114
|
* `below`/`to` range against the primary store can never exclude an
|
|
115
115
|
* absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
|
|
116
|
-
* `between` stay index-eligible on
|
|
116
|
+
* `between` stay index-eligible on any orderable column, primary or secondary:
|
|
117
117
|
* each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
|
|
118
118
|
* `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
|
|
119
119
|
* in the total order, so an absent/null-valued row can never satisfy them — the
|
|
@@ -127,7 +127,7 @@ function conditionToRange(condition) {
|
|
|
127
127
|
* metadata.
|
|
128
128
|
*
|
|
129
129
|
* When no condition qualifies the plan is a full scan (`{}`) and the engine
|
|
130
|
-
* does everything. The plan is always a
|
|
130
|
+
* does everything. The plan is always a superset of the
|
|
131
131
|
* matching rows — the only correctness contract — so the driver may safely run
|
|
132
132
|
* the exact engine over it.
|
|
133
133
|
*
|
|
@@ -209,7 +209,7 @@ function mapIndexedDBError(error) {
|
|
|
209
209
|
}
|
|
210
210
|
/**
|
|
211
211
|
* Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
212
|
-
* for use
|
|
212
|
+
* for use inside `migrate()` — the one context where `UPGRADE` means the
|
|
213
213
|
* migration itself failed, not a generic driver fault.
|
|
214
214
|
*
|
|
215
215
|
* @remarks
|
|
@@ -234,14 +234,14 @@ function mapMigrationError(error) {
|
|
|
234
234
|
*
|
|
235
235
|
* @remarks
|
|
236
236
|
* Naming a compound index by joining its columns with `_` (`['a', 'b'] →
|
|
237
|
-
* 'a_b'`) collides with a single-column index over a column
|
|
237
|
+
* 'a_b'`) collides with a single-column index over a column literally named
|
|
238
238
|
* `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
|
|
239
239
|
* which either throws a native `ConstraintError` from a duplicate
|
|
240
240
|
* `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
|
|
241
|
-
* lookup match the wrong index. A single-column index keeps the
|
|
241
|
+
* lookup match the wrong index. A single-column index keeps the bare column
|
|
242
242
|
* name — {@link selectPlan} matches `available.includes(condition.column)` by
|
|
243
243
|
* that exact name, so a single-column index must stay named after its column
|
|
244
|
-
* verbatim. A compound index instead encodes each column as a
|
|
244
|
+
* verbatim. A compound index instead encodes each column as a length-prefixed
|
|
245
245
|
* segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
|
|
246
246
|
* and cannot be reconstructed by any other column list — including one
|
|
247
247
|
* containing a column that happens to look like an encoded segment.
|
|
@@ -263,6 +263,11 @@ function deriveIndexedDBIndexName(columns) {
|
|
|
263
263
|
/**
|
|
264
264
|
* Projects a table schema into the IndexedDB wrapper's store definition.
|
|
265
265
|
*
|
|
266
|
+
* @remarks
|
|
267
|
+
* The definition is what an ordered versionchange migration creates the store
|
|
268
|
+
* from, carrying each declared index under the name {@link deriveIndexedDBIndexName}
|
|
269
|
+
* derives for its column group.
|
|
270
|
+
*
|
|
266
271
|
* @param schema - Portable table schema
|
|
267
272
|
* @returns Store definition with declared indexes
|
|
268
273
|
*/
|
|
@@ -312,22 +317,22 @@ function schemaToStore(schema) {
|
|
|
312
317
|
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
313
318
|
* store, creating/dropping an index) is legal only inside a versionchange
|
|
314
319
|
* transaction (`onupgradeneeded`), so `migrate` closes the current connection
|
|
315
|
-
* and opens a
|
|
320
|
+
* and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
|
|
316
321
|
* plan's steps — dropping stores, adding/removing indexes on the raw
|
|
317
322
|
* `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
|
|
318
323
|
* (the one step needing to touch existing data; `column.add` is a no-op — this
|
|
319
324
|
* driver stores whatever a row carries, so there is nothing to backfill). A
|
|
320
|
-
* step referencing an unknown table is validated
|
|
325
|
+
* step referencing an unknown table is validated before the reconnect, so a
|
|
321
326
|
* `MIGRATION` `DatabaseError` never wastes a version bump.
|
|
322
327
|
*
|
|
323
328
|
* @remarks
|
|
324
|
-
* This unit deliberately
|
|
329
|
+
* This unit deliberately omits `aggregate` / `transaction`. There is no native
|
|
325
330
|
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
326
331
|
* `records` covers it. `transaction` is impossible here: the wrapper
|
|
327
332
|
* auto-commits an `IDBTransaction` when control yields outside its request
|
|
328
333
|
* chain, so arbitrary callback awaits cannot remain inside one native
|
|
329
334
|
* transaction. Every atomic multi-operation sequence in this driver
|
|
330
|
-
* (`snapshot`'s rollback) instead runs entirely inside
|
|
335
|
+
* (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
|
|
331
336
|
* scope.
|
|
332
337
|
*/
|
|
333
338
|
var IndexedDBDriver = class {
|
|
@@ -568,11 +573,11 @@ var IndexedDBDriver = class {
|
|
|
568
573
|
*
|
|
569
574
|
* @remarks
|
|
570
575
|
* IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
|
|
571
|
-
* the current connection and opens a
|
|
572
|
-
* every
|
|
573
|
-
* and applying `table.remove` / `index.add` / `index.remove` /
|
|
576
|
+
* the current connection and opens a fresh one at `version + 1`, declaring
|
|
577
|
+
* every store known at that point (plus {@link METADATA_STORE}) so nothing is
|
|
578
|
+
* lost, and applying `table.remove` / `index.add` / `index.remove` /
|
|
574
579
|
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
575
|
-
* the driver's own `#schema`
|
|
580
|
+
* the driver's own `#schema` before the reconnect — an unknown-table step
|
|
576
581
|
* throws `DatabaseError` `MIGRATION` without ever bumping the version.
|
|
577
582
|
* `table.add` / `column.add` need no upgrade-time action: `table.add` is
|
|
578
583
|
* created by the wrapper's built-in create-missing-stores pass (its
|
|
@@ -824,7 +829,7 @@ var IndexedDBDriver = class {
|
|
|
824
829
|
//#endregion
|
|
825
830
|
//#region src/browser/factories.ts
|
|
826
831
|
/**
|
|
827
|
-
* Creates a persistent IndexedDB {@link DriverInterface} for
|
|
832
|
+
* Creates a persistent IndexedDB {@link DriverInterface} for a browser database name.
|
|
828
833
|
*
|
|
829
834
|
* @remarks
|
|
830
835
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
|