@bjornpagen/bumbledb 0.14.0 → 0.17.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 +58 -62
- package/README.md +82 -56
- package/dist/capacity.d.ts +24 -136
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +18 -40
- package/dist/capacity.js.map +1 -1
- package/dist/closed.d.ts +0 -156
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +0 -104
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +93 -290
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +713 -556
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +0 -133
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +0 -33
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +1 -145
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +2 -91
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -20
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +111 -93
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +23 -27
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +9 -35
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +8 -53
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +0 -65
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +0 -72
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +97 -390
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +39 -61
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +10 -276
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +1 -96
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +10 -76
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +0 -30
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +64 -146
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +19 -256
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +0 -7
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +1 -13
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +0 -36
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +0 -44
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +24 -180
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +2 -66
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +2 -50
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +2 -37
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +13 -63
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +118 -92
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +1 -140
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +1 -68
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +6 -137
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +16 -119
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/capacity.ts +26 -140
- package/src/closed.ts +5 -206
- package/src/db.ts +997 -854
- package/src/face.ts +0 -142
- package/src/fields.ts +4 -172
- package/src/index.ts +32 -35
- package/src/law.ts +201 -129
- package/src/lower.ts +8 -53
- package/src/marshal.ts +1 -85
- package/src/native.ts +192 -413
- package/src/query/atom.ts +26 -313
- package/src/query/find.ts +24 -110
- package/src/query/lower.ts +132 -377
- package/src/query/parse-ir.ts +1 -14
- package/src/query/run.ts +0 -45
- package/src/query/scope.ts +25 -186
- package/src/relation.ts +2 -66
- package/src/schema.ts +143 -122
- package/src/spec.ts +1 -160
- package/src/statements.ts +22 -174
- 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,33 +22,38 @@
|
|
|
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 { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
37
|
+
import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
41
38
|
|
|
42
39
|
import type {
|
|
40
|
+
AdmitResult,
|
|
41
|
+
BuilderHandle,
|
|
43
42
|
DbHandle,
|
|
44
43
|
FactValue,
|
|
44
|
+
InstanceHandle,
|
|
45
45
|
Manifest,
|
|
46
|
+
NativeWriteOutcome,
|
|
47
|
+
OwnedHandle,
|
|
46
48
|
PreparedHandle,
|
|
47
|
-
SnapshotHandle,
|
|
48
49
|
TxHandle,
|
|
49
50
|
WireFreshRange,
|
|
51
|
+
WireMutationReport,
|
|
50
52
|
Violation as WireViolation,
|
|
51
|
-
ViolationFact as WireViolationFact
|
|
53
|
+
ViolationFact as WireViolationFact,
|
|
54
|
+
WitnessHandle
|
|
52
55
|
} from "#native.ts"
|
|
53
|
-
import { bridged, native } from "#native.ts"
|
|
56
|
+
import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
|
|
54
57
|
import type { FindColumn } from "#query/atom.ts"
|
|
55
58
|
import type { Query } from "#query/lower.ts"
|
|
56
59
|
import { lowerQuery } from "#query/lower.ts"
|
|
@@ -60,26 +63,60 @@ import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
|
|
|
60
63
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
61
64
|
import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
62
65
|
|
|
63
|
-
/**
|
|
64
|
-
* The ordinary (writable, scannable) relations of a schema's record — the
|
|
65
|
-
* only values the runtime methods accept: closed relations lack the
|
|
66
|
-
* relation shape entirely, so passing one is a type error.
|
|
67
|
-
*/
|
|
68
66
|
type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
|
|
69
67
|
|
|
70
|
-
/**
|
|
71
|
-
* Facts consumed vs facts that changed the in-memory final-state view.
|
|
72
|
-
* The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
|
|
73
|
-
*/
|
|
74
68
|
interface MutationReport {
|
|
75
69
|
readonly submitted: bigint
|
|
76
70
|
readonly changed: bigint
|
|
77
71
|
}
|
|
78
72
|
|
|
73
|
+
type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>>
|
|
74
|
+
|
|
75
|
+
interface FlatCollection {
|
|
76
|
+
readonly rows: bigint
|
|
77
|
+
readonly cells: readonly FactValue[]
|
|
78
|
+
}
|
|
79
|
+
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
81
|
+
* The flat projector: every fact's cells land in ONE row-major
|
|
82
|
+
* `FactValue` array (length rows×arity) — no JS array per fact exists
|
|
83
|
+
* anywhere between the caller's objects and the native crossing
|
|
84
|
+
* (proposals/one-representation/20, V1) — and the row count is counted
|
|
85
|
+
* while projecting (the {@link FlatCollection} law: the stated count is
|
|
86
|
+
* what the bridge verifies against `rows × arity`, exactly, for every
|
|
87
|
+
* arity). The per-cell judgment is `cellOf` — the one cell judge `rowOf`
|
|
88
|
+
* also speaks (closed handle→id, well-formedness, interval shape) — and
|
|
89
|
+
* the missing-field refusal is `rowOf`'s, byte for byte; only the output
|
|
90
|
+
* form differs (flat, never per-row).
|
|
82
91
|
*/
|
|
92
|
+
function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FlatCollection {
|
|
93
|
+
const data = relation.data
|
|
94
|
+
const cells: FactValue[] = []
|
|
95
|
+
let rows = 0n
|
|
96
|
+
for (const fact of facts) {
|
|
97
|
+
rows += 1n
|
|
98
|
+
const record = recordOf(fact)
|
|
99
|
+
for (const declared of data.fields) {
|
|
100
|
+
const value = record[declared.name]
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
throw errors.new(`relation ${data.name}: fact is missing field ${declared.name}`)
|
|
103
|
+
}
|
|
104
|
+
cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { rows, cells }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function mutateCollection<R extends AnyRelation>(
|
|
111
|
+
relation: R,
|
|
112
|
+
facts: CollectionWrite<R>,
|
|
113
|
+
apply: (rows: bigint, cells: readonly FactValue[]) => WireMutationReport
|
|
114
|
+
): MutationReport {
|
|
115
|
+
const flat = rowsOf(relation, facts)
|
|
116
|
+
const report = apply(flat.rows, flat.cells)
|
|
117
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
type FreshRange =
|
|
84
121
|
| {
|
|
85
122
|
readonly empty: true
|
|
@@ -131,80 +168,36 @@ function freshRangeOf(wire: WireFreshRange): FreshRange {
|
|
|
131
168
|
})
|
|
132
169
|
}
|
|
133
170
|
|
|
134
|
-
/**
|
|
135
|
-
* The key object of a key-statement-selected `get`: exactly the selected
|
|
136
|
-
* `key()` statement's projection fields, each at the relation's own BARE
|
|
137
|
-
* structural value type — the {@link KeyFact} rule generalized from the
|
|
138
|
-
* primary key to ANY declared key statement.
|
|
139
|
-
*/
|
|
140
171
|
type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]> = {
|
|
141
172
|
readonly [K in Projection[number] & keyof Fact<R>]: Fact<R>[K]
|
|
142
173
|
}
|
|
143
174
|
|
|
144
|
-
/**
|
|
145
|
-
* One offending fact of a violation: the cited relation's name (a member
|
|
146
|
-
* of the schema's record) and the fact decoded to a named natural-value
|
|
147
|
-
* object — partial exactly as the engine cites it. Closed-referencing
|
|
148
|
-
* cells arrive as handle NAMES (the marshal bijection's read half), so the
|
|
149
|
-
* record and the violation's `canonical` string — which the engine already
|
|
150
|
-
* renders with handle names — agree on the one spelling.
|
|
151
|
-
*/
|
|
152
175
|
interface OffendingFact<Rels extends SchemaRelations> {
|
|
153
176
|
readonly relation: keyof Rels & string
|
|
154
177
|
readonly fact: Readonly<Record<string, FactValue>>
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
/**
|
|
158
|
-
* Shared body of every violation arm: the engine's canonical rendering and
|
|
159
|
-
* the cited facts. `statement` is NOT here — its presence is the
|
|
160
|
-
* discriminant. Implied auto-keys have no SDK spelling (`statement` is
|
|
161
|
-
* the value `undefined`); every declared form carries the IDENTICAL
|
|
162
|
-
* statement value the schema declared (consumers `===`-match it).
|
|
163
|
-
*/
|
|
164
180
|
type ViolationBody<Rels extends SchemaRelations> = {
|
|
165
181
|
readonly canonical: string
|
|
166
182
|
readonly facts: readonly OffendingFact<Rels>[]
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
/**
|
|
170
|
-
* A functionality violation of an engine-materialized fresh-implied or
|
|
171
|
-
* closed auto-key. These slots have no declared spelling (`schema()`
|
|
172
|
-
* rejects an explicit duplicate); `statement` is present and `undefined`.
|
|
173
|
-
*/
|
|
174
185
|
type ImpliedKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
175
186
|
readonly kind: "functionality"
|
|
176
187
|
readonly statement: undefined
|
|
177
188
|
}
|
|
178
189
|
|
|
179
|
-
/**
|
|
180
|
-
* A functionality violation of a declared `key()` statement. `statement`
|
|
181
|
-
* is the IDENTICAL SDK value the schema declared.
|
|
182
|
-
*/
|
|
183
190
|
type DeclaredKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
184
191
|
readonly kind: "functionality"
|
|
185
192
|
readonly statement: Statement
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
/**
|
|
189
|
-
* A containment violation of a declared `contained()` statement (no
|
|
190
|
-
* `orientation` — that property exists exactly on {@link MirrorViolation}).
|
|
191
|
-
*/
|
|
192
195
|
type ContainmentViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
193
196
|
readonly kind: "containment"
|
|
194
197
|
readonly statement: Statement
|
|
195
198
|
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
/**
|
|
199
|
-
* A containment violation of one slot of a declared `mirrors()` statement.
|
|
200
|
-
* BOTH materialized slots render as the one `==` utterance in the written
|
|
201
|
-
* orientation (identical `canonical` strings; the engine's `render.rs`
|
|
202
|
-
* never emits a bare `<=` for a mirrored pair). `direction` is relative
|
|
203
|
-
* to the violated SLOT's own orientation, so it alone cannot say which
|
|
204
|
-
* side of the `==` was violated: `written` is the `source <= target` slot
|
|
205
|
-
* as the statement was spelled, `mirrored` the engine-materialized
|
|
206
|
-
* `target <= source` partner.
|
|
207
|
-
*/
|
|
208
201
|
type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
209
202
|
readonly kind: "containment"
|
|
210
203
|
readonly statement: Statement
|
|
@@ -212,25 +205,12 @@ type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
|
212
205
|
readonly orientation: "written" | "mirrored"
|
|
213
206
|
}
|
|
214
207
|
|
|
215
|
-
/**
|
|
216
|
-
* A capacity violation of a declared `capacity()` statement. `measure` is
|
|
217
|
-
* the engine's witnessed group total — u128-wide, crossing whole as
|
|
218
|
-
* bigint (C3: truncation is unrepresentable).
|
|
219
|
-
*/
|
|
220
208
|
type CapacityViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
221
209
|
readonly kind: "capacity"
|
|
222
210
|
readonly statement: Statement
|
|
223
211
|
readonly measure: bigint
|
|
224
212
|
}
|
|
225
213
|
|
|
226
|
-
/**
|
|
227
|
-
* One violated statement of a rejected commit, as a typed value. The
|
|
228
|
-
* arms are a true discriminant: `statement === undefined` is exactly the
|
|
229
|
-
* implied-auto-key arm; every declared form carries `Statement` (not
|
|
230
|
-
* `Statement | undefined`, not an omit-optional). `canonical` is the
|
|
231
|
-
* ENGINE's rendering. `direction` / `measure` pass through from the
|
|
232
|
-
* engine VERBATIM.
|
|
233
|
-
*/
|
|
234
214
|
type Violation<Rels extends SchemaRelations> =
|
|
235
215
|
| ImpliedKeyViolation<Rels>
|
|
236
216
|
| DeclaredKeyViolation<Rels>
|
|
@@ -245,42 +225,37 @@ type Violation<Rels extends SchemaRelations> =
|
|
|
245
225
|
* `never` and the arm vanishes from the sum. The outcome is in the type;
|
|
246
226
|
* a dead arm is never handled.
|
|
247
227
|
*/
|
|
248
|
-
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly
|
|
228
|
+
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
|
|
249
229
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
230
|
+
type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
|
|
231
|
+
|
|
232
|
+
interface Committed<T> {
|
|
233
|
+
readonly value: T
|
|
234
|
+
readonly generation: bigint
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type Admission<Rels extends SchemaRelations, T> =
|
|
238
|
+
| { readonly tag: "accepted"; readonly value: T }
|
|
239
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
240
|
+
|
|
241
|
+
type WriteOutcome<Rels extends SchemaRelations, R> =
|
|
242
|
+
| { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
|
|
243
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
262
244
|
| AbandonedArm<R>
|
|
263
245
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
*/
|
|
270
|
-
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
|
|
246
|
+
type WriteFromOutcome<Rels extends SchemaRelations, R> =
|
|
247
|
+
| WriteOutcome<Rels, R>
|
|
248
|
+
| { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
|
|
249
|
+
|
|
250
|
+
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
|
|
271
251
|
|
|
272
|
-
/**
|
|
273
|
-
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
274
|
-
* how `write`/`writeFrom` distinguish "abort without committing" from an
|
|
275
|
-
* ordinary callback result, never a guess about the host's own value shapes.
|
|
276
|
-
*/
|
|
277
252
|
const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
278
253
|
|
|
279
254
|
/**
|
|
280
255
|
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
281
256
|
* or `writeFrom` callback rolls the transaction back WITHOUT
|
|
282
257
|
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
283
|
-
* `{
|
|
258
|
+
* `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
284
259
|
* sentinel's contract is unconditional, whichever write verb received it).
|
|
285
260
|
*/
|
|
286
261
|
interface Abandon<P> {
|
|
@@ -288,98 +263,43 @@ interface Abandon<P> {
|
|
|
288
263
|
readonly payload: P
|
|
289
264
|
}
|
|
290
265
|
|
|
291
|
-
/**
|
|
292
|
-
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
293
|
-
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
294
|
-
* (nothing is committed, not even an empty commit) and the write resolves
|
|
295
|
-
* to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
|
|
296
|
-
* alike (R10).
|
|
297
|
-
*/
|
|
298
266
|
function abandon<P>(payload: P): Abandon<P> {
|
|
299
267
|
return Object.freeze({ [abandonMark]: true as const, payload })
|
|
300
268
|
}
|
|
301
269
|
|
|
302
|
-
/**
|
|
303
|
-
* The abandon payload type a write callback's return type implies: the
|
|
304
|
-
* payload of its `Abandon` arm, `never` when the callback can never
|
|
305
|
-
* abandon (the `abandoned` outcome is then statically unreachable and
|
|
306
|
-
* {@link AbandonedArm} erases it from the sum).
|
|
307
|
-
*/
|
|
308
270
|
type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
|
|
309
271
|
|
|
310
|
-
/**
|
|
311
|
-
* Narrows a write callback result to the abandon sentinel. The probe is
|
|
312
|
-
* the private {@link abandonMark} symbol only {@link abandon} sets, and
|
|
313
|
-
* `R`'s `Abandon` arm is the only way a sentinel can flow out of the
|
|
314
|
-
* callback — so the narrowed payload type is sound by construction.
|
|
315
|
-
*/
|
|
316
272
|
function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
317
273
|
return typeof value === "object" && value !== null && abandonMark in value
|
|
318
274
|
}
|
|
319
275
|
|
|
320
|
-
/**
|
|
321
|
-
* The abandon outcome's trusted admission seam: the value's shape is the
|
|
322
|
-
* checkable half (the sentinel mark only {@link abandon} mints, and the
|
|
323
|
-
* outcome carrying that sentinel's own payload), and the sentinel's
|
|
324
|
-
* existence IS the proof `R` carries an `Abandon` arm — so the outcome is
|
|
325
|
-
* admitted at the conditional {@link AbandonedArm} face the type tier
|
|
326
|
-
* cannot resolve over an open `R`.
|
|
327
|
-
*/
|
|
328
276
|
function isAbandonedOutcome<Rels extends SchemaRelations, R>(
|
|
329
|
-
outcome: { readonly
|
|
277
|
+
outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
|
|
330
278
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
331
|
-
): outcome is { readonly
|
|
279
|
+
): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
|
|
332
280
|
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
|
|
333
281
|
}
|
|
334
282
|
|
|
335
|
-
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
336
283
|
function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
337
284
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
338
|
-
):
|
|
339
|
-
const outcome = Object.freeze({
|
|
285
|
+
): WriteOutcome<Rels, R> {
|
|
286
|
+
const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
|
|
340
287
|
if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
|
|
341
288
|
throw errors.new("bumbledb abandon outcome construction incomplete")
|
|
342
289
|
}
|
|
343
290
|
return outcome
|
|
344
291
|
}
|
|
345
292
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
* commit judgment judges, so check-then-act is race-free by construction).
|
|
350
|
-
* Spent when its owning `write`/`writeFrom` call resolves the attempt;
|
|
351
|
-
* any later use throws.
|
|
352
|
-
*/
|
|
353
|
-
interface Tx<Rels extends SchemaRelations> {
|
|
354
|
-
/**
|
|
355
|
-
* Records a collection of inserts. Singleton is `[fact]`. Empty is
|
|
356
|
-
* lawful. Returns how many facts were consumed and how many changed
|
|
357
|
-
* the in-memory final-state view. Every fact is complete — omitted
|
|
358
|
-
* fresh cells are a type error; mint first with {@link Tx.reserve}.
|
|
359
|
-
*/
|
|
360
|
-
insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
361
|
-
/**
|
|
362
|
-
* Records a collection of deletes. Singleton is `[fact]`. Returns
|
|
363
|
-
* how many facts were consumed and how many changed the view.
|
|
364
|
-
*/
|
|
293
|
+
interface WriteTx<Rels extends SchemaRelations> {
|
|
294
|
+
insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
295
|
+
|
|
365
296
|
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
366
|
-
|
|
367
|
-
* Mints `count` consecutive fresh values for a `.fresh` field.
|
|
368
|
-
* `count === 0n` is empty and does not yield a start.
|
|
369
|
-
*/
|
|
297
|
+
|
|
370
298
|
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
371
|
-
|
|
299
|
+
|
|
372
300
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
373
|
-
|
|
374
|
-
* Final-state point lookup through the relation's primary key (the
|
|
375
|
-
* {@link KeyFact} rule); `undefined` on a miss.
|
|
376
|
-
*/
|
|
301
|
+
|
|
377
302
|
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
378
|
-
/**
|
|
379
|
-
* Final-state point lookup through a DECLARED `key()` statement of this
|
|
380
|
-
* schema — the key object is typed by the statement's own projection;
|
|
381
|
-
* `undefined` on a miss.
|
|
382
|
-
*/
|
|
383
303
|
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
384
304
|
relation: R,
|
|
385
305
|
keyStatement: KeyStatement<R, P>,
|
|
@@ -387,67 +307,40 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
387
307
|
): Fact<R> | undefined
|
|
388
308
|
}
|
|
389
309
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
* error, and the underlying snapshot (with its LMDB reader slot) is
|
|
398
|
-
* already closed.
|
|
399
|
-
*/
|
|
400
|
-
interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
401
|
-
/**
|
|
402
|
-
* The committed generation this scope witnessed — carried by the
|
|
403
|
-
* snapshot open itself (one crossing, inside the snapshot's own
|
|
404
|
-
* transaction), so it is atomic with the snapshot by construction.
|
|
405
|
-
*/
|
|
310
|
+
const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
|
|
311
|
+
|
|
312
|
+
interface Witness<Rels extends SchemaRelations> extends Disposable {
|
|
313
|
+
readonly [witnessTypes]?: Rels
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
interface ReadInstance<Rels extends SchemaRelations> {
|
|
406
317
|
readonly generation: bigint
|
|
407
|
-
|
|
318
|
+
|
|
408
319
|
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
*/
|
|
320
|
+
|
|
321
|
+
count<R extends MemberRelation<Rels>>(relation: R): bigint
|
|
322
|
+
|
|
413
323
|
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
414
|
-
|
|
415
|
-
* Committed-state point lookup through a DECLARED `key()` statement of
|
|
416
|
-
* this schema — the key object is typed by the statement's own
|
|
417
|
-
* projection; `undefined` on a miss.
|
|
418
|
-
*/
|
|
324
|
+
|
|
419
325
|
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
420
326
|
relation: R,
|
|
421
327
|
keyStatement: KeyStatement<R, P>,
|
|
422
328
|
key: DeclaredKeyFact<R, P>
|
|
423
329
|
): Fact<R> | undefined
|
|
424
|
-
|
|
330
|
+
|
|
425
331
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
426
|
-
|
|
427
|
-
* Executes a prepared query against this scope's snapshot with the
|
|
428
|
-
* typed params object; returns the answer SET as plain rows with
|
|
429
|
-
* bare structural values (no order — the host sorts). This is the ONE
|
|
430
|
-
* execution spelling ({@link Prepared} carries no `execute`).
|
|
431
|
-
*/
|
|
332
|
+
|
|
432
333
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
334
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
433
335
|
}
|
|
434
336
|
|
|
435
|
-
/**
|
|
436
|
-
* The module-private inference slot of {@link Prepared}: an optional symbol
|
|
437
|
-
* property (never set at runtime) that keeps the prepared value's `Row` and
|
|
438
|
-
* `Params` type arguments load-bearing, so `execute` infers the typed rows
|
|
439
|
-
* and the typed params object from the value alone — the query module's
|
|
440
|
-
* `inferred` pattern, local to this module. A type-level carrier only:
|
|
441
|
-
* values stay bare, nothing is asserted.
|
|
442
|
-
*/
|
|
443
337
|
const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
|
|
444
338
|
|
|
445
339
|
/**
|
|
446
340
|
* One prepared query as a plain VALUE: explicit visible compilation
|
|
447
341
|
* (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
|
|
448
342
|
* refusal), no lifecycle. Execution happens ONLY through
|
|
449
|
-
* `
|
|
450
|
-
* symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
|
|
343
|
+
* `instance.execute(prepared, params)`. The engine-side plan is reclaimed by a GC
|
|
451
344
|
* finalizer when this value becomes unreachable (reclamation only, never
|
|
452
345
|
* correctness — an unreclaimed plan is idle memory, and process exit frees
|
|
453
346
|
* everything).
|
|
@@ -456,64 +349,21 @@ interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecor
|
|
|
456
349
|
readonly [preparedTypes]?: { readonly rels: Rels; readonly row: Row; readonly params: Params }
|
|
457
350
|
}
|
|
458
351
|
|
|
459
|
-
/**
|
|
460
|
-
* An open store. There is no close: read through `read`/the read sugar,
|
|
461
|
-
* write through `write`/`writeFrom`, and let the process own the
|
|
462
|
-
* environment's lifetime (the engine fsyncs every commit, so durability
|
|
463
|
-
* never waits on a close). A second `open`/`create` of the same path
|
|
464
|
-
* while this handle lives is the engine's `EnvironmentLocked`.
|
|
465
|
-
*/
|
|
466
352
|
interface Db<Rels extends SchemaRelations> {
|
|
467
|
-
/** The theory this store was opened with (fingerprint-verified by the engine). */
|
|
468
353
|
readonly schema: Schema<Rels>
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* SYNCHRONOUSLY against it, and closes the snapshot unconditionally
|
|
472
|
-
* before returning `fn`'s result. The {@link ReadScope} is invalidated
|
|
473
|
-
* when `fn` returns — a used-after-scope call throws a typed error.
|
|
474
|
-
*/
|
|
475
|
-
read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
476
|
-
/**
|
|
477
|
-
* The `using` acquisition (ruled 2026-07-23, R12): `using snap =
|
|
478
|
-
* db.read()` — the caller owns the scope's lifetime, and the scope's
|
|
479
|
-
* `Symbol.dispose` releases the snapshot deterministically at scope
|
|
480
|
-
* exit, in the language's own syntax. Lifetimes are disposables, never
|
|
481
|
-
* `close()`.
|
|
482
|
-
*/
|
|
483
|
-
read(): ReadScope<Rels>
|
|
484
|
-
/** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
|
|
485
|
-
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
486
|
-
/** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
|
|
487
|
-
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
488
|
-
/** `db.get(r, s, k)` === `db.read(snap => snap.get(r, s, k))` — the symmetry rule, keyed form. */
|
|
489
|
-
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
490
|
-
relation: R,
|
|
491
|
-
keyStatement: KeyStatement<R, P>,
|
|
492
|
-
key: DeclaredKeyFact<R, P>
|
|
493
|
-
): Fact<R> | undefined
|
|
494
|
-
/** `db.contains(r, f)` === `db.read(snap => snap.contains(r, f))` — the symmetry rule. */
|
|
495
|
-
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
496
|
-
/** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
|
|
497
|
-
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
354
|
+
|
|
355
|
+
read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
|
|
498
356
|
/**
|
|
499
357
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
500
358
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
501
359
|
* the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
|
|
502
|
-
* commit by returning {@link abandon}`(payload)
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* an arm the result type carries exactly when the callback can abandon.
|
|
506
|
-
*/
|
|
507
|
-
write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
|
|
508
|
-
/**
|
|
509
|
-
* One-shot witnessed write from a live read snapshot: begins a
|
|
510
|
-
* transaction that commits only if no state-changing commit landed
|
|
511
|
-
* since `snap` was taken. Must run inside the read callback that owns
|
|
512
|
-
* `snap`. A moved generation is the typed {@link ErrGenerationMoved}
|
|
513
|
-
* — retry is host policy; this method never loops. `fn` may decline to
|
|
514
|
-
* commit by returning {@link abandon}`(payload)`.
|
|
360
|
+
* commit by returning {@link abandon}`(payload)`: the transaction rolls
|
|
361
|
+
* back — nothing is committed, not even an empty commit — and the
|
|
362
|
+
* outcome is `{ tag: "abandoned", abandoned: payload }`.
|
|
515
363
|
*/
|
|
516
|
-
|
|
364
|
+
write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
|
|
365
|
+
|
|
366
|
+
writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
|
|
517
367
|
/**
|
|
518
368
|
* Prepares a query value built against THIS schema (identity is the
|
|
519
369
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
@@ -524,7 +374,6 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
524
374
|
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
525
375
|
}
|
|
526
376
|
|
|
527
|
-
/** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
|
|
528
377
|
interface RelationEntry {
|
|
529
378
|
readonly id: number
|
|
530
379
|
readonly member: SchemaRelation
|
|
@@ -532,19 +381,11 @@ interface RelationEntry {
|
|
|
532
381
|
readonly primaryKey: PrimaryKey | undefined
|
|
533
382
|
}
|
|
534
383
|
|
|
535
|
-
/** One relation's primary candidate key: its materialized statement id and projection. */
|
|
536
384
|
interface PrimaryKey {
|
|
537
385
|
readonly statementId: number
|
|
538
386
|
readonly projection: readonly string[]
|
|
539
387
|
}
|
|
540
388
|
|
|
541
|
-
/**
|
|
542
|
-
* One materialized-statement slot as the SDK mirrors it. Implied auto-keys
|
|
543
|
-
* omit `statement` (the engine owns those slots); every declared form
|
|
544
|
-
* carries the SDK value that lowered to it. Functionality forms also
|
|
545
|
-
* carry the key's owner and projection (what keyed point reads resolve
|
|
546
|
-
* through).
|
|
547
|
-
*/
|
|
548
389
|
type ImpliedKeyEntry = {
|
|
549
390
|
readonly kind: "functionality"
|
|
550
391
|
readonly owner: string
|
|
@@ -565,17 +406,72 @@ type StatementEntry =
|
|
|
565
406
|
| { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
|
|
566
407
|
| { readonly kind: "capacity"; readonly statement: Statement }
|
|
567
408
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
409
|
+
function decodeOffendingFact<Rels extends SchemaRelations>(
|
|
410
|
+
member: SchemaRelation,
|
|
411
|
+
relation: keyof Rels & string,
|
|
412
|
+
fact: WireViolationFact
|
|
413
|
+
): OffendingFact<Rels> {
|
|
414
|
+
const declared = sealedFieldsOf(member)
|
|
415
|
+
const decoded: Record<string, FactValue> = {}
|
|
416
|
+
for (const cell of fact.fields) {
|
|
417
|
+
const cited = declared.find(function byName(candidate) {
|
|
418
|
+
return candidate.name === cell.name
|
|
419
|
+
})
|
|
420
|
+
const roster = rosterOf(cited?.field)
|
|
421
|
+
decoded[cell.name] =
|
|
422
|
+
roster !== undefined
|
|
423
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
424
|
+
: cell.value
|
|
425
|
+
}
|
|
426
|
+
return Object.freeze({ relation, fact: Object.freeze(decoded) })
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function violationFromEntry<Rels extends SchemaRelations>(
|
|
430
|
+
entry: StatementEntry,
|
|
431
|
+
wire: WireViolation,
|
|
432
|
+
facts: readonly OffendingFact<Rels>[]
|
|
433
|
+
): Violation<Rels> {
|
|
434
|
+
const canonical = wire.canonical
|
|
435
|
+
if (entry.kind === "functionality") {
|
|
436
|
+
if (!("statement" in entry)) {
|
|
437
|
+
return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
|
|
438
|
+
}
|
|
439
|
+
return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
|
|
440
|
+
}
|
|
441
|
+
if (entry.kind === "capacity") {
|
|
442
|
+
if (wire.kind !== "capacity") {
|
|
443
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
|
|
444
|
+
}
|
|
445
|
+
return Object.freeze({
|
|
446
|
+
kind: "capacity",
|
|
447
|
+
statement: entry.statement,
|
|
448
|
+
canonical,
|
|
449
|
+
measure: wire.measure,
|
|
450
|
+
facts
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
if (wire.kind !== "containment") {
|
|
454
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
|
|
455
|
+
}
|
|
456
|
+
if (entry.kind === "mirrors") {
|
|
457
|
+
return Object.freeze({
|
|
458
|
+
kind: "containment",
|
|
459
|
+
statement: entry.statement,
|
|
460
|
+
canonical,
|
|
461
|
+
direction: wire.direction,
|
|
462
|
+
orientation: entry.orientation,
|
|
463
|
+
facts
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
return Object.freeze({
|
|
467
|
+
kind: "containment",
|
|
468
|
+
statement: entry.statement,
|
|
469
|
+
canonical,
|
|
470
|
+
direction: wire.direction,
|
|
471
|
+
facts
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
579
475
|
function materializedEntries(theory: AnySchema): StatementEntry[] {
|
|
580
476
|
const entries = impliedKeyEntries(theory)
|
|
581
477
|
for (const statement of theory.statements) {
|
|
@@ -584,13 +480,6 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
|
|
|
584
480
|
return entries
|
|
585
481
|
}
|
|
586
482
|
|
|
587
|
-
/**
|
|
588
|
-
* The engine-materialized implied keys, in the engine's pinned order: one
|
|
589
|
-
* auto-key per fresh field (relation declaration order, then field order),
|
|
590
|
-
* then one closed auto-key `R(id) -> R` per closed relation (declaration
|
|
591
|
-
* order). These slots carry no SDK statement value — the engine owns them
|
|
592
|
-
* (`schema()` rejects an explicit duplicate).
|
|
593
|
-
*/
|
|
594
483
|
function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
595
484
|
const entries: StatementEntry[] = []
|
|
596
485
|
for (const member of Object.values(theory.relations)) {
|
|
@@ -619,12 +508,6 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
|
619
508
|
return entries
|
|
620
509
|
}
|
|
621
510
|
|
|
622
|
-
/**
|
|
623
|
-
* One declared statement's materialized slots: a key or capacity statement
|
|
624
|
-
* occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
625
|
-
* `==` to two containments, `source <= target` first), both owned by the
|
|
626
|
-
* one SDK value.
|
|
627
|
-
*/
|
|
628
511
|
function declaredEntries(statement: Statement): StatementEntry[] {
|
|
629
512
|
const data = statement.data
|
|
630
513
|
switch (data.kind) {
|
|
@@ -663,29 +546,12 @@ function isThenable(value: unknown): boolean {
|
|
|
663
546
|
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function"
|
|
664
547
|
}
|
|
665
548
|
|
|
666
|
-
/**
|
|
667
|
-
* Narrows a keyed-get middle argument to a statement value (vs a key
|
|
668
|
-
* object) through the statement module's admission brand — a
|
|
669
|
-
* REPRESENTATION, never a shape probe: fact cell shapes are structurally
|
|
670
|
-
* OPEN (an interval value carrying an excess `kind` property is a legal
|
|
671
|
-
* cell), so no property probe could ever be sound here, but no host-built
|
|
672
|
-
* key object can spell the module-private brand symbol.
|
|
673
|
-
*/
|
|
674
549
|
function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
|
|
675
550
|
value: KeyFact<R> | KeyStatement<R, P>
|
|
676
551
|
): value is KeyStatement<R, P> {
|
|
677
552
|
return isStatement(value)
|
|
678
553
|
}
|
|
679
554
|
|
|
680
|
-
/**
|
|
681
|
-
* THE one selector dispatch of the `get` overload pair (primary-key vs
|
|
682
|
-
* key-statement, `docs/architecture/70-api.md` § the freeze): judges the
|
|
683
|
-
* middle argument once and hands the narrowed pieces to the chosen
|
|
684
|
-
* continuation. `Db.get` and the read scope's `get` both dispatch through
|
|
685
|
-
* here, so the two mismatch refusals speak with one voice and the symmetry
|
|
686
|
-
* rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
|
|
687
|
-
* construction.
|
|
688
|
-
*/
|
|
689
555
|
function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
|
|
690
556
|
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
691
557
|
declaredKey: DeclaredKeyFact<R, P> | undefined,
|
|
@@ -704,22 +570,11 @@ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
|
|
|
704
570
|
return byPrimary(keyOrStatement)
|
|
705
571
|
}
|
|
706
572
|
|
|
707
|
-
/** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
|
|
708
573
|
interface Tables {
|
|
709
574
|
readonly relations: ReadonlyMap<string, RelationEntry>
|
|
710
575
|
readonly statements: readonly StatementEntry[]
|
|
711
576
|
}
|
|
712
577
|
|
|
713
|
-
/**
|
|
714
|
-
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
715
|
-
* positional mirror against the engine's reported order — any drift
|
|
716
|
-
* (count, kind, id, or membership) is a construction-time failure, never a
|
|
717
|
-
* silent misattribution of a violation to the wrong statement value. The
|
|
718
|
-
* declaration-ordinal law the query lowering leans on is verified in the
|
|
719
|
-
* same walks: relation ids and sealed field ids both equal declaration
|
|
720
|
-
* order, so a constructed `Tables` IS the proof and `prepare` inherits it
|
|
721
|
-
* structurally — never a silently misaddressed query.
|
|
722
|
-
*/
|
|
723
578
|
function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
724
579
|
const entries = materializedEntries(theory)
|
|
725
580
|
if (entries.length !== manifest.statements.length) {
|
|
@@ -780,39 +635,59 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
780
635
|
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
781
636
|
}
|
|
782
637
|
|
|
783
|
-
|
|
638
|
+
function tablesFromTheory(theory: AnySchema): Tables {
|
|
639
|
+
const entries = materializedEntries(theory)
|
|
640
|
+
const relations = new Map<string, RelationEntry>()
|
|
641
|
+
Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
|
|
642
|
+
const member = theory.relations[name]
|
|
643
|
+
if (member === undefined) {
|
|
644
|
+
throw errors.new(`bumbledb theory has no relation ${name}`)
|
|
645
|
+
}
|
|
646
|
+
const fieldIds = new Map<string, number>()
|
|
647
|
+
sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
|
|
648
|
+
fieldIds.set(declared.name, fieldOrdinal)
|
|
649
|
+
})
|
|
650
|
+
let primaryKey: PrimaryKey | undefined
|
|
651
|
+
entries.forEach(function firstOwnedKey(entry, index) {
|
|
652
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
|
|
653
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
|
|
657
|
+
})
|
|
658
|
+
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
659
|
+
}
|
|
660
|
+
|
|
784
661
|
interface PointReads {
|
|
785
662
|
contains(relationId: number, row: readonly FactValue[]): boolean
|
|
786
663
|
get(relationId: number, statementId: number, key: readonly FactValue[]): FactValue[] | null
|
|
787
664
|
}
|
|
788
665
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
* generation it witnessed (carried by the snapshot open itself — one
|
|
792
|
-
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
793
|
-
* `read` callback returns, or by the scope's own
|
|
794
|
-
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
795
|
-
* exactly once, whichever of the owner and the dispose gets there first),
|
|
796
|
-
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
797
|
-
* the snapshot handle is never a public value.
|
|
798
|
-
*/
|
|
799
|
-
interface ScopeState {
|
|
800
|
-
readonly handle: SnapshotHandle
|
|
801
|
-
readonly generation: bigint
|
|
666
|
+
interface InstanceState {
|
|
667
|
+
readonly handle: InstanceHandle
|
|
802
668
|
live: boolean
|
|
803
|
-
closed: boolean
|
|
804
669
|
readonly owner: object
|
|
805
670
|
}
|
|
806
671
|
|
|
807
|
-
|
|
808
|
-
|
|
672
|
+
const instanceStates = new WeakMap<object, InstanceState>()
|
|
673
|
+
|
|
674
|
+
interface WitnessState {
|
|
675
|
+
readonly handle: WitnessHandle
|
|
676
|
+
spent: boolean
|
|
677
|
+
readonly owner: object
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const witnessStates = new WeakMap<object, WitnessState>()
|
|
681
|
+
|
|
682
|
+
const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
|
|
683
|
+
const closed = errors.trySync(function closeWitness() {
|
|
684
|
+
native.witnessClose(handle)
|
|
685
|
+
})
|
|
686
|
+
if (closed.error) {
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
})
|
|
809
690
|
|
|
810
|
-
/**
|
|
811
|
-
* One prepared value's PRIVATE engine half: the pinned plan handle, the
|
|
812
|
-
* owning store's identity token, and the query's marshaling tables (params
|
|
813
|
-
* in declaration order, select columns in head order). Held in
|
|
814
|
-
* {@link preparedPlans} — the plan handle is never a public value.
|
|
815
|
-
*/
|
|
816
691
|
interface PreparedPlan {
|
|
817
692
|
readonly handle: PreparedHandle
|
|
818
693
|
readonly owner: object
|
|
@@ -820,15 +695,8 @@ interface PreparedPlan {
|
|
|
820
695
|
readonly finds: readonly FindColumn[]
|
|
821
696
|
}
|
|
822
697
|
|
|
823
|
-
/** The private engine halves of this module's prepared values. */
|
|
824
698
|
const preparedPlans = new WeakMap<object, PreparedPlan>()
|
|
825
699
|
|
|
826
|
-
/**
|
|
827
|
-
* Reclaims the engine-side plan of a garbage-collected {@link Prepared}
|
|
828
|
-
* value. RECLAMATION ONLY, never correctness: a plan the collector never
|
|
829
|
-
* visits is idle engine memory until process exit, and a failure to close
|
|
830
|
-
* is swallowed (there is no one left to care — the owning value is gone).
|
|
831
|
-
*/
|
|
832
700
|
const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimPlan(handle) {
|
|
833
701
|
const closed = errors.trySync(function closePlan() {
|
|
834
702
|
native.preparedClose(handle)
|
|
@@ -838,145 +706,314 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
|
|
|
838
706
|
}
|
|
839
707
|
})
|
|
840
708
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
* state-changing commit landed since the witness snapshot: retry is host
|
|
844
|
-
* policy. Match with `errors.is`.
|
|
845
|
-
*/
|
|
846
|
-
const ErrGenerationMoved = errors.new(
|
|
847
|
-
"bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
|
|
709
|
+
const ErrAsyncCallback = errors.new(
|
|
710
|
+
"bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
|
|
848
711
|
)
|
|
712
|
+
const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
|
|
713
|
+
const ErrUseAfterScope = errors.new(
|
|
714
|
+
"bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
|
|
715
|
+
)
|
|
716
|
+
const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
|
|
717
|
+
const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
|
|
718
|
+
|
|
719
|
+
interface CatalogNative {
|
|
720
|
+
scan(relationId: number): FactValue[][]
|
|
721
|
+
count(relationId: number): bigint
|
|
722
|
+
contains(relationId: number, values: readonly FactValue[]): boolean
|
|
723
|
+
get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
|
|
724
|
+
prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
|
|
725
|
+
execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
|
|
726
|
+
}
|
|
849
727
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
function isMemberName(name: string): name is keyof Rels & string {
|
|
862
|
-
return tables.relations.has(name)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function resolveOrdinary(relation: AnyRelation): RelationEntry {
|
|
866
|
-
const entry = tables.relations.get(relation.name)
|
|
867
|
-
if (entry === undefined || entry.member !== relation) {
|
|
868
|
-
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
728
|
+
function catalogMethods<Rels extends SchemaRelations>(
|
|
729
|
+
theory: Schema<Rels>,
|
|
730
|
+
tables: Tables,
|
|
731
|
+
owner: object,
|
|
732
|
+
assertLive: () => void,
|
|
733
|
+
ops: CatalogNative
|
|
734
|
+
): Pick<ReadInstance<Rels>, "scan" | "count" | "get" | "contains" | "execute" | "prepare"> {
|
|
735
|
+
function planOf(prepared: object): PreparedPlan {
|
|
736
|
+
const plan = preparedPlans.get(prepared)
|
|
737
|
+
if (plan === undefined) {
|
|
738
|
+
throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
|
|
869
739
|
}
|
|
870
|
-
if (
|
|
871
|
-
throw errors.
|
|
872
|
-
|
|
740
|
+
if (plan.owner !== owner) {
|
|
741
|
+
throw errors.wrap(
|
|
742
|
+
ErrForeignPrepared,
|
|
743
|
+
`bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
|
|
873
744
|
)
|
|
874
745
|
}
|
|
875
|
-
return
|
|
746
|
+
return plan
|
|
876
747
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const entry = tables
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
const declared = sealedFieldsOf(entry.member)
|
|
884
|
-
const decoded: Record<string, FactValue> = {}
|
|
885
|
-
for (const cell of fact.fields) {
|
|
886
|
-
const cited = declared.find(function byName(candidate) {
|
|
887
|
-
return candidate.name === cell.name
|
|
888
|
-
})
|
|
889
|
-
const roster = rosterOf(cited?.field)
|
|
890
|
-
decoded[cell.name] =
|
|
891
|
-
roster !== undefined
|
|
892
|
-
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
893
|
-
: cell.value
|
|
894
|
-
}
|
|
895
|
-
return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
|
|
748
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
749
|
+
assertLive()
|
|
750
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
751
|
+
return bridged("bumbledb instance contains", function readContains() {
|
|
752
|
+
return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
753
|
+
})
|
|
896
754
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
755
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
756
|
+
relation: R,
|
|
757
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
758
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
759
|
+
): Fact<R> | undefined {
|
|
760
|
+
assertLive()
|
|
761
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
762
|
+
return selectKeyRead(
|
|
763
|
+
keyOrStatement,
|
|
764
|
+
declaredKey,
|
|
765
|
+
function byStatement(statement, key) {
|
|
766
|
+
const selected = declaredKeyOf(tables, theory, relation, statement)
|
|
767
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
768
|
+
return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
|
|
769
|
+
})
|
|
770
|
+
return row === null ? undefined : factOf(relation, row)
|
|
771
|
+
},
|
|
772
|
+
function byPrimary(key) {
|
|
773
|
+
const primaryKey = entry.primaryKey
|
|
774
|
+
if (primaryKey === undefined) {
|
|
775
|
+
throw errors.new(
|
|
776
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
780
|
+
return ops.get(
|
|
781
|
+
entry.id,
|
|
782
|
+
primaryKey.statementId,
|
|
783
|
+
keyRowOf(relation.data, primaryKey.projection, recordOf(key))
|
|
784
|
+
)
|
|
785
|
+
})
|
|
786
|
+
return row === null ? undefined : factOf(relation, row)
|
|
914
787
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
if (entry.kind === "mirrors") {
|
|
927
|
-
return Object.freeze({
|
|
928
|
-
kind: "containment",
|
|
929
|
-
statement: entry.statement,
|
|
930
|
-
canonical,
|
|
931
|
-
direction: wire.direction,
|
|
932
|
-
orientation: entry.orientation,
|
|
933
|
-
facts
|
|
934
|
-
})
|
|
935
|
-
}
|
|
936
|
-
return Object.freeze({
|
|
937
|
-
kind: "containment",
|
|
938
|
-
statement: entry.statement,
|
|
939
|
-
canonical,
|
|
940
|
-
direction: wire.direction,
|
|
941
|
-
facts
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
791
|
+
assertLive()
|
|
792
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
793
|
+
const rows = bridged("bumbledb instance scan", function readScan() {
|
|
794
|
+
return ops.scan(entry.id)
|
|
795
|
+
})
|
|
796
|
+
return rows.map(function decodeRow(row) {
|
|
797
|
+
return factOf(relation, row)
|
|
942
798
|
})
|
|
943
799
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
* id comes from the positional mirror, so the engine point-reads through
|
|
950
|
-
* exactly the declared projection.
|
|
951
|
-
*/
|
|
952
|
-
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
953
|
-
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
954
|
-
return "statement" in candidate && candidate.statement === statement
|
|
800
|
+
function count<R extends MemberRelation<Rels>>(relation: R): bigint {
|
|
801
|
+
assertLive()
|
|
802
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
803
|
+
return bridged("bumbledb instance count", function readCount() {
|
|
804
|
+
return ops.count(entry.id)
|
|
955
805
|
})
|
|
956
|
-
|
|
957
|
-
|
|
806
|
+
}
|
|
807
|
+
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
808
|
+
assertLive()
|
|
809
|
+
const plan = planOf(prepared)
|
|
810
|
+
const wire = wireParams(plan.params, recordOf(params))
|
|
811
|
+
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
812
|
+
return ops.execute(plan.handle, wire)
|
|
813
|
+
})
|
|
814
|
+
return decodeAnswers<Row>(plan.finds, rows)
|
|
815
|
+
}
|
|
816
|
+
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
817
|
+
assertLive()
|
|
818
|
+
if (q.schema !== theory) {
|
|
958
819
|
throw errors.new(
|
|
959
|
-
`
|
|
820
|
+
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
960
821
|
)
|
|
961
822
|
}
|
|
962
|
-
|
|
963
|
-
|
|
823
|
+
const queryIr = lowerQuery(q)
|
|
824
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
825
|
+
return ops.prepare(queryIr)
|
|
826
|
+
})
|
|
827
|
+
if (!outcome.ok) {
|
|
828
|
+
throwPrepareRefusal(outcome.message)
|
|
964
829
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
830
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
831
|
+
preparedPlans.set(
|
|
832
|
+
prepared,
|
|
833
|
+
Object.freeze({
|
|
834
|
+
handle: outcome.prepared,
|
|
835
|
+
owner,
|
|
836
|
+
params: q.data.params,
|
|
837
|
+
finds: q.data.finds
|
|
838
|
+
})
|
|
839
|
+
)
|
|
840
|
+
planReclaimer.register(prepared, outcome.prepared)
|
|
841
|
+
return prepared
|
|
842
|
+
}
|
|
843
|
+
return { scan, count, get, contains, execute, prepare }
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
|
|
847
|
+
const entry = tables.relations.get(relation.name)
|
|
848
|
+
if (entry === undefined || entry.member !== relation) {
|
|
849
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
850
|
+
}
|
|
851
|
+
if (isClosedMember(relation)) {
|
|
852
|
+
throw errors.new(
|
|
853
|
+
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
return entry
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function declaredKeyOf(tables: Tables, theory: AnySchema, relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
860
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
861
|
+
return "statement" in candidate && candidate.statement === statement
|
|
862
|
+
})
|
|
863
|
+
const entry = tables.statements[statementId]
|
|
864
|
+
if (entry === undefined) {
|
|
865
|
+
throw errors.new(
|
|
866
|
+
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
867
|
+
)
|
|
868
|
+
}
|
|
869
|
+
if (entry.kind !== "functionality") {
|
|
870
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
871
|
+
}
|
|
872
|
+
if (entry.owner !== relation.name) {
|
|
873
|
+
throw errors.new(
|
|
874
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function overlayMethods<Rels extends SchemaRelations>(
|
|
881
|
+
theory: Schema<Rels>,
|
|
882
|
+
tables: Tables,
|
|
883
|
+
assertLive: () => void,
|
|
884
|
+
reads: PointReads
|
|
885
|
+
): Pick<WriteTx<Rels>, "contains" | "get"> {
|
|
886
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
887
|
+
assertLive()
|
|
888
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
889
|
+
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
890
|
+
}
|
|
891
|
+
function readThroughKey<R extends MemberRelation<Rels>>(
|
|
892
|
+
relation: R,
|
|
893
|
+
entry: RelationEntry,
|
|
894
|
+
selected: PrimaryKey,
|
|
895
|
+
key: Readonly<Record<string, unknown>>
|
|
896
|
+
): Fact<R> | undefined {
|
|
897
|
+
const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
|
|
898
|
+
if (row === null) {
|
|
899
|
+
return undefined
|
|
900
|
+
}
|
|
901
|
+
return factOf(relation, row)
|
|
902
|
+
}
|
|
903
|
+
function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
904
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
905
|
+
relation: R,
|
|
906
|
+
keyStatement: KeyStatement<R, P>,
|
|
907
|
+
key: DeclaredKeyFact<R, P>
|
|
908
|
+
): Fact<R> | undefined
|
|
909
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
910
|
+
relation: R,
|
|
911
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
912
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
913
|
+
): Fact<R> | undefined {
|
|
914
|
+
assertLive()
|
|
915
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
916
|
+
return selectKeyRead(
|
|
917
|
+
keyOrStatement,
|
|
918
|
+
declaredKey,
|
|
919
|
+
function byStatement(statement, key) {
|
|
920
|
+
return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
|
|
921
|
+
},
|
|
922
|
+
function byPrimary(key) {
|
|
923
|
+
const primaryKey = entry.primaryKey
|
|
924
|
+
if (primaryKey === undefined) {
|
|
925
|
+
throw errors.new(
|
|
926
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
return readThroughKey(relation, entry, primaryKey, recordOf(key))
|
|
930
|
+
}
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
return { contains, get }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function createReadInstance<Rels extends SchemaRelations>(
|
|
937
|
+
nativeHandle: InstanceHandle,
|
|
938
|
+
theory: Schema<Rels>,
|
|
939
|
+
tables: Tables,
|
|
940
|
+
owner: object
|
|
941
|
+
): ReadInstance<Rels> {
|
|
942
|
+
const state: InstanceState = { handle: nativeHandle, live: true, owner }
|
|
943
|
+
function assertLive(): void {
|
|
944
|
+
if (!state.live) {
|
|
945
|
+
throw errors.wrap(
|
|
946
|
+
ErrUseAfterScope,
|
|
947
|
+
"bumbledb read instance is invalidated — its owning callback already returned"
|
|
968
948
|
)
|
|
969
949
|
}
|
|
970
|
-
|
|
950
|
+
}
|
|
951
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
952
|
+
scan(relationId) {
|
|
953
|
+
return native.instanceScan(state.handle, relationId)
|
|
954
|
+
},
|
|
955
|
+
count(relationId) {
|
|
956
|
+
return native.instanceCount(state.handle, relationId)
|
|
957
|
+
},
|
|
958
|
+
contains(relationId, values) {
|
|
959
|
+
return native.instanceContains(state.handle, relationId, values)
|
|
960
|
+
},
|
|
961
|
+
get(relationId, statementId, keyValues) {
|
|
962
|
+
return native.instanceGet(state.handle, relationId, statementId, keyValues)
|
|
963
|
+
},
|
|
964
|
+
prepare(query) {
|
|
965
|
+
return native.instancePrepare(state.handle, query)
|
|
966
|
+
},
|
|
967
|
+
execute(prepared, params) {
|
|
968
|
+
return native.preparedExecute(prepared, state.handle, params)
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
const instance: ReadInstance<Rels> = Object.freeze({
|
|
972
|
+
get generation() {
|
|
973
|
+
assertLive()
|
|
974
|
+
return bridged("bumbledb instance generation", function readGeneration() {
|
|
975
|
+
return native.instanceGeneration(state.handle)
|
|
976
|
+
})
|
|
977
|
+
},
|
|
978
|
+
...methods
|
|
979
|
+
})
|
|
980
|
+
instanceStates.set(instance, state)
|
|
981
|
+
return instance
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<Rels>, manifest: Manifest): Db<Rels> {
|
|
985
|
+
const tables = tablesOf(theory, manifest)
|
|
986
|
+
/** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
|
|
987
|
+
const owner = Object.freeze({})
|
|
988
|
+
|
|
989
|
+
function isMemberName(name: string): name is keyof Rels & string {
|
|
990
|
+
return tables.relations.has(name)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function violationOf(wire: WireViolation): Violation<Rels> {
|
|
994
|
+
const entry = tables.statements[wire.statementId]
|
|
995
|
+
if (entry === undefined) {
|
|
996
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
997
|
+
}
|
|
998
|
+
const facts = Object.freeze(
|
|
999
|
+
wire.facts.map(function offending(fact) {
|
|
1000
|
+
const rel = tables.relations.get(fact.relation)
|
|
1001
|
+
if (rel === undefined || !isMemberName(fact.relation)) {
|
|
1002
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
1003
|
+
}
|
|
1004
|
+
return decodeOffendingFact<Rels>(rel.member, fact.relation, fact)
|
|
1005
|
+
})
|
|
1006
|
+
)
|
|
1007
|
+
return violationFromEntry(entry, wire, facts)
|
|
971
1008
|
}
|
|
972
1009
|
|
|
973
1010
|
function pointReadsOf(assertLive: () => void, reads: PointReads) {
|
|
974
1011
|
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
975
1012
|
assertLive()
|
|
976
|
-
const entry =
|
|
1013
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
977
1014
|
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
978
1015
|
}
|
|
979
|
-
|
|
1016
|
+
|
|
980
1017
|
function readThroughKey<R extends MemberRelation<Rels>>(
|
|
981
1018
|
relation: R,
|
|
982
1019
|
entry: RelationEntry,
|
|
@@ -1001,12 +1038,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1001
1038
|
declaredKey?: DeclaredKeyFact<R, P>
|
|
1002
1039
|
): Fact<R> | undefined {
|
|
1003
1040
|
assertLive()
|
|
1004
|
-
const entry =
|
|
1041
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1005
1042
|
return selectKeyRead(
|
|
1006
1043
|
keyOrStatement,
|
|
1007
1044
|
declaredKey,
|
|
1008
1045
|
function byStatement(statement, key) {
|
|
1009
|
-
return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
|
|
1046
|
+
return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
|
|
1010
1047
|
},
|
|
1011
1048
|
function byPrimary(key) {
|
|
1012
1049
|
const primaryKey = entry.primaryKey
|
|
@@ -1022,201 +1059,72 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1022
1059
|
return { contains, get }
|
|
1023
1060
|
}
|
|
1024
1061
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
)
|
|
1038
|
-
|
|
1039
|
-
|
|
1062
|
+
function pinPrepared<Row, Params extends ParamsRecord>(
|
|
1063
|
+
preparedHandle: PreparedHandle,
|
|
1064
|
+
q: Query<Rels, Row, Params>
|
|
1065
|
+
): Prepared<Rels, Row, Params> {
|
|
1066
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1067
|
+
preparedPlans.set(
|
|
1068
|
+
prepared,
|
|
1069
|
+
Object.freeze({
|
|
1070
|
+
handle: preparedHandle,
|
|
1071
|
+
owner,
|
|
1072
|
+
params: q.data.params,
|
|
1073
|
+
finds: q.data.finds
|
|
1074
|
+
})
|
|
1075
|
+
)
|
|
1076
|
+
planReclaimer.register(prepared, preparedHandle)
|
|
1077
|
+
return prepared
|
|
1040
1078
|
}
|
|
1041
1079
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
const reads = pointReadsOf(assertLive, {
|
|
1056
|
-
contains(relationId, row) {
|
|
1057
|
-
return bridged("bumbledb snapshot contains", function readContains() {
|
|
1058
|
-
return native.snapshotContains(state.handle, relationId, row)
|
|
1059
|
-
})
|
|
1060
|
-
},
|
|
1061
|
-
get(relationId, statementId, key) {
|
|
1062
|
-
return bridged("bumbledb snapshot get", function readGet() {
|
|
1063
|
-
return native.snapshotGet(state.handle, relationId, statementId, key)
|
|
1080
|
+
function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
|
|
1081
|
+
const state: WitnessState = { handle: nativeHandle, spent: false, owner }
|
|
1082
|
+
const witness: Witness<Rels> = Object.freeze({
|
|
1083
|
+
[Symbol.dispose](): void {
|
|
1084
|
+
if (state.spent) {
|
|
1085
|
+
return
|
|
1086
|
+
}
|
|
1087
|
+
state.spent = true
|
|
1088
|
+
bridged("close bumbledb witness", function closeWitness() {
|
|
1089
|
+
native.witnessClose(nativeHandle)
|
|
1064
1090
|
})
|
|
1065
1091
|
}
|
|
1066
1092
|
})
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const rows = bridged("bumbledb snapshot scan", function readScan() {
|
|
1071
|
-
return native.snapshotScan(state.handle, entry.id)
|
|
1072
|
-
})
|
|
1073
|
-
return rows.map(function decodeRow(row) {
|
|
1074
|
-
return factOf(relation, row)
|
|
1075
|
-
})
|
|
1076
|
-
}
|
|
1077
|
-
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1078
|
-
assertLive()
|
|
1079
|
-
const plan = planOf(prepared)
|
|
1080
|
-
const wire = wireParams(plan.params, recordOf(params))
|
|
1081
|
-
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
1082
|
-
return native.preparedExecute(plan.handle, state.handle, wire)
|
|
1083
|
-
})
|
|
1084
|
-
return decodeAnswers<Row>(plan.finds, rows)
|
|
1085
|
-
}
|
|
1086
|
-
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1087
|
-
function dispose(): void {
|
|
1088
|
-
state.live = false
|
|
1089
|
-
closeScopeState(state)
|
|
1090
|
-
}
|
|
1091
|
-
const scope: ReadScope<Rels> = Object.freeze({
|
|
1092
|
-
generation: state.generation,
|
|
1093
|
-
scan,
|
|
1094
|
-
get: reads.get,
|
|
1095
|
-
contains: reads.contains,
|
|
1096
|
-
execute,
|
|
1097
|
-
[Symbol.dispose]: dispose
|
|
1098
|
-
})
|
|
1099
|
-
scopeStates.set(scope, state)
|
|
1100
|
-
return scope
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
|
|
1105
|
-
* snapshot open/close is counted so a write-begin failure can report how
|
|
1106
|
-
* many read handles were live at the fault — a leaked scope is invisible
|
|
1107
|
-
* until the exact moment it matters, so the failure carries the census.
|
|
1108
|
-
*/
|
|
1109
|
-
let liveSnapshots = 0
|
|
1110
|
-
|
|
1111
|
-
/**
|
|
1112
|
-
* Opens one snapshot and its scope state (live until the owner flips
|
|
1113
|
-
* it). The witnessed generation rides the snapshot open itself — one
|
|
1114
|
-
* crossing carries both (finding 016), so the fault-pairing close
|
|
1115
|
-
* branch a second `dbGeneration` call needed is structurally gone.
|
|
1116
|
-
*/
|
|
1117
|
-
function openScopeState(): ScopeState {
|
|
1118
|
-
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
1119
|
-
return native.dbSnapshot(handle)
|
|
1120
|
-
})
|
|
1121
|
-
liveSnapshots += 1
|
|
1122
|
-
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
|
|
1093
|
+
witnessStates.set(witness, state)
|
|
1094
|
+
witnessReclaimer.register(witness, nativeHandle)
|
|
1095
|
+
return witness
|
|
1123
1096
|
}
|
|
1124
1097
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
* the snapshot closes exactly once, whichever of the owning call and
|
|
1128
|
-
* the scope's own `Symbol.dispose` gets there first, so an early
|
|
1129
|
-
* in-callback disposal never double-closes (and never double-counts
|
|
1130
|
-
* the census).
|
|
1131
|
-
*/
|
|
1132
|
-
function closeScopeState(state: ScopeState): void {
|
|
1133
|
-
if (state.closed) {
|
|
1134
|
-
return
|
|
1135
|
-
}
|
|
1136
|
-
state.closed = true
|
|
1137
|
-
bridged("close bumbledb snapshot", function closeSnapshot() {
|
|
1138
|
-
native.snapshotClose(state.handle)
|
|
1139
|
-
})
|
|
1140
|
-
liveSnapshots -= 1
|
|
1098
|
+
function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
|
|
1099
|
+
return createReadInstance(nativeHandle, theory, tables, owner)
|
|
1141
1100
|
}
|
|
1142
1101
|
|
|
1143
|
-
function read<
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
* own syntax.
|
|
1154
|
-
*/
|
|
1155
|
-
return scope
|
|
1156
|
-
}
|
|
1157
|
-
const result = errors.trySync(function runRead() {
|
|
1158
|
-
return fn(scope)
|
|
1159
|
-
})
|
|
1160
|
-
state.live = false
|
|
1161
|
-
closeScopeState(state)
|
|
1162
|
-
if (result.error) {
|
|
1163
|
-
throw errors.wrap(result.error, "bumbledb read")
|
|
1164
|
-
}
|
|
1165
|
-
return result.data
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
1169
|
-
return read(function scanInScope(snap) {
|
|
1170
|
-
return snap.scan(relation)
|
|
1171
|
-
})
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1175
|
-
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1176
|
-
relation: R,
|
|
1177
|
-
keyStatement: KeyStatement<R, P>,
|
|
1178
|
-
key: DeclaredKeyFact<R, P>
|
|
1179
|
-
): Fact<R> | undefined
|
|
1180
|
-
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1181
|
-
relation: R,
|
|
1182
|
-
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1183
|
-
declaredKey?: DeclaredKeyFact<R, P>
|
|
1184
|
-
): Fact<R> | undefined {
|
|
1185
|
-
return read(function getInScope(snap) {
|
|
1186
|
-
return selectKeyRead(
|
|
1187
|
-
keyOrStatement,
|
|
1188
|
-
declaredKey,
|
|
1189
|
-
function byStatement(statement, key) {
|
|
1190
|
-
return snap.get(relation, statement, key)
|
|
1191
|
-
},
|
|
1192
|
-
function byPrimary(key) {
|
|
1193
|
-
return snap.get(relation, key)
|
|
1102
|
+
function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
|
|
1103
|
+
let captured: R | undefined
|
|
1104
|
+
const result = bridged("bumbledb read", function runRead() {
|
|
1105
|
+
return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
|
|
1106
|
+
const instance = makeInstance(nativeInstance)
|
|
1107
|
+
const witness = makeWitness(nativeWitness)
|
|
1108
|
+
const value = body(instance, witness)
|
|
1109
|
+
const state = instanceStates.get(instance)
|
|
1110
|
+
if (state !== undefined) {
|
|
1111
|
+
state.live = false
|
|
1194
1112
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
return snap.contains(relation, fact)
|
|
1202
|
-
})
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1206
|
-
return read(function executeInScope(snap) {
|
|
1207
|
-
return snap.execute(prepared, params)
|
|
1113
|
+
if (isThenable(value)) {
|
|
1114
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
|
|
1115
|
+
}
|
|
1116
|
+
captured = value
|
|
1117
|
+
return value
|
|
1118
|
+
})
|
|
1208
1119
|
})
|
|
1120
|
+
return (captured ?? result) as SyncResult<R>
|
|
1209
1121
|
}
|
|
1210
1122
|
|
|
1211
|
-
|
|
1212
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
1213
|
-
* `writeFrom` pass an already-begun handle.
|
|
1214
|
-
*/
|
|
1215
|
-
function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
|
|
1123
|
+
function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
|
|
1216
1124
|
const txState = { spent: false }
|
|
1217
1125
|
function assertLive(): void {
|
|
1218
1126
|
if (txState.spent) {
|
|
1219
|
-
throw errors.
|
|
1127
|
+
throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
|
|
1220
1128
|
}
|
|
1221
1129
|
}
|
|
1222
1130
|
const reads = pointReadsOf(assertLive, {
|
|
@@ -1233,29 +1141,23 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1233
1141
|
})
|
|
1234
1142
|
}
|
|
1235
1143
|
})
|
|
1236
|
-
function insert<R extends MemberRelation<Rels>>(relation: R, facts:
|
|
1144
|
+
function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
1237
1145
|
assertLive()
|
|
1238
|
-
const
|
|
1239
|
-
for (const fact of facts) {
|
|
1240
|
-
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1241
|
-
}
|
|
1242
|
-
const entry = resolveOrdinary(relation)
|
|
1146
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1243
1147
|
const txHandle = resolveTx()
|
|
1244
|
-
|
|
1245
|
-
return
|
|
1148
|
+
return mutateCollection(relation, facts, function applyCells(rows, cells) {
|
|
1149
|
+
return bridged("bumbledb tx insert", function record() {
|
|
1150
|
+
return native.txInsert(txHandle, entry.id, rows, cells)
|
|
1151
|
+
})
|
|
1246
1152
|
})
|
|
1247
|
-
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1248
1153
|
}
|
|
1249
1154
|
function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1250
1155
|
assertLive()
|
|
1251
|
-
const
|
|
1252
|
-
for (const fact of facts) {
|
|
1253
|
-
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1254
|
-
}
|
|
1255
|
-
const entry = resolveOrdinary(relation)
|
|
1156
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1256
1157
|
const txHandle = resolveTx()
|
|
1158
|
+
const flat = rowsOf(relation, facts)
|
|
1257
1159
|
const report = bridged("bumbledb tx delete", function record() {
|
|
1258
|
-
return native.txDelete(txHandle, entry.id, rows)
|
|
1160
|
+
return native.txDelete(txHandle, entry.id, flat.rows, flat.cells)
|
|
1259
1161
|
})
|
|
1260
1162
|
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1261
1163
|
}
|
|
@@ -1265,7 +1167,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1265
1167
|
count: bigint
|
|
1266
1168
|
): FreshRange {
|
|
1267
1169
|
assertLive()
|
|
1268
|
-
const entry =
|
|
1170
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1269
1171
|
const declared = relation.data.fields.find(function byName(candidate) {
|
|
1270
1172
|
return candidate.name === field
|
|
1271
1173
|
})
|
|
@@ -1282,7 +1184,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1282
1184
|
})
|
|
1283
1185
|
return freshRangeOf(range)
|
|
1284
1186
|
}
|
|
1285
|
-
const tx:
|
|
1187
|
+
const tx: WriteTx<Rels> = Object.freeze({
|
|
1286
1188
|
insert,
|
|
1287
1189
|
delete: remove,
|
|
1288
1190
|
reserve,
|
|
@@ -1295,111 +1197,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1295
1197
|
return { tx, spend }
|
|
1296
1198
|
}
|
|
1297
1199
|
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
return
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
})
|
|
1305
|
-
made.spend()
|
|
1306
|
-
if (built.error) {
|
|
1307
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1308
|
-
native.txAbort(txHandle)
|
|
1200
|
+
function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
|
|
1201
|
+
if (nativeOutcome.tag === "moved") {
|
|
1202
|
+
return Object.freeze({
|
|
1203
|
+
tag: "moved" as const,
|
|
1204
|
+
witnessed: nativeOutcome.witnessed,
|
|
1205
|
+
current: nativeOutcome.current
|
|
1309
1206
|
})
|
|
1310
|
-
throw errors.wrap(built.error, "build write delta")
|
|
1311
1207
|
}
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
* spent: committing here would be a silent EMPTY commit reported
|
|
1317
|
-
* ok while the callback's real inserts throw "spent" as unhandled
|
|
1318
|
-
* rejections. Refused typed instead — abort, nothing committed
|
|
1319
|
-
* (the same one-writer law as the thrown-callback path).
|
|
1320
|
-
*/
|
|
1321
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1322
|
-
native.txAbort(txHandle)
|
|
1323
|
-
})
|
|
1324
|
-
throw errors.new(
|
|
1325
|
-
"bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1326
|
-
)
|
|
1327
|
-
}
|
|
1328
|
-
if (isAbandon(built.data)) {
|
|
1329
|
-
/**
|
|
1330
|
-
* The caller's explicit decline to commit (R10): the sentinel's
|
|
1331
|
-
* contract is unconditional — roll back, nothing committed, not
|
|
1332
|
-
* even an empty commit; commit is unreachable for a sentinel
|
|
1333
|
-
* result.
|
|
1334
|
-
*/
|
|
1335
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1336
|
-
native.txAbort(txHandle)
|
|
1208
|
+
if (nativeOutcome.tag === "rejected") {
|
|
1209
|
+
return Object.freeze({
|
|
1210
|
+
tag: "rejected" as const,
|
|
1211
|
+
violations: Object.freeze(nativeOutcome.violations.map(violationOf))
|
|
1337
1212
|
})
|
|
1338
|
-
return abandonedOutcome<Rels, R>(built.data)
|
|
1339
1213
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
})
|
|
1344
|
-
})
|
|
1345
|
-
if (committed.error) {
|
|
1346
|
-
/**
|
|
1347
|
-
* A THROWN commit (engine I/O failure, bridge fault) must never
|
|
1348
|
-
* leave the write transaction live: LMDB holds one writer per
|
|
1349
|
-
* environment, and a leaked handle turns every later begin into
|
|
1350
|
-
* EINVAL for the process's lifetime. The abort is best-effort —
|
|
1351
|
-
* the native side may already have consumed the handle.
|
|
1352
|
-
*/
|
|
1353
|
-
const aborted = errors.trySync(function abortAfterFailedCommit() {
|
|
1354
|
-
native.txAbort(txHandle)
|
|
1355
|
-
})
|
|
1356
|
-
if (aborted.error) {
|
|
1214
|
+
if (nativeOutcome.tag === "abandoned") {
|
|
1215
|
+
if (built === undefined || !isAbandon(built)) {
|
|
1216
|
+
throw errors.new("bumbledb write abandoned without an abandon sentinel")
|
|
1357
1217
|
}
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1360
|
-
const outcome = committed.data
|
|
1361
|
-
if (outcome.ok) {
|
|
1362
|
-
return Object.freeze({ ok: true, generation: outcome.generation })
|
|
1218
|
+
return abandonedOutcome<Rels, R>(built)
|
|
1363
1219
|
}
|
|
1364
1220
|
return Object.freeze({
|
|
1365
|
-
|
|
1366
|
-
|
|
1221
|
+
tag: "accepted" as const,
|
|
1222
|
+
value: Object.freeze({
|
|
1223
|
+
value: built as Exclude<R, Abandon<unknown>>,
|
|
1224
|
+
generation: nativeOutcome.generation
|
|
1225
|
+
})
|
|
1367
1226
|
})
|
|
1368
1227
|
}
|
|
1369
1228
|
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1229
|
+
function runWrite<R>(
|
|
1230
|
+
invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
|
|
1231
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1232
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1233
|
+
let built: SyncResult<R> | undefined
|
|
1234
|
+
const nativeOutcome = bridged("bumbledb write", function callWrite() {
|
|
1235
|
+
return invoke(function onWrite(txHandle) {
|
|
1236
|
+
const made = makeTx(function resolveTx() {
|
|
1237
|
+
return txHandle
|
|
1238
|
+
})
|
|
1239
|
+
const result = errors.trySync(function buildDelta() {
|
|
1240
|
+
return fn(made.tx)
|
|
1241
|
+
})
|
|
1242
|
+
made.spend()
|
|
1243
|
+
if (result.error) {
|
|
1244
|
+
throw errors.wrap(result.error, "build write delta")
|
|
1245
|
+
}
|
|
1246
|
+
if (isThenable(result.data)) {
|
|
1247
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
|
|
1248
|
+
}
|
|
1249
|
+
built = result.data
|
|
1250
|
+
return !isAbandon(result.data)
|
|
1251
|
+
})
|
|
1373
1252
|
})
|
|
1374
|
-
return
|
|
1253
|
+
return mapNativeWrite(nativeOutcome, built)
|
|
1375
1254
|
}
|
|
1376
1255
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const snapState = scopeStates.get(snap)
|
|
1384
|
-
if (snapState === undefined) {
|
|
1385
|
-
throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
|
|
1256
|
+
function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
|
|
1257
|
+
const outcome = runWrite(function invoke(callback) {
|
|
1258
|
+
return native.dbWrite(handle, callback)
|
|
1259
|
+
}, fn)
|
|
1260
|
+
if (outcome.tag === "moved") {
|
|
1261
|
+
throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
|
|
1386
1262
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1263
|
+
return outcome
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function writeFrom<R>(
|
|
1267
|
+
witness: Witness<Rels>,
|
|
1268
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1269
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1270
|
+
const state = witnessStates.get(witness)
|
|
1271
|
+
if (state === undefined) {
|
|
1272
|
+
throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
|
|
1392
1273
|
}
|
|
1393
|
-
|
|
1394
|
-
return native.dbWriteFrom(handle, snapState.handle)
|
|
1395
|
-
})
|
|
1396
|
-
if (!witnessed.ok) {
|
|
1274
|
+
if (state.owner !== owner) {
|
|
1397
1275
|
throw errors.wrap(
|
|
1398
|
-
|
|
1399
|
-
`writeFrom
|
|
1276
|
+
ErrForeignWitness,
|
|
1277
|
+
`bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
|
|
1400
1278
|
)
|
|
1401
1279
|
}
|
|
1402
|
-
|
|
1280
|
+
if (state.spent) {
|
|
1281
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
|
|
1282
|
+
}
|
|
1283
|
+
return runWrite(function invoke(callback) {
|
|
1284
|
+
return native.dbWriteFrom(handle, state.handle, callback)
|
|
1285
|
+
}, fn)
|
|
1403
1286
|
}
|
|
1404
1287
|
|
|
1405
1288
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
@@ -1413,133 +1296,376 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1413
1296
|
return native.dbPrepare(handle, queryIr)
|
|
1414
1297
|
})
|
|
1415
1298
|
if (!outcome.ok) {
|
|
1416
|
-
|
|
1299
|
+
throwPrepareRefusal(outcome.message)
|
|
1417
1300
|
}
|
|
1418
|
-
|
|
1419
|
-
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1420
|
-
preparedPlans.set(
|
|
1421
|
-
prepared,
|
|
1422
|
-
Object.freeze({
|
|
1423
|
-
handle: preparedHandle,
|
|
1424
|
-
owner,
|
|
1425
|
-
params: q.data.params,
|
|
1426
|
-
finds: q.data.finds
|
|
1427
|
-
})
|
|
1428
|
-
)
|
|
1429
|
-
planReclaimer.register(prepared, preparedHandle)
|
|
1430
|
-
return prepared
|
|
1301
|
+
return pinPrepared(outcome.prepared, q)
|
|
1431
1302
|
}
|
|
1432
1303
|
|
|
1433
1304
|
return Object.freeze({
|
|
1434
1305
|
schema: theory,
|
|
1435
1306
|
read,
|
|
1436
|
-
scan,
|
|
1437
|
-
get,
|
|
1438
|
-
contains,
|
|
1439
|
-
execute,
|
|
1440
1307
|
write,
|
|
1441
1308
|
writeFrom,
|
|
1442
1309
|
prepare
|
|
1443
1310
|
})
|
|
1444
1311
|
}
|
|
1445
1312
|
|
|
1446
|
-
/**
|
|
1447
|
-
* The engine twin of the schema-level class wall, as a matchable value
|
|
1448
|
-
* (`errors.is`): the shared lowering rejected a spec whose statement pairs
|
|
1449
|
-
* faces with disagreeing newtype labels — the faces of a dependency agree
|
|
1450
|
-
* on their newtype, or neither carries one. UNREACHABLE through the typed
|
|
1451
|
-
* builder (the SDK computes every label from the laws, so its lowered
|
|
1452
|
-
* specs cohere by construction); a raw spec handed to the bridge is the
|
|
1453
|
-
* one road here, and the runtime referee that proves the engine judges
|
|
1454
|
-
* what the types claim.
|
|
1455
|
-
*/
|
|
1456
1313
|
const ErrNewtypeMismatch = errors.new(
|
|
1457
1314
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1458
1315
|
)
|
|
1316
|
+
const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
|
|
1317
|
+
const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory")
|
|
1318
|
+
const ErrIrError = errors.new("bumbledb irError: the query failed validation")
|
|
1319
|
+
|
|
1320
|
+
function throwOpenRefusal(
|
|
1321
|
+
verb: string,
|
|
1322
|
+
canonical: string,
|
|
1323
|
+
kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
|
|
1324
|
+
message: string
|
|
1325
|
+
): never {
|
|
1326
|
+
const detail = `${verb} ${canonical}: ${message}`
|
|
1327
|
+
if (kind === "newtypeMismatch") {
|
|
1328
|
+
throw errors.wrap(ErrNewtypeMismatch, detail)
|
|
1329
|
+
}
|
|
1330
|
+
if (kind === "schemaError") {
|
|
1331
|
+
throw errors.wrap(ErrSchemaError, detail)
|
|
1332
|
+
}
|
|
1333
|
+
throw errors.wrap(ErrFingerprintMismatch, detail)
|
|
1334
|
+
}
|
|
1459
1335
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
function
|
|
1472
|
-
verb: "create" | "open",
|
|
1336
|
+
function throwPrepareRefusal(message: string): never {
|
|
1337
|
+
throw errors.wrap(ErrIrError, `prepare: ${message}`)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
|
|
1341
|
+
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
1342
|
+
return native.dbManifest(dbHandle)
|
|
1343
|
+
})
|
|
1344
|
+
return openDb(dbHandle, theory, manifest)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async function createStore<Rels extends SchemaRelations>(
|
|
1473
1348
|
storePath: string,
|
|
1474
1349
|
theory: Schema<Rels>
|
|
1475
|
-
): Db<Rels
|
|
1350
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
1476
1351
|
const canonical = path.resolve(storePath)
|
|
1477
1352
|
const spec = lower(theory)
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1353
|
+
const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
|
|
1354
|
+
return native.dbCreate(canonical, spec)
|
|
1355
|
+
})
|
|
1356
|
+
if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
|
|
1357
|
+
throwOpenRefusal("create", canonical, created.tag, created.message)
|
|
1358
|
+
}
|
|
1359
|
+
if (created.tag === "rejected") {
|
|
1360
|
+
return Object.freeze({
|
|
1361
|
+
tag: "rejected" as const,
|
|
1362
|
+
violations: Object.freeze(
|
|
1363
|
+
created.violations.map(function mapWire(wire) {
|
|
1364
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
1365
|
+
})
|
|
1366
|
+
)
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function mapViolationWithoutStore<Rels extends SchemaRelations>(
|
|
1373
|
+
theory: Schema<Rels>,
|
|
1374
|
+
wire: WireViolation
|
|
1375
|
+
): Violation<Rels> {
|
|
1376
|
+
const entries = materializedEntries(theory)
|
|
1377
|
+
const entry = entries[wire.statementId]
|
|
1378
|
+
if (entry === undefined) {
|
|
1379
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
1380
|
+
}
|
|
1381
|
+
const facts = Object.freeze(
|
|
1382
|
+
wire.facts.map(function offending(fact) {
|
|
1383
|
+
const member = theory.relations[fact.relation]
|
|
1384
|
+
if (member === undefined || !(fact.relation in theory.relations)) {
|
|
1385
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
1386
|
+
}
|
|
1387
|
+
return decodeOffendingFact<Rels>(member, fact.relation as keyof Rels & string, fact)
|
|
1388
|
+
})
|
|
1389
|
+
)
|
|
1390
|
+
return violationFromEntry(entry, wire, facts)
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
async function openStore<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
1394
|
+
const canonical = path.resolve(storePath)
|
|
1395
|
+
const spec = lower(theory)
|
|
1396
|
+
const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
|
|
1482
1397
|
return native.dbOpen(canonical, spec)
|
|
1483
1398
|
})
|
|
1484
1399
|
if (!opened.ok) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1400
|
+
throwOpenRefusal("open", canonical, opened.kind, opened.message)
|
|
1401
|
+
}
|
|
1402
|
+
return openFromHandle(opened.db, theory)
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
|
|
1406
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
1407
|
+
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
1408
|
+
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
1409
|
+
count<R extends MemberRelation<Rels>>(relation: R): bigint
|
|
1410
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1411
|
+
|
|
1412
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1413
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1414
|
+
relation: R,
|
|
1415
|
+
keyStatement: KeyStatement<R, P>,
|
|
1416
|
+
key: DeclaredKeyFact<R, P>
|
|
1417
|
+
): Fact<R> | undefined
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
|
|
1421
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
1422
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
1423
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
1424
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1425
|
+
|
|
1426
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1427
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1428
|
+
relation: R,
|
|
1429
|
+
keyStatement: KeyStatement<R, P>,
|
|
1430
|
+
key: DeclaredKeyFact<R, P>
|
|
1431
|
+
): Fact<R> | undefined
|
|
1432
|
+
admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
|
|
1436
|
+
const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
|
|
1437
|
+
|
|
1438
|
+
const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
|
|
1439
|
+
const closed = errors.trySync(function closeOwned() {
|
|
1440
|
+
native.ownedInstanceClose(handle)
|
|
1441
|
+
})
|
|
1442
|
+
if (closed.error) {
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
|
|
1448
|
+
const closed = errors.trySync(function closeBuilder() {
|
|
1449
|
+
native.instanceBuilderClose(handle)
|
|
1450
|
+
})
|
|
1451
|
+
if (closed.error) {
|
|
1452
|
+
return
|
|
1453
|
+
}
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
|
|
1457
|
+
const owner = Object.freeze({})
|
|
1458
|
+
const rec = { handle: nativeHandle, theory, spent: false, owner }
|
|
1459
|
+
const tables = tablesFromTheory(theory)
|
|
1460
|
+
function assertLive(): void {
|
|
1461
|
+
if (rec.spent) {
|
|
1462
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
|
|
1487
1463
|
}
|
|
1488
|
-
throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
|
|
1489
1464
|
}
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1465
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
1466
|
+
scan(relationId) {
|
|
1467
|
+
return native.ownedScan(nativeHandle, relationId)
|
|
1468
|
+
},
|
|
1469
|
+
count(relationId) {
|
|
1470
|
+
return native.ownedCount(nativeHandle, relationId)
|
|
1471
|
+
},
|
|
1472
|
+
contains(relationId, values) {
|
|
1473
|
+
return native.ownedContains(nativeHandle, relationId, values)
|
|
1474
|
+
},
|
|
1475
|
+
get(relationId, statementId, keyValues) {
|
|
1476
|
+
return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
|
|
1477
|
+
},
|
|
1478
|
+
prepare(query) {
|
|
1479
|
+
return native.ownedPrepare(nativeHandle, query)
|
|
1480
|
+
},
|
|
1481
|
+
execute(prepared, params) {
|
|
1482
|
+
return native.ownedExecute(prepared, nativeHandle, params)
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1485
|
+
const instance: OwnedInstance<Rels> = Object.freeze({
|
|
1486
|
+
...methods,
|
|
1487
|
+
[Symbol.dispose](): void {
|
|
1488
|
+
if (rec.spent) {
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
native.ownedInstanceClose(nativeHandle)
|
|
1493
|
+
} catch (caught) {
|
|
1494
|
+
const error = errorFromThrow(caught)
|
|
1495
|
+
if (/leased for publish/.test(error.message)) {
|
|
1496
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
|
|
1497
|
+
}
|
|
1498
|
+
throw errors.wrap(error, "close bumbledb owned instance")
|
|
1499
|
+
}
|
|
1500
|
+
rec.spent = true
|
|
1501
|
+
ownedReclaimer.unregister(instance)
|
|
1502
|
+
}
|
|
1503
|
+
})
|
|
1504
|
+
ownedRecords.set(instance, rec)
|
|
1505
|
+
ownedReclaimer.register(instance, nativeHandle, instance)
|
|
1506
|
+
return instance
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function wrapBuilder<Rels extends SchemaRelations>(
|
|
1510
|
+
nativeHandle: BuilderHandle,
|
|
1511
|
+
theory: Schema<Rels>
|
|
1512
|
+
): InstanceBuilder<Rels> {
|
|
1513
|
+
const rec = { handle: nativeHandle, theory, spent: false }
|
|
1514
|
+
const tables = tablesFromTheory(theory)
|
|
1515
|
+
function assertLive(): void {
|
|
1516
|
+
if (rec.spent) {
|
|
1517
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const overlay = overlayMethods(theory, tables, assertLive, {
|
|
1521
|
+
contains(relationId, row) {
|
|
1522
|
+
return bridged("bumbledb builder contains", function readContains() {
|
|
1523
|
+
return native.instanceBuilderContains(nativeHandle, relationId, row)
|
|
1524
|
+
})
|
|
1525
|
+
},
|
|
1526
|
+
get(relationId, statementId, key) {
|
|
1527
|
+
return bridged("bumbledb builder get", function readGet() {
|
|
1528
|
+
return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
|
|
1529
|
+
})
|
|
1530
|
+
}
|
|
1492
1531
|
})
|
|
1493
|
-
|
|
1532
|
+
const builder: InstanceBuilder<Rels> = Object.freeze({
|
|
1533
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
1534
|
+
assertLive()
|
|
1535
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1536
|
+
return mutateCollection(relation, facts, function applyCells(rows, cells) {
|
|
1537
|
+
return bridged("bumbledb builder load", function loadCells() {
|
|
1538
|
+
return native.instanceBuilderLoad(nativeHandle, entry.id, rows, cells)
|
|
1539
|
+
})
|
|
1540
|
+
})
|
|
1541
|
+
},
|
|
1542
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1543
|
+
assertLive()
|
|
1544
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1545
|
+
const flat = rowsOf(relation, facts)
|
|
1546
|
+
const report = bridged("bumbledb builder delete", function remove() {
|
|
1547
|
+
return native.instanceBuilderDelete(nativeHandle, entry.id, flat.rows, flat.cells)
|
|
1548
|
+
})
|
|
1549
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1550
|
+
},
|
|
1551
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange {
|
|
1552
|
+
assertLive()
|
|
1553
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1554
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
1555
|
+
return candidate.name === field
|
|
1556
|
+
})
|
|
1557
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
1558
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
1559
|
+
}
|
|
1560
|
+
const fieldId = entry.fieldIds.get(field)
|
|
1561
|
+
if (fieldId === undefined) {
|
|
1562
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
1563
|
+
}
|
|
1564
|
+
const range = bridged("bumbledb builder reserve", function mint() {
|
|
1565
|
+
return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
|
|
1566
|
+
})
|
|
1567
|
+
return freshRangeOf(range)
|
|
1568
|
+
},
|
|
1569
|
+
contains: overlay.contains,
|
|
1570
|
+
get: overlay.get,
|
|
1571
|
+
async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
|
|
1572
|
+
if (rec.spent) {
|
|
1573
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
1574
|
+
}
|
|
1575
|
+
rec.spent = true
|
|
1576
|
+
builderReclaimer.unregister(builder)
|
|
1577
|
+
let outcome: AdmitResult
|
|
1578
|
+
try {
|
|
1579
|
+
outcome = await native.instanceBuilderAdmit(nativeHandle)
|
|
1580
|
+
} catch (caught) {
|
|
1581
|
+
throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
|
|
1582
|
+
}
|
|
1583
|
+
if (outcome.tag === "rejected") {
|
|
1584
|
+
return Object.freeze({
|
|
1585
|
+
tag: "rejected" as const,
|
|
1586
|
+
violations: Object.freeze(
|
|
1587
|
+
outcome.violations.map(function mapWire(wire) {
|
|
1588
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
1589
|
+
})
|
|
1590
|
+
)
|
|
1591
|
+
})
|
|
1592
|
+
}
|
|
1593
|
+
return Object.freeze({
|
|
1594
|
+
tag: "accepted" as const,
|
|
1595
|
+
value: wrapOwned(outcome.value, theory)
|
|
1596
|
+
})
|
|
1597
|
+
},
|
|
1598
|
+
[Symbol.dispose](): void {
|
|
1599
|
+
if (rec.spent) {
|
|
1600
|
+
return
|
|
1601
|
+
}
|
|
1602
|
+
rec.spent = true
|
|
1603
|
+
builderReclaimer.unregister(builder)
|
|
1604
|
+
bridged("close bumbledb instance builder", function closeBuilder() {
|
|
1605
|
+
native.instanceBuilderClose(nativeHandle)
|
|
1606
|
+
})
|
|
1607
|
+
}
|
|
1608
|
+
})
|
|
1609
|
+
builderRecords.set(builder, rec)
|
|
1610
|
+
builderReclaimer.register(builder, nativeHandle, builder)
|
|
1611
|
+
return builder
|
|
1494
1612
|
}
|
|
1495
1613
|
|
|
1614
|
+
const InstanceBuilder = Object.freeze({
|
|
1615
|
+
create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
|
|
1616
|
+
const spec = lower(theory)
|
|
1617
|
+
const handle = bridged("create bumbledb instance builder", function make() {
|
|
1618
|
+
return native.instanceBuilderNew(spec)
|
|
1619
|
+
})
|
|
1620
|
+
return wrapBuilder(handle, theory)
|
|
1621
|
+
}
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1496
1624
|
/**
|
|
1497
1625
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
1498
1626
|
* Create refuses an already-initialized directory; open verifies format
|
|
1499
|
-
* version
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1627
|
+
* version and the schema fingerprint. A second live handle on the same
|
|
1628
|
+
* path is the engine's `EnvironmentLocked`. There is no close anywhere:
|
|
1629
|
+
* the process owns the environment until GC/exit (durability is the
|
|
1630
|
+
* engine's per-commit fsync). Resume = reopen in a fresh process, or
|
|
1631
|
+
* hold the `Db` this process opened.
|
|
1504
1632
|
*/
|
|
1505
1633
|
const Db = Object.freeze({
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1634
|
+
async create<Rels extends SchemaRelations>(
|
|
1635
|
+
storePath: string,
|
|
1636
|
+
theory: Schema<Rels>
|
|
1637
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
1638
|
+
return createStore(storePath, theory)
|
|
1509
1639
|
},
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
* schema descriptor when it is absent (self-describing stores, engine
|
|
1514
|
-
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
1515
|
-
* exhumable after one ordinary open — adoption is automatic, never a
|
|
1516
|
-
* separate verb. A second open of a still-live path is
|
|
1517
|
-
* `EnvironmentLocked`.
|
|
1518
|
-
*/
|
|
1519
|
-
async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
1520
|
-
return admit("open", path, theory)
|
|
1640
|
+
|
|
1641
|
+
async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
1642
|
+
return openStore(storePath, theory)
|
|
1521
1643
|
},
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1644
|
+
async fromInstance<Rels extends SchemaRelations>(
|
|
1645
|
+
storePath: string,
|
|
1646
|
+
instance: OwnedInstance<Rels>
|
|
1647
|
+
): Promise<Db<Rels>> {
|
|
1648
|
+
const rec = ownedRecords.get(instance)
|
|
1649
|
+
if (rec === undefined) {
|
|
1650
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
|
|
1651
|
+
}
|
|
1652
|
+
if (rec.spent) {
|
|
1653
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
|
|
1654
|
+
}
|
|
1655
|
+
const canonical = path.resolve(storePath)
|
|
1656
|
+
const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
|
|
1657
|
+
return native.dbFromInstance(canonical, rec.handle)
|
|
1658
|
+
})
|
|
1659
|
+
return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
|
|
1536
1660
|
}
|
|
1537
1661
|
})
|
|
1538
1662
|
|
|
1539
1663
|
export type {
|
|
1540
1664
|
Abandon,
|
|
1541
1665
|
AbandonedArm,
|
|
1666
|
+
Admission,
|
|
1542
1667
|
CapacityViolation,
|
|
1668
|
+
Committed,
|
|
1543
1669
|
ContainmentViolation,
|
|
1544
1670
|
DeclaredKeyFact,
|
|
1545
1671
|
DeclaredKeyViolation,
|
|
@@ -1550,10 +1676,27 @@ export type {
|
|
|
1550
1676
|
MirrorViolation,
|
|
1551
1677
|
MutationReport,
|
|
1552
1678
|
OffendingFact,
|
|
1679
|
+
OwnedInstance,
|
|
1553
1680
|
Prepared,
|
|
1554
|
-
|
|
1555
|
-
|
|
1681
|
+
ReadInstance,
|
|
1682
|
+
SyncResult,
|
|
1556
1683
|
Violation,
|
|
1557
|
-
|
|
1684
|
+
Witness,
|
|
1685
|
+
WriteFromOutcome,
|
|
1686
|
+
WriteOutcome,
|
|
1687
|
+
WriteTx
|
|
1688
|
+
}
|
|
1689
|
+
export {
|
|
1690
|
+
abandon,
|
|
1691
|
+
Db,
|
|
1692
|
+
ErrAsyncCallback,
|
|
1693
|
+
ErrFingerprintMismatch,
|
|
1694
|
+
ErrForeignPrepared,
|
|
1695
|
+
ErrForeignWitness,
|
|
1696
|
+
ErrIrError,
|
|
1697
|
+
ErrNewtypeMismatch,
|
|
1698
|
+
ErrSchemaError,
|
|
1699
|
+
ErrSpentHandle,
|
|
1700
|
+
ErrUseAfterScope,
|
|
1701
|
+
InstanceBuilder
|
|
1558
1702
|
}
|
|
1559
|
-
export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }
|