@bjornpagen/bumbledb 0.6.0 → 0.7.0
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/COOKBOOK.md +45 -6
- package/README.md +15 -2
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +170 -114
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +72 -7
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -9
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +8 -0
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +80 -13
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +5 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +7 -12
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +15 -5
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +289 -173
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +1 -1
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +119 -15
- package/src/query/lower.ts +117 -15
- package/src/query/run.ts +7 -12
- package/src/query/scope.ts +15 -6
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- package/src/statements.ts +30 -6
package/src/db.ts
CHANGED
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
* keyed to statements, read through scoped snapshots, and run the witnessed
|
|
6
6
|
* read-compute-write loop — all typed by the schema's relations record.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
|
|
9
|
+
* value this module returns carries a close spelling — release is
|
|
10
|
+
* deterministic and scope-shaped in the language's own syntax. `Db` values
|
|
11
|
+
* are CACHED per canonical path for the life of the process (a best-effort
|
|
12
|
+
* exit hook closes the cached environments; correctness never depends on it
|
|
13
|
+
* — the engine fsyncs every commit, so a process that dies without the hook
|
|
14
|
+
* loses nothing that was committed). Snapshots are scope-shaped both ways:
|
|
15
|
+
* `read(fn)` opens one before `fn` and closes it after unconditionally
|
|
16
|
+
* (the {@link ReadScope} handed to `fn` is invalidated the moment `fn`
|
|
17
|
+
* returns), and `using snap = db.read()` hands the caller the lifetime,
|
|
18
|
+
* released by the scope's own `Symbol.dispose` at scope exit. Prepared
|
|
19
|
+
* plans are plain values whose engine-side half is reclaimed by a GC
|
|
20
|
+
* finalizer — reclamation only, never correctness.
|
|
18
21
|
*
|
|
19
22
|
* PROCESS MODEL: one process, one exclusive-lock handle per store. The
|
|
20
23
|
* cached `Db` value owns the LMDB environment's exclusive lock until
|
|
@@ -41,7 +44,7 @@ import {
|
|
|
41
44
|
factOf,
|
|
42
45
|
handleOf,
|
|
43
46
|
isFreshField,
|
|
44
|
-
|
|
47
|
+
isInserted,
|
|
45
48
|
type KeyFact,
|
|
46
49
|
keyRowOf,
|
|
47
50
|
type Minted,
|
|
@@ -51,6 +54,7 @@ import {
|
|
|
51
54
|
|
|
52
55
|
import type {
|
|
53
56
|
DbHandle,
|
|
57
|
+
Explain,
|
|
54
58
|
FactValue,
|
|
55
59
|
Manifest,
|
|
56
60
|
PreparedHandle,
|
|
@@ -69,7 +73,7 @@ import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
|
69
73
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
70
74
|
import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
|
|
71
75
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
72
|
-
import type
|
|
76
|
+
import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
79
|
* The ordinary (writable, scannable) relations of a schema's record — the
|
|
@@ -132,16 +136,35 @@ interface Violation<Rels extends SchemaRelations> {
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
139
|
+
* The abandoned arm of a write result (ruled 2026-07-23, R10): present in
|
|
140
|
+
* the type EXACTLY when the callback can abandon — the conditional
|
|
141
|
+
* distributes over `R`, so a callback with no `Abandon` arm contributes
|
|
142
|
+
* `never` and the arm vanishes from the sum. The outcome is in the type;
|
|
143
|
+
* a dead arm is never handled.
|
|
138
144
|
*/
|
|
139
|
-
type
|
|
145
|
+
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
|
|
149
|
+
* R10): the committed generation; the COMPLETE violation set (every
|
|
150
|
+
* violated statement cited once, per direction for a containment, in
|
|
151
|
+
* materialized statement order); or the callback's own abandon payload —
|
|
152
|
+
* commit-vs-abandon is in the type, so a caller's explicit decline to
|
|
153
|
+
* commit can never be silently discarded. Narrows on `.ok`, then (when the
|
|
154
|
+
* callback can abandon) on `"violations" in result`.
|
|
155
|
+
*/
|
|
156
|
+
type WriteResult<Rels extends SchemaRelations, R = void> =
|
|
140
157
|
| { readonly ok: true; readonly generation: bigint }
|
|
141
158
|
| { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
|
|
159
|
+
| AbandonedArm<R>
|
|
142
160
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The delta-building callback of a write: runs synchronously against the
|
|
163
|
+
* live transaction. Returning {@link abandon}`(payload)` rolls the
|
|
164
|
+
* transaction back (R10) — the result type carries the payload arm exactly
|
|
165
|
+
* then.
|
|
166
|
+
*/
|
|
167
|
+
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
|
|
145
168
|
|
|
146
169
|
/**
|
|
147
170
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
@@ -151,10 +174,11 @@ type DeltaBuild<Rels extends SchemaRelations> = (tx: Tx<Rels>) => void
|
|
|
151
174
|
const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
152
175
|
|
|
153
176
|
/**
|
|
154
|
-
* The abandon sentinel {@link abandon} builds: returning one from a
|
|
155
|
-
* `writeWitnessed` callback
|
|
156
|
-
* commit is ever issued) and surfaces the payload as
|
|
157
|
-
* `{ ok: false, abandoned: payload }
|
|
177
|
+
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
178
|
+
* or `writeWitnessed` callback rolls the transaction back WITHOUT
|
|
179
|
+
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
180
|
+
* `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
181
|
+
* sentinel's contract is unconditional, whichever write verb received it).
|
|
158
182
|
*/
|
|
159
183
|
interface Abandon<P> {
|
|
160
184
|
readonly [abandonMark]: true
|
|
@@ -162,26 +186,28 @@ interface Abandon<P> {
|
|
|
162
186
|
}
|
|
163
187
|
|
|
164
188
|
/**
|
|
165
|
-
* Wraps a payload in the {@link Abandon} sentinel — the one way a
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
189
|
+
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
190
|
+
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
191
|
+
* (nothing is committed, not even an empty commit) and the write resolves
|
|
192
|
+
* to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
|
|
193
|
+
* alike (R10).
|
|
169
194
|
*/
|
|
170
195
|
function abandon<P>(payload: P): Abandon<P> {
|
|
171
196
|
return Object.freeze({ [abandonMark]: true as const, payload })
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
/**
|
|
175
|
-
* The abandon payload type a
|
|
176
|
-
*
|
|
177
|
-
*
|
|
200
|
+
* The abandon payload type a write callback's return type implies: the
|
|
201
|
+
* payload of its `Abandon` arm, `never` when the callback can never
|
|
202
|
+
* abandon (the `abandoned` outcome is then statically unreachable and
|
|
203
|
+
* {@link AbandonedArm} erases it from the sum).
|
|
178
204
|
*/
|
|
179
205
|
type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
|
|
180
206
|
|
|
181
207
|
/**
|
|
182
|
-
* Narrows a
|
|
183
|
-
*
|
|
184
|
-
*
|
|
208
|
+
* Narrows a write callback result to the abandon sentinel. The probe is
|
|
209
|
+
* the private {@link abandonMark} symbol only {@link abandon} sets, and
|
|
210
|
+
* `R`'s `Abandon` arm is the only way a sentinel can flow out of the
|
|
185
211
|
* callback — so the narrowed payload type is sound by construction.
|
|
186
212
|
*/
|
|
187
213
|
function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
@@ -189,15 +215,30 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
218
|
+
* The abandon outcome's trusted admission seam: the value's shape is the
|
|
219
|
+
* checkable half (the sentinel mark only {@link abandon} mints, and the
|
|
220
|
+
* outcome carrying that sentinel's own payload), and the sentinel's
|
|
221
|
+
* existence IS the proof `R` carries an `Abandon` arm — so the outcome is
|
|
222
|
+
* admitted at the conditional {@link AbandonedArm} face the type tier
|
|
223
|
+
* cannot resolve over an open `R`.
|
|
196
224
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
function isAbandonedOutcome<Rels extends SchemaRelations, R>(
|
|
226
|
+
outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
|
|
227
|
+
sentinel: Abandon<AbandonedPayload<R>>
|
|
228
|
+
): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
|
|
229
|
+
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
233
|
+
function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
234
|
+
sentinel: Abandon<AbandonedPayload<R>>
|
|
235
|
+
): WriteResult<Rels, R> {
|
|
236
|
+
const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
|
|
237
|
+
if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
|
|
238
|
+
throw errors.new("bumbledb abandon outcome construction incomplete")
|
|
239
|
+
}
|
|
240
|
+
return outcome
|
|
241
|
+
}
|
|
201
242
|
|
|
202
243
|
/**
|
|
203
244
|
* One live write transaction: the submitted delta with the engine's
|
|
@@ -210,10 +251,17 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
210
251
|
/**
|
|
211
252
|
* Records one insert. Omitted fresh fields are MINTED through the
|
|
212
253
|
* engine's alloc lane and returned as bare bigints; supplying them instead
|
|
213
|
-
* preserves identity (the resupply idiom). Returns
|
|
214
|
-
*
|
|
254
|
+
* preserves identity (the resupply idiom). Returns `{ changed, ...fresh }`
|
|
255
|
+
* (ruled 2026-07-23, R11): the engine's changed-state report — the Rust
|
|
256
|
+
* surface's `insert(&fact) -> bool` bijection `delete` always honored —
|
|
257
|
+
* beside the relation's fresh cells, minted or resupplied. The
|
|
258
|
+
* idempotent-replay lane reads the bit from the insert itself; no extra
|
|
259
|
+
* `contains` round trip exists. The flattened shape cannot carry a FRESH
|
|
260
|
+
* cell literally named `changed` beside the report, so admission refuses
|
|
261
|
+
* that one spelling ({@link refuseShadowedChanged}) — never a silent
|
|
262
|
+
* shadow here.
|
|
215
263
|
*/
|
|
216
|
-
insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R>
|
|
264
|
+
insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): { readonly changed: boolean } & Minted<R>
|
|
217
265
|
/** Records one delete; `true` iff the final state changed. */
|
|
218
266
|
delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
219
267
|
/** Final-state membership of one complete fact. */
|
|
@@ -236,19 +284,20 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
/**
|
|
239
|
-
* The read view one `db.read(fn)` call scopes
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
287
|
+
* The read view one `db.read(fn)` call scopes — or one `using snap =
|
|
288
|
+
* db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
|
|
289
|
+
* disposables, never `close()`). The callback form invalidates the value
|
|
290
|
+
* when `fn` returns; the `using` form releases it at scope exit through
|
|
291
|
+
* `Symbol.dispose` — either way the release is deterministic and
|
|
292
|
+
* scope-shaped, every later verb call throws a typed used-after-scope
|
|
293
|
+
* error, and the underlying snapshot (with its LMDB reader slot) is
|
|
294
|
+
* already closed.
|
|
245
295
|
*/
|
|
246
|
-
interface ReadScope<Rels extends SchemaRelations> {
|
|
296
|
+
interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
247
297
|
/**
|
|
248
|
-
* The committed generation this scope witnessed —
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* open and the generation read.
|
|
298
|
+
* The committed generation this scope witnessed — carried by the
|
|
299
|
+
* snapshot open itself (one crossing, inside the snapshot's own
|
|
300
|
+
* transaction), so it is atomic with the snapshot by construction.
|
|
252
301
|
*/
|
|
253
302
|
readonly generation: bigint
|
|
254
303
|
/** Full-relation export in row-id order, decoded to bare structural facts. */
|
|
@@ -277,6 +326,16 @@ interface ReadScope<Rels extends SchemaRelations> {
|
|
|
277
326
|
* execution spelling ({@link Prepared} carries no `execute`).
|
|
278
327
|
*/
|
|
279
328
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
329
|
+
/**
|
|
330
|
+
* Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
|
|
331
|
+
* query against this scope's snapshot with counting instrumentation
|
|
332
|
+
* (the engine's ANALYZE semantics) and returns the structured
|
|
333
|
+
* {@link Explain} report — plan sections and counters as plain values,
|
|
334
|
+
* so the host reads what the engine did with its query without a
|
|
335
|
+
* second toolchain. A diagnostic surface, EXPLICITLY UNFROZEN: the
|
|
336
|
+
* shape follows the plan representation wherever it goes.
|
|
337
|
+
*/
|
|
338
|
+
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
280
339
|
}
|
|
281
340
|
|
|
282
341
|
/**
|
|
@@ -326,6 +385,14 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
326
385
|
* when `fn` returns — a used-after-scope call throws a typed error.
|
|
327
386
|
*/
|
|
328
387
|
read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
388
|
+
/**
|
|
389
|
+
* The `using` acquisition (ruled 2026-07-23, R12): `using snap =
|
|
390
|
+
* db.read()` — the caller owns the scope's lifetime, and the scope's
|
|
391
|
+
* `Symbol.dispose` releases the snapshot deterministically at scope
|
|
392
|
+
* exit, in the language's own syntax. Lifetimes are disposables, never
|
|
393
|
+
* `close()`.
|
|
394
|
+
*/
|
|
395
|
+
read(): ReadScope<Rels>
|
|
329
396
|
/** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
|
|
330
397
|
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
331
398
|
/** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
|
|
@@ -340,12 +407,18 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
340
407
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
341
408
|
/** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
|
|
342
409
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
410
|
+
/** `db.explain(p, params)` === `db.read(snap => snap.explain(p, params))` — the symmetry rule (R13). */
|
|
411
|
+
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
343
412
|
/**
|
|
344
413
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
345
414
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
346
|
-
* the delta (LMDB untouched) and rethrows wrapped.
|
|
415
|
+
* the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
|
|
416
|
+
* commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
|
|
417
|
+
* R10): the transaction rolls back — nothing is committed, not even an
|
|
418
|
+
* empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
|
|
419
|
+
* an arm the result type carries exactly when the callback can abandon.
|
|
347
420
|
*/
|
|
348
|
-
write(fn: DeltaBuild<Rels>): WriteResult<Rels>
|
|
421
|
+
write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
|
|
349
422
|
/**
|
|
350
423
|
* The ONE witnessed-write form: snapshot → `fn` (premise reads via
|
|
351
424
|
* `snap`, delta via `tx`) → witnessed commit, which lands only if no
|
|
@@ -364,7 +437,7 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
364
437
|
* `{ ok: false, abandoned: payload }` and NO commit (not even an empty
|
|
365
438
|
* one) is issued.
|
|
366
439
|
*/
|
|
367
|
-
writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R):
|
|
440
|
+
writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R>
|
|
368
441
|
/**
|
|
369
442
|
* Prepares a query value built against THIS schema (identity is the
|
|
370
443
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
@@ -507,18 +580,16 @@ function isThenable(value: unknown): boolean {
|
|
|
507
580
|
|
|
508
581
|
/**
|
|
509
582
|
* Narrows a keyed-get middle argument to a statement value (vs a key
|
|
510
|
-
* object)
|
|
511
|
-
*
|
|
512
|
-
*
|
|
583
|
+
* object) through the statement module's admission brand — a
|
|
584
|
+
* REPRESENTATION, never a shape probe: fact cell shapes are structurally
|
|
585
|
+
* OPEN (an interval value carrying an excess `kind` property is a legal
|
|
586
|
+
* cell), so no property probe could ever be sound here, but no host-built
|
|
587
|
+
* key object can spell the module-private brand symbol.
|
|
513
588
|
*/
|
|
514
589
|
function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
|
|
515
590
|
value: KeyFact<R> | KeyStatement<R, P>
|
|
516
591
|
): value is KeyStatement<R, P> {
|
|
517
|
-
|
|
518
|
-
return false
|
|
519
|
-
}
|
|
520
|
-
const data: unknown = value.data
|
|
521
|
-
return typeof data === "object" && data !== null && "kind" in data
|
|
592
|
+
return isStatement(value)
|
|
522
593
|
}
|
|
523
594
|
|
|
524
595
|
/**
|
|
@@ -569,7 +640,11 @@ interface Tables {
|
|
|
569
640
|
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
570
641
|
* positional mirror against the engine's reported order — any drift
|
|
571
642
|
* (count, kind, id, or membership) is a construction-time failure, never a
|
|
572
|
-
* silent misattribution of a violation to the wrong statement value.
|
|
643
|
+
* silent misattribution of a violation to the wrong statement value. The
|
|
644
|
+
* declaration-ordinal law the query lowering leans on is verified in the
|
|
645
|
+
* same walks: relation ids and sealed field ids both equal declaration
|
|
646
|
+
* order, so a constructed `Tables` IS the proof and `prepare` inherits it
|
|
647
|
+
* structurally — never a silently misaddressed query.
|
|
573
648
|
*/
|
|
574
649
|
function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
575
650
|
const entries = materializedEntries(theory)
|
|
@@ -596,6 +671,13 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
596
671
|
for (const field of relation.fields) {
|
|
597
672
|
fieldIds.set(field.name, field.id)
|
|
598
673
|
}
|
|
674
|
+
sealedFieldsOf(member).forEach(function verifyField(declared, fieldOrdinal) {
|
|
675
|
+
if (fieldIds.get(declared.name) !== fieldOrdinal) {
|
|
676
|
+
throw errors.new(
|
|
677
|
+
`bumbledb manifest drift: ${relation.name}.${declared.name} has engine field id ${fieldIds.get(declared.name)}, its sealed ordinal is ${fieldOrdinal}`
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
})
|
|
599
681
|
let primaryKey: PrimaryKey | undefined
|
|
600
682
|
entries.forEach(function firstOwnedKey(entry, index) {
|
|
601
683
|
if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
|
|
@@ -604,11 +686,17 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
604
686
|
})
|
|
605
687
|
relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
|
|
606
688
|
}
|
|
607
|
-
|
|
608
|
-
|
|
689
|
+
Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
|
|
690
|
+
const entry = relations.get(name)
|
|
691
|
+
if (entry === undefined) {
|
|
609
692
|
throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`)
|
|
610
693
|
}
|
|
611
|
-
|
|
694
|
+
if (entry.id !== ordinal) {
|
|
695
|
+
throw errors.new(
|
|
696
|
+
`bumbledb manifest drift: relation ${name} has engine id ${entry.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
})
|
|
612
700
|
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
613
701
|
}
|
|
614
702
|
|
|
@@ -619,14 +707,20 @@ interface PointReads {
|
|
|
619
707
|
}
|
|
620
708
|
|
|
621
709
|
/**
|
|
622
|
-
* One read scope's PRIVATE lifetime record: its live snapshot handle,
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
710
|
+
* One read scope's PRIVATE lifetime record: its live snapshot handle, the
|
|
711
|
+
* generation it witnessed (carried by the snapshot open itself — one
|
|
712
|
+
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
713
|
+
* `read`/`writeWitnessed` callback returns, or by the scope's own
|
|
714
|
+
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
715
|
+
* exactly once, whichever of the owner and the dispose gets there first),
|
|
716
|
+
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
717
|
+
* the snapshot handle is never a public value.
|
|
626
718
|
*/
|
|
627
719
|
interface ScopeState {
|
|
628
720
|
readonly handle: SnapshotHandle
|
|
721
|
+
readonly generation: bigint
|
|
629
722
|
live: boolean
|
|
723
|
+
closed: boolean
|
|
630
724
|
readonly owner: object
|
|
631
725
|
}
|
|
632
726
|
|
|
@@ -900,10 +994,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
900
994
|
/**
|
|
901
995
|
* Builds one {@link ReadScope} over a live scope state. Every verb
|
|
902
996
|
* asserts liveness first: the owning call flips `state.live` the moment
|
|
903
|
-
* its callback returns
|
|
997
|
+
* its callback returns (or the scope's own `Symbol.dispose` does, for a
|
|
998
|
+
* `using`-acquired scope), so a leaked scope is a typed refusal forever
|
|
904
999
|
* after.
|
|
905
1000
|
*/
|
|
906
|
-
function makeScope(state: ScopeState
|
|
1001
|
+
function makeScope(state: ScopeState): ReadScope<Rels> {
|
|
907
1002
|
function assertLive(): void {
|
|
908
1003
|
if (!state.live) {
|
|
909
1004
|
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
@@ -940,12 +1035,27 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
940
1035
|
})
|
|
941
1036
|
return decodeAnswers<Row>(plan.finds, rows)
|
|
942
1037
|
}
|
|
1038
|
+
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1039
|
+
assertLive()
|
|
1040
|
+
const plan = planOf(prepared)
|
|
1041
|
+
const wire = wireParams(plan.params, recordOf(params))
|
|
1042
|
+
return bridged("explain bumbledb prepared query", function callExplain() {
|
|
1043
|
+
return native.preparedExplain(plan.handle, state.handle, wire)
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1047
|
+
function dispose(): void {
|
|
1048
|
+
state.live = false
|
|
1049
|
+
closeScopeState(state)
|
|
1050
|
+
}
|
|
943
1051
|
const scope: ReadScope<Rels> = Object.freeze({
|
|
944
|
-
generation,
|
|
1052
|
+
generation: state.generation,
|
|
945
1053
|
scan,
|
|
946
1054
|
get: reads.get,
|
|
947
1055
|
contains: reads.contains,
|
|
948
|
-
execute
|
|
1056
|
+
execute,
|
|
1057
|
+
explain,
|
|
1058
|
+
[Symbol.dispose]: dispose
|
|
949
1059
|
})
|
|
950
1060
|
scopeStates.set(scope, state)
|
|
951
1061
|
return scope
|
|
@@ -959,50 +1069,52 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
959
1069
|
*/
|
|
960
1070
|
let liveSnapshots = 0
|
|
961
1071
|
|
|
962
|
-
/**
|
|
1072
|
+
/**
|
|
1073
|
+
* Opens one snapshot and its scope state (live until the owner flips
|
|
1074
|
+
* it). The witnessed generation rides the snapshot open itself — one
|
|
1075
|
+
* crossing carries both (finding 016), so the fault-pairing close
|
|
1076
|
+
* branch a second `dbGeneration` call needed is structurally gone.
|
|
1077
|
+
*/
|
|
963
1078
|
function openScopeState(): ScopeState {
|
|
964
|
-
const
|
|
1079
|
+
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
965
1080
|
return native.dbSnapshot(handle)
|
|
966
1081
|
})
|
|
967
1082
|
liveSnapshots += 1
|
|
968
|
-
return { handle:
|
|
1083
|
+
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
|
|
969
1084
|
}
|
|
970
1085
|
|
|
971
|
-
/**
|
|
1086
|
+
/**
|
|
1087
|
+
* Closes a scope's snapshot after the owner invalidated it — a LATCH:
|
|
1088
|
+
* the snapshot closes exactly once, whichever of the owning call and
|
|
1089
|
+
* the scope's own `Symbol.dispose` gets there first, so an early
|
|
1090
|
+
* in-callback disposal never double-closes (and never double-counts
|
|
1091
|
+
* the census).
|
|
1092
|
+
*/
|
|
972
1093
|
function closeScopeState(state: ScopeState): void {
|
|
1094
|
+
if (state.closed) {
|
|
1095
|
+
return
|
|
1096
|
+
}
|
|
1097
|
+
state.closed = true
|
|
973
1098
|
bridged("close bumbledb snapshot", function closeSnapshot() {
|
|
974
1099
|
native.snapshotClose(state.handle)
|
|
975
1100
|
})
|
|
976
1101
|
liveSnapshots -= 1
|
|
977
1102
|
}
|
|
978
1103
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
* engine read txn, so reader-table exhaustion is precisely the state in
|
|
983
|
-
* which it throws — with one snapshot already open. An unpaired fault
|
|
984
|
-
* here would park a snapshot worker and consume one of the engine's
|
|
985
|
-
* reader slots FOREVER (and undercount the liveSnapshots census), each
|
|
986
|
-
* fault ratcheting toward ReadersFull-for-the-process's-lifetime.
|
|
987
|
-
*/
|
|
988
|
-
function generationForScope(state: ScopeState): bigint {
|
|
989
|
-
const generation = errors.trySync(function readGeneration() {
|
|
990
|
-
return bridged("read bumbledb generation", function callGeneration() {
|
|
991
|
-
return native.dbGeneration(handle)
|
|
992
|
-
})
|
|
993
|
-
})
|
|
994
|
-
if (generation.error) {
|
|
995
|
-
state.live = false
|
|
996
|
-
closeScopeState(state)
|
|
997
|
-
throw generation.error
|
|
998
|
-
}
|
|
999
|
-
return generation.data
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function read<T>(fn: (snap: ReadScope<Rels>) => T): T {
|
|
1104
|
+
function read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
1105
|
+
function read(): ReadScope<Rels>
|
|
1106
|
+
function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
|
|
1003
1107
|
const state = openScopeState()
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1108
|
+
const scope = makeScope(state)
|
|
1109
|
+
if (fn === undefined) {
|
|
1110
|
+
/**
|
|
1111
|
+
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
1112
|
+
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
1113
|
+
* is the deterministic release, scope-shaped in the language's
|
|
1114
|
+
* own syntax.
|
|
1115
|
+
*/
|
|
1116
|
+
return scope
|
|
1117
|
+
}
|
|
1006
1118
|
const result = errors.trySync(function runRead() {
|
|
1007
1119
|
return fn(scope)
|
|
1008
1120
|
})
|
|
@@ -1057,6 +1169,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1057
1169
|
})
|
|
1058
1170
|
}
|
|
1059
1171
|
|
|
1172
|
+
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1173
|
+
return read(function explainInScope(snap) {
|
|
1174
|
+
return snap.explain(prepared, params)
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1060
1178
|
/**
|
|
1061
1179
|
* Builds one {@link Tx} over a transaction-handle thunk: `write` passes
|
|
1062
1180
|
* an already-begun handle; `writeWitnessed` passes a LAZY thunk that
|
|
@@ -1085,21 +1203,25 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1085
1203
|
})
|
|
1086
1204
|
}
|
|
1087
1205
|
})
|
|
1088
|
-
function insert<R extends MemberRelation<Rels>>(
|
|
1206
|
+
function insert<R extends MemberRelation<Rels>>(
|
|
1207
|
+
relation: R,
|
|
1208
|
+
fact: InsertFact<R>
|
|
1209
|
+
): { readonly changed: boolean } & Minted<R> {
|
|
1089
1210
|
assertLive()
|
|
1090
1211
|
const entry = resolveOrdinary(relation)
|
|
1091
1212
|
const txHandle = resolveTx()
|
|
1092
|
-
|
|
1213
|
+
/** The one spread copy of the write path: `mintFreshCells` writes minted cells in place, and they must never land in the caller's own fact object. */
|
|
1214
|
+
const values: Record<string, unknown> = { ...recordOf(fact) }
|
|
1093
1215
|
const fresh = mintFreshCells(txHandle, entry, relation, values)
|
|
1094
1216
|
const row = rowOf(relation.data, values)
|
|
1095
|
-
bridged("bumbledb tx insert", function record() {
|
|
1096
|
-
native.txInsert(txHandle, entry.id, row)
|
|
1217
|
+
const changed = bridged("bumbledb tx insert", function record() {
|
|
1218
|
+
return native.txInsert(txHandle, entry.id, row)
|
|
1097
1219
|
})
|
|
1098
|
-
Object.freeze(fresh)
|
|
1099
|
-
if (!
|
|
1100
|
-
throw errors.new(`relation ${relation.name}:
|
|
1220
|
+
const inserted: Readonly<Record<string, FactValue | boolean>> = Object.freeze({ changed, ...fresh })
|
|
1221
|
+
if (!isInserted(relation, inserted)) {
|
|
1222
|
+
throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
|
|
1101
1223
|
}
|
|
1102
|
-
return
|
|
1224
|
+
return inserted
|
|
1103
1225
|
}
|
|
1104
1226
|
function remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1105
1227
|
assertLive()
|
|
@@ -1122,7 +1244,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1122
1244
|
return { tx, spend }
|
|
1123
1245
|
}
|
|
1124
1246
|
|
|
1125
|
-
function runDelta(txHandle: TxHandle, fn: DeltaBuild<Rels>): WriteResult<Rels> {
|
|
1247
|
+
function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
|
|
1126
1248
|
const made = makeTx(function resolveTx() {
|
|
1127
1249
|
return txHandle
|
|
1128
1250
|
})
|
|
@@ -1152,6 +1274,18 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1152
1274
|
"bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1153
1275
|
)
|
|
1154
1276
|
}
|
|
1277
|
+
if (isAbandon(built.data)) {
|
|
1278
|
+
/**
|
|
1279
|
+
* The caller's explicit decline to commit (R10): the sentinel's
|
|
1280
|
+
* contract is unconditional — roll back, nothing committed, not
|
|
1281
|
+
* even an empty commit; commit is unreachable for a sentinel
|
|
1282
|
+
* result.
|
|
1283
|
+
*/
|
|
1284
|
+
bridged("abort bumbledb write transaction", function abort() {
|
|
1285
|
+
native.txAbort(txHandle)
|
|
1286
|
+
})
|
|
1287
|
+
return abandonedOutcome<Rels, R>(built.data)
|
|
1288
|
+
}
|
|
1155
1289
|
const committed = errors.trySync(function commitDelta() {
|
|
1156
1290
|
return bridged("commit bumbledb write transaction", function commit() {
|
|
1157
1291
|
return native.txCommit(txHandle)
|
|
@@ -1182,16 +1316,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1182
1316
|
})
|
|
1183
1317
|
}
|
|
1184
1318
|
|
|
1185
|
-
function write(fn: DeltaBuild<Rels>): WriteResult<Rels> {
|
|
1186
|
-
const
|
|
1187
|
-
return
|
|
1188
|
-
return native.dbWriteBegin(handle)
|
|
1189
|
-
})
|
|
1319
|
+
function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
|
|
1320
|
+
const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
|
|
1321
|
+
return native.dbWriteBegin(handle)
|
|
1190
1322
|
})
|
|
1191
|
-
|
|
1192
|
-
throw errors.wrap(begun.error, `begin bumbledb write transaction (live snapshots at fault: ${liveSnapshots})`)
|
|
1193
|
-
}
|
|
1194
|
-
return runDelta(begun.data, fn)
|
|
1323
|
+
return runDelta(txHandle, fn)
|
|
1195
1324
|
}
|
|
1196
1325
|
|
|
1197
1326
|
/**
|
|
@@ -1199,7 +1328,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1199
1328
|
* attempt's snapshot: the committed generation, or the engine's
|
|
1200
1329
|
* complete violation set as data.
|
|
1201
1330
|
*/
|
|
1202
|
-
function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle):
|
|
1331
|
+
function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WriteResult<Rels, R> {
|
|
1203
1332
|
const committed = errors.trySync(function commitWitnessedDelta() {
|
|
1204
1333
|
return bridged("commit bumbledb witnessed write transaction", function commit() {
|
|
1205
1334
|
return native.txCommit(txHandle)
|
|
@@ -1235,12 +1364,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1235
1364
|
* `undefined` exactly when the generation moved and the whole callback
|
|
1236
1365
|
* must rerun on a fresh snapshot.
|
|
1237
1366
|
*/
|
|
1238
|
-
function witnessedAttempt<R>(
|
|
1239
|
-
fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R
|
|
1240
|
-
): WitnessedWriteResult<Rels, R> | undefined {
|
|
1367
|
+
function witnessedAttempt<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> | undefined {
|
|
1241
1368
|
const state = openScopeState()
|
|
1242
|
-
const
|
|
1243
|
-
const scope = makeScope(state, generation)
|
|
1369
|
+
const scope = makeScope(state)
|
|
1244
1370
|
const pending: { tx: TxHandle | undefined } = { tx: undefined }
|
|
1245
1371
|
function beginWitnessed(): TxHandle | undefined {
|
|
1246
1372
|
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
@@ -1304,7 +1430,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1304
1430
|
if (isAbandon(built.data)) {
|
|
1305
1431
|
abortPending()
|
|
1306
1432
|
closeScopeState(state)
|
|
1307
|
-
return
|
|
1433
|
+
return abandonedOutcome<Rels, R>(built.data)
|
|
1308
1434
|
}
|
|
1309
1435
|
const late = errors.trySync(function resolveCommitTx() {
|
|
1310
1436
|
if (pending.tx === undefined) {
|
|
@@ -1340,7 +1466,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1340
1466
|
* error, never a loop — retry is host policy, and this is the host
|
|
1341
1467
|
* policy's own honesty bound).
|
|
1342
1468
|
*/
|
|
1343
|
-
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R):
|
|
1469
|
+
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> {
|
|
1344
1470
|
for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
|
|
1345
1471
|
const attempt = witnessedAttempt(fn)
|
|
1346
1472
|
if (attempt !== undefined) {
|
|
@@ -1353,45 +1479,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1353
1479
|
)
|
|
1354
1480
|
}
|
|
1355
1481
|
|
|
1356
|
-
/**
|
|
1357
|
-
* Verifies the declaration-ordinal law the query lowering leans on
|
|
1358
|
-
* against the live manifest tables: relation ids and sealed field ids
|
|
1359
|
-
* both equal declaration order (`dbManifest` is the engine's own
|
|
1360
|
-
* pinning of it). Any drift is a construction-time failure here, never
|
|
1361
|
-
* a silently misaddressed query.
|
|
1362
|
-
*/
|
|
1363
|
-
function assertOrdinalAlignment(): void {
|
|
1364
|
-
Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
|
|
1365
|
-
const entry = tables.relations.get(name)
|
|
1366
|
-
if (entry === undefined || entry.id !== ordinal) {
|
|
1367
|
-
throw errors.new(
|
|
1368
|
-
`bumbledb manifest drift: relation ${name} has engine id ${entry?.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
|
|
1369
|
-
)
|
|
1370
|
-
}
|
|
1371
|
-
const member = theory.relations[name]
|
|
1372
|
-
if (member === undefined) {
|
|
1373
|
-
throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
|
|
1374
|
-
}
|
|
1375
|
-
const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
|
|
1376
|
-
return declared.name
|
|
1377
|
-
})
|
|
1378
|
-
sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
|
|
1379
|
-
if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
|
|
1380
|
-
throw errors.new(
|
|
1381
|
-
`bumbledb manifest drift: ${name}.${fieldName} has engine field id ${entry.fieldIds.get(fieldName)}, its sealed ordinal is ${fieldOrdinal}`
|
|
1382
|
-
)
|
|
1383
|
-
}
|
|
1384
|
-
})
|
|
1385
|
-
})
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
1482
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
1389
1483
|
if (q.schema !== theory) {
|
|
1390
1484
|
throw errors.new(
|
|
1391
1485
|
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
1392
1486
|
)
|
|
1393
1487
|
}
|
|
1394
|
-
assertOrdinalAlignment()
|
|
1395
1488
|
const program = lowerQuery(q)
|
|
1396
1489
|
const outcome = bridged("prepare bumbledb program", function callPrepare() {
|
|
1397
1490
|
return native.dbPrepare(handle, program)
|
|
@@ -1438,6 +1531,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1438
1531
|
get,
|
|
1439
1532
|
contains,
|
|
1440
1533
|
execute,
|
|
1534
|
+
explain,
|
|
1441
1535
|
write,
|
|
1442
1536
|
writeWitnessed,
|
|
1443
1537
|
prepare
|
|
@@ -1508,6 +1602,27 @@ const ErrNewtypeMismatch = errors.new(
|
|
|
1508
1602
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1509
1603
|
)
|
|
1510
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
|
|
1607
|
+
* where the spread wins: a FRESH field literally named `changed` would
|
|
1608
|
+
* shadow the engine's changed-state report on every insert of its
|
|
1609
|
+
* relation. No field name is reserved SILENTLY — the one unspeakable
|
|
1610
|
+
* spelling is refused here at admission, before any store is touched.
|
|
1611
|
+
* Supplied (non-fresh) fields named `changed` never enter the return
|
|
1612
|
+
* record and stay legal.
|
|
1613
|
+
*/
|
|
1614
|
+
function refuseShadowedChanged(theory: AnySchema): void {
|
|
1615
|
+
for (const [name, member] of Object.entries(theory.relations)) {
|
|
1616
|
+
for (const declared of sealedFieldsOf(member)) {
|
|
1617
|
+
if (declared.name === "changed" && isFreshField(declared.field)) {
|
|
1618
|
+
throw errors.new(
|
|
1619
|
+
`relation ${name}: a fresh field named "changed" would shadow tx.insert's changed-state report in its { changed, ...fresh } return (R11) — rename the fresh field; a supplied field named "changed" stays legal (only fresh cells ride the return)`
|
|
1620
|
+
)
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1511
1626
|
/**
|
|
1512
1627
|
* The one admission path both verbs share: canonical-path cache lookup
|
|
1513
1628
|
* first (a hit returns the SAME `Db` value for the identical theory, a
|
|
@@ -1525,6 +1640,7 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1525
1640
|
storePath: string,
|
|
1526
1641
|
theory: Schema<Rels>
|
|
1527
1642
|
): Db<Rels> {
|
|
1643
|
+
refuseShadowedChanged(theory)
|
|
1528
1644
|
const canonical = path.resolve(storePath)
|
|
1529
1645
|
const cached = openStores.get(canonical)
|
|
1530
1646
|
if (cached !== undefined) {
|
|
@@ -1594,12 +1710,12 @@ const Db = Object.freeze({
|
|
|
1594
1710
|
* one schema-independent read path (no theory, no fingerprint check; the
|
|
1595
1711
|
* store rebirth tool's entry). Lives beside `open`/`create` so the path
|
|
1596
1712
|
* law stays in one place: the same `node:path.resolve` canonicalization,
|
|
1597
|
-
* applied here. The value is NOT cached and
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
* the descriptor).
|
|
1713
|
+
* applied here. The value is NOT cached and is a DISPOSABLE lifetime
|
|
1714
|
+
* (R12) — `using exhumed = await Db.exhume(path)` releases the engine
|
|
1715
|
+
* handle and the store's exclusive lock at scope exit, so a same-path
|
|
1716
|
+
* reopen never waits on GC. A store not yet adopted rejects with the
|
|
1717
|
+
* typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
|
|
1718
|
+
* `Db.open` under the creating schema back-fills the descriptor).
|
|
1603
1719
|
*/
|
|
1604
1720
|
async exhume(storePath: string): Promise<Exhumed> {
|
|
1605
1721
|
return exhumeStore(path.resolve(storePath))
|
|
@@ -1608,6 +1724,7 @@ const Db = Object.freeze({
|
|
|
1608
1724
|
|
|
1609
1725
|
export type {
|
|
1610
1726
|
Abandon,
|
|
1727
|
+
AbandonedArm,
|
|
1611
1728
|
DeclaredKeyFact,
|
|
1612
1729
|
DeltaBuild,
|
|
1613
1730
|
MemberRelation,
|
|
@@ -1616,7 +1733,6 @@ export type {
|
|
|
1616
1733
|
ReadScope,
|
|
1617
1734
|
Tx,
|
|
1618
1735
|
Violation,
|
|
1619
|
-
WitnessedWriteResult,
|
|
1620
1736
|
WriteResult
|
|
1621
1737
|
}
|
|
1622
1738
|
export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
|