@bjornpagen/bumbledb 0.12.2 → 0.15.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 +35 -21
- package/README.md +82 -55
- package/dist/db.d.ts +173 -130
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +782 -371
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +7 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/marshal.d.ts +5 -27
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +4 -21
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +157 -168
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +46 -8
- package/dist/native.js.map +1 -1
- package/dist/query/find.d.ts +14 -9
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +2 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +5 -4
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +11 -9
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/relation.d.ts +4 -25
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -4
- package/dist/relation.js.map +1 -1
- package/package.json +3 -3
- package/src/db.ts +1151 -500
- package/src/index.ts +28 -24
- package/src/marshal.ts +5 -35
- package/src/native.ts +248 -175
- package/src/query/find.ts +19 -14
- package/src/query/lower.ts +7 -6
- package/src/query/parse-ir.ts +11 -9
- package/src/relation.ts +3 -28
- package/dist/exhume.d.ts +0 -143
- package/dist/exhume.d.ts.map +0 -1
- package/dist/exhume.js +0 -166
- package/dist/exhume.js.map +0 -1
- package/src/exhume.ts +0 -267
package/src/db.ts
CHANGED
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
* `Db` — the living half of the SDK (PRD-07): open/create a store from a
|
|
3
3
|
* `Schema`, write typed facts through delta transactions with race-free
|
|
4
4
|
* final-state point reads, receive rejections as typed violation VALUES
|
|
5
|
-
* keyed to statements, and read through
|
|
6
|
-
* the schema's relations record.
|
|
5
|
+
* keyed to statements, and read through a synchronous instance callback —
|
|
6
|
+
* all typed by the schema's relations record.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* at scope exit. Prepared plans are plain values whose engine-side half
|
|
16
|
-
* is reclaimed by a GC finalizer — reclamation only, never correctness.
|
|
8
|
+
* A store read is one callback: `db.read((instance, witness) => …)`. The
|
|
9
|
+
* instance is invalid the moment the callback returns; the witness is a
|
|
10
|
+
* cloneable token and may escape. There is no handle-shaped read and no
|
|
11
|
+
* `using snap = db.read()`. Builder, owned instance, and witness
|
|
12
|
+
* implement `Symbol.dispose`. Prepared plans are plain values whose
|
|
13
|
+
* engine-side half is reclaimed by a GC finalizer — reclamation only,
|
|
14
|
+
* never correctness.
|
|
17
15
|
*
|
|
18
16
|
* PROCESS MODEL: one process, one exclusive-lock handle per store. The
|
|
19
17
|
* `Db` value owns the LMDB environment's exclusive lock until process
|
|
@@ -24,48 +22,44 @@
|
|
|
24
22
|
* policy.
|
|
25
23
|
*
|
|
26
24
|
* REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
|
|
27
|
-
* LLM repair prompt downstream), returned as a {@link
|
|
28
|
-
* {@link Violation} values.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* LLM repair prompt downstream), returned as a {@link WriteOutcome}
|
|
26
|
+
* carrying {@link Violation} values. A moved generation on
|
|
27
|
+
* {@link Db.writeFrom} is the `{ tag: "moved" }` arm, not an exception.
|
|
28
|
+
* Genuine failures — I/O, used-after-scope, spent handle, marshal shape —
|
|
29
|
+
* throw `@superbuilders/errors` wrapped errors instead.
|
|
31
30
|
*/
|
|
32
31
|
|
|
33
32
|
import * as path from "node:path"
|
|
34
33
|
import * as errors from "@superbuilders/errors"
|
|
35
34
|
import { isClosedMember, sealedFieldsOf } from "#closed.ts"
|
|
36
|
-
import type { Exhumed } from "#exhume.ts"
|
|
37
|
-
import { exhumeStore } from "#exhume.ts"
|
|
38
35
|
import { rosterOf } from "#fields.ts"
|
|
39
36
|
import { lower } from "#lower.ts"
|
|
40
|
-
import {
|
|
41
|
-
factOf,
|
|
42
|
-
handleOf,
|
|
43
|
-
isFreshField,
|
|
44
|
-
isInserted,
|
|
45
|
-
type KeyFact,
|
|
46
|
-
keyRowOf,
|
|
47
|
-
type Minted,
|
|
48
|
-
recordOf,
|
|
49
|
-
rowOf
|
|
50
|
-
} from "#marshal.ts"
|
|
37
|
+
import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
51
38
|
|
|
52
39
|
import type {
|
|
40
|
+
AdmitResult,
|
|
41
|
+
BuilderHandle,
|
|
53
42
|
DbHandle,
|
|
54
43
|
FactValue,
|
|
44
|
+
InstanceHandle,
|
|
55
45
|
Manifest,
|
|
46
|
+
NativeWriteOutcome,
|
|
47
|
+
OwnedHandle,
|
|
56
48
|
PreparedHandle,
|
|
57
|
-
SnapshotHandle,
|
|
58
49
|
TxHandle,
|
|
50
|
+
WireFreshRange,
|
|
59
51
|
Violation as WireViolation,
|
|
60
|
-
ViolationFact as WireViolationFact
|
|
52
|
+
ViolationFact as WireViolationFact,
|
|
53
|
+
WireMutationReport,
|
|
54
|
+
WitnessHandle
|
|
61
55
|
} from "#native.ts"
|
|
62
|
-
import { bridged, native } from "#native.ts"
|
|
56
|
+
import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
|
|
63
57
|
import type { FindColumn } from "#query/atom.ts"
|
|
64
58
|
import type { Query } from "#query/lower.ts"
|
|
65
59
|
import { lowerQuery } from "#query/lower.ts"
|
|
66
60
|
import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
67
61
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
68
|
-
import type { AnyRelation, Fact,
|
|
62
|
+
import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
|
|
69
63
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
70
64
|
import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
71
65
|
|
|
@@ -76,6 +70,130 @@ import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
|
76
70
|
*/
|
|
77
71
|
type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
|
|
78
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Facts consumed vs facts that changed the in-memory final-state view.
|
|
75
|
+
* The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
|
|
76
|
+
*/
|
|
77
|
+
interface MutationReport {
|
|
78
|
+
readonly submitted: bigint
|
|
79
|
+
readonly changed: bigint
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Column-major collection write: one array per declared field, every
|
|
84
|
+
* column the same length. The second transport of `load` / `insert` —
|
|
85
|
+
* objects and columns are two ways to spell the same batch.
|
|
86
|
+
*/
|
|
87
|
+
type ColumnBatch<R extends AnyRelation> = {
|
|
88
|
+
readonly [K in keyof Fact<R> & string]: readonly Fact<R>[K][]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>> | ColumnBatch<R>
|
|
92
|
+
|
|
93
|
+
function isColumnBatch(value: object): boolean {
|
|
94
|
+
return !(Symbol.iterator in value)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FactValue[][] {
|
|
98
|
+
const rows: FactValue[][] = []
|
|
99
|
+
for (const fact of facts) {
|
|
100
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
101
|
+
}
|
|
102
|
+
return rows
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Lowers a column batch to per-field wire arrays in sealed field order.
|
|
107
|
+
* Allocates one array per field — never a JS array per row.
|
|
108
|
+
*/
|
|
109
|
+
function columnsOf(relation: AnyRelation, batch: object): FactValue[][] {
|
|
110
|
+
const record = recordOf(batch)
|
|
111
|
+
let count: number | undefined
|
|
112
|
+
return relation.data.fields.map(function marshalColumn(declared) {
|
|
113
|
+
const raw = record[declared.name]
|
|
114
|
+
if (!Array.isArray(raw)) {
|
|
115
|
+
throw errors.new(`relation ${relation.name}: column ${declared.name} is not an array`)
|
|
116
|
+
}
|
|
117
|
+
if (count === undefined) {
|
|
118
|
+
count = raw.length
|
|
119
|
+
} else if (raw.length !== count) {
|
|
120
|
+
throw errors.new(
|
|
121
|
+
`relation ${relation.name}: column ${declared.name} has length ${raw.length}, expected ${count}`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return raw.map(function marshalCell(value: unknown) {
|
|
125
|
+
return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mutateCollection<R extends AnyRelation>(
|
|
131
|
+
relation: R,
|
|
132
|
+
facts: CollectionWrite<R>,
|
|
133
|
+
applyRows: (rows: readonly FactValue[][]) => WireMutationReport,
|
|
134
|
+
applyColumns: (columns: readonly FactValue[][]) => WireMutationReport
|
|
135
|
+
): MutationReport {
|
|
136
|
+
const report = isColumnBatch(facts)
|
|
137
|
+
? applyColumns(columnsOf(relation, facts))
|
|
138
|
+
: applyRows(rowsOf(relation, facts as Iterable<Fact<R>>))
|
|
139
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Half-open fresh-id range from one `reserve`. Empty cannot yield a
|
|
144
|
+
* minted id — `start` exists only on the nonempty arm.
|
|
145
|
+
*/
|
|
146
|
+
type FreshRange =
|
|
147
|
+
| {
|
|
148
|
+
readonly empty: true
|
|
149
|
+
readonly count: 0n
|
|
150
|
+
at(index: bigint): undefined
|
|
151
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
152
|
+
}
|
|
153
|
+
| {
|
|
154
|
+
readonly empty: false
|
|
155
|
+
readonly start: bigint
|
|
156
|
+
readonly endExclusive: bigint
|
|
157
|
+
readonly count: bigint
|
|
158
|
+
at(index: bigint): bigint | undefined
|
|
159
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function freshRangeOf(wire: WireFreshRange): FreshRange {
|
|
163
|
+
if (wire.empty) {
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
empty: true,
|
|
166
|
+
count: 0n,
|
|
167
|
+
at(_index: bigint) {
|
|
168
|
+
return undefined
|
|
169
|
+
},
|
|
170
|
+
*[Symbol.iterator](): IterableIterator<bigint> {}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
const start = wire.start
|
|
174
|
+
const endExclusive = wire.endExclusive
|
|
175
|
+
const count = endExclusive - start
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
empty: false,
|
|
178
|
+
start,
|
|
179
|
+
endExclusive,
|
|
180
|
+
get count() {
|
|
181
|
+
return count
|
|
182
|
+
},
|
|
183
|
+
at(index: bigint) {
|
|
184
|
+
if (index < 0n || index >= count) {
|
|
185
|
+
return undefined
|
|
186
|
+
}
|
|
187
|
+
return start + index
|
|
188
|
+
},
|
|
189
|
+
*[Symbol.iterator](): IterableIterator<bigint> {
|
|
190
|
+
for (let id = start; id < endExclusive; id++) {
|
|
191
|
+
yield id
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
79
197
|
/**
|
|
80
198
|
* The key object of a key-statement-selected `get`: exactly the selected
|
|
81
199
|
* `key()` statement's projection fields, each at the relation's own BARE
|
|
@@ -190,29 +308,40 @@ type Violation<Rels extends SchemaRelations> =
|
|
|
190
308
|
* `never` and the arm vanishes from the sum. The outcome is in the type;
|
|
191
309
|
* a dead arm is never handled.
|
|
192
310
|
*/
|
|
193
|
-
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly
|
|
311
|
+
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
|
|
312
|
+
|
|
313
|
+
/** A callback return that is not Promise-like. TypeScript `never` is not a runtime boundary. */
|
|
314
|
+
type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
|
|
315
|
+
|
|
316
|
+
interface Committed<T> {
|
|
317
|
+
readonly value: T
|
|
318
|
+
readonly generation: bigint
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type Admission<Rels extends SchemaRelations, T> =
|
|
322
|
+
| { readonly tag: "accepted"; readonly value: T }
|
|
323
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
194
324
|
|
|
195
325
|
/**
|
|
196
|
-
* A write's domain outcome
|
|
197
|
-
*
|
|
198
|
-
* violated statement cited once, per direction for a containment, in
|
|
199
|
-
* materialized statement order); or the callback's own abandon payload —
|
|
200
|
-
* commit-vs-abandon is in the type, so a caller's explicit decline to
|
|
201
|
-
* commit can never be silently discarded. Narrows on `.ok`, then (when the
|
|
202
|
-
* callback can abandon) on `"violations" in result`.
|
|
326
|
+
* A write's domain outcome. One discriminant: narrow on `tag`. The
|
|
327
|
+
* abandoned arm is present exactly when the callback can abandon.
|
|
203
328
|
*/
|
|
204
|
-
type
|
|
205
|
-
| { readonly
|
|
206
|
-
| { readonly
|
|
329
|
+
type WriteOutcome<Rels extends SchemaRelations, R> =
|
|
330
|
+
| { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
|
|
331
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
207
332
|
| AbandonedArm<R>
|
|
208
333
|
|
|
334
|
+
type WriteFromOutcome<Rels extends SchemaRelations, R> =
|
|
335
|
+
| WriteOutcome<Rels, R>
|
|
336
|
+
| { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
|
|
337
|
+
|
|
209
338
|
/**
|
|
210
339
|
* The delta-building callback of a write: runs synchronously against the
|
|
211
340
|
* live transaction. Returning {@link abandon}`(payload)` rolls the
|
|
212
341
|
* transaction back (R10) — the result type carries the payload arm exactly
|
|
213
342
|
* then.
|
|
214
343
|
*/
|
|
215
|
-
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx:
|
|
344
|
+
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
|
|
216
345
|
|
|
217
346
|
/**
|
|
218
347
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
@@ -225,7 +354,7 @@ const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
|
225
354
|
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
226
355
|
* or `writeFrom` callback rolls the transaction back WITHOUT
|
|
227
356
|
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
228
|
-
* `{
|
|
357
|
+
* `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
229
358
|
* sentinel's contract is unconditional, whichever write verb received it).
|
|
230
359
|
*/
|
|
231
360
|
interface Abandon<P> {
|
|
@@ -237,7 +366,7 @@ interface Abandon<P> {
|
|
|
237
366
|
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
238
367
|
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
239
368
|
* (nothing is committed, not even an empty commit) and the write resolves
|
|
240
|
-
* to `{
|
|
369
|
+
* to `{ tag: "abandoned", abandoned: payload }`, from `write` and `writeFrom`
|
|
241
370
|
* alike (R10).
|
|
242
371
|
*/
|
|
243
372
|
function abandon<P>(payload: P): Abandon<P> {
|
|
@@ -271,17 +400,17 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
|
271
400
|
* cannot resolve over an open `R`.
|
|
272
401
|
*/
|
|
273
402
|
function isAbandonedOutcome<Rels extends SchemaRelations, R>(
|
|
274
|
-
outcome: { readonly
|
|
403
|
+
outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
|
|
275
404
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
276
|
-
): outcome is { readonly
|
|
405
|
+
): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
|
|
277
406
|
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
|
|
278
407
|
}
|
|
279
408
|
|
|
280
409
|
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
281
410
|
function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
282
411
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
283
|
-
):
|
|
284
|
-
const outcome = Object.freeze({
|
|
412
|
+
): WriteOutcome<Rels, R> {
|
|
413
|
+
const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
|
|
285
414
|
if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
|
|
286
415
|
throw errors.new("bumbledb abandon outcome construction incomplete")
|
|
287
416
|
}
|
|
@@ -295,23 +424,24 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
|
295
424
|
* Spent when its owning `write`/`writeFrom` call resolves the attempt;
|
|
296
425
|
* any later use throws.
|
|
297
426
|
*/
|
|
298
|
-
interface
|
|
427
|
+
interface WriteTx<Rels extends SchemaRelations> {
|
|
299
428
|
/**
|
|
300
|
-
* Records
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* surface's `insert(&fact) -> bool` bijection `delete` always honored —
|
|
305
|
-
* beside the relation's fresh cells, minted or resupplied. The
|
|
306
|
-
* idempotent-replay lane reads the bit from the insert itself; no extra
|
|
307
|
-
* `contains` round trip exists. The flattened shape cannot carry a FRESH
|
|
308
|
-
* cell literally named `changed` beside the report, so admission refuses
|
|
309
|
-
* that one spelling ({@link refuseShadowedChanged}) — never a silent
|
|
310
|
-
* shadow here.
|
|
429
|
+
* Records a collection of inserts. Singleton is `[fact]`. Empty is
|
|
430
|
+
* lawful. Returns how many facts were consumed and how many changed
|
|
431
|
+
* the in-memory final-state view. Every fact is complete — omitted
|
|
432
|
+
* fresh cells are a type error; mint first with {@link WriteTx.reserve}.
|
|
311
433
|
*/
|
|
312
|
-
insert<R extends MemberRelation<Rels>>(relation: R,
|
|
313
|
-
/**
|
|
314
|
-
|
|
434
|
+
insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
435
|
+
/**
|
|
436
|
+
* Records a collection of deletes. Singleton is `[fact]`. Returns
|
|
437
|
+
* how many facts were consumed and how many changed the view.
|
|
438
|
+
*/
|
|
439
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
440
|
+
/**
|
|
441
|
+
* Mints `count` consecutive fresh values for a `.fresh` field.
|
|
442
|
+
* `count === 0n` is empty and does not yield a start.
|
|
443
|
+
*/
|
|
444
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
315
445
|
/** Final-state membership of one complete fact. */
|
|
316
446
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
317
447
|
/**
|
|
@@ -331,21 +461,27 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
331
461
|
): Fact<R> | undefined
|
|
332
462
|
}
|
|
333
463
|
|
|
464
|
+
type Tx<Rels extends SchemaRelations> = WriteTx<Rels>
|
|
465
|
+
|
|
466
|
+
const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
|
|
467
|
+
|
|
334
468
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* disposables, never `close()`). The callback form invalidates the value
|
|
338
|
-
* when `fn` returns; the `using` form releases it at scope exit through
|
|
339
|
-
* `Symbol.dispose` — either way the release is deterministic and
|
|
340
|
-
* scope-shaped, every later verb call throws a typed used-after-scope
|
|
341
|
-
* error, and the underlying snapshot (with its LMDB reader slot) is
|
|
342
|
-
* already closed.
|
|
469
|
+
* Cloneable generation evidence from one store read. May cross `await`.
|
|
470
|
+
* Disposal is idempotent; later use throws {@link ErrSpentHandle}.
|
|
343
471
|
*/
|
|
344
|
-
interface
|
|
472
|
+
interface Witness<Rels extends SchemaRelations> extends Disposable {
|
|
473
|
+
readonly [witnessTypes]?: Rels
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The borrowed instance one `db.read((instance, witness) => …)` callback
|
|
478
|
+
* receives. Invalid the moment the callback returns. A stashed value
|
|
479
|
+
* throws {@link ErrUseAfterScope}. Not a handle: there is no `db.read()`.
|
|
480
|
+
*/
|
|
481
|
+
interface ReadInstance<Rels extends SchemaRelations> {
|
|
345
482
|
/**
|
|
346
|
-
* The committed generation this
|
|
347
|
-
*
|
|
348
|
-
* transaction), so it is atomic with the snapshot by construction.
|
|
483
|
+
* The committed generation this instance witnessed — read inside the
|
|
484
|
+
* lease's own transaction.
|
|
349
485
|
*/
|
|
350
486
|
readonly generation: bigint
|
|
351
487
|
/** Full-relation export in row-id order, decoded to bare structural facts. */
|
|
@@ -368,12 +504,13 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
|
368
504
|
/** Committed-state membership of one complete fact. */
|
|
369
505
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
370
506
|
/**
|
|
371
|
-
* Executes a prepared query against this
|
|
372
|
-
*
|
|
373
|
-
*
|
|
507
|
+
* Executes a prepared query against this instance with the typed
|
|
508
|
+
* params object; returns the answer SET as plain rows with bare
|
|
509
|
+
* structural values (no order — the host sorts). This is the ONE
|
|
374
510
|
* execution spelling ({@link Prepared} carries no `execute`).
|
|
375
511
|
*/
|
|
376
512
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
513
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
377
514
|
}
|
|
378
515
|
|
|
379
516
|
/**
|
|
@@ -390,7 +527,7 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
|
|
|
390
527
|
* One prepared query as a plain VALUE: explicit visible compilation
|
|
391
528
|
* (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
|
|
392
529
|
* refusal), no lifecycle. Execution happens ONLY through
|
|
393
|
-
* `
|
|
530
|
+
* `instance.execute(prepared, params)` / `db.execute(prepared, params)` — the
|
|
394
531
|
* symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
|
|
395
532
|
* finalizer when this value becomes unreachable (reclamation only, never
|
|
396
533
|
* correctness — an unreclaimed plan is idle memory, and process exit frees
|
|
@@ -411,53 +548,42 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
411
548
|
/** The theory this store was opened with (fingerprint-verified by the engine). */
|
|
412
549
|
readonly schema: Schema<Rels>
|
|
413
550
|
/**
|
|
414
|
-
* One
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
|
|
419
|
-
read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
420
|
-
/**
|
|
421
|
-
* The `using` acquisition (ruled 2026-07-23, R12): `using snap =
|
|
422
|
-
* db.read()` — the caller owns the scope's lifetime, and the scope's
|
|
423
|
-
* `Symbol.dispose` releases the snapshot deterministically at scope
|
|
424
|
-
* exit, in the language's own syntax. Lifetimes are disposables, never
|
|
425
|
-
* `close()`.
|
|
551
|
+
* One store read: runs `body` SYNCHRONOUSLY inside the engine lease
|
|
552
|
+
* and returns its result. The {@link ReadInstance} is invalidated
|
|
553
|
+
* when `body` returns — a stashed use throws {@link ErrUseAfterScope}.
|
|
554
|
+
* The {@link Witness} is a clone and may escape. A thenable return
|
|
555
|
+
* throws {@link ErrAsyncCallback}.
|
|
426
556
|
*/
|
|
427
|
-
read(
|
|
428
|
-
/** `db.scan(r)` === `db.read(
|
|
557
|
+
read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
|
|
558
|
+
/** `db.scan(r)` === `db.read(instance => instance.scan(r))` — the symmetry rule. */
|
|
429
559
|
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
430
|
-
/** `db.get(r, k)` === `db.read(
|
|
560
|
+
/** `db.get(r, k)` === `db.read(instance => instance.get(r, k))` — the symmetry rule. */
|
|
431
561
|
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
432
|
-
/** `db.get(r, s, k)` === `db.read(
|
|
562
|
+
/** `db.get(r, s, k)` === `db.read(instance => instance.get(r, s, k))` — the symmetry rule, keyed form. */
|
|
433
563
|
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
434
564
|
relation: R,
|
|
435
565
|
keyStatement: KeyStatement<R, P>,
|
|
436
566
|
key: DeclaredKeyFact<R, P>
|
|
437
567
|
): Fact<R> | undefined
|
|
438
|
-
/** `db.contains(r, f)` === `db.read(
|
|
568
|
+
/** `db.contains(r, f)` === `db.read(instance => instance.contains(r, f))` — the symmetry rule. */
|
|
439
569
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
440
|
-
/** `db.execute(p, params)` === `db.read(
|
|
570
|
+
/** `db.execute(p, params)` === `db.read(instance => instance.execute(p, params))` — the symmetry rule. */
|
|
441
571
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
442
572
|
/**
|
|
443
573
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
444
574
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
445
575
|
* the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
|
|
446
|
-
* commit by returning {@link abandon}`(payload)
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* an arm the result type carries exactly when the callback can abandon.
|
|
576
|
+
* commit by returning {@link abandon}`(payload)`: the transaction rolls
|
|
577
|
+
* back — nothing is committed, not even an empty commit — and the
|
|
578
|
+
* outcome is `{ tag: "abandoned", abandoned: payload }`.
|
|
450
579
|
*/
|
|
451
|
-
write<R
|
|
580
|
+
write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
|
|
452
581
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* `snap`. A moved generation is the typed {@link ErrGenerationMoved}
|
|
457
|
-
* — retry is host policy; this method never loops. `fn` may decline to
|
|
458
|
-
* commit by returning {@link abandon}`(payload)`.
|
|
582
|
+
* Witnessed write: commits only if no state-changing commit landed
|
|
583
|
+
* since `witness` was minted. A moved generation is
|
|
584
|
+
* `{ tag: "moved" }` — retry is host policy; this method never loops.
|
|
459
585
|
*/
|
|
460
|
-
writeFrom<R>(
|
|
586
|
+
writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
|
|
461
587
|
/**
|
|
462
588
|
* Prepares a query value built against THIS schema (identity is the
|
|
463
589
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
@@ -724,6 +850,29 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
724
850
|
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
725
851
|
}
|
|
726
852
|
|
|
853
|
+
function tablesFromTheory(theory: AnySchema): Tables {
|
|
854
|
+
const entries = materializedEntries(theory)
|
|
855
|
+
const relations = new Map<string, RelationEntry>()
|
|
856
|
+
Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
|
|
857
|
+
const member = theory.relations[name]
|
|
858
|
+
if (member === undefined) {
|
|
859
|
+
throw errors.new(`bumbledb theory has no relation ${name}`)
|
|
860
|
+
}
|
|
861
|
+
const fieldIds = new Map<string, number>()
|
|
862
|
+
sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
|
|
863
|
+
fieldIds.set(declared.name, fieldOrdinal)
|
|
864
|
+
})
|
|
865
|
+
let primaryKey: PrimaryKey | undefined
|
|
866
|
+
entries.forEach(function firstOwnedKey(entry, index) {
|
|
867
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
|
|
868
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
|
|
872
|
+
})
|
|
873
|
+
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
874
|
+
}
|
|
875
|
+
|
|
727
876
|
/** The point-read half a transaction and a read scope share, over their own handle. */
|
|
728
877
|
interface PointReads {
|
|
729
878
|
contains(relationId: number, row: readonly FactValue[]): boolean
|
|
@@ -731,25 +880,33 @@ interface PointReads {
|
|
|
731
880
|
}
|
|
732
881
|
|
|
733
882
|
/**
|
|
734
|
-
* One
|
|
735
|
-
*
|
|
736
|
-
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
737
|
-
* `read` callback returns, or by the scope's own
|
|
738
|
-
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
739
|
-
* exactly once, whichever of the owner and the dispose gets there first),
|
|
740
|
-
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
741
|
-
* the snapshot handle is never a public value.
|
|
883
|
+
* One borrowed instance's PRIVATE lifetime record. Held in
|
|
884
|
+
* {@link instanceStates} — the native handle is never a public value.
|
|
742
885
|
*/
|
|
743
|
-
interface
|
|
744
|
-
readonly handle:
|
|
745
|
-
readonly generation: bigint
|
|
886
|
+
interface InstanceState {
|
|
887
|
+
readonly handle: InstanceHandle
|
|
746
888
|
live: boolean
|
|
747
|
-
closed: boolean
|
|
748
889
|
readonly owner: object
|
|
749
890
|
}
|
|
750
891
|
|
|
751
|
-
|
|
752
|
-
|
|
892
|
+
const instanceStates = new WeakMap<object, InstanceState>()
|
|
893
|
+
|
|
894
|
+
interface WitnessState {
|
|
895
|
+
readonly handle: WitnessHandle
|
|
896
|
+
spent: boolean
|
|
897
|
+
readonly owner: object
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const witnessStates = new WeakMap<object, WitnessState>()
|
|
901
|
+
|
|
902
|
+
const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
|
|
903
|
+
const closed = errors.trySync(function closeWitness() {
|
|
904
|
+
native.witnessClose(handle)
|
|
905
|
+
})
|
|
906
|
+
if (closed.error) {
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
})
|
|
753
910
|
|
|
754
911
|
/**
|
|
755
912
|
* One prepared value's PRIVATE engine half: the pinned plan handle, the
|
|
@@ -782,51 +939,304 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
|
|
|
782
939
|
}
|
|
783
940
|
})
|
|
784
941
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
"bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
|
|
942
|
+
const ErrAsyncCallback = errors.new(
|
|
943
|
+
"bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
|
|
944
|
+
)
|
|
945
|
+
const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
|
|
946
|
+
const ErrUseAfterScope = errors.new(
|
|
947
|
+
"bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
|
|
792
948
|
)
|
|
949
|
+
const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
|
|
950
|
+
const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
|
|
793
951
|
|
|
794
952
|
/**
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* insert's return. Mutates `values` in place with the minted cells.
|
|
953
|
+
* The shared typed read surface: store leases and owned instances both
|
|
954
|
+
* expose scan/get/contains/execute/prepare. The native ops are the only
|
|
955
|
+
* difference — one way to read, two handle kinds.
|
|
799
956
|
*/
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
957
|
+
interface CatalogNative {
|
|
958
|
+
scan(relationId: number): FactValue[][]
|
|
959
|
+
contains(relationId: number, values: readonly FactValue[]): boolean
|
|
960
|
+
get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
|
|
961
|
+
prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
|
|
962
|
+
execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function catalogMethods<Rels extends SchemaRelations>(
|
|
966
|
+
theory: Schema<Rels>,
|
|
967
|
+
tables: Tables,
|
|
968
|
+
owner: object,
|
|
969
|
+
assertLive: () => void,
|
|
970
|
+
ops: CatalogNative
|
|
971
|
+
): Pick<ReadInstance<Rels>, "scan" | "get" | "contains" | "execute" | "prepare"> {
|
|
972
|
+
function resolveOrdinary(relation: AnyRelation): RelationEntry {
|
|
973
|
+
const entry = tables.relations.get(relation.name)
|
|
974
|
+
if (entry === undefined || entry.member !== relation) {
|
|
975
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
810
976
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
977
|
+
if (isClosedMember(relation)) {
|
|
978
|
+
throw errors.new(
|
|
979
|
+
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
return entry
|
|
983
|
+
}
|
|
984
|
+
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
985
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
986
|
+
return "statement" in candidate && candidate.statement === statement
|
|
987
|
+
})
|
|
988
|
+
const entry = tables.statements[statementId]
|
|
989
|
+
if (entry === undefined) {
|
|
990
|
+
throw errors.new(
|
|
991
|
+
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
if (entry.kind !== "functionality") {
|
|
995
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
996
|
+
}
|
|
997
|
+
if (entry.owner !== relation.name) {
|
|
998
|
+
throw errors.new(
|
|
999
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
1003
|
+
}
|
|
1004
|
+
function planOf(prepared: object): PreparedPlan {
|
|
1005
|
+
const plan = preparedPlans.get(prepared)
|
|
1006
|
+
if (plan === undefined) {
|
|
1007
|
+
throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
|
|
1008
|
+
}
|
|
1009
|
+
if (plan.owner !== owner) {
|
|
1010
|
+
throw errors.wrap(
|
|
1011
|
+
ErrForeignPrepared,
|
|
1012
|
+
`bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
return plan
|
|
1016
|
+
}
|
|
1017
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1018
|
+
assertLive()
|
|
1019
|
+
const entry = resolveOrdinary(relation)
|
|
1020
|
+
return bridged("bumbledb instance contains", function readContains() {
|
|
1021
|
+
return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1025
|
+
relation: R,
|
|
1026
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1027
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
1028
|
+
): Fact<R> | undefined {
|
|
1029
|
+
assertLive()
|
|
1030
|
+
const entry = resolveOrdinary(relation)
|
|
1031
|
+
return selectKeyRead(
|
|
1032
|
+
keyOrStatement,
|
|
1033
|
+
declaredKey,
|
|
1034
|
+
function byStatement(statement, key) {
|
|
1035
|
+
const selected = declaredKeyOf(relation, statement)
|
|
1036
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
1037
|
+
return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
|
|
1038
|
+
})
|
|
1039
|
+
return row === null ? undefined : factOf(relation, row)
|
|
1040
|
+
},
|
|
1041
|
+
function byPrimary(key) {
|
|
1042
|
+
const primaryKey = entry.primaryKey
|
|
1043
|
+
if (primaryKey === undefined) {
|
|
1044
|
+
throw errors.new(
|
|
1045
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
1046
|
+
)
|
|
1047
|
+
}
|
|
1048
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
1049
|
+
return ops.get(
|
|
1050
|
+
entry.id,
|
|
1051
|
+
primaryKey.statementId,
|
|
1052
|
+
keyRowOf(relation.data, primaryKey.projection, recordOf(key))
|
|
1053
|
+
)
|
|
1054
|
+
})
|
|
1055
|
+
return row === null ? undefined : factOf(relation, row)
|
|
816
1056
|
}
|
|
817
|
-
|
|
818
|
-
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
1060
|
+
assertLive()
|
|
1061
|
+
const entry = resolveOrdinary(relation)
|
|
1062
|
+
const rows = bridged("bumbledb instance scan", function readScan() {
|
|
1063
|
+
return ops.scan(entry.id)
|
|
1064
|
+
})
|
|
1065
|
+
return rows.map(function decodeRow(row) {
|
|
1066
|
+
return factOf(relation, row)
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1070
|
+
assertLive()
|
|
1071
|
+
const plan = planOf(prepared)
|
|
1072
|
+
const wire = wireParams(plan.params, recordOf(params))
|
|
1073
|
+
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
1074
|
+
return ops.execute(plan.handle, wire)
|
|
1075
|
+
})
|
|
1076
|
+
return decodeAnswers<Row>(plan.finds, rows)
|
|
1077
|
+
}
|
|
1078
|
+
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
1079
|
+
assertLive()
|
|
1080
|
+
if (q.schema !== theory) {
|
|
1081
|
+
throw errors.new(
|
|
1082
|
+
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
const queryIr = lowerQuery(q)
|
|
1086
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
1087
|
+
return ops.prepare(queryIr)
|
|
1088
|
+
})
|
|
1089
|
+
if (!outcome.ok) {
|
|
1090
|
+
throwPrepareRefusal(outcome.message)
|
|
1091
|
+
}
|
|
1092
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1093
|
+
preparedPlans.set(
|
|
1094
|
+
prepared,
|
|
1095
|
+
Object.freeze({
|
|
1096
|
+
handle: outcome.prepared,
|
|
1097
|
+
owner,
|
|
1098
|
+
params: q.data.params,
|
|
1099
|
+
finds: q.data.finds
|
|
819
1100
|
})
|
|
820
|
-
|
|
1101
|
+
)
|
|
1102
|
+
planReclaimer.register(prepared, outcome.prepared)
|
|
1103
|
+
return prepared
|
|
1104
|
+
}
|
|
1105
|
+
return { scan, get, contains, execute, prepare }
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
|
|
1109
|
+
const entry = tables.relations.get(relation.name)
|
|
1110
|
+
if (entry === undefined || entry.member !== relation) {
|
|
1111
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
1112
|
+
}
|
|
1113
|
+
if (isClosedMember(relation)) {
|
|
1114
|
+
throw errors.new(
|
|
1115
|
+
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
return entry
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function overlayMethods<Rels extends SchemaRelations>(
|
|
1122
|
+
theory: Schema<Rels>,
|
|
1123
|
+
tables: Tables,
|
|
1124
|
+
assertLive: () => void,
|
|
1125
|
+
reads: PointReads
|
|
1126
|
+
): Pick<WriteTx<Rels>, "contains" | "get"> {
|
|
1127
|
+
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
1128
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
1129
|
+
return "statement" in candidate && candidate.statement === statement
|
|
1130
|
+
})
|
|
1131
|
+
const entry = tables.statements[statementId]
|
|
1132
|
+
if (entry === undefined) {
|
|
1133
|
+
throw errors.new(
|
|
1134
|
+
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
if (entry.kind !== "functionality") {
|
|
1138
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
821
1139
|
}
|
|
822
|
-
if (
|
|
1140
|
+
if (entry.owner !== relation.name) {
|
|
823
1141
|
throw errors.new(
|
|
824
|
-
`
|
|
1142
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
1146
|
+
}
|
|
1147
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1148
|
+
assertLive()
|
|
1149
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1150
|
+
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
1151
|
+
}
|
|
1152
|
+
function readThroughKey<R extends MemberRelation<Rels>>(
|
|
1153
|
+
relation: R,
|
|
1154
|
+
entry: RelationEntry,
|
|
1155
|
+
selected: PrimaryKey,
|
|
1156
|
+
key: Readonly<Record<string, unknown>>
|
|
1157
|
+
): Fact<R> | undefined {
|
|
1158
|
+
const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
|
|
1159
|
+
if (row === null) {
|
|
1160
|
+
return undefined
|
|
1161
|
+
}
|
|
1162
|
+
return factOf(relation, row)
|
|
1163
|
+
}
|
|
1164
|
+
function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1165
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1166
|
+
relation: R,
|
|
1167
|
+
keyStatement: KeyStatement<R, P>,
|
|
1168
|
+
key: DeclaredKeyFact<R, P>
|
|
1169
|
+
): Fact<R> | undefined
|
|
1170
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1171
|
+
relation: R,
|
|
1172
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1173
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
1174
|
+
): Fact<R> | undefined {
|
|
1175
|
+
assertLive()
|
|
1176
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1177
|
+
return selectKeyRead(
|
|
1178
|
+
keyOrStatement,
|
|
1179
|
+
declaredKey,
|
|
1180
|
+
function byStatement(statement, key) {
|
|
1181
|
+
return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
|
|
1182
|
+
},
|
|
1183
|
+
function byPrimary(key) {
|
|
1184
|
+
const primaryKey = entry.primaryKey
|
|
1185
|
+
if (primaryKey === undefined) {
|
|
1186
|
+
throw errors.new(
|
|
1187
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
1188
|
+
)
|
|
1189
|
+
}
|
|
1190
|
+
return readThroughKey(relation, entry, primaryKey, recordOf(key))
|
|
1191
|
+
}
|
|
1192
|
+
)
|
|
1193
|
+
}
|
|
1194
|
+
return { contains, get }
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function createReadInstance<Rels extends SchemaRelations>(
|
|
1198
|
+
nativeHandle: InstanceHandle,
|
|
1199
|
+
theory: Schema<Rels>,
|
|
1200
|
+
tables: Tables,
|
|
1201
|
+
owner: object
|
|
1202
|
+
): ReadInstance<Rels> {
|
|
1203
|
+
const state: InstanceState = { handle: nativeHandle, live: true, owner }
|
|
1204
|
+
function assertLive(): void {
|
|
1205
|
+
if (!state.live) {
|
|
1206
|
+
throw errors.wrap(
|
|
1207
|
+
ErrUseAfterScope,
|
|
1208
|
+
"bumbledb read instance is invalidated — its owning callback already returned"
|
|
825
1209
|
)
|
|
826
1210
|
}
|
|
827
|
-
fresh[declared.name] = cell
|
|
828
1211
|
}
|
|
829
|
-
|
|
1212
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
1213
|
+
scan(relationId) {
|
|
1214
|
+
return native.instanceScan(state.handle, relationId)
|
|
1215
|
+
},
|
|
1216
|
+
contains(relationId, values) {
|
|
1217
|
+
return native.instanceContains(state.handle, relationId, values)
|
|
1218
|
+
},
|
|
1219
|
+
get(relationId, statementId, keyValues) {
|
|
1220
|
+
return native.instanceGet(state.handle, relationId, statementId, keyValues)
|
|
1221
|
+
},
|
|
1222
|
+
prepare(query) {
|
|
1223
|
+
return native.instancePrepare(state.handle, query)
|
|
1224
|
+
},
|
|
1225
|
+
execute(prepared, params) {
|
|
1226
|
+
return native.preparedExecute(prepared, state.handle, params)
|
|
1227
|
+
}
|
|
1228
|
+
})
|
|
1229
|
+
const instance: ReadInstance<Rels> = Object.freeze({
|
|
1230
|
+
get generation() {
|
|
1231
|
+
assertLive()
|
|
1232
|
+
return bridged("bumbledb instance generation", function readGeneration() {
|
|
1233
|
+
return native.instanceGeneration(state.handle)
|
|
1234
|
+
})
|
|
1235
|
+
},
|
|
1236
|
+
...methods
|
|
1237
|
+
})
|
|
1238
|
+
instanceStates.set(instance, state)
|
|
1239
|
+
return instance
|
|
830
1240
|
}
|
|
831
1241
|
|
|
832
1242
|
/**
|
|
@@ -1004,152 +1414,70 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1004
1414
|
return { contains, get }
|
|
1005
1415
|
}
|
|
1006
1416
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
)
|
|
1020
|
-
|
|
1021
|
-
|
|
1417
|
+
function pinPrepared<Row, Params extends ParamsRecord>(
|
|
1418
|
+
preparedHandle: PreparedHandle,
|
|
1419
|
+
q: Query<Rels, Row, Params>
|
|
1420
|
+
): Prepared<Rels, Row, Params> {
|
|
1421
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1422
|
+
preparedPlans.set(
|
|
1423
|
+
prepared,
|
|
1424
|
+
Object.freeze({
|
|
1425
|
+
handle: preparedHandle,
|
|
1426
|
+
owner,
|
|
1427
|
+
params: q.data.params,
|
|
1428
|
+
finds: q.data.finds
|
|
1429
|
+
})
|
|
1430
|
+
)
|
|
1431
|
+
planReclaimer.register(prepared, preparedHandle)
|
|
1432
|
+
return prepared
|
|
1022
1433
|
}
|
|
1023
1434
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
const reads = pointReadsOf(assertLive, {
|
|
1038
|
-
contains(relationId, row) {
|
|
1039
|
-
return bridged("bumbledb snapshot contains", function readContains() {
|
|
1040
|
-
return native.snapshotContains(state.handle, relationId, row)
|
|
1041
|
-
})
|
|
1042
|
-
},
|
|
1043
|
-
get(relationId, statementId, key) {
|
|
1044
|
-
return bridged("bumbledb snapshot get", function readGet() {
|
|
1045
|
-
return native.snapshotGet(state.handle, relationId, statementId, key)
|
|
1435
|
+
function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
|
|
1436
|
+
const state: WitnessState = { handle: nativeHandle, spent: false, owner }
|
|
1437
|
+
const witness: Witness<Rels> = Object.freeze({
|
|
1438
|
+
[Symbol.dispose](): void {
|
|
1439
|
+
if (state.spent) {
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1442
|
+
state.spent = true
|
|
1443
|
+
bridged("close bumbledb witness", function closeWitness() {
|
|
1444
|
+
native.witnessClose(nativeHandle)
|
|
1046
1445
|
})
|
|
1047
1446
|
}
|
|
1048
1447
|
})
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const rows = bridged("bumbledb snapshot scan", function readScan() {
|
|
1053
|
-
return native.snapshotScan(state.handle, entry.id)
|
|
1054
|
-
})
|
|
1055
|
-
return rows.map(function decodeRow(row) {
|
|
1056
|
-
return factOf(relation, row)
|
|
1057
|
-
})
|
|
1058
|
-
}
|
|
1059
|
-
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1060
|
-
assertLive()
|
|
1061
|
-
const plan = planOf(prepared)
|
|
1062
|
-
const wire = wireParams(plan.params, recordOf(params))
|
|
1063
|
-
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
1064
|
-
return native.preparedExecute(plan.handle, state.handle, wire)
|
|
1065
|
-
})
|
|
1066
|
-
return decodeAnswers<Row>(plan.finds, rows)
|
|
1067
|
-
}
|
|
1068
|
-
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1069
|
-
function dispose(): void {
|
|
1070
|
-
state.live = false
|
|
1071
|
-
closeScopeState(state)
|
|
1072
|
-
}
|
|
1073
|
-
const scope: ReadScope<Rels> = Object.freeze({
|
|
1074
|
-
generation: state.generation,
|
|
1075
|
-
scan,
|
|
1076
|
-
get: reads.get,
|
|
1077
|
-
contains: reads.contains,
|
|
1078
|
-
execute,
|
|
1079
|
-
[Symbol.dispose]: dispose
|
|
1080
|
-
})
|
|
1081
|
-
scopeStates.set(scope, state)
|
|
1082
|
-
return scope
|
|
1448
|
+
witnessStates.set(witness, state)
|
|
1449
|
+
witnessReclaimer.register(witness, nativeHandle)
|
|
1450
|
+
return witness
|
|
1083
1451
|
}
|
|
1084
1452
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
* snapshot open/close is counted so a write-begin failure can report how
|
|
1088
|
-
* many read handles were live at the fault — a leaked scope is invisible
|
|
1089
|
-
* until the exact moment it matters, so the failure carries the census.
|
|
1090
|
-
*/
|
|
1091
|
-
let liveSnapshots = 0
|
|
1092
|
-
|
|
1093
|
-
/**
|
|
1094
|
-
* Opens one snapshot and its scope state (live until the owner flips
|
|
1095
|
-
* it). The witnessed generation rides the snapshot open itself — one
|
|
1096
|
-
* crossing carries both (finding 016), so the fault-pairing close
|
|
1097
|
-
* branch a second `dbGeneration` call needed is structurally gone.
|
|
1098
|
-
*/
|
|
1099
|
-
function openScopeState(): ScopeState {
|
|
1100
|
-
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
1101
|
-
return native.dbSnapshot(handle)
|
|
1102
|
-
})
|
|
1103
|
-
liveSnapshots += 1
|
|
1104
|
-
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
|
|
1453
|
+
function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
|
|
1454
|
+
return createReadInstance(nativeHandle, theory, tables, owner)
|
|
1105
1455
|
}
|
|
1106
1456
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
function read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
1126
|
-
function read(): ReadScope<Rels>
|
|
1127
|
-
function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
|
|
1128
|
-
const state = openScopeState()
|
|
1129
|
-
const scope = makeScope(state)
|
|
1130
|
-
if (fn === undefined) {
|
|
1131
|
-
/**
|
|
1132
|
-
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
1133
|
-
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
1134
|
-
* is the deterministic release, scope-shaped in the language's
|
|
1135
|
-
* own syntax.
|
|
1136
|
-
*/
|
|
1137
|
-
return scope
|
|
1138
|
-
}
|
|
1139
|
-
const result = errors.trySync(function runRead() {
|
|
1140
|
-
return fn(scope)
|
|
1457
|
+
function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
|
|
1458
|
+
let captured: R | undefined
|
|
1459
|
+
const result = bridged("bumbledb read", function runRead() {
|
|
1460
|
+
return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
|
|
1461
|
+
const instance = makeInstance(nativeInstance)
|
|
1462
|
+
const witness = makeWitness(nativeWitness)
|
|
1463
|
+
const value = body(instance, witness)
|
|
1464
|
+
const state = instanceStates.get(instance)
|
|
1465
|
+
if (state !== undefined) {
|
|
1466
|
+
state.live = false
|
|
1467
|
+
}
|
|
1468
|
+
if (isThenable(value)) {
|
|
1469
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
|
|
1470
|
+
}
|
|
1471
|
+
captured = value
|
|
1472
|
+
return value
|
|
1473
|
+
})
|
|
1141
1474
|
})
|
|
1142
|
-
|
|
1143
|
-
closeScopeState(state)
|
|
1144
|
-
if (result.error) {
|
|
1145
|
-
throw errors.wrap(result.error, "bumbledb read")
|
|
1146
|
-
}
|
|
1147
|
-
return result.data
|
|
1475
|
+
return (captured ?? result) as SyncResult<R>
|
|
1148
1476
|
}
|
|
1149
1477
|
|
|
1150
1478
|
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
1151
|
-
return read(function scanInScope(
|
|
1152
|
-
return
|
|
1479
|
+
return read(function scanInScope(instance) {
|
|
1480
|
+
return instance.scan(relation)
|
|
1153
1481
|
})
|
|
1154
1482
|
}
|
|
1155
1483
|
|
|
@@ -1164,41 +1492,39 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1164
1492
|
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1165
1493
|
declaredKey?: DeclaredKeyFact<R, P>
|
|
1166
1494
|
): Fact<R> | undefined {
|
|
1167
|
-
|
|
1168
|
-
|
|
1495
|
+
let found: Fact<R> | undefined
|
|
1496
|
+
read(function getInScope(instance) {
|
|
1497
|
+
found = selectKeyRead(
|
|
1169
1498
|
keyOrStatement,
|
|
1170
1499
|
declaredKey,
|
|
1171
1500
|
function byStatement(statement, key) {
|
|
1172
|
-
return
|
|
1501
|
+
return instance.get(relation, statement, key)
|
|
1173
1502
|
},
|
|
1174
1503
|
function byPrimary(key) {
|
|
1175
|
-
return
|
|
1504
|
+
return instance.get(relation, key)
|
|
1176
1505
|
}
|
|
1177
1506
|
)
|
|
1178
1507
|
})
|
|
1508
|
+
return found
|
|
1179
1509
|
}
|
|
1180
1510
|
|
|
1181
1511
|
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1182
|
-
return read(function containsInScope(
|
|
1183
|
-
return
|
|
1512
|
+
return read(function containsInScope(instance) {
|
|
1513
|
+
return instance.contains(relation, fact)
|
|
1184
1514
|
})
|
|
1185
1515
|
}
|
|
1186
1516
|
|
|
1187
1517
|
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1188
|
-
return read(function executeInScope(
|
|
1189
|
-
return
|
|
1518
|
+
return read(function executeInScope(instance) {
|
|
1519
|
+
return instance.execute(prepared, params)
|
|
1190
1520
|
})
|
|
1191
1521
|
}
|
|
1192
1522
|
|
|
1193
|
-
|
|
1194
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
1195
|
-
* `writeFrom` pass an already-begun handle.
|
|
1196
|
-
*/
|
|
1197
|
-
function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
|
|
1523
|
+
function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
|
|
1198
1524
|
const txState = { spent: false }
|
|
1199
1525
|
function assertLive(): void {
|
|
1200
1526
|
if (txState.spent) {
|
|
1201
|
-
throw errors.
|
|
1527
|
+
throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
|
|
1202
1528
|
}
|
|
1203
1529
|
}
|
|
1204
1530
|
const reads = pointReadsOf(assertLive, {
|
|
@@ -1215,38 +1541,61 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1215
1541
|
})
|
|
1216
1542
|
}
|
|
1217
1543
|
})
|
|
1218
|
-
function insert<R extends MemberRelation<Rels>>(
|
|
1219
|
-
relation: R,
|
|
1220
|
-
fact: InsertFact<R>
|
|
1221
|
-
): { readonly changed: boolean } & Minted<R> {
|
|
1544
|
+
function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
1222
1545
|
assertLive()
|
|
1223
1546
|
const entry = resolveOrdinary(relation)
|
|
1224
1547
|
const txHandle = resolveTx()
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1548
|
+
return mutateCollection(
|
|
1549
|
+
relation,
|
|
1550
|
+
facts,
|
|
1551
|
+
function applyRows(rows) {
|
|
1552
|
+
return bridged("bumbledb tx insert", function record() {
|
|
1553
|
+
return native.txInsert(txHandle, entry.id, rows)
|
|
1554
|
+
})
|
|
1555
|
+
},
|
|
1556
|
+
function applyColumns(columns) {
|
|
1557
|
+
return bridged("bumbledb tx insert", function recordColumns() {
|
|
1558
|
+
return native.txInsertColumns(txHandle, entry.id, columns)
|
|
1559
|
+
})
|
|
1560
|
+
}
|
|
1561
|
+
)
|
|
1562
|
+
}
|
|
1563
|
+
function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1564
|
+
assertLive()
|
|
1565
|
+
const entry = resolveOrdinary(relation)
|
|
1566
|
+
const txHandle = resolveTx()
|
|
1567
|
+
const report = bridged("bumbledb tx delete", function record() {
|
|
1568
|
+
return native.txDelete(txHandle, entry.id, rowsOf(relation, facts))
|
|
1231
1569
|
})
|
|
1232
|
-
|
|
1233
|
-
if (!isInserted(relation, inserted)) {
|
|
1234
|
-
throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
|
|
1235
|
-
}
|
|
1236
|
-
return inserted
|
|
1570
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1237
1571
|
}
|
|
1238
|
-
function
|
|
1572
|
+
function reserve<R extends MemberRelation<Rels>>(
|
|
1573
|
+
relation: R,
|
|
1574
|
+
field: FreshKeys<R> & string,
|
|
1575
|
+
count: bigint
|
|
1576
|
+
): FreshRange {
|
|
1239
1577
|
assertLive()
|
|
1240
1578
|
const entry = resolveOrdinary(relation)
|
|
1579
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
1580
|
+
return candidate.name === field
|
|
1581
|
+
})
|
|
1582
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
1583
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
1584
|
+
}
|
|
1585
|
+
const fieldId = entry.fieldIds.get(field)
|
|
1586
|
+
if (fieldId === undefined) {
|
|
1587
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
1588
|
+
}
|
|
1241
1589
|
const txHandle = resolveTx()
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
return native.txDelete(txHandle, entry.id, row)
|
|
1590
|
+
const range = bridged("bumbledb tx reserve", function mint() {
|
|
1591
|
+
return native.txReserve(txHandle, entry.id, fieldId, count)
|
|
1245
1592
|
})
|
|
1593
|
+
return freshRangeOf(range)
|
|
1246
1594
|
}
|
|
1247
|
-
const tx:
|
|
1595
|
+
const tx: WriteTx<Rels> = Object.freeze({
|
|
1248
1596
|
insert,
|
|
1249
1597
|
delete: remove,
|
|
1598
|
+
reserve,
|
|
1250
1599
|
contains: reads.contains,
|
|
1251
1600
|
get: reads.get
|
|
1252
1601
|
})
|
|
@@ -1256,111 +1605,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1256
1605
|
return { tx, spend }
|
|
1257
1606
|
}
|
|
1258
1607
|
|
|
1259
|
-
function
|
|
1260
|
-
|
|
1261
|
-
return
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
})
|
|
1266
|
-
made.spend()
|
|
1267
|
-
if (built.error) {
|
|
1268
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1269
|
-
native.txAbort(txHandle)
|
|
1270
|
-
})
|
|
1271
|
-
throw errors.wrap(built.error, "build write delta")
|
|
1272
|
-
}
|
|
1273
|
-
if (isThenable(built.data)) {
|
|
1274
|
-
/**
|
|
1275
|
-
* An `async` callback TYPECHECKS (Promise<void> is assignable where
|
|
1276
|
-
* a `void` return is expected) but its body runs after the tx is
|
|
1277
|
-
* spent: committing here would be a silent EMPTY commit reported
|
|
1278
|
-
* ok while the callback's real inserts throw "spent" as unhandled
|
|
1279
|
-
* rejections. Refused typed instead — abort, nothing committed
|
|
1280
|
-
* (the same one-writer law as the thrown-callback path).
|
|
1281
|
-
*/
|
|
1282
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1283
|
-
native.txAbort(txHandle)
|
|
1608
|
+
function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
|
|
1609
|
+
if (nativeOutcome.tag === "moved") {
|
|
1610
|
+
return Object.freeze({
|
|
1611
|
+
tag: "moved" as const,
|
|
1612
|
+
witnessed: nativeOutcome.witnessed,
|
|
1613
|
+
current: nativeOutcome.current
|
|
1284
1614
|
})
|
|
1285
|
-
throw errors.new(
|
|
1286
|
-
"bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1287
|
-
)
|
|
1288
1615
|
}
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
* even an empty commit; commit is unreachable for a sentinel
|
|
1294
|
-
* result.
|
|
1295
|
-
*/
|
|
1296
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1297
|
-
native.txAbort(txHandle)
|
|
1616
|
+
if (nativeOutcome.tag === "rejected") {
|
|
1617
|
+
return Object.freeze({
|
|
1618
|
+
tag: "rejected" as const,
|
|
1619
|
+
violations: Object.freeze(nativeOutcome.violations.map(violationOf))
|
|
1298
1620
|
})
|
|
1299
|
-
return abandonedOutcome<Rels, R>(built.data)
|
|
1300
1621
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
})
|
|
1305
|
-
})
|
|
1306
|
-
if (committed.error) {
|
|
1307
|
-
/**
|
|
1308
|
-
* A THROWN commit (engine I/O failure, bridge fault) must never
|
|
1309
|
-
* leave the write transaction live: LMDB holds one writer per
|
|
1310
|
-
* environment, and a leaked handle turns every later begin into
|
|
1311
|
-
* EINVAL for the process's lifetime. The abort is best-effort —
|
|
1312
|
-
* the native side may already have consumed the handle.
|
|
1313
|
-
*/
|
|
1314
|
-
const aborted = errors.trySync(function abortAfterFailedCommit() {
|
|
1315
|
-
native.txAbort(txHandle)
|
|
1316
|
-
})
|
|
1317
|
-
if (aborted.error) {
|
|
1622
|
+
if (nativeOutcome.tag === "abandoned") {
|
|
1623
|
+
if (built === undefined || !isAbandon(built)) {
|
|
1624
|
+
throw errors.new("bumbledb write abandoned without an abandon sentinel")
|
|
1318
1625
|
}
|
|
1319
|
-
|
|
1320
|
-
}
|
|
1321
|
-
const outcome = committed.data
|
|
1322
|
-
if (outcome.ok) {
|
|
1323
|
-
return Object.freeze({ ok: true, generation: outcome.generation })
|
|
1626
|
+
return abandonedOutcome<Rels, R>(built)
|
|
1324
1627
|
}
|
|
1325
1628
|
return Object.freeze({
|
|
1326
|
-
|
|
1327
|
-
|
|
1629
|
+
tag: "accepted" as const,
|
|
1630
|
+
value: Object.freeze({
|
|
1631
|
+
value: built as Exclude<R, Abandon<unknown>>,
|
|
1632
|
+
generation: nativeOutcome.generation
|
|
1633
|
+
})
|
|
1328
1634
|
})
|
|
1329
1635
|
}
|
|
1330
1636
|
|
|
1331
|
-
function
|
|
1332
|
-
|
|
1333
|
-
|
|
1637
|
+
function runWrite<R>(
|
|
1638
|
+
invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
|
|
1639
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1640
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1641
|
+
let built: SyncResult<R> | undefined
|
|
1642
|
+
const nativeOutcome = bridged("bumbledb write", function callWrite() {
|
|
1643
|
+
return invoke(function onWrite(txHandle) {
|
|
1644
|
+
const made = makeTx(function resolveTx() {
|
|
1645
|
+
return txHandle
|
|
1646
|
+
})
|
|
1647
|
+
const result = errors.trySync(function buildDelta() {
|
|
1648
|
+
return fn(made.tx)
|
|
1649
|
+
})
|
|
1650
|
+
made.spend()
|
|
1651
|
+
if (result.error) {
|
|
1652
|
+
throw errors.wrap(result.error, "build write delta")
|
|
1653
|
+
}
|
|
1654
|
+
if (isThenable(result.data)) {
|
|
1655
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
|
|
1656
|
+
}
|
|
1657
|
+
built = result.data
|
|
1658
|
+
return !isAbandon(result.data)
|
|
1659
|
+
})
|
|
1334
1660
|
})
|
|
1335
|
-
return
|
|
1661
|
+
return mapNativeWrite(nativeOutcome, built)
|
|
1336
1662
|
}
|
|
1337
1663
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
const snapState = scopeStates.get(snap)
|
|
1345
|
-
if (snapState === undefined) {
|
|
1346
|
-
throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
|
|
1347
|
-
}
|
|
1348
|
-
if (snapState.owner !== owner) {
|
|
1349
|
-
throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
|
|
1664
|
+
function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
|
|
1665
|
+
const outcome = runWrite(function invoke(callback) {
|
|
1666
|
+
return native.dbWrite(handle, callback)
|
|
1667
|
+
}, fn)
|
|
1668
|
+
if (outcome.tag === "moved") {
|
|
1669
|
+
throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
|
|
1350
1670
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1671
|
+
return outcome
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function writeFrom<R>(
|
|
1675
|
+
witness: Witness<Rels>,
|
|
1676
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1677
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1678
|
+
const state = witnessStates.get(witness)
|
|
1679
|
+
if (state === undefined) {
|
|
1680
|
+
throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
|
|
1353
1681
|
}
|
|
1354
|
-
|
|
1355
|
-
return native.dbWriteFrom(handle, snapState.handle)
|
|
1356
|
-
})
|
|
1357
|
-
if (!witnessed.ok) {
|
|
1682
|
+
if (state.owner !== owner) {
|
|
1358
1683
|
throw errors.wrap(
|
|
1359
|
-
|
|
1360
|
-
`writeFrom
|
|
1684
|
+
ErrForeignWitness,
|
|
1685
|
+
`bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
|
|
1361
1686
|
)
|
|
1362
1687
|
}
|
|
1363
|
-
|
|
1688
|
+
if (state.spent) {
|
|
1689
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
|
|
1690
|
+
}
|
|
1691
|
+
return runWrite(function invoke(callback) {
|
|
1692
|
+
return native.dbWriteFrom(handle, state.handle, callback)
|
|
1693
|
+
}, fn)
|
|
1364
1694
|
}
|
|
1365
1695
|
|
|
1366
1696
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
@@ -1374,21 +1704,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1374
1704
|
return native.dbPrepare(handle, queryIr)
|
|
1375
1705
|
})
|
|
1376
1706
|
if (!outcome.ok) {
|
|
1377
|
-
|
|
1707
|
+
throwPrepareRefusal(outcome.message)
|
|
1378
1708
|
}
|
|
1379
|
-
|
|
1380
|
-
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1381
|
-
preparedPlans.set(
|
|
1382
|
-
prepared,
|
|
1383
|
-
Object.freeze({
|
|
1384
|
-
handle: preparedHandle,
|
|
1385
|
-
owner,
|
|
1386
|
-
params: q.data.params,
|
|
1387
|
-
finds: q.data.finds
|
|
1388
|
-
})
|
|
1389
|
-
)
|
|
1390
|
-
planReclaimer.register(prepared, preparedHandle)
|
|
1391
|
-
return prepared
|
|
1709
|
+
return pinPrepared(outcome.prepared, q)
|
|
1392
1710
|
}
|
|
1393
1711
|
|
|
1394
1712
|
return Object.freeze({
|
|
@@ -1417,124 +1735,457 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1417
1735
|
const ErrNewtypeMismatch = errors.new(
|
|
1418
1736
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1419
1737
|
)
|
|
1738
|
+
const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
|
|
1739
|
+
const ErrFingerprintMismatch = errors.new(
|
|
1740
|
+
"bumbledb fingerprintMismatch: the store's schema does not match this theory"
|
|
1741
|
+
)
|
|
1742
|
+
const ErrIrError = errors.new("bumbledb irError: the query failed validation")
|
|
1743
|
+
|
|
1744
|
+
function throwOpenRefusal(
|
|
1745
|
+
verb: string,
|
|
1746
|
+
canonical: string,
|
|
1747
|
+
kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
|
|
1748
|
+
message: string
|
|
1749
|
+
): never {
|
|
1750
|
+
const detail = `${verb} ${canonical}: ${message}`
|
|
1751
|
+
if (kind === "newtypeMismatch") {
|
|
1752
|
+
throw errors.wrap(ErrNewtypeMismatch, detail)
|
|
1753
|
+
}
|
|
1754
|
+
if (kind === "schemaError") {
|
|
1755
|
+
throw errors.wrap(ErrSchemaError, detail)
|
|
1756
|
+
}
|
|
1757
|
+
throw errors.wrap(ErrFingerprintMismatch, detail)
|
|
1758
|
+
}
|
|
1420
1759
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1760
|
+
function throwPrepareRefusal(message: string): never {
|
|
1761
|
+
throw errors.wrap(ErrIrError, `prepare: ${message}`)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
|
|
1765
|
+
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
1766
|
+
return native.dbManifest(dbHandle)
|
|
1767
|
+
})
|
|
1768
|
+
return openDb(dbHandle, theory, manifest)
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
async function createStore<Rels extends SchemaRelations>(
|
|
1772
|
+
storePath: string,
|
|
1773
|
+
theory: Schema<Rels>
|
|
1774
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
1775
|
+
const canonical = path.resolve(storePath)
|
|
1776
|
+
const spec = lower(theory)
|
|
1777
|
+
const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
|
|
1778
|
+
return native.dbCreate(canonical, spec)
|
|
1779
|
+
})
|
|
1780
|
+
if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
|
|
1781
|
+
throwOpenRefusal("create", canonical, created.tag, created.message)
|
|
1782
|
+
}
|
|
1783
|
+
if (created.tag === "rejected") {
|
|
1784
|
+
return Object.freeze({
|
|
1785
|
+
tag: "rejected" as const,
|
|
1786
|
+
violations: Object.freeze(
|
|
1787
|
+
created.violations.map(function mapWire(wire) {
|
|
1788
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
1789
|
+
})
|
|
1790
|
+
)
|
|
1791
|
+
})
|
|
1792
|
+
}
|
|
1793
|
+
return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function mapViolationWithoutStore<Rels extends SchemaRelations>(
|
|
1797
|
+
theory: Schema<Rels>,
|
|
1798
|
+
wire: WireViolation
|
|
1799
|
+
): Violation<Rels> {
|
|
1800
|
+
const entries = materializedEntries(theory)
|
|
1801
|
+
const entry = entries[wire.statementId]
|
|
1802
|
+
if (entry === undefined) {
|
|
1803
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
1804
|
+
}
|
|
1805
|
+
function offending(fact: WireViolationFact): OffendingFact<Rels> {
|
|
1806
|
+
const member = theory.relations[fact.relation]
|
|
1807
|
+
if (member === undefined || !(fact.relation in theory.relations)) {
|
|
1808
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
1809
|
+
}
|
|
1810
|
+
const declared = sealedFieldsOf(member)
|
|
1811
|
+
const decoded: Record<string, FactValue> = {}
|
|
1812
|
+
for (const cell of fact.fields) {
|
|
1813
|
+
const cited = declared.find(function byName(candidate) {
|
|
1814
|
+
return candidate.name === cell.name
|
|
1815
|
+
})
|
|
1816
|
+
const roster = rosterOf(cited?.field)
|
|
1817
|
+
decoded[cell.name] =
|
|
1818
|
+
roster !== undefined
|
|
1819
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
1820
|
+
: cell.value
|
|
1821
|
+
}
|
|
1822
|
+
return Object.freeze({ relation: fact.relation as keyof Rels & string, fact: Object.freeze(decoded) })
|
|
1823
|
+
}
|
|
1824
|
+
const facts = Object.freeze(wire.facts.map(offending))
|
|
1825
|
+
const canonical = wire.canonical
|
|
1826
|
+
if (entry.kind === "functionality") {
|
|
1827
|
+
if (!("statement" in entry)) {
|
|
1828
|
+
return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
|
|
1438
1829
|
}
|
|
1830
|
+
return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
|
|
1831
|
+
}
|
|
1832
|
+
if (entry.kind === "capacity") {
|
|
1833
|
+
if (wire.kind !== "capacity") {
|
|
1834
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
|
|
1835
|
+
}
|
|
1836
|
+
return Object.freeze({
|
|
1837
|
+
kind: "capacity",
|
|
1838
|
+
statement: entry.statement,
|
|
1839
|
+
canonical,
|
|
1840
|
+
measure: wire.measure,
|
|
1841
|
+
facts
|
|
1842
|
+
})
|
|
1843
|
+
}
|
|
1844
|
+
if (wire.kind !== "containment") {
|
|
1845
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
|
|
1846
|
+
}
|
|
1847
|
+
if (entry.kind === "mirrors") {
|
|
1848
|
+
return Object.freeze({
|
|
1849
|
+
kind: "containment",
|
|
1850
|
+
statement: entry.statement,
|
|
1851
|
+
canonical,
|
|
1852
|
+
direction: wire.direction,
|
|
1853
|
+
orientation: entry.orientation,
|
|
1854
|
+
facts
|
|
1855
|
+
})
|
|
1439
1856
|
}
|
|
1857
|
+
return Object.freeze({
|
|
1858
|
+
kind: "containment",
|
|
1859
|
+
statement: entry.statement,
|
|
1860
|
+
canonical,
|
|
1861
|
+
direction: wire.direction,
|
|
1862
|
+
facts
|
|
1863
|
+
})
|
|
1440
1864
|
}
|
|
1441
1865
|
|
|
1442
|
-
|
|
1443
|
-
* The one admission path both verbs share: lower the theory, run one
|
|
1444
|
-
* bridge call, and wrap the domain refusals — `schemaError` (spec
|
|
1445
|
-
* resolution + schema validation, every issue in one message),
|
|
1446
|
-
* `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
|
|
1447
|
-
* and `fingerprintMismatch` (a different theory cannot open the store)
|
|
1448
|
-
* — into typed errors carrying the engine's message intact.
|
|
1449
|
-
* Environment failures (a second live writer on the same path, IO)
|
|
1450
|
-
* throw from the bridge; the engine's `EnvironmentLocked` message is
|
|
1451
|
-
* "another live handle holds this environment's lock".
|
|
1452
|
-
*/
|
|
1453
|
-
function admit<Rels extends SchemaRelations>(
|
|
1454
|
-
verb: "create" | "open",
|
|
1866
|
+
async function openStore<Rels extends SchemaRelations>(
|
|
1455
1867
|
storePath: string,
|
|
1456
1868
|
theory: Schema<Rels>
|
|
1457
|
-
): Db<Rels
|
|
1458
|
-
refuseShadowedChanged(theory)
|
|
1869
|
+
): Promise<Db<Rels>> {
|
|
1459
1870
|
const canonical = path.resolve(storePath)
|
|
1460
1871
|
const spec = lower(theory)
|
|
1461
|
-
const opened =
|
|
1462
|
-
if (verb === "create") {
|
|
1463
|
-
return native.dbCreate(canonical, spec)
|
|
1464
|
-
}
|
|
1872
|
+
const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
|
|
1465
1873
|
return native.dbOpen(canonical, spec)
|
|
1466
1874
|
})
|
|
1467
1875
|
if (!opened.ok) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1876
|
+
throwOpenRefusal("open", canonical, opened.kind, opened.message)
|
|
1877
|
+
}
|
|
1878
|
+
return openFromHandle(opened.db, theory)
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
|
|
1882
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
1883
|
+
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
1884
|
+
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
1885
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1886
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1887
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1888
|
+
relation: R,
|
|
1889
|
+
keyStatement: KeyStatement<R, P>,
|
|
1890
|
+
key: DeclaredKeyFact<R, P>
|
|
1891
|
+
): Fact<R> | undefined
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
|
|
1895
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
1896
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
1897
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
1898
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1899
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1900
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1901
|
+
relation: R,
|
|
1902
|
+
keyStatement: KeyStatement<R, P>,
|
|
1903
|
+
key: DeclaredKeyFact<R, P>
|
|
1904
|
+
): Fact<R> | undefined
|
|
1905
|
+
admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
|
|
1909
|
+
const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
|
|
1910
|
+
|
|
1911
|
+
const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
|
|
1912
|
+
const closed = errors.trySync(function closeOwned() {
|
|
1913
|
+
native.ownedInstanceClose(handle)
|
|
1914
|
+
})
|
|
1915
|
+
if (closed.error) {
|
|
1916
|
+
return
|
|
1917
|
+
}
|
|
1918
|
+
})
|
|
1919
|
+
|
|
1920
|
+
const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
|
|
1921
|
+
const closed = errors.trySync(function closeBuilder() {
|
|
1922
|
+
native.instanceBuilderClose(handle)
|
|
1923
|
+
})
|
|
1924
|
+
if (closed.error) {
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
})
|
|
1928
|
+
|
|
1929
|
+
function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
|
|
1930
|
+
const owner = Object.freeze({})
|
|
1931
|
+
const rec = { handle: nativeHandle, theory, spent: false, owner }
|
|
1932
|
+
const tables = tablesFromTheory(theory)
|
|
1933
|
+
function assertLive(): void {
|
|
1934
|
+
if (rec.spent) {
|
|
1935
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
|
|
1470
1936
|
}
|
|
1471
|
-
throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
|
|
1472
1937
|
}
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1938
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
1939
|
+
scan(relationId) {
|
|
1940
|
+
return native.ownedScan(nativeHandle, relationId)
|
|
1941
|
+
},
|
|
1942
|
+
contains(relationId, values) {
|
|
1943
|
+
return native.ownedContains(nativeHandle, relationId, values)
|
|
1944
|
+
},
|
|
1945
|
+
get(relationId, statementId, keyValues) {
|
|
1946
|
+
return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
|
|
1947
|
+
},
|
|
1948
|
+
prepare(query) {
|
|
1949
|
+
return native.ownedPrepare(nativeHandle, query)
|
|
1950
|
+
},
|
|
1951
|
+
execute(prepared, params) {
|
|
1952
|
+
return native.ownedExecute(prepared, nativeHandle, params)
|
|
1953
|
+
}
|
|
1954
|
+
})
|
|
1955
|
+
const instance: OwnedInstance<Rels> = Object.freeze({
|
|
1956
|
+
...methods,
|
|
1957
|
+
[Symbol.dispose](): void {
|
|
1958
|
+
if (rec.spent) {
|
|
1959
|
+
return
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
native.ownedInstanceClose(nativeHandle)
|
|
1963
|
+
} catch (caught) {
|
|
1964
|
+
const error = errorFromThrow(caught)
|
|
1965
|
+
if (/leased for publish/.test(error.message)) {
|
|
1966
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
|
|
1967
|
+
}
|
|
1968
|
+
throw errors.wrap(error, "close bumbledb owned instance")
|
|
1969
|
+
}
|
|
1970
|
+
rec.spent = true
|
|
1971
|
+
ownedReclaimer.unregister(instance)
|
|
1972
|
+
}
|
|
1973
|
+
})
|
|
1974
|
+
ownedRecords.set(instance, rec)
|
|
1975
|
+
ownedReclaimer.register(instance, nativeHandle, instance)
|
|
1976
|
+
return instance
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function wrapBuilder<Rels extends SchemaRelations>(
|
|
1980
|
+
nativeHandle: BuilderHandle,
|
|
1981
|
+
theory: Schema<Rels>
|
|
1982
|
+
): InstanceBuilder<Rels> {
|
|
1983
|
+
const rec = { handle: nativeHandle, theory, spent: false }
|
|
1984
|
+
const tables = tablesFromTheory(theory)
|
|
1985
|
+
function assertLive(): void {
|
|
1986
|
+
if (rec.spent) {
|
|
1987
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const overlay = overlayMethods(theory, tables, assertLive, {
|
|
1991
|
+
contains(relationId, row) {
|
|
1992
|
+
return bridged("bumbledb builder contains", function readContains() {
|
|
1993
|
+
return native.instanceBuilderContains(nativeHandle, relationId, row)
|
|
1994
|
+
})
|
|
1995
|
+
},
|
|
1996
|
+
get(relationId, statementId, key) {
|
|
1997
|
+
return bridged("bumbledb builder get", function readGet() {
|
|
1998
|
+
return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
|
|
1999
|
+
})
|
|
2000
|
+
}
|
|
2001
|
+
})
|
|
2002
|
+
const builder: InstanceBuilder<Rels> = Object.freeze({
|
|
2003
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
2004
|
+
assertLive()
|
|
2005
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2006
|
+
return mutateCollection(
|
|
2007
|
+
relation,
|
|
2008
|
+
facts,
|
|
2009
|
+
function applyRows(rows) {
|
|
2010
|
+
return bridged("bumbledb builder load", function loadRows() {
|
|
2011
|
+
return native.instanceBuilderLoad(nativeHandle, entry.id, rows)
|
|
2012
|
+
})
|
|
2013
|
+
},
|
|
2014
|
+
function applyColumns(columns) {
|
|
2015
|
+
return bridged("bumbledb builder load", function loadColumns() {
|
|
2016
|
+
return native.instanceBuilderLoadColumns(nativeHandle, entry.id, columns)
|
|
2017
|
+
})
|
|
2018
|
+
}
|
|
2019
|
+
)
|
|
2020
|
+
},
|
|
2021
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
2022
|
+
assertLive()
|
|
2023
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2024
|
+
const report = bridged("bumbledb builder delete", function remove() {
|
|
2025
|
+
return native.instanceBuilderDelete(nativeHandle, entry.id, rowsOf(relation, facts))
|
|
2026
|
+
})
|
|
2027
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
2028
|
+
},
|
|
2029
|
+
reserve<R extends MemberRelation<Rels>>(
|
|
2030
|
+
relation: R,
|
|
2031
|
+
field: FreshKeys<R> & string,
|
|
2032
|
+
count: bigint
|
|
2033
|
+
): FreshRange {
|
|
2034
|
+
assertLive()
|
|
2035
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2036
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
2037
|
+
return candidate.name === field
|
|
2038
|
+
})
|
|
2039
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
2040
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
2041
|
+
}
|
|
2042
|
+
const fieldId = entry.fieldIds.get(field)
|
|
2043
|
+
if (fieldId === undefined) {
|
|
2044
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
2045
|
+
}
|
|
2046
|
+
const range = bridged("bumbledb builder reserve", function mint() {
|
|
2047
|
+
return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
|
|
2048
|
+
})
|
|
2049
|
+
return freshRangeOf(range)
|
|
2050
|
+
},
|
|
2051
|
+
contains: overlay.contains,
|
|
2052
|
+
get: overlay.get,
|
|
2053
|
+
async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
|
|
2054
|
+
if (rec.spent) {
|
|
2055
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
2056
|
+
}
|
|
2057
|
+
rec.spent = true
|
|
2058
|
+
builderReclaimer.unregister(builder)
|
|
2059
|
+
let outcome: AdmitResult
|
|
2060
|
+
try {
|
|
2061
|
+
outcome = await native.instanceBuilderAdmit(nativeHandle)
|
|
2062
|
+
} catch (caught) {
|
|
2063
|
+
throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
|
|
2064
|
+
}
|
|
2065
|
+
if (outcome.tag === "rejected") {
|
|
2066
|
+
return Object.freeze({
|
|
2067
|
+
tag: "rejected" as const,
|
|
2068
|
+
violations: Object.freeze(
|
|
2069
|
+
outcome.violations.map(function mapWire(wire) {
|
|
2070
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
2071
|
+
})
|
|
2072
|
+
)
|
|
2073
|
+
})
|
|
2074
|
+
}
|
|
2075
|
+
return Object.freeze({
|
|
2076
|
+
tag: "accepted" as const,
|
|
2077
|
+
value: wrapOwned(outcome.value, theory)
|
|
2078
|
+
})
|
|
2079
|
+
},
|
|
2080
|
+
[Symbol.dispose](): void {
|
|
2081
|
+
if (rec.spent) {
|
|
2082
|
+
return
|
|
2083
|
+
}
|
|
2084
|
+
rec.spent = true
|
|
2085
|
+
builderReclaimer.unregister(builder)
|
|
2086
|
+
bridged("close bumbledb instance builder", function closeBuilder() {
|
|
2087
|
+
native.instanceBuilderClose(nativeHandle)
|
|
2088
|
+
})
|
|
2089
|
+
}
|
|
1475
2090
|
})
|
|
1476
|
-
|
|
2091
|
+
builderRecords.set(builder, rec)
|
|
2092
|
+
builderReclaimer.register(builder, nativeHandle, builder)
|
|
2093
|
+
return builder
|
|
1477
2094
|
}
|
|
1478
2095
|
|
|
2096
|
+
const InstanceBuilder = Object.freeze({
|
|
2097
|
+
create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
|
|
2098
|
+
const spec = lower(theory)
|
|
2099
|
+
const handle = bridged("create bumbledb instance builder", function make() {
|
|
2100
|
+
return native.instanceBuilderNew(spec)
|
|
2101
|
+
})
|
|
2102
|
+
return wrapBuilder(handle, theory)
|
|
2103
|
+
}
|
|
2104
|
+
})
|
|
2105
|
+
|
|
1479
2106
|
/**
|
|
1480
2107
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
1481
2108
|
* Create refuses an already-initialized directory; open verifies format
|
|
1482
|
-
* version
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
2109
|
+
* version and the schema fingerprint. A second live handle on the same
|
|
2110
|
+
* path is the engine's `EnvironmentLocked`. There is no close anywhere:
|
|
2111
|
+
* the process owns the environment until GC/exit (durability is the
|
|
2112
|
+
* engine's per-commit fsync). Resume = reopen in a fresh process, or
|
|
2113
|
+
* hold the `Db` this process opened.
|
|
1487
2114
|
*/
|
|
1488
2115
|
const Db = Object.freeze({
|
|
1489
2116
|
/** Creates a fresh durable store at `path` from the schema. */
|
|
1490
|
-
async create<Rels extends SchemaRelations>(
|
|
1491
|
-
|
|
2117
|
+
async create<Rels extends SchemaRelations>(
|
|
2118
|
+
storePath: string,
|
|
2119
|
+
theory: Schema<Rels>
|
|
2120
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
2121
|
+
return createStore(storePath, theory)
|
|
1492
2122
|
},
|
|
1493
2123
|
/**
|
|
1494
2124
|
* Opens an existing durable store at `path` with the same theory.
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
1498
|
-
* exhumable after one ordinary open — adoption is automatic, never a
|
|
1499
|
-
* separate verb. A second open of a still-live path is
|
|
1500
|
-
* `EnvironmentLocked`.
|
|
2125
|
+
* Format 8 open never back-fills a descriptor. A second open of a
|
|
2126
|
+
* still-live path is `EnvironmentLocked`.
|
|
1501
2127
|
*/
|
|
1502
|
-
async open<Rels extends SchemaRelations>(
|
|
1503
|
-
return
|
|
2128
|
+
async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
2129
|
+
return openStore(storePath, theory)
|
|
1504
2130
|
},
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
2131
|
+
async fromInstance<Rels extends SchemaRelations>(
|
|
2132
|
+
storePath: string,
|
|
2133
|
+
instance: OwnedInstance<Rels>
|
|
2134
|
+
): Promise<Db<Rels>> {
|
|
2135
|
+
const rec = ownedRecords.get(instance)
|
|
2136
|
+
if (rec === undefined) {
|
|
2137
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
|
|
2138
|
+
}
|
|
2139
|
+
if (rec.spent) {
|
|
2140
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
|
|
2141
|
+
}
|
|
2142
|
+
const canonical = path.resolve(storePath)
|
|
2143
|
+
const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
|
|
2144
|
+
return native.dbFromInstance(canonical, rec.handle)
|
|
2145
|
+
})
|
|
2146
|
+
return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
|
|
1519
2147
|
}
|
|
1520
2148
|
})
|
|
1521
2149
|
|
|
1522
2150
|
export type {
|
|
1523
2151
|
Abandon,
|
|
1524
2152
|
AbandonedArm,
|
|
2153
|
+
Admission,
|
|
1525
2154
|
CapacityViolation,
|
|
2155
|
+
ColumnBatch,
|
|
2156
|
+
Committed,
|
|
1526
2157
|
ContainmentViolation,
|
|
1527
2158
|
DeclaredKeyFact,
|
|
1528
2159
|
DeclaredKeyViolation,
|
|
1529
2160
|
DeltaBuild,
|
|
2161
|
+
FreshRange,
|
|
1530
2162
|
ImpliedKeyViolation,
|
|
1531
2163
|
MemberRelation,
|
|
1532
2164
|
MirrorViolation,
|
|
2165
|
+
MutationReport,
|
|
1533
2166
|
OffendingFact,
|
|
2167
|
+
OwnedInstance,
|
|
1534
2168
|
Prepared,
|
|
1535
|
-
|
|
2169
|
+
ReadInstance,
|
|
2170
|
+
SyncResult,
|
|
1536
2171
|
Tx,
|
|
1537
2172
|
Violation,
|
|
1538
|
-
|
|
2173
|
+
Witness,
|
|
2174
|
+
WriteFromOutcome,
|
|
2175
|
+
WriteOutcome,
|
|
2176
|
+
WriteTx
|
|
2177
|
+
}
|
|
2178
|
+
export {
|
|
2179
|
+
abandon,
|
|
2180
|
+
Db,
|
|
2181
|
+
ErrAsyncCallback,
|
|
2182
|
+
ErrForeignPrepared,
|
|
2183
|
+
ErrForeignWitness,
|
|
2184
|
+
ErrFingerprintMismatch,
|
|
2185
|
+
ErrIrError,
|
|
2186
|
+
ErrNewtypeMismatch,
|
|
2187
|
+
ErrSchemaError,
|
|
2188
|
+
ErrSpentHandle,
|
|
2189
|
+
ErrUseAfterScope,
|
|
2190
|
+
InstanceBuilder
|
|
1539
2191
|
}
|
|
1540
|
-
export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }
|