@bjornpagen/bumbledb 0.6.0 → 0.8.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 +150 -9
- package/README.md +18 -5
- package/dist/capacity.d.ts +310 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +138 -0
- package/dist/capacity.js.map +1 -0
- 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/db.d.ts +115 -76
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +178 -122
- 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 +11 -7
- 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 +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +3 -2
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +1 -1
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +20 -14
- 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 +66 -12
- 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 +95 -32
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +39 -7
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +62 -25
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +105 -22
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/capacity.ts +454 -0
- package/src/closed.ts +13 -7
- package/src/db.ts +302 -184
- package/src/exhume.ts +48 -11
- package/src/face.ts +14 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +20 -9
- package/src/law.ts +3 -2
- package/src/lower.ts +20 -14
- package/src/marshal.ts +38 -13
- package/src/native.ts +65 -11
- 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 +112 -34
- package/src/statements.ts +188 -38
- package/dist/count.d.ts +0 -102
- package/dist/count.d.ts.map +0 -1
- package/dist/count.js +0 -115
- package/dist/count.js.map +0 -1
- package/src/count.ts +0 -211
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
|
|
@@ -113,8 +117,10 @@ interface OffendingFact<Rels extends SchemaRelations> {
|
|
|
113
117
|
* orientation (identical strings; the engine's `render.rs` renders each
|
|
114
118
|
* partner of a mirrored pair as the `==` spelling, never a bare `<=`
|
|
115
119
|
* direction). `direction` (`sourceUnsatisfied` | `targetRequired`) and
|
|
116
|
-
* `
|
|
117
|
-
* the engine VERBATIM
|
|
120
|
+
* `measure` are the containment/capacity form payloads, passed through
|
|
121
|
+
* from the engine VERBATIM (`measure` the capacity form's witnessed group
|
|
122
|
+
* total — u128-wide, crossing whole as bigint, C3) — `direction` is
|
|
123
|
+
* relative to the violated SLOT's
|
|
118
124
|
* own orientation, so for a `mirrors` statement it alone cannot say which
|
|
119
125
|
* side of the `==` was violated: the slot identity is carried by
|
|
120
126
|
* `orientation`, present exactly for `mirrors` slots — `written` is the
|
|
@@ -127,21 +133,40 @@ interface Violation<Rels extends SchemaRelations> {
|
|
|
127
133
|
readonly canonical: string
|
|
128
134
|
readonly direction?: "sourceUnsatisfied" | "targetRequired"
|
|
129
135
|
readonly orientation?: "written" | "mirrored"
|
|
130
|
-
readonly
|
|
136
|
+
readonly measure?: bigint
|
|
131
137
|
readonly facts: readonly OffendingFact<Rels>[]
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
141
|
+
* The abandoned arm of a write result (ruled 2026-07-23, R10): present in
|
|
142
|
+
* the type EXACTLY when the callback can abandon — the conditional
|
|
143
|
+
* distributes over `R`, so a callback with no `Abandon` arm contributes
|
|
144
|
+
* `never` and the arm vanishes from the sum. The outcome is in the type;
|
|
145
|
+
* a dead arm is never handled.
|
|
138
146
|
*/
|
|
139
|
-
type
|
|
147
|
+
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
|
|
151
|
+
* R10): the committed generation; the COMPLETE violation set (every
|
|
152
|
+
* violated statement cited once, per direction for a containment, in
|
|
153
|
+
* materialized statement order); or the callback's own abandon payload —
|
|
154
|
+
* commit-vs-abandon is in the type, so a caller's explicit decline to
|
|
155
|
+
* commit can never be silently discarded. Narrows on `.ok`, then (when the
|
|
156
|
+
* callback can abandon) on `"violations" in result`.
|
|
157
|
+
*/
|
|
158
|
+
type WriteResult<Rels extends SchemaRelations, R = void> =
|
|
140
159
|
| { readonly ok: true; readonly generation: bigint }
|
|
141
160
|
| { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
|
|
161
|
+
| AbandonedArm<R>
|
|
142
162
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The delta-building callback of a write: runs synchronously against the
|
|
165
|
+
* live transaction. Returning {@link abandon}`(payload)` rolls the
|
|
166
|
+
* transaction back (R10) — the result type carries the payload arm exactly
|
|
167
|
+
* then.
|
|
168
|
+
*/
|
|
169
|
+
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
|
|
145
170
|
|
|
146
171
|
/**
|
|
147
172
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
@@ -151,10 +176,11 @@ type DeltaBuild<Rels extends SchemaRelations> = (tx: Tx<Rels>) => void
|
|
|
151
176
|
const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
152
177
|
|
|
153
178
|
/**
|
|
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 }
|
|
179
|
+
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
180
|
+
* or `writeWitnessed` callback rolls the transaction back WITHOUT
|
|
181
|
+
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
182
|
+
* `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
183
|
+
* sentinel's contract is unconditional, whichever write verb received it).
|
|
158
184
|
*/
|
|
159
185
|
interface Abandon<P> {
|
|
160
186
|
readonly [abandonMark]: true
|
|
@@ -162,26 +188,28 @@ interface Abandon<P> {
|
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
/**
|
|
165
|
-
* Wraps a payload in the {@link Abandon} sentinel — the one way a
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
191
|
+
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
192
|
+
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
193
|
+
* (nothing is committed, not even an empty commit) and the write resolves
|
|
194
|
+
* to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
|
|
195
|
+
* alike (R10).
|
|
169
196
|
*/
|
|
170
197
|
function abandon<P>(payload: P): Abandon<P> {
|
|
171
198
|
return Object.freeze({ [abandonMark]: true as const, payload })
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
/**
|
|
175
|
-
* The abandon payload type a
|
|
176
|
-
*
|
|
177
|
-
*
|
|
202
|
+
* The abandon payload type a write callback's return type implies: the
|
|
203
|
+
* payload of its `Abandon` arm, `never` when the callback can never
|
|
204
|
+
* abandon (the `abandoned` outcome is then statically unreachable and
|
|
205
|
+
* {@link AbandonedArm} erases it from the sum).
|
|
178
206
|
*/
|
|
179
207
|
type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
|
|
180
208
|
|
|
181
209
|
/**
|
|
182
|
-
* Narrows a
|
|
183
|
-
*
|
|
184
|
-
*
|
|
210
|
+
* Narrows a write callback result to the abandon sentinel. The probe is
|
|
211
|
+
* the private {@link abandonMark} symbol only {@link abandon} sets, and
|
|
212
|
+
* `R`'s `Abandon` arm is the only way a sentinel can flow out of the
|
|
185
213
|
* callback — so the narrowed payload type is sound by construction.
|
|
186
214
|
*/
|
|
187
215
|
function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
@@ -189,15 +217,30 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
|
189
217
|
}
|
|
190
218
|
|
|
191
219
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
220
|
+
* The abandon outcome's trusted admission seam: the value's shape is the
|
|
221
|
+
* checkable half (the sentinel mark only {@link abandon} mints, and the
|
|
222
|
+
* outcome carrying that sentinel's own payload), and the sentinel's
|
|
223
|
+
* existence IS the proof `R` carries an `Abandon` arm — so the outcome is
|
|
224
|
+
* admitted at the conditional {@link AbandonedArm} face the type tier
|
|
225
|
+
* cannot resolve over an open `R`.
|
|
196
226
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
function isAbandonedOutcome<Rels extends SchemaRelations, R>(
|
|
228
|
+
outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
|
|
229
|
+
sentinel: Abandon<AbandonedPayload<R>>
|
|
230
|
+
): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
|
|
231
|
+
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
235
|
+
function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
236
|
+
sentinel: Abandon<AbandonedPayload<R>>
|
|
237
|
+
): WriteResult<Rels, R> {
|
|
238
|
+
const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
|
|
239
|
+
if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
|
|
240
|
+
throw errors.new("bumbledb abandon outcome construction incomplete")
|
|
241
|
+
}
|
|
242
|
+
return outcome
|
|
243
|
+
}
|
|
201
244
|
|
|
202
245
|
/**
|
|
203
246
|
* One live write transaction: the submitted delta with the engine's
|
|
@@ -210,10 +253,17 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
210
253
|
/**
|
|
211
254
|
* Records one insert. Omitted fresh fields are MINTED through the
|
|
212
255
|
* engine's alloc lane and returned as bare bigints; supplying them instead
|
|
213
|
-
* preserves identity (the resupply idiom). Returns
|
|
214
|
-
*
|
|
256
|
+
* preserves identity (the resupply idiom). Returns `{ changed, ...fresh }`
|
|
257
|
+
* (ruled 2026-07-23, R11): the engine's changed-state report — the Rust
|
|
258
|
+
* surface's `insert(&fact) -> bool` bijection `delete` always honored —
|
|
259
|
+
* beside the relation's fresh cells, minted or resupplied. The
|
|
260
|
+
* idempotent-replay lane reads the bit from the insert itself; no extra
|
|
261
|
+
* `contains` round trip exists. The flattened shape cannot carry a FRESH
|
|
262
|
+
* cell literally named `changed` beside the report, so admission refuses
|
|
263
|
+
* that one spelling ({@link refuseShadowedChanged}) — never a silent
|
|
264
|
+
* shadow here.
|
|
215
265
|
*/
|
|
216
|
-
insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R>
|
|
266
|
+
insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): { readonly changed: boolean } & Minted<R>
|
|
217
267
|
/** Records one delete; `true` iff the final state changed. */
|
|
218
268
|
delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
219
269
|
/** Final-state membership of one complete fact. */
|
|
@@ -236,19 +286,20 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
/**
|
|
239
|
-
* The read view one `db.read(fn)` call scopes
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
289
|
+
* The read view one `db.read(fn)` call scopes — or one `using snap =
|
|
290
|
+
* db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
|
|
291
|
+
* disposables, never `close()`). The callback form invalidates the value
|
|
292
|
+
* when `fn` returns; the `using` form releases it at scope exit through
|
|
293
|
+
* `Symbol.dispose` — either way the release is deterministic and
|
|
294
|
+
* scope-shaped, every later verb call throws a typed used-after-scope
|
|
295
|
+
* error, and the underlying snapshot (with its LMDB reader slot) is
|
|
296
|
+
* already closed.
|
|
245
297
|
*/
|
|
246
|
-
interface ReadScope<Rels extends SchemaRelations> {
|
|
298
|
+
interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
247
299
|
/**
|
|
248
|
-
* The committed generation this scope witnessed —
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* open and the generation read.
|
|
300
|
+
* The committed generation this scope witnessed — carried by the
|
|
301
|
+
* snapshot open itself (one crossing, inside the snapshot's own
|
|
302
|
+
* transaction), so it is atomic with the snapshot by construction.
|
|
252
303
|
*/
|
|
253
304
|
readonly generation: bigint
|
|
254
305
|
/** Full-relation export in row-id order, decoded to bare structural facts. */
|
|
@@ -277,6 +328,16 @@ interface ReadScope<Rels extends SchemaRelations> {
|
|
|
277
328
|
* execution spelling ({@link Prepared} carries no `execute`).
|
|
278
329
|
*/
|
|
279
330
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
331
|
+
/**
|
|
332
|
+
* Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
|
|
333
|
+
* query against this scope's snapshot with counting instrumentation
|
|
334
|
+
* (the engine's ANALYZE semantics) and returns the structured
|
|
335
|
+
* {@link Explain} report — plan sections and counters as plain values,
|
|
336
|
+
* so the host reads what the engine did with its query without a
|
|
337
|
+
* second toolchain. A diagnostic surface, EXPLICITLY UNFROZEN: the
|
|
338
|
+
* shape follows the plan representation wherever it goes.
|
|
339
|
+
*/
|
|
340
|
+
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
280
341
|
}
|
|
281
342
|
|
|
282
343
|
/**
|
|
@@ -326,6 +387,14 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
326
387
|
* when `fn` returns — a used-after-scope call throws a typed error.
|
|
327
388
|
*/
|
|
328
389
|
read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
390
|
+
/**
|
|
391
|
+
* The `using` acquisition (ruled 2026-07-23, R12): `using snap =
|
|
392
|
+
* db.read()` — the caller owns the scope's lifetime, and the scope's
|
|
393
|
+
* `Symbol.dispose` releases the snapshot deterministically at scope
|
|
394
|
+
* exit, in the language's own syntax. Lifetimes are disposables, never
|
|
395
|
+
* `close()`.
|
|
396
|
+
*/
|
|
397
|
+
read(): ReadScope<Rels>
|
|
329
398
|
/** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
|
|
330
399
|
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
331
400
|
/** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
|
|
@@ -340,12 +409,18 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
340
409
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
341
410
|
/** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
|
|
342
411
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
412
|
+
/** `db.explain(p, params)` === `db.read(snap => snap.explain(p, params))` — the symmetry rule (R13). */
|
|
413
|
+
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
343
414
|
/**
|
|
344
415
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
345
416
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
346
|
-
* the delta (LMDB untouched) and rethrows wrapped.
|
|
417
|
+
* the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
|
|
418
|
+
* commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
|
|
419
|
+
* R10): the transaction rolls back — nothing is committed, not even an
|
|
420
|
+
* empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
|
|
421
|
+
* an arm the result type carries exactly when the callback can abandon.
|
|
347
422
|
*/
|
|
348
|
-
write(fn: DeltaBuild<Rels>): WriteResult<Rels>
|
|
423
|
+
write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
|
|
349
424
|
/**
|
|
350
425
|
* The ONE witnessed-write form: snapshot → `fn` (premise reads via
|
|
351
426
|
* `snap`, delta via `tx`) → witnessed commit, which lands only if no
|
|
@@ -364,7 +439,7 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
364
439
|
* `{ ok: false, abandoned: payload }` and NO commit (not even an empty
|
|
365
440
|
* one) is issued.
|
|
366
441
|
*/
|
|
367
|
-
writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R):
|
|
442
|
+
writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R>
|
|
368
443
|
/**
|
|
369
444
|
* Prepares a query value built against THIS schema (identity is the
|
|
370
445
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
@@ -463,10 +538,10 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
|
463
538
|
}
|
|
464
539
|
|
|
465
540
|
/**
|
|
466
|
-
* One declared statement's materialized slots: a key or
|
|
467
|
-
* one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
468
|
-
* two containments, `source <= target` first), both owned by the
|
|
469
|
-
* value.
|
|
541
|
+
* One declared statement's materialized slots: a key or capacity statement
|
|
542
|
+
* occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
543
|
+
* `==` to two containments, `source <= target` first), both owned by the
|
|
544
|
+
* one SDK value.
|
|
470
545
|
*/
|
|
471
546
|
function declaredEntries(statement: Statement): StatementEntry[] {
|
|
472
547
|
const data = statement.data
|
|
@@ -489,8 +564,8 @@ function declaredEntries(statement: Statement): StatementEntry[] {
|
|
|
489
564
|
}
|
|
490
565
|
return [{ kind: "containment", statement, key: undefined }]
|
|
491
566
|
}
|
|
492
|
-
case "
|
|
493
|
-
return [{ kind: "
|
|
567
|
+
case "capacity": {
|
|
568
|
+
return [{ kind: "capacity", statement, key: undefined }]
|
|
494
569
|
}
|
|
495
570
|
}
|
|
496
571
|
}
|
|
@@ -507,18 +582,16 @@ function isThenable(value: unknown): boolean {
|
|
|
507
582
|
|
|
508
583
|
/**
|
|
509
584
|
* Narrows a keyed-get middle argument to a statement value (vs a key
|
|
510
|
-
* object)
|
|
511
|
-
*
|
|
512
|
-
*
|
|
585
|
+
* object) through the statement module's admission brand — a
|
|
586
|
+
* REPRESENTATION, never a shape probe: fact cell shapes are structurally
|
|
587
|
+
* OPEN (an interval value carrying an excess `kind` property is a legal
|
|
588
|
+
* cell), so no property probe could ever be sound here, but no host-built
|
|
589
|
+
* key object can spell the module-private brand symbol.
|
|
513
590
|
*/
|
|
514
591
|
function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
|
|
515
592
|
value: KeyFact<R> | KeyStatement<R, P>
|
|
516
593
|
): 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
|
|
594
|
+
return isStatement(value)
|
|
522
595
|
}
|
|
523
596
|
|
|
524
597
|
/**
|
|
@@ -569,7 +642,11 @@ interface Tables {
|
|
|
569
642
|
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
570
643
|
* positional mirror against the engine's reported order — any drift
|
|
571
644
|
* (count, kind, id, or membership) is a construction-time failure, never a
|
|
572
|
-
* silent misattribution of a violation to the wrong statement value.
|
|
645
|
+
* silent misattribution of a violation to the wrong statement value. The
|
|
646
|
+
* declaration-ordinal law the query lowering leans on is verified in the
|
|
647
|
+
* same walks: relation ids and sealed field ids both equal declaration
|
|
648
|
+
* order, so a constructed `Tables` IS the proof and `prepare` inherits it
|
|
649
|
+
* structurally — never a silently misaddressed query.
|
|
573
650
|
*/
|
|
574
651
|
function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
575
652
|
const entries = materializedEntries(theory)
|
|
@@ -596,6 +673,13 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
596
673
|
for (const field of relation.fields) {
|
|
597
674
|
fieldIds.set(field.name, field.id)
|
|
598
675
|
}
|
|
676
|
+
sealedFieldsOf(member).forEach(function verifyField(declared, fieldOrdinal) {
|
|
677
|
+
if (fieldIds.get(declared.name) !== fieldOrdinal) {
|
|
678
|
+
throw errors.new(
|
|
679
|
+
`bumbledb manifest drift: ${relation.name}.${declared.name} has engine field id ${fieldIds.get(declared.name)}, its sealed ordinal is ${fieldOrdinal}`
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
})
|
|
599
683
|
let primaryKey: PrimaryKey | undefined
|
|
600
684
|
entries.forEach(function firstOwnedKey(entry, index) {
|
|
601
685
|
if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
|
|
@@ -604,11 +688,17 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
604
688
|
})
|
|
605
689
|
relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
|
|
606
690
|
}
|
|
607
|
-
|
|
608
|
-
|
|
691
|
+
Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
|
|
692
|
+
const entry = relations.get(name)
|
|
693
|
+
if (entry === undefined) {
|
|
609
694
|
throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`)
|
|
610
695
|
}
|
|
611
|
-
|
|
696
|
+
if (entry.id !== ordinal) {
|
|
697
|
+
throw errors.new(
|
|
698
|
+
`bumbledb manifest drift: relation ${name} has engine id ${entry.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
})
|
|
612
702
|
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
613
703
|
}
|
|
614
704
|
|
|
@@ -619,14 +709,20 @@ interface PointReads {
|
|
|
619
709
|
}
|
|
620
710
|
|
|
621
711
|
/**
|
|
622
|
-
* One read scope's PRIVATE lifetime record: its live snapshot handle,
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
712
|
+
* One read scope's PRIVATE lifetime record: its live snapshot handle, the
|
|
713
|
+
* generation it witnessed (carried by the snapshot open itself — one
|
|
714
|
+
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
715
|
+
* `read`/`writeWitnessed` callback returns, or by the scope's own
|
|
716
|
+
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
717
|
+
* exactly once, whichever of the owner and the dispose gets there first),
|
|
718
|
+
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
719
|
+
* the snapshot handle is never a public value.
|
|
626
720
|
*/
|
|
627
721
|
interface ScopeState {
|
|
628
722
|
readonly handle: SnapshotHandle
|
|
723
|
+
readonly generation: bigint
|
|
629
724
|
live: boolean
|
|
725
|
+
closed: boolean
|
|
630
726
|
readonly owner: object
|
|
631
727
|
}
|
|
632
728
|
|
|
@@ -795,7 +891,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
795
891
|
canonical: wire.canonical,
|
|
796
892
|
direction: wire.direction,
|
|
797
893
|
orientation: orientationOf(entry.reversed),
|
|
798
|
-
|
|
894
|
+
measure: wire.measure,
|
|
799
895
|
facts: Object.freeze(wire.facts.map(offendingFactOf))
|
|
800
896
|
})
|
|
801
897
|
}
|
|
@@ -818,7 +914,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
818
914
|
)
|
|
819
915
|
}
|
|
820
916
|
if (entry.kind !== "functionality" || entry.key === undefined) {
|
|
821
|
-
throw errors.new("keyed get takes a key() statement — containments and
|
|
917
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
822
918
|
}
|
|
823
919
|
if (entry.key.owner !== relation.name) {
|
|
824
920
|
throw errors.new(
|
|
@@ -900,10 +996,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
900
996
|
/**
|
|
901
997
|
* Builds one {@link ReadScope} over a live scope state. Every verb
|
|
902
998
|
* asserts liveness first: the owning call flips `state.live` the moment
|
|
903
|
-
* its callback returns
|
|
999
|
+
* its callback returns (or the scope's own `Symbol.dispose` does, for a
|
|
1000
|
+
* `using`-acquired scope), so a leaked scope is a typed refusal forever
|
|
904
1001
|
* after.
|
|
905
1002
|
*/
|
|
906
|
-
function makeScope(state: ScopeState
|
|
1003
|
+
function makeScope(state: ScopeState): ReadScope<Rels> {
|
|
907
1004
|
function assertLive(): void {
|
|
908
1005
|
if (!state.live) {
|
|
909
1006
|
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
@@ -940,12 +1037,27 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
940
1037
|
})
|
|
941
1038
|
return decodeAnswers<Row>(plan.finds, rows)
|
|
942
1039
|
}
|
|
1040
|
+
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1041
|
+
assertLive()
|
|
1042
|
+
const plan = planOf(prepared)
|
|
1043
|
+
const wire = wireParams(plan.params, recordOf(params))
|
|
1044
|
+
return bridged("explain bumbledb prepared query", function callExplain() {
|
|
1045
|
+
return native.preparedExplain(plan.handle, state.handle, wire)
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1049
|
+
function dispose(): void {
|
|
1050
|
+
state.live = false
|
|
1051
|
+
closeScopeState(state)
|
|
1052
|
+
}
|
|
943
1053
|
const scope: ReadScope<Rels> = Object.freeze({
|
|
944
|
-
generation,
|
|
1054
|
+
generation: state.generation,
|
|
945
1055
|
scan,
|
|
946
1056
|
get: reads.get,
|
|
947
1057
|
contains: reads.contains,
|
|
948
|
-
execute
|
|
1058
|
+
execute,
|
|
1059
|
+
explain,
|
|
1060
|
+
[Symbol.dispose]: dispose
|
|
949
1061
|
})
|
|
950
1062
|
scopeStates.set(scope, state)
|
|
951
1063
|
return scope
|
|
@@ -959,50 +1071,52 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
959
1071
|
*/
|
|
960
1072
|
let liveSnapshots = 0
|
|
961
1073
|
|
|
962
|
-
/**
|
|
1074
|
+
/**
|
|
1075
|
+
* Opens one snapshot and its scope state (live until the owner flips
|
|
1076
|
+
* it). The witnessed generation rides the snapshot open itself — one
|
|
1077
|
+
* crossing carries both (finding 016), so the fault-pairing close
|
|
1078
|
+
* branch a second `dbGeneration` call needed is structurally gone.
|
|
1079
|
+
*/
|
|
963
1080
|
function openScopeState(): ScopeState {
|
|
964
|
-
const
|
|
1081
|
+
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
965
1082
|
return native.dbSnapshot(handle)
|
|
966
1083
|
})
|
|
967
1084
|
liveSnapshots += 1
|
|
968
|
-
return { handle:
|
|
1085
|
+
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
|
|
969
1086
|
}
|
|
970
1087
|
|
|
971
|
-
/**
|
|
1088
|
+
/**
|
|
1089
|
+
* Closes a scope's snapshot after the owner invalidated it — a LATCH:
|
|
1090
|
+
* the snapshot closes exactly once, whichever of the owning call and
|
|
1091
|
+
* the scope's own `Symbol.dispose` gets there first, so an early
|
|
1092
|
+
* in-callback disposal never double-closes (and never double-counts
|
|
1093
|
+
* the census).
|
|
1094
|
+
*/
|
|
972
1095
|
function closeScopeState(state: ScopeState): void {
|
|
1096
|
+
if (state.closed) {
|
|
1097
|
+
return
|
|
1098
|
+
}
|
|
1099
|
+
state.closed = true
|
|
973
1100
|
bridged("close bumbledb snapshot", function closeSnapshot() {
|
|
974
1101
|
native.snapshotClose(state.handle)
|
|
975
1102
|
})
|
|
976
1103
|
liveSnapshots -= 1
|
|
977
1104
|
}
|
|
978
1105
|
|
|
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 {
|
|
1106
|
+
function read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
1107
|
+
function read(): ReadScope<Rels>
|
|
1108
|
+
function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
|
|
1003
1109
|
const state = openScopeState()
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1110
|
+
const scope = makeScope(state)
|
|
1111
|
+
if (fn === undefined) {
|
|
1112
|
+
/**
|
|
1113
|
+
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
1114
|
+
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
1115
|
+
* is the deterministic release, scope-shaped in the language's
|
|
1116
|
+
* own syntax.
|
|
1117
|
+
*/
|
|
1118
|
+
return scope
|
|
1119
|
+
}
|
|
1006
1120
|
const result = errors.trySync(function runRead() {
|
|
1007
1121
|
return fn(scope)
|
|
1008
1122
|
})
|
|
@@ -1057,6 +1171,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1057
1171
|
})
|
|
1058
1172
|
}
|
|
1059
1173
|
|
|
1174
|
+
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1175
|
+
return read(function explainInScope(snap) {
|
|
1176
|
+
return snap.explain(prepared, params)
|
|
1177
|
+
})
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1060
1180
|
/**
|
|
1061
1181
|
* Builds one {@link Tx} over a transaction-handle thunk: `write` passes
|
|
1062
1182
|
* an already-begun handle; `writeWitnessed` passes a LAZY thunk that
|
|
@@ -1085,21 +1205,25 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1085
1205
|
})
|
|
1086
1206
|
}
|
|
1087
1207
|
})
|
|
1088
|
-
function insert<R extends MemberRelation<Rels>>(
|
|
1208
|
+
function insert<R extends MemberRelation<Rels>>(
|
|
1209
|
+
relation: R,
|
|
1210
|
+
fact: InsertFact<R>
|
|
1211
|
+
): { readonly changed: boolean } & Minted<R> {
|
|
1089
1212
|
assertLive()
|
|
1090
1213
|
const entry = resolveOrdinary(relation)
|
|
1091
1214
|
const txHandle = resolveTx()
|
|
1092
|
-
|
|
1215
|
+
/** 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. */
|
|
1216
|
+
const values: Record<string, unknown> = { ...recordOf(fact) }
|
|
1093
1217
|
const fresh = mintFreshCells(txHandle, entry, relation, values)
|
|
1094
1218
|
const row = rowOf(relation.data, values)
|
|
1095
|
-
bridged("bumbledb tx insert", function record() {
|
|
1096
|
-
native.txInsert(txHandle, entry.id, row)
|
|
1219
|
+
const changed = bridged("bumbledb tx insert", function record() {
|
|
1220
|
+
return native.txInsert(txHandle, entry.id, row)
|
|
1097
1221
|
})
|
|
1098
|
-
Object.freeze(fresh)
|
|
1099
|
-
if (!
|
|
1100
|
-
throw errors.new(`relation ${relation.name}:
|
|
1222
|
+
const inserted: Readonly<Record<string, FactValue | boolean>> = Object.freeze({ changed, ...fresh })
|
|
1223
|
+
if (!isInserted(relation, inserted)) {
|
|
1224
|
+
throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
|
|
1101
1225
|
}
|
|
1102
|
-
return
|
|
1226
|
+
return inserted
|
|
1103
1227
|
}
|
|
1104
1228
|
function remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1105
1229
|
assertLive()
|
|
@@ -1122,7 +1246,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1122
1246
|
return { tx, spend }
|
|
1123
1247
|
}
|
|
1124
1248
|
|
|
1125
|
-
function runDelta(txHandle: TxHandle, fn: DeltaBuild<Rels>): WriteResult<Rels> {
|
|
1249
|
+
function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
|
|
1126
1250
|
const made = makeTx(function resolveTx() {
|
|
1127
1251
|
return txHandle
|
|
1128
1252
|
})
|
|
@@ -1152,6 +1276,18 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1152
1276
|
"bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1153
1277
|
)
|
|
1154
1278
|
}
|
|
1279
|
+
if (isAbandon(built.data)) {
|
|
1280
|
+
/**
|
|
1281
|
+
* The caller's explicit decline to commit (R10): the sentinel's
|
|
1282
|
+
* contract is unconditional — roll back, nothing committed, not
|
|
1283
|
+
* even an empty commit; commit is unreachable for a sentinel
|
|
1284
|
+
* result.
|
|
1285
|
+
*/
|
|
1286
|
+
bridged("abort bumbledb write transaction", function abort() {
|
|
1287
|
+
native.txAbort(txHandle)
|
|
1288
|
+
})
|
|
1289
|
+
return abandonedOutcome<Rels, R>(built.data)
|
|
1290
|
+
}
|
|
1155
1291
|
const committed = errors.trySync(function commitDelta() {
|
|
1156
1292
|
return bridged("commit bumbledb write transaction", function commit() {
|
|
1157
1293
|
return native.txCommit(txHandle)
|
|
@@ -1182,16 +1318,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1182
1318
|
})
|
|
1183
1319
|
}
|
|
1184
1320
|
|
|
1185
|
-
function write(fn: DeltaBuild<Rels>): WriteResult<Rels> {
|
|
1186
|
-
const
|
|
1187
|
-
return
|
|
1188
|
-
return native.dbWriteBegin(handle)
|
|
1189
|
-
})
|
|
1321
|
+
function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
|
|
1322
|
+
const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
|
|
1323
|
+
return native.dbWriteBegin(handle)
|
|
1190
1324
|
})
|
|
1191
|
-
|
|
1192
|
-
throw errors.wrap(begun.error, `begin bumbledb write transaction (live snapshots at fault: ${liveSnapshots})`)
|
|
1193
|
-
}
|
|
1194
|
-
return runDelta(begun.data, fn)
|
|
1325
|
+
return runDelta(txHandle, fn)
|
|
1195
1326
|
}
|
|
1196
1327
|
|
|
1197
1328
|
/**
|
|
@@ -1199,7 +1330,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1199
1330
|
* attempt's snapshot: the committed generation, or the engine's
|
|
1200
1331
|
* complete violation set as data.
|
|
1201
1332
|
*/
|
|
1202
|
-
function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle):
|
|
1333
|
+
function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WriteResult<Rels, R> {
|
|
1203
1334
|
const committed = errors.trySync(function commitWitnessedDelta() {
|
|
1204
1335
|
return bridged("commit bumbledb witnessed write transaction", function commit() {
|
|
1205
1336
|
return native.txCommit(txHandle)
|
|
@@ -1235,12 +1366,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1235
1366
|
* `undefined` exactly when the generation moved and the whole callback
|
|
1236
1367
|
* must rerun on a fresh snapshot.
|
|
1237
1368
|
*/
|
|
1238
|
-
function witnessedAttempt<R>(
|
|
1239
|
-
fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R
|
|
1240
|
-
): WitnessedWriteResult<Rels, R> | undefined {
|
|
1369
|
+
function witnessedAttempt<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> | undefined {
|
|
1241
1370
|
const state = openScopeState()
|
|
1242
|
-
const
|
|
1243
|
-
const scope = makeScope(state, generation)
|
|
1371
|
+
const scope = makeScope(state)
|
|
1244
1372
|
const pending: { tx: TxHandle | undefined } = { tx: undefined }
|
|
1245
1373
|
function beginWitnessed(): TxHandle | undefined {
|
|
1246
1374
|
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
@@ -1304,7 +1432,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1304
1432
|
if (isAbandon(built.data)) {
|
|
1305
1433
|
abortPending()
|
|
1306
1434
|
closeScopeState(state)
|
|
1307
|
-
return
|
|
1435
|
+
return abandonedOutcome<Rels, R>(built.data)
|
|
1308
1436
|
}
|
|
1309
1437
|
const late = errors.trySync(function resolveCommitTx() {
|
|
1310
1438
|
if (pending.tx === undefined) {
|
|
@@ -1340,7 +1468,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1340
1468
|
* error, never a loop — retry is host policy, and this is the host
|
|
1341
1469
|
* policy's own honesty bound).
|
|
1342
1470
|
*/
|
|
1343
|
-
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R):
|
|
1471
|
+
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> {
|
|
1344
1472
|
for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
|
|
1345
1473
|
const attempt = witnessedAttempt(fn)
|
|
1346
1474
|
if (attempt !== undefined) {
|
|
@@ -1353,45 +1481,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1353
1481
|
)
|
|
1354
1482
|
}
|
|
1355
1483
|
|
|
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
1484
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
1389
1485
|
if (q.schema !== theory) {
|
|
1390
1486
|
throw errors.new(
|
|
1391
1487
|
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
1392
1488
|
)
|
|
1393
1489
|
}
|
|
1394
|
-
assertOrdinalAlignment()
|
|
1395
1490
|
const program = lowerQuery(q)
|
|
1396
1491
|
const outcome = bridged("prepare bumbledb program", function callPrepare() {
|
|
1397
1492
|
return native.dbPrepare(handle, program)
|
|
@@ -1438,6 +1533,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1438
1533
|
get,
|
|
1439
1534
|
contains,
|
|
1440
1535
|
execute,
|
|
1536
|
+
explain,
|
|
1441
1537
|
write,
|
|
1442
1538
|
writeWitnessed,
|
|
1443
1539
|
prepare
|
|
@@ -1508,6 +1604,27 @@ const ErrNewtypeMismatch = errors.new(
|
|
|
1508
1604
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1509
1605
|
)
|
|
1510
1606
|
|
|
1607
|
+
/**
|
|
1608
|
+
* `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
|
|
1609
|
+
* where the spread wins: a FRESH field literally named `changed` would
|
|
1610
|
+
* shadow the engine's changed-state report on every insert of its
|
|
1611
|
+
* relation. No field name is reserved SILENTLY — the one unspeakable
|
|
1612
|
+
* spelling is refused here at admission, before any store is touched.
|
|
1613
|
+
* Supplied (non-fresh) fields named `changed` never enter the return
|
|
1614
|
+
* record and stay legal.
|
|
1615
|
+
*/
|
|
1616
|
+
function refuseShadowedChanged(theory: AnySchema): void {
|
|
1617
|
+
for (const [name, member] of Object.entries(theory.relations)) {
|
|
1618
|
+
for (const declared of sealedFieldsOf(member)) {
|
|
1619
|
+
if (declared.name === "changed" && isFreshField(declared.field)) {
|
|
1620
|
+
throw errors.new(
|
|
1621
|
+
`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)`
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1511
1628
|
/**
|
|
1512
1629
|
* The one admission path both verbs share: canonical-path cache lookup
|
|
1513
1630
|
* first (a hit returns the SAME `Db` value for the identical theory, a
|
|
@@ -1525,6 +1642,7 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1525
1642
|
storePath: string,
|
|
1526
1643
|
theory: Schema<Rels>
|
|
1527
1644
|
): Db<Rels> {
|
|
1645
|
+
refuseShadowedChanged(theory)
|
|
1528
1646
|
const canonical = path.resolve(storePath)
|
|
1529
1647
|
const cached = openStores.get(canonical)
|
|
1530
1648
|
if (cached !== undefined) {
|
|
@@ -1594,12 +1712,12 @@ const Db = Object.freeze({
|
|
|
1594
1712
|
* one schema-independent read path (no theory, no fingerprint check; the
|
|
1595
1713
|
* store rebirth tool's entry). Lives beside `open`/`create` so the path
|
|
1596
1714
|
* 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).
|
|
1715
|
+
* applied here. The value is NOT cached and is a DISPOSABLE lifetime
|
|
1716
|
+
* (R12) — `using exhumed = await Db.exhume(path)` releases the engine
|
|
1717
|
+
* handle and the store's exclusive lock at scope exit, so a same-path
|
|
1718
|
+
* reopen never waits on GC. A store not yet adopted rejects with the
|
|
1719
|
+
* typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
|
|
1720
|
+
* `Db.open` under the creating schema back-fills the descriptor).
|
|
1603
1721
|
*/
|
|
1604
1722
|
async exhume(storePath: string): Promise<Exhumed> {
|
|
1605
1723
|
return exhumeStore(path.resolve(storePath))
|
|
@@ -1608,6 +1726,7 @@ const Db = Object.freeze({
|
|
|
1608
1726
|
|
|
1609
1727
|
export type {
|
|
1610
1728
|
Abandon,
|
|
1729
|
+
AbandonedArm,
|
|
1611
1730
|
DeclaredKeyFact,
|
|
1612
1731
|
DeltaBuild,
|
|
1613
1732
|
MemberRelation,
|
|
@@ -1616,7 +1735,6 @@ export type {
|
|
|
1616
1735
|
ReadScope,
|
|
1617
1736
|
Tx,
|
|
1618
1737
|
Violation,
|
|
1619
|
-
WitnessedWriteResult,
|
|
1620
1738
|
WriteResult
|
|
1621
1739
|
}
|
|
1622
1740
|
export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
|