@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 +2 -2
- package/dist/src/browser/index.d.ts +48 -26
- package/dist/src/browser/index.js +87 -63
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +502 -384
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +455 -269
- package/dist/src/core/index.d.ts +455 -269
- package/dist/src/core/index.js +495 -383
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +190 -306
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +126 -153
- package/dist/src/server/index.d.ts +126 -153
- package/dist/src/server/index.js +184 -298
- package/dist/src/server/index.js.map +1 -1
- package/package.json +23 -17
package/README.md
CHANGED
|
@@ -23,10 +23,10 @@ npm install @orkestrel/database
|
|
|
23
23
|
|
|
24
24
|
## Status
|
|
25
25
|
|
|
26
|
-
Pre-release
|
|
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/
|
|
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
|
-
*
|
|
20
|
-
* of the
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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`
|
|
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
|
|
158
|
-
* an `IDBTransaction` when control yields outside its request
|
|
159
|
-
* callback awaits cannot remain inside one native
|
|
160
|
-
* multi-operation sequence in this driver
|
|
161
|
-
* (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
14
|
-
* of the
|
|
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 (
|
|
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) ?
|
|
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 ?
|
|
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
|
-
*
|
|
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 =
|
|
133
|
-
if (column === void 0 || !INDEXABLE_STORAGE.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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`
|
|
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
|
|
306
|
-
* an `IDBTransaction` when control yields outside its request
|
|
307
|
-
* callback awaits cannot remain inside one native
|
|
308
|
-
* multi-operation sequence in this driver
|
|
309
|
-
* (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|