@orkestrel/database 0.0.13 → 0.0.15
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 +37 -31
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +170 -117
- 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 +171 -118
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +78 -58
- 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 +79 -59
- package/dist/src/server/index.js.map +1 -1
- package/package.json +17 -18
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
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isString } from "@orkestrel/contract";
|
|
1
2
|
import { DatabaseError, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, compareValues, equalsValue, extractKey, findColumn, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
|
|
2
3
|
import { createIndexedDBDatabase, isIndexedDBError, rangeAboveKey, rangeBelowKey, rangeFromKey, rangeToKey } from "@orkestrel/indexeddb";
|
|
3
4
|
//#region src/browser/constants.ts
|
|
@@ -19,13 +20,13 @@ var INDEXABLE_STORAGE = Object.freeze([
|
|
|
19
20
|
"real"
|
|
20
21
|
]);
|
|
21
22
|
/**
|
|
22
|
-
* Names the reserved out-of-line store the {@link IndexedDBDriver}
|
|
23
|
-
* {@link DriverMetadata} into.
|
|
23
|
+
* Names the reserved out-of-line store `__metadata__` the {@link IndexedDBDriver}
|
|
24
|
+
* stamps its {@link DriverMetadata} into.
|
|
24
25
|
*
|
|
25
26
|
* @remarks
|
|
26
|
-
* Backs the driver's `metadata` / `stamp` hooks. A user table
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Backs the driver's `metadata` / `stamp` hooks. A user table named `__metadata__`
|
|
28
|
+
* collides with the driver's own bookkeeping, so a caller must avoid it; the
|
|
29
|
+
* collision is caught at `open`.
|
|
29
30
|
*/
|
|
30
31
|
var METADATA_STORE = "__metadata__";
|
|
31
32
|
//#endregion
|
|
@@ -39,14 +40,14 @@ var METADATA_STORE = "__metadata__";
|
|
|
39
40
|
* Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
|
|
40
41
|
* translate to a key range that a typed (string/number) column can back with an
|
|
41
42
|
* IndexedDB store/index read — see {@link selectPlan} for the caveats that
|
|
42
|
-
* decide
|
|
43
|
+
* decide which of `below`/`to` may drive a secondary-index read versus the
|
|
43
44
|
* primary store only (a column-type / absent-row concern, not a range-shape
|
|
44
45
|
* one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
|
|
45
46
|
* the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
|
|
46
47
|
* `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
|
|
47
48
|
* guard (`typeof` string/number) rejects a non-scalar value (for example an array, a
|
|
48
49
|
* boolean) that is not a usable key. `between` additionally guards against a
|
|
49
|
-
*
|
|
50
|
+
* reversed pair (`first > second`): native `IDBKeyRange.bound` throws a raw
|
|
50
51
|
* `DataError` `DOMException` for a lower bound above the upper bound, so a
|
|
51
52
|
* reversed pair returns `undefined` here (falls back to a full scan, which the
|
|
52
53
|
* engine then correctly resolves to an empty result) rather than letting a
|
|
@@ -85,7 +86,7 @@ function conditionToRange(condition) {
|
|
|
85
86
|
* store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
|
|
86
87
|
*
|
|
87
88
|
* @remarks
|
|
88
|
-
* Pushdown is sound
|
|
89
|
+
* Pushdown is sound only when every condition is `and`-joined: the engine folds
|
|
89
90
|
* conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
|
|
90
91
|
* each — narrowing on any one is then a valid superset. A single `or` breaks that
|
|
91
92
|
* (a row can match through a later condition the range would exclude), so any `or`
|
|
@@ -99,21 +100,21 @@ function conditionToRange(condition) {
|
|
|
99
100
|
* type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
|
|
100
101
|
* non-scalar operand cannot push and is skipped.
|
|
101
102
|
*
|
|
102
|
-
* **`below`/`to` may drive a
|
|
103
|
+
* **`below`/`to` may drive a secondary-index range only when the column has no
|
|
103
104
|
* absent/null rows to lose — which this planner cannot verify from the schema
|
|
104
|
-
* alone, so it restricts them to the
|
|
105
|
+
* alone, so it restricts them to the primary store, where that is always true.**
|
|
105
106
|
* 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
|
|
107
|
+
* `undefined` (absent) and `null` below every number/string, so
|
|
108
|
+
* `matchesCondition('below' | 'to', …)` is true for a row whose field is absent
|
|
109
|
+
* or `null` — but a secondary IndexedDB index has no entry for a row whose
|
|
109
110
|
* indexed field is absent/`null`, so a `below`/`to` range read against that
|
|
110
|
-
* index would
|
|
111
|
+
* index would silently drop those rows (they can never be over-fetched, only
|
|
111
112
|
* missed — the one shape of lossiness this planner must never produce). The
|
|
112
|
-
* table's
|
|
113
|
+
* table's primary key is exempt: a row's primary-key value is always present
|
|
113
114
|
* and never `null` (it is the row's identity, enforced at write time), so a
|
|
114
115
|
* `below`/`to` range against the primary store can never exclude an
|
|
115
116
|
* absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
|
|
116
|
-
* `between` stay index-eligible on
|
|
117
|
+
* `between` stay index-eligible on any orderable column, primary or secondary:
|
|
117
118
|
* each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
|
|
118
119
|
* `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
|
|
119
120
|
* in the total order, so an absent/null-valued row can never satisfy them — the
|
|
@@ -127,7 +128,7 @@ function conditionToRange(condition) {
|
|
|
127
128
|
* metadata.
|
|
128
129
|
*
|
|
129
130
|
* When no condition qualifies the plan is a full scan (`{}`) and the engine
|
|
130
|
-
* does everything. The plan is always a
|
|
131
|
+
* does everything. The plan is always a superset of the
|
|
131
132
|
* matching rows — the only correctness contract — so the driver may safely run
|
|
132
133
|
* the exact engine over it.
|
|
133
134
|
*
|
|
@@ -149,7 +150,7 @@ function selectPlan(input, schema, available) {
|
|
|
149
150
|
const conditions = input?.conditions ?? [];
|
|
150
151
|
if (conditions.slice(1).some((condition) => condition.connector === "or")) return {};
|
|
151
152
|
for (const condition of conditions) {
|
|
152
|
-
if (
|
|
153
|
+
if (!isString(condition.column)) continue;
|
|
153
154
|
const column = findColumn(condition.column, schema);
|
|
154
155
|
if (column === void 0 || !INDEXABLE_STORAGE.includes(column.storage)) continue;
|
|
155
156
|
const range = conditionToRange(condition);
|
|
@@ -209,7 +210,7 @@ function mapIndexedDBError(error) {
|
|
|
209
210
|
}
|
|
210
211
|
/**
|
|
211
212
|
* Maps a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
212
|
-
* for use
|
|
213
|
+
* for use inside `migrate()` — the one context where `UPGRADE` means the
|
|
213
214
|
* migration itself failed, not a generic driver fault.
|
|
214
215
|
*
|
|
215
216
|
* @remarks
|
|
@@ -234,14 +235,14 @@ function mapMigrationError(error) {
|
|
|
234
235
|
*
|
|
235
236
|
* @remarks
|
|
236
237
|
* Naming a compound index by joining its columns with `_` (`['a', 'b'] →
|
|
237
|
-
* 'a_b'`) collides with a single-column index over a column
|
|
238
|
+
* 'a_b'`) collides with a single-column index over a column literally named
|
|
238
239
|
* `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
|
|
239
240
|
* which either throws a native `ConstraintError` from a duplicate
|
|
240
241
|
* `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
|
|
241
|
-
* lookup match the wrong index. A single-column index keeps the
|
|
242
|
+
* lookup match the wrong index. A single-column index keeps the bare column
|
|
242
243
|
* name — {@link selectPlan} matches `available.includes(condition.column)` by
|
|
243
244
|
* 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
|
|
245
|
+
* verbatim. A compound index instead encodes each column as a length-prefixed
|
|
245
246
|
* segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
|
|
246
247
|
* and cannot be reconstructed by any other column list — including one
|
|
247
248
|
* containing a column that happens to look like an encoded segment.
|
|
@@ -263,6 +264,11 @@ function deriveIndexedDBIndexName(columns) {
|
|
|
263
264
|
/**
|
|
264
265
|
* Projects a table schema into the IndexedDB wrapper's store definition.
|
|
265
266
|
*
|
|
267
|
+
* @remarks
|
|
268
|
+
* The definition is what an ordered versionchange migration creates the store
|
|
269
|
+
* from, carrying each declared index under the name {@link deriveIndexedDBIndexName}
|
|
270
|
+
* derives for its column group.
|
|
271
|
+
*
|
|
266
272
|
* @param schema - Portable table schema
|
|
267
273
|
* @returns Store definition with declared indexes
|
|
268
274
|
*/
|
|
@@ -312,22 +318,22 @@ function schemaToStore(schema) {
|
|
|
312
318
|
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
313
319
|
* store, creating/dropping an index) is legal only inside a versionchange
|
|
314
320
|
* transaction (`onupgradeneeded`), so `migrate` closes the current connection
|
|
315
|
-
* and opens a
|
|
321
|
+
* and opens a fresh one at `version + 1` with an `upgrade` hook that walks the
|
|
316
322
|
* plan's steps — dropping stores, adding/removing indexes on the raw
|
|
317
323
|
* `IDBTransaction`, and rewriting rows for `column.remove` through a cursor walk
|
|
318
324
|
* (the one step needing to touch existing data; `column.add` is a no-op — this
|
|
319
325
|
* driver stores whatever a row carries, so there is nothing to backfill). A
|
|
320
|
-
* step referencing an unknown table is validated
|
|
326
|
+
* step referencing an unknown table is validated before the reconnect, so a
|
|
321
327
|
* `MIGRATION` `DatabaseError` never wastes a version bump.
|
|
322
328
|
*
|
|
323
329
|
* @remarks
|
|
324
|
-
* This unit deliberately
|
|
330
|
+
* This unit deliberately omits `aggregate` / `transaction`. There is no native
|
|
325
331
|
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
326
332
|
* `records` covers it. `transaction` is impossible here: the wrapper
|
|
327
333
|
* auto-commits an `IDBTransaction` when control yields outside its request
|
|
328
334
|
* chain, so arbitrary callback awaits cannot remain inside one native
|
|
329
335
|
* transaction. Every atomic multi-operation sequence in this driver
|
|
330
|
-
* (`snapshot`'s rollback) instead runs entirely inside
|
|
336
|
+
* (`snapshot`'s rollback) instead runs entirely inside one `db.write(...)`
|
|
331
337
|
* scope.
|
|
332
338
|
*/
|
|
333
339
|
var IndexedDBDriver = class {
|
|
@@ -568,11 +574,11 @@ var IndexedDBDriver = class {
|
|
|
568
574
|
*
|
|
569
575
|
* @remarks
|
|
570
576
|
* 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` /
|
|
577
|
+
* the current connection and opens a fresh one at `version + 1`, declaring
|
|
578
|
+
* every store known at that point (plus {@link METADATA_STORE}) so nothing is
|
|
579
|
+
* lost, and applying `table.remove` / `index.add` / `index.remove` /
|
|
574
580
|
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
575
|
-
* the driver's own `#schema`
|
|
581
|
+
* the driver's own `#schema` before the reconnect — an unknown-table step
|
|
576
582
|
* throws `DatabaseError` `MIGRATION` without ever bumping the version.
|
|
577
583
|
* `table.add` / `column.add` need no upgrade-time action: `table.add` is
|
|
578
584
|
* created by the wrapper's built-in create-missing-stores pass (its
|
|
@@ -824,7 +830,7 @@ var IndexedDBDriver = class {
|
|
|
824
830
|
//#endregion
|
|
825
831
|
//#region src/browser/factories.ts
|
|
826
832
|
/**
|
|
827
|
-
* Creates a persistent IndexedDB {@link DriverInterface} for
|
|
833
|
+
* Creates a persistent IndexedDB {@link DriverInterface} for a browser database name.
|
|
828
834
|
*
|
|
829
835
|
* @remarks
|
|
830
836
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed database
|