@orkestrel/database 0.0.1 → 0.0.3
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 +43 -10
- package/dist/src/browser/index.d.ts +342 -0
- package/dist/src/browser/index.js +686 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +2494 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1766 -0
- package/dist/src/core/index.d.ts +1766 -11
- package/dist/src/core/index.js +237 -1142
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +836 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1054 -0
- package/dist/src/server/index.d.ts +1054 -5
- package/dist/src/server/index.js +1663 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +40 -16
- package/dist/src/core/Clause.d.ts +0 -30
- package/dist/src/core/Cursor.d.ts +0 -21
- package/dist/src/core/Database.d.ts +0 -80
- package/dist/src/core/Query.d.ts +0 -47
- package/dist/src/core/Table.d.ts +0 -69
- package/dist/src/core/constants.d.ts +0 -22
- package/dist/src/core/drivers/MemoryDriver.d.ts +0 -94
- package/dist/src/core/errors.d.ts +0 -38
- package/dist/src/core/factories.d.ts +0 -43
- package/dist/src/core/helpers.d.ts +0 -383
- package/dist/src/core/types.d.ts +0 -739
- package/dist/src/server/compilers.d.ts +0 -169
- package/dist/src/server/drivers/JSONDriver.d.ts +0 -106
- package/dist/src/server/factories.d.ts +0 -31
- package/dist/src/server/helpers.d.ts +0 -222
- package/dist/src/server/types.d.ts +0 -36
package/README.md
CHANGED
|
@@ -14,22 +14,55 @@ npm install @orkestrel/database
|
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
- Node.js >= 24
|
|
18
|
-
|
|
17
|
+
- Node.js >= 24 (`node:sqlite`, used by the `./server` SQLite driver, emits an
|
|
18
|
+
`ExperimentalWarning` on Node's current stable line)
|
|
19
|
+
- Core is ESM; the `./server` subpath ships dual ESM+CJS builds; `./browser`
|
|
20
|
+
is ESM-only
|
|
19
21
|
|
|
20
22
|
## Status
|
|
21
23
|
|
|
22
|
-
Pre-release (`0.0.
|
|
23
|
-
are implemented and tested, but the public API is
|
|
24
|
-
change without notice. See
|
|
25
|
-
for the full documented
|
|
24
|
+
Pre-release (`0.0.2`): the core engine, and the memory, JSON file, SQLite,
|
|
25
|
+
and IndexedDB drivers are all implemented and tested, but the public API is
|
|
26
|
+
still unstable and may change without notice. See
|
|
27
|
+
[guides/src/database.md](./guides/src/database.md) for the full documented
|
|
28
|
+
surface.
|
|
26
29
|
|
|
27
30
|
## Package
|
|
28
31
|
|
|
29
|
-
Published as
|
|
30
|
-
`package.json`:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
Published as three environment-scoped entry points per the `exports` field
|
|
33
|
+
in `package.json`: `.` (the shared, environment-agnostic core engine plus
|
|
34
|
+
the in-memory driver), `./server` (adds the JSON file and SQLite drivers),
|
|
35
|
+
and `./browser` (adds the IndexedDB driver). Core and `./server` ship dual
|
|
36
|
+
ESM+CJS builds; `./browser` is ESM-only.
|
|
37
|
+
|
|
38
|
+
### Release order
|
|
39
|
+
|
|
40
|
+
Everything currently on the npm registry is at `0.0.1` — the wrapper repos'
|
|
41
|
+
`0.0.2`s were never published, so `0.0.2` is the next version for all three
|
|
42
|
+
packages and absorbs every change on this line.
|
|
43
|
+
|
|
44
|
+
This package's SQLite and IndexedDB drivers are built against the wrapper
|
|
45
|
+
surfaces documented by the mirrored guides in this repo — that is,
|
|
46
|
+
`@orkestrel/sqlite@0.0.2` and `@orkestrel/indexeddb@0.0.2` — and the
|
|
47
|
+
dependency ranges pin exactly those versions (`^0.0.2`; on a `0.0.x` version
|
|
48
|
+
a caret means exactly that patch: `>=0.0.2 <0.0.3`). Publish in this order:
|
|
49
|
+
|
|
50
|
+
1. Publish `@orkestrel/sqlite@0.0.2` and `@orkestrel/indexeddb@0.0.2` — they
|
|
51
|
+
are independent of each other (either order; both depend only on the
|
|
52
|
+
already-published `@orkestrel/contract`).
|
|
53
|
+
2. In this repo, run `npm install` to re-resolve `package-lock.json` against
|
|
54
|
+
the newly published wrappers and commit the refreshed lockfile.
|
|
55
|
+
3. Run the `prepublishOnly` gates and publish `@orkestrel/database@0.0.2`.
|
|
56
|
+
|
|
57
|
+
Until step 1 happens, a fresh `npm ci` in this repo fails to resolve
|
|
58
|
+
`^0.0.2` — deliberately. The exact pin makes it impossible to install or
|
|
59
|
+
publish this package against the older `0.0.1` wrappers, which lack driver
|
|
60
|
+
fixes this package's behavior relies on (the SQLite wrapper's mid-stream
|
|
61
|
+
`iterate` fault mapping, the IndexedDB wrapper's abnormal-close recovery and
|
|
62
|
+
`READONLY` fault code) and whose surfaces the mirrored guides here no longer
|
|
63
|
+
describe. The same discipline applies to every future wrapper release: bump
|
|
64
|
+
the pinned range, re-mirror the wrapper guides, and republish this package
|
|
65
|
+
deliberately — wrapper changes never flow in silently.
|
|
33
66
|
|
|
34
67
|
## License
|
|
35
68
|
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { ColumnType } from '../core/index.js';
|
|
2
|
+
import { Condition } from '../core/index.js';
|
|
3
|
+
import { Criteria } from '../core/index.js';
|
|
4
|
+
import { Criteria as Criteria_2 } from '../core/index.js';
|
|
5
|
+
import { DatabaseError } from '../core/index.js';
|
|
6
|
+
import { DriverInterface } from '../core/index.js';
|
|
7
|
+
import { DriverInterface as DriverInterface_2 } from '../core/index.js';
|
|
8
|
+
import { DriverMeta } from '../core/index.js';
|
|
9
|
+
import { IndexedDBError } from '@orkestrel/indexeddb';
|
|
10
|
+
import { Key } from '../core/index.js';
|
|
11
|
+
import { Migration } from '../core/index.js';
|
|
12
|
+
import { Row } from '../core/index.js';
|
|
13
|
+
import { TableSchema } from '../core/index.js';
|
|
14
|
+
import { TableSchema as TableSchema_2 } from '../core/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `IDBKeyRange` a single {@link Condition} maps to, when its operator is one
|
|
18
|
+
* of the six exact key comparisons over scalar operands — else `null`.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Only the comparison operators (`equals`/`above`/`below`/`from`/`to`/`between`)
|
|
22
|
+
* translate to a key range that a typed (string/number) column can back with an
|
|
23
|
+
* IndexedDB store/index read — see {@link selectPlan} for the caveats that
|
|
24
|
+
* decide WHICH of `below`/`to` may drive a SECONDARY-index read versus the
|
|
25
|
+
* primary store only (a column-type / absent-row concern, not a range-shape
|
|
26
|
+
* one). `starts` is excluded — its prefix range can miss strings past U+FFFF;
|
|
27
|
+
* the membership / negation / pattern / existence operators (`not`/`like`/`glob`/
|
|
28
|
+
* `ends`/`any`/`none`/`absent`/`present`) have no single exact range. The operand
|
|
29
|
+
* guard (`typeof` string/number) rejects a non-scalar value (e.g. an array, a
|
|
30
|
+
* boolean) that is not a usable key. `between` additionally guards against a
|
|
31
|
+
* REVERSED pair (`first > second`): native `IDBKeyRange.bound` throws a raw
|
|
32
|
+
* `DataError` `DOMException` for a lower bound above the upper bound, so a
|
|
33
|
+
* reversed pair returns `null` here (falls back to a full scan, which the
|
|
34
|
+
* engine then correctly resolves to an empty result) rather than letting a
|
|
35
|
+
* native exception escape untyped — the same defensive posture as every other
|
|
36
|
+
* backend, which returns empty for a reversed/empty range instead of throwing.
|
|
37
|
+
* The switch is exhaustive over every {@link ConditionOperator}, so a new
|
|
38
|
+
* operator forces a deliberate decision here rather than silently defaulting
|
|
39
|
+
* to a (possibly lossy) range.
|
|
40
|
+
*
|
|
41
|
+
* @param condition - The condition to translate
|
|
42
|
+
* @returns Its exact key range, or `null` when the operator/operands cannot push
|
|
43
|
+
*/
|
|
44
|
+
export declare function conditionRange(condition: Condition): IDBKeyRange | null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a persistent IndexedDB {@link DriverInterface} for the core database layer.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +
|
|
51
|
+
* relations stack against IndexedDB instead of memory — the `Database` / `Table` /
|
|
52
|
+
* `Query` / relations API is unchanged; only where the bytes live changes. The
|
|
53
|
+
* driver is built on the published `@orkestrel/indexeddb` wrapper in auto-managed
|
|
54
|
+
* mode, so a table added to the `tables` map is created on the next open with no
|
|
55
|
+
* version bump. This unit omits `transaction` / `aggregate` (see
|
|
56
|
+
* {@link IndexedDBDriver} `@remarks`).
|
|
57
|
+
*
|
|
58
|
+
* @param name - The IndexedDB database name to open or create
|
|
59
|
+
* @returns A {@link DriverInterface} backed by IndexedDB
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { createDatabase } from '@orkestrel/database'
|
|
64
|
+
* import { stringShape } from '@orkestrel/contract'
|
|
65
|
+
* import { createIndexedDBDriver } from '@orkestrel/database/browser'
|
|
66
|
+
*
|
|
67
|
+
* const db = createDatabase({
|
|
68
|
+
* driver: createIndexedDBDriver('app'),
|
|
69
|
+
* tables: { users: { id: stringShape(), name: stringShape() } },
|
|
70
|
+
* })
|
|
71
|
+
* await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to IndexedDB
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function createIndexedDBDriver(name: string): DriverInterface_2;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Derive an IndexedDB index name for a declared column group — a bare column
|
|
78
|
+
* name for a single-column index, a deterministic collision-free encoding for a
|
|
79
|
+
* compound one.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* Naming a compound index by joining its columns with `_` (`['a', 'b'] →
|
|
83
|
+
* 'a_b'`) collides with a single-column index over a column LITERALLY named
|
|
84
|
+
* `'a_b'` — the same name, two different key paths (`'a_b'` vs `['a', 'b']`),
|
|
85
|
+
* which either throws a native `ConstraintError` from a duplicate
|
|
86
|
+
* `createIndex` call at open, or (worse) lets {@link selectPlan}'s name-based
|
|
87
|
+
* lookup match the wrong index. A single-column index keeps the BARE column
|
|
88
|
+
* name — {@link selectPlan} matches `available.includes(condition.column)` by
|
|
89
|
+
* that exact name, so a single-column index must stay named after its column
|
|
90
|
+
* verbatim. A compound index instead encodes each column as a LENGTH-PREFIXED
|
|
91
|
+
* segment (`'2#1:a1:b'`), so the boundary between columns is self-describing
|
|
92
|
+
* and cannot be reconstructed by any other column list — including one
|
|
93
|
+
* containing a column that happens to look like an encoded segment.
|
|
94
|
+
*
|
|
95
|
+
* @param columns - The index's column group, in declared order
|
|
96
|
+
* @returns The index name to pass to `createIndex` / read back from `indexNames`
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* deriveIndexName(['age']) // 'age'
|
|
101
|
+
* deriveIndexName(['a', 'b']) // '2#1:a1:b'
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function deriveIndexName(columns: readonly string[]): string;
|
|
105
|
+
|
|
106
|
+
export declare const INDEXABLE_TYPES: ReadonlySet<ColumnType>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The IndexedDB {@link DriverInterface} — the persistent browser backend, built on
|
|
110
|
+
* the published `@orkestrel/indexeddb` wrapper.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* A thin adapter: it implements the storage primitives the core database layer
|
|
114
|
+
* needs (`open` / `close` / `read` / `write` / `delete` / `keys` / `scan` / `clear`
|
|
115
|
+
* / `snapshot`) by delegating to the wrapper's typed store operations — it never
|
|
116
|
+
* touches raw IndexedDB. Rows are stored with **out-of-line keys** (the database
|
|
117
|
+
* passes the key explicitly, `store.set(row, key)`), so each table is declared as a
|
|
118
|
+
* key-path-less store. The wrapper opens in **auto-managed** mode (no fixed
|
|
119
|
+
* version), creating any missing store on demand, so a table added to the schema is
|
|
120
|
+
* created on the next open with no manual version bump. The driver's bulk reads
|
|
121
|
+
* (`scan` / `keys`) use the wrapper's native `getAll` / `getAllKeys`, and `snapshot`
|
|
122
|
+
* rolls back through one atomic wrapper transaction.
|
|
123
|
+
*
|
|
124
|
+
* It also implements the optional native `records` / `count` / `stream` hooks
|
|
125
|
+
* (AGENTS §21): `selectPlan` ({@link selectPlan}) turns the {@link Criteria} into a
|
|
126
|
+
* key-range pushdown over the primary key or a single-column secondary index,
|
|
127
|
+
* fetching a candidate **superset** that the core engine (`applyCriteria` /
|
|
128
|
+
* `matchesCriteria`) then refines — so a native read is byte-identical to a full
|
|
129
|
+
* scan, just cheaper. Pushdown is conservative: only the exact-comparison
|
|
130
|
+
* operators over orderable columns narrow to a range; everything else falls back
|
|
131
|
+
* to a full scan + the engine.
|
|
132
|
+
*
|
|
133
|
+
* @remarks
|
|
134
|
+
* This driver also implements `migrate` / `meta` / `stamp`. `meta` / `stamp`
|
|
135
|
+
* persist the {@link DriverMeta} in a reserved out-of-line store,
|
|
136
|
+
* {@link META_STORE} (`__meta__`) — excluded from a whole-store `snapshot`
|
|
137
|
+
* capture, since it is driver bookkeeping, not caller data. `migrate` applies a
|
|
138
|
+
* {@link Migration} plan natively: IndexedDB schema DDL (creating/dropping a
|
|
139
|
+
* store, creating/dropping an index) is legal only inside a versionchange
|
|
140
|
+
* transaction (`onupgradeneeded`), so `migrate` closes the current connection
|
|
141
|
+
* and opens a FRESH one at `version + 1` with an `upgrade` hook that walks the
|
|
142
|
+
* plan's steps — dropping stores, adding/removing indexes on the raw
|
|
143
|
+
* `IDBTransaction`, and rewriting rows for `column.remove` via a cursor walk
|
|
144
|
+
* (the one step needing to touch existing data; `column.add` is a no-op — this
|
|
145
|
+
* driver stores whatever a row carries, so there is nothing to backfill). A
|
|
146
|
+
* step referencing an unknown table is validated BEFORE the reconnect, so a
|
|
147
|
+
* `MIGRATION` `DatabaseError` never wastes a version bump.
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* This unit deliberately OMITS `aggregate` / `transaction`. There is no native
|
|
151
|
+
* `aggregate` (IndexedDB has no native SUM/AVG); the engine over the narrowed
|
|
152
|
+
* `records` covers it. `transaction` is impossible here: the wrapper auto-commits
|
|
153
|
+
* an `IDBTransaction` the moment control yields to a non-IDB `await`, so a
|
|
154
|
+
* BEGIN-now / commit-or-rollback-later handle spanning arbitrary caller code
|
|
155
|
+
* cannot be built on top of it — every atomic multi-op sequence in this driver
|
|
156
|
+
* (`snapshot`'s rollback) instead runs entirely inside ONE `db.write(...)` scope.
|
|
157
|
+
*/
|
|
158
|
+
export declare class IndexedDBDriver implements DriverInterface {
|
|
159
|
+
#private;
|
|
160
|
+
constructor(name: string);
|
|
161
|
+
open(schema: readonly TableSchema_2[]): Promise<void>;
|
|
162
|
+
close(): Promise<void>;
|
|
163
|
+
read(table: string, key: Key): Promise<Row | undefined>;
|
|
164
|
+
write(table: string, key: Key, row: Row): Promise<void>;
|
|
165
|
+
delete(table: string, key: Key): Promise<boolean>;
|
|
166
|
+
keys(table: string): Promise<readonly Key[]>;
|
|
167
|
+
scan(table: string): AsyncIterable<Row>;
|
|
168
|
+
clear(table: string): Promise<void>;
|
|
169
|
+
records(table: string, criteria: Criteria_2): Promise<readonly Row[]>;
|
|
170
|
+
count(table: string, criteria: Criteria_2): Promise<number>;
|
|
171
|
+
stream(table: string, criteria: Criteria_2): AsyncIterable<Row>;
|
|
172
|
+
snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
|
|
173
|
+
/**
|
|
174
|
+
* Return the persisted {@link DriverMeta}, or `undefined` when the store has
|
|
175
|
+
* never been stamped.
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* Reads `'meta'` from the reserved {@link META_STORE}, narrowing the
|
|
179
|
+
* structured-clone value with the core {@link isDriverMeta} guard (never
|
|
180
|
+
* asserted, AGENTS §14) — a missing or malformed record returns `undefined`,
|
|
181
|
+
* exactly like a fresh, never-stamped store.
|
|
182
|
+
*
|
|
183
|
+
* @returns The last-stamped {@link DriverMeta}, or `undefined`
|
|
184
|
+
*/
|
|
185
|
+
meta(): Promise<DriverMeta | undefined>;
|
|
186
|
+
/**
|
|
187
|
+
* Persist `meta` verbatim for a later `meta()` to return.
|
|
188
|
+
*
|
|
189
|
+
* @param meta - The {@link DriverMeta} to persist
|
|
190
|
+
*/
|
|
191
|
+
stamp(meta: DriverMeta): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Apply a {@link Migration} plan by reconnecting at a bumped version and
|
|
194
|
+
* running the plan's steps inside the wrapper's `upgrade` hook.
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* IndexedDB schema DDL is legal only inside `onupgradeneeded`, so this closes
|
|
198
|
+
* the current connection and opens a FRESH one at `version + 1`, declaring
|
|
199
|
+
* every currently-known store (plus {@link META_STORE}) so nothing is lost,
|
|
200
|
+
* and applying `table.remove` / `index.add` / `index.remove` /
|
|
201
|
+
* `column.remove` inside `upgrade`. Every step's `table` is validated against
|
|
202
|
+
* the driver's own `#schema` BEFORE the reconnect — an unknown-table step
|
|
203
|
+
* throws `DatabaseError` `MIGRATION` without ever bumping the version.
|
|
204
|
+
* `table.add` / `column.add` need no upgrade-time action: `table.add` is
|
|
205
|
+
* created by the wrapper's built-in create-missing-stores pass (its
|
|
206
|
+
* definition is already in the declared `stores`), and this driver stores
|
|
207
|
+
* whatever a row carries — there is nothing to backfill for a new column.
|
|
208
|
+
* `#schema` bookkeeping is updated to match the applied plan, mirroring what
|
|
209
|
+
* `open` tracks, so subsequent pushdown planning and a later `migrate` /
|
|
210
|
+
* `open` see the new shape.
|
|
211
|
+
*
|
|
212
|
+
* @param plan - The migration plan to apply
|
|
213
|
+
*/
|
|
214
|
+
migrate(plan: Migration): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export declare function isKey(value: unknown): value is string | number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
221
|
+
* — the default mapping used everywhere except inside `migrate()`.
|
|
222
|
+
*
|
|
223
|
+
* @remarks
|
|
224
|
+
* No backend fault may leak through `DriverInterface` as a raw `IndexedDBError`.
|
|
225
|
+
* `CONSTRAINT` (a unique-key violation) is a `CONFLICT` — the same code every
|
|
226
|
+
* other backend uses for a duplicate key. `CLOSED`/`NOT_OPEN`/`INVALID` (the
|
|
227
|
+
* connection is gone, never opened, or the native handle is stale) collapse to
|
|
228
|
+
* `CLOSED`. `QUOTA` and `BLOCKED` are genuine infrastructure faults (`DRIVER`),
|
|
229
|
+
* carrying a machine-readable `context.code` (`'QUOTA'` / `'BLOCKED'`) so a
|
|
230
|
+
* caller can branch without parsing the message; `BLOCKED` additionally marks
|
|
231
|
+
* `context.retryable: true` — a concurrent connection holding the database open
|
|
232
|
+
* is a transient condition, not a permanent one. Every other code (`UPGRADE`
|
|
233
|
+
* here — see {@link mapMigrationError} for the `migrate()`-only remapping to
|
|
234
|
+
* `MIGRATION` — `ABORTED`, `NOT_FOUND`, `DATA`, `OPEN`, `INACTIVE`, `READONLY`,
|
|
235
|
+
* `UNKNOWN`) is an unexpected infrastructure fault and maps to `DRIVER` — the
|
|
236
|
+
* driver opens its own readwrite transactions, so a `READONLY` fault can only
|
|
237
|
+
* mean the backend behaved unexpectedly. The original error is always
|
|
238
|
+
* preserved as `context.cause` for diagnostics.
|
|
239
|
+
*
|
|
240
|
+
* @param error - The backend error to translate
|
|
241
|
+
* @returns The portable `DatabaseError`
|
|
242
|
+
*/
|
|
243
|
+
export declare function mapIndexedDBError(error: IndexedDBError): DatabaseError;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Map a backend {@link IndexedDBError} to the portable `DatabaseError` taxonomy
|
|
247
|
+
* for use INSIDE `migrate()` — the one context where `UPGRADE` means the
|
|
248
|
+
* migration itself failed, not a generic driver fault.
|
|
249
|
+
*
|
|
250
|
+
* @remarks
|
|
251
|
+
* `migrate()` reconnects at a bumped version inside `onupgradeneeded`; a
|
|
252
|
+
* rejection there (an inapplicable step, a native `ConstraintError` from a
|
|
253
|
+
* duplicate index, …) surfaces as `IndexedDBError` `UPGRADE` and must become a
|
|
254
|
+
* `MIGRATION` `DatabaseError` so a caller can distinguish "this migration plan
|
|
255
|
+
* failed" from "the driver hit an unrelated infrastructure fault". Every other
|
|
256
|
+
* code defers to {@link mapIndexedDBError} unchanged.
|
|
257
|
+
*
|
|
258
|
+
* @param error - The backend error to translate
|
|
259
|
+
* @returns The portable `DatabaseError`
|
|
260
|
+
*/
|
|
261
|
+
export declare function mapMigrationError(error: IndexedDBError): DatabaseError;
|
|
262
|
+
|
|
263
|
+
export declare const META_STORE = "__meta__";
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A pushdown plan — which index (or the primary store, `null`) to read and the
|
|
267
|
+
* `IDBKeyRange` to narrow by (`null` = full scan). Always a SUPERSET of the
|
|
268
|
+
* matching rows; the core engine refines it to the exact result.
|
|
269
|
+
*/
|
|
270
|
+
export declare interface QueryPlan {
|
|
271
|
+
readonly index: string | null;
|
|
272
|
+
readonly range: IDBKeyRange | null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Plan an IndexedDB read for a {@link Criteria} — pick the index (or the primary
|
|
277
|
+
* store) and {@link IDBKeyRange} to narrow by, falling back to a full scan.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* Pushdown is sound ONLY when every condition is `and`-joined: the engine folds
|
|
281
|
+
* conditions left-to-right (`c1 && c2 && … && cn`), so the result is a subset of
|
|
282
|
+
* each — narrowing on any one is then a valid superset. A single `or` breaks that
|
|
283
|
+
* (a row can match through a later condition the range would exclude), so any `or`
|
|
284
|
+
* forces a full scan. Otherwise it scans the conditions in order and selects the
|
|
285
|
+
* **first** one that is provably range-exact and backed by a key: a comparison
|
|
286
|
+
* operator (`conditionRange`) over a single, orderable (`text`/`integer`/`real`)
|
|
287
|
+
* column that is either the table's primary key (read the store directly, `index:
|
|
288
|
+
* null`) or has a single-column secondary index (named exactly the column — read
|
|
289
|
+
* that index). A condition whose column is a nested {@link FieldPath} array
|
|
290
|
+
* (descends a json value, not a key), is absent from the schema, is a non-orderable
|
|
291
|
+
* type (`boolean`/`json`/`blob`), uses a non-comparison operator, or has a
|
|
292
|
+
* non-scalar operand cannot push and is skipped.
|
|
293
|
+
*
|
|
294
|
+
* **`below`/`to` may drive a SECONDARY-index range only when the column has NO
|
|
295
|
+
* absent/null rows to lose — which this planner cannot verify from the schema
|
|
296
|
+
* alone, so it restricts them to the PRIMARY store, where that is always true.**
|
|
297
|
+
* The engine's total order (`compareValues`, see `@src/core`) ranks
|
|
298
|
+
* `undefined` (absent) and `null` BELOW every number/string, so
|
|
299
|
+
* `matchesCondition('below' | 'to', …)` is TRUE for a row whose field is absent
|
|
300
|
+
* or `null` — but a secondary IndexedDB index has NO ENTRY for a row whose
|
|
301
|
+
* indexed field is absent/`null`, so a `below`/`to` range read against that
|
|
302
|
+
* index would SILENTLY DROP those rows (they can never be over-fetched, only
|
|
303
|
+
* missed — the one shape of lossiness this planner must never produce). The
|
|
304
|
+
* table's PRIMARY key is exempt: a row's primary-key value is always present
|
|
305
|
+
* and never `null` (it is the row's identity, enforced at write time), so a
|
|
306
|
+
* `below`/`to` range against the primary store can never exclude an
|
|
307
|
+
* absent/null-keyed row because no such row exists. `equals`/`above`/`from`/
|
|
308
|
+
* `between` stay index-eligible on ANY orderable column, primary or secondary:
|
|
309
|
+
* each is bounded below by a scalar (`equals`/`between`'s lower bound, `above`/
|
|
310
|
+
* `from`'s lower bound), and every scalar strictly out-ranks `undefined`/`null`
|
|
311
|
+
* in the total order, so an absent/null-valued row can never satisfy them — the
|
|
312
|
+
* index's silence on such a row is harmless (it was never going to match).
|
|
313
|
+
* **Declared-type trust caveat:** this reasoning holds under the contract that
|
|
314
|
+
* an {@link INDEXABLE_TYPES} column, once contract-validated at write time,
|
|
315
|
+
* holds only `string | number | null` (or is absent) — never some other
|
|
316
|
+
* runtime value that could rank differently; a driver bypassing the write
|
|
317
|
+
* contract (writing raw rows directly to the store) could defeat this
|
|
318
|
+
* argument, but that is out of scope for a planner reading validated schema
|
|
319
|
+
* metadata.
|
|
320
|
+
*
|
|
321
|
+
* When no condition qualifies the plan is a full scan (`{ index: null, range:
|
|
322
|
+
* null }`) and the engine does everything. The plan is always a SUPERSET of the
|
|
323
|
+
* matching rows — the only correctness contract — so the driver may safely run
|
|
324
|
+
* the exact engine over it.
|
|
325
|
+
*
|
|
326
|
+
* @param criteria - The read specification (its `conditions` drive the plan), or
|
|
327
|
+
* `undefined` for an unconditional read
|
|
328
|
+
* @param schema - The table's schema — its `primary` key and column types
|
|
329
|
+
* @param available - The secondary-index names that physically exist on the store
|
|
330
|
+
* (`store.indexes`); a single-column index is named exactly its column
|
|
331
|
+
* @returns The index + range to read, narrowing to a superset (never lossy)
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* selectPlan({ conditions: [eq('id', 'u1')] }, schema, []) // { index: null, range: only('u1') }
|
|
336
|
+
* selectPlan({ conditions: [from('age', 18)] }, schema, ['age']) // { index: 'age', range: from(18) }
|
|
337
|
+
* selectPlan({ conditions: [contains('name', 'a')] }, schema, []) // { index: null, range: null }
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
export declare function selectPlan(criteria: Criteria | undefined, schema: TableSchema, available: readonly string[]): QueryPlan;
|
|
341
|
+
|
|
342
|
+
export { }
|