@bjornpagen/bumbledb 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COOKBOOK.md +45 -6
- package/README.md +15 -2
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +170 -114
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +72 -7
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -9
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +8 -0
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +80 -13
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +5 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +7 -12
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +15 -5
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +289 -173
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +1 -1
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +119 -15
- package/src/query/lower.ts +117 -15
- package/src/query/run.ts +7 -12
- package/src/query/scope.ts +15 -6
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- package/src/statements.ts +30 -6
package/src/exhume.ts
CHANGED
|
@@ -17,11 +17,20 @@
|
|
|
17
17
|
* reconstructs a `Schema` value from the descriptor — that inverse mapping
|
|
18
18
|
* is deliberately out of scope; the rebirth tool keys by name.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): the
|
|
21
|
+
* `Exhumed` value implements `Symbol.dispose` — teardown is synchronous
|
|
22
|
+
* (one environment close) — and `using` is the documented idiom:
|
|
23
|
+
*
|
|
24
|
+
* using exhumed = await Db.exhume(path)
|
|
25
|
+
*
|
|
26
|
+
* Disposal releases the engine handle AND the store's exclusive advisory
|
|
27
|
+
* lock deterministically, scope-shaped in the language's own syntax, so a
|
|
28
|
+
* same-path reopen (retry after a half-failed migration, a second forensic
|
|
29
|
+
* read) never waits on an unforceable GC finalizer. Every verb after
|
|
30
|
+
* disposal is a typed used-after-dispose refusal. The engine-side drop
|
|
31
|
+
* remains the reclamation-only backstop for a collected-but-undisposed
|
|
32
|
+
* value; the store is never written through this surface, so there is
|
|
33
|
+
* nothing to flush.
|
|
25
34
|
*/
|
|
26
35
|
|
|
27
36
|
import * as errors from "@superbuilders/errors"
|
|
@@ -116,9 +125,11 @@ interface ExhumedDescriptor {
|
|
|
116
125
|
/**
|
|
117
126
|
* One exhumed store: the self-described relation shapes and the raw facts
|
|
118
127
|
* by relation name. Read-only by construction — no write verb, no prepare
|
|
119
|
-
* verb
|
|
128
|
+
* verb. A DISPOSABLE lifetime (R12): `Symbol.dispose` releases the engine
|
|
129
|
+
* handle and the store's exclusive lock deterministically; `using` is the
|
|
130
|
+
* idiom, and a disposed value's verbs are typed refusals.
|
|
120
131
|
*/
|
|
121
|
-
interface Exhumed {
|
|
132
|
+
interface Exhumed extends Disposable {
|
|
122
133
|
/** The store's own persisted schema, as its creator declared it. */
|
|
123
134
|
readonly descriptor: ExhumedDescriptor
|
|
124
135
|
/**
|
|
@@ -167,9 +178,10 @@ function descriptorOf(manifest: Manifest): ExhumedDescriptor {
|
|
|
167
178
|
* The bridge's three domain refusals become the typed error constants
|
|
168
179
|
* ({@link ErrExhumeNoDescriptor}, {@link ErrExhumeFormatMismatch},
|
|
169
180
|
* {@link ErrExhumeCorruption}), each carrying the engine's message in its
|
|
170
|
-
* wrap. The returned value is NOT cached: exhume is a forensic read
|
|
171
|
-
*
|
|
172
|
-
*
|
|
181
|
+
* wrap. The returned value is NOT cached: exhume is a forensic read whose
|
|
182
|
+
* lifetime is the caller's `using` scope — disposal releases the store's
|
|
183
|
+
* exclusive lock deterministically (R12), so the path is reusable the
|
|
184
|
+
* moment the scope exits.
|
|
173
185
|
*/
|
|
174
186
|
function exhumeStore(canonical: string): Exhumed {
|
|
175
187
|
const outcome = bridged(`exhume bumbledb store at ${canonical}`, function callExhume() {
|
|
@@ -199,7 +211,27 @@ function exhumeStore(canonical: string): Exhumed {
|
|
|
199
211
|
})
|
|
200
212
|
)
|
|
201
213
|
}
|
|
214
|
+
/** The lifetime record: flipped once by {@link dispose}, judged by every verb. */
|
|
215
|
+
const lifetime = { live: true }
|
|
216
|
+
/**
|
|
217
|
+
* The `Symbol.dispose` teardown (R12): idempotent — a manual call inside
|
|
218
|
+
* a `using` scope must not double-close — and deterministic: the engine
|
|
219
|
+
* handle and the store's exclusive lock release HERE, never on a GC
|
|
220
|
+
* schedule.
|
|
221
|
+
*/
|
|
222
|
+
function dispose(): void {
|
|
223
|
+
if (!lifetime.live) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
lifetime.live = false
|
|
227
|
+
bridged(`close bumbledb exhumed store at ${canonical}`, function close() {
|
|
228
|
+
native.exhumeClose(handle)
|
|
229
|
+
})
|
|
230
|
+
}
|
|
202
231
|
function scan(relation: string): readonly ExhumedFact[] {
|
|
232
|
+
if (!lifetime.live) {
|
|
233
|
+
throw errors.new("bumbledb exhumed store is disposed — its using scope already exited")
|
|
234
|
+
}
|
|
203
235
|
const names = fieldNames.get(relation)
|
|
204
236
|
if (names === undefined) {
|
|
205
237
|
throw errors.new(`bumbledb exhume: the store's descriptor declares no relation ${relation}`)
|
|
@@ -209,6 +241,11 @@ function exhumeStore(canonical: string): Exhumed {
|
|
|
209
241
|
})
|
|
210
242
|
return Object.freeze(
|
|
211
243
|
rows.map(function factOf(row): ExhumedFact {
|
|
244
|
+
if (row.length !== names.length) {
|
|
245
|
+
throw errors.new(
|
|
246
|
+
`bumbledb exhume drift: relation ${relation} row arity ${row.length} does not match the ${names.length} descriptor fields`
|
|
247
|
+
)
|
|
248
|
+
}
|
|
212
249
|
const fact: Record<string, FactValue> = {}
|
|
213
250
|
names.forEach(function pair(name, index) {
|
|
214
251
|
const cell = row[index]
|
|
@@ -223,7 +260,7 @@ function exhumeStore(canonical: string): Exhumed {
|
|
|
223
260
|
})
|
|
224
261
|
)
|
|
225
262
|
}
|
|
226
|
-
return Object.freeze({ descriptor, scan })
|
|
263
|
+
return Object.freeze({ descriptor, scan, [Symbol.dispose]: dispose })
|
|
227
264
|
}
|
|
228
265
|
|
|
229
266
|
export type { Exhumed, ExhumedAxiom, ExhumedDescriptor, ExhumedFact, ExhumedField, ExhumedRelation }
|
package/src/face.ts
CHANGED
|
@@ -121,18 +121,25 @@ type FaceFields<S extends FaceSource> = S extends AnySelected
|
|
|
121
121
|
* mirrors, recipe 29's mixed-width zones) and the width slot reads
|
|
122
122
|
* `undefined` for every interval. Elements stay bound: u64-vs-i64 interval
|
|
123
123
|
* pairs still mismatch. The ROSTER slot is SDK-only structure (the engine's
|
|
124
|
-
* wire carries plain u64s): a closed reference contributes its
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
124
|
+
* wire carries plain u64s): a closed reference contributes its vocabulary
|
|
125
|
+
* NAME literal paired with its handle union — the faithful encoding of the
|
|
126
|
+
* runtime's roster VALUE-IDENTITY judgment, so two same-shaped vocabularies
|
|
127
|
+
* mismatch at compile time exactly as they throw at construction — every
|
|
128
|
+
* other kind `undefined`, so a plain u64 face cannot pair with a closed
|
|
129
|
+
* `[id]` face — the vocabulary's own descriptor (`Kind.id`) is the ONE
|
|
130
|
+
* spelling of a closed reference at this surface, and a bare column cannot
|
|
131
|
+
* alias a vocabulary through a declared law. The runtime twin is the
|
|
129
132
|
* statement constructors' roster-identity walk (`statements.ts`).
|
|
130
133
|
*/
|
|
131
134
|
type ShapeOf<F extends AnyField> = readonly [
|
|
132
135
|
F["kind"],
|
|
133
136
|
F extends { readonly element: unknown } ? undefined : F extends { readonly width: infer W } ? W : undefined,
|
|
134
137
|
F extends { readonly element: infer E } ? E : undefined,
|
|
135
|
-
F extends {
|
|
138
|
+
F extends {
|
|
139
|
+
readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
|
|
140
|
+
}
|
|
141
|
+
? readonly [N, H]
|
|
142
|
+
: undefined
|
|
136
143
|
]
|
|
137
144
|
|
|
138
145
|
/** One field's structural shape within a declared field block (`undefined` when the name is foreign). */
|
package/src/fields.ts
CHANGED
|
@@ -50,14 +50,19 @@ function span(start: bigint, end: bigint): IntervalValue {
|
|
|
50
50
|
* A closed relation's roster as seen from a referencing field: the handle
|
|
51
51
|
* namespace `where()` selections and ground axioms resolve bare handle ids
|
|
52
52
|
* through (the macro's own rule: a handle is legal exactly on a field that
|
|
53
|
-
* references a closed relation). The
|
|
54
|
-
* the literal
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* references a closed relation). The roster is PRECISE in BOTH slots —
|
|
54
|
+
* `Name` carries the vocabulary's literal name and `H` the literal handle
|
|
55
|
+
* names in declaration order (the unbound `string` defaults exist only as
|
|
56
|
+
* the fallback where no roster is in scope). The name literal is
|
|
57
|
+
* load-bearing: the runtime twins judge roster VALUE IDENTITY, so two
|
|
58
|
+
* same-shaped vocabularies (`Yes/No` twice) are distinct — carrying the
|
|
59
|
+
* name makes the type the faithful encoding of that judgment, and a
|
|
60
|
+
* cross-vocabulary pairing fails at compile time, not construction. The
|
|
61
|
+
* runtime twin is the same frozen declaration-order value that was always
|
|
57
62
|
* there.
|
|
58
63
|
*/
|
|
59
|
-
interface ClosedRoster<H extends string = string> {
|
|
60
|
-
readonly name:
|
|
64
|
+
interface ClosedRoster<Name extends string = string, H extends string = string> {
|
|
65
|
+
readonly name: Name
|
|
61
66
|
readonly handles: readonly H[]
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -136,9 +141,9 @@ interface IntervalField<
|
|
|
136
141
|
* map and JoinOk, which compare kind/class/width/element. Terminal: no
|
|
137
142
|
* `.fresh` — a vocabulary's rows are ground axioms, never minted.
|
|
138
143
|
*/
|
|
139
|
-
interface ClosedIdField<H extends string = string> {
|
|
144
|
+
interface ClosedIdField<Name extends string = string, H extends string = string> {
|
|
140
145
|
readonly kind: "u64"
|
|
141
|
-
readonly closed: ClosedRoster<H>
|
|
146
|
+
readonly closed: ClosedRoster<Name, H>
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
/** Any field descriptor, whatever its kind or marks. */
|
|
@@ -238,14 +243,18 @@ function intervalLiteral(element: "u64" | "i64", value: unknown): LiteralSpec {
|
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
/**
|
|
241
|
-
* Rejects a declaration name that JavaScript would re-order
|
|
242
|
-
*
|
|
243
|
-
*
|
|
246
|
+
* Rejects a declaration name that JavaScript would re-order, and a name
|
|
247
|
+
* that would break the class map's coordinate encoding. Declaration order =
|
|
248
|
+
* ordinal ids is the law relations, columns, and schemas all lean on, and
|
|
249
|
+
* it is carried by object-literal key order — which ECMA-262's
|
|
244
250
|
* OrdinaryOwnPropertyKeys breaks for integer-index keys (they enumerate
|
|
245
|
-
* first, ascending, regardless of where they were written).
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
251
|
+
* first, ascending, regardless of where they were written). A `.` in a name
|
|
252
|
+
* would make the law engine's `${relation}.${field}` coordinate template
|
|
253
|
+
* non-injective at BOTH tiers (relation `"A.B"` field `"x"` and relation
|
|
254
|
+
* `"A"` field `"B.x"` are one coordinate), silently merging unrelated law
|
|
255
|
+
* classes — banned here, which is exact macro parity: Rust identifiers
|
|
256
|
+
* cannot contain dots. Both are construction errors, exactly as an
|
|
257
|
+
* unparseable name is a macro expansion error.
|
|
249
258
|
*/
|
|
250
259
|
function assertDeclarationOrderKey(where: string, name: string): void {
|
|
251
260
|
if (/^(?:0|[1-9][0-9]*)$/.test(name)) {
|
|
@@ -253,6 +262,32 @@ function assertDeclarationOrderKey(where: string, name: string): void {
|
|
|
253
262
|
`${where}: name ${name} is an integer index — JavaScript object keys re-order integer indices, breaking the declaration-order law; use a non-numeric name`
|
|
254
263
|
)
|
|
255
264
|
}
|
|
265
|
+
if (name.includes(".")) {
|
|
266
|
+
throw errors.new(
|
|
267
|
+
`${where}: name ${name} contains a dot — the law classes key on the \`relation.field\` coordinate, so a dotted name would alias unrelated slots (macro parity: Rust identifiers cannot contain dots); use a dot-free name`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Rejects a declaration record whose prototype was replaced. A plain
|
|
274
|
+
* `__proto__: {...}` property in an object literal is ECMA-262 Annex B's
|
|
275
|
+
* prototype SETTER, not a data property — the entry never becomes an own
|
|
276
|
+
* enumerable key, so the declared handle/field/relation would silently
|
|
277
|
+
* vanish from every `Object.keys`/`Object.entries` walk while the type
|
|
278
|
+
* tier still admits its name. A non-default prototype on a declaration
|
|
279
|
+
* literal proves exactly that spelling, so it is a construction error; the
|
|
280
|
+
* computed spelling `["__proto__"]: {...}` creates an own data property
|
|
281
|
+
* and is admitted (no name is reserved). `Object.create(null)` records
|
|
282
|
+
* stay admissible.
|
|
283
|
+
*/
|
|
284
|
+
function assertDeclarationRecord(where: string, record: object): void {
|
|
285
|
+
const proto = Object.getPrototypeOf(record)
|
|
286
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
287
|
+
throw errors.new(
|
|
288
|
+
`${where}: the declaration record's prototype was replaced — a plain \`__proto__: {...}\` entry is the prototype setter, so its key silently vanishes from the declaration; spell it computed (["__proto__"]: {...}) to declare it as data`
|
|
289
|
+
)
|
|
290
|
+
}
|
|
256
291
|
}
|
|
257
292
|
|
|
258
293
|
/** The one fresh-marked u64 descriptor (the `.fresh` property of the unmarked one). */
|
|
@@ -390,6 +425,7 @@ export type {
|
|
|
390
425
|
}
|
|
391
426
|
export {
|
|
392
427
|
assertDeclarationOrderKey,
|
|
428
|
+
assertDeclarationRecord,
|
|
393
429
|
bool,
|
|
394
430
|
bytes,
|
|
395
431
|
i64,
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type { Count } from "#count.ts"
|
|
|
44
44
|
export { atLeast, atMost, between, exactly, none } from "#count.ts"
|
|
45
45
|
export type {
|
|
46
46
|
Abandon,
|
|
47
|
+
AbandonedArm,
|
|
47
48
|
DeclaredKeyFact,
|
|
48
49
|
DeltaBuild,
|
|
49
50
|
MemberRelation,
|
|
@@ -52,7 +53,6 @@ export type {
|
|
|
52
53
|
ReadScope,
|
|
53
54
|
Tx,
|
|
54
55
|
Violation,
|
|
55
|
-
WitnessedWriteResult,
|
|
56
56
|
WriteResult
|
|
57
57
|
} from "#db.ts"
|
|
58
58
|
export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP } from "#db.ts"
|
package/src/lower.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* fixed key order, so serialization is deterministic (byte-stable).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import * as errors from "@superbuilders/errors"
|
|
11
12
|
import type { AnyClosed } from "#closed.ts"
|
|
12
13
|
import { isClosedMember } from "#closed.ts"
|
|
13
14
|
import type { FaceData } from "#face.ts"
|
|
@@ -111,34 +112,38 @@ function lowerStatement(statement: Statement): StatementSpec {
|
|
|
111
112
|
* Lowers one ordinary relation to its `RelationSpec` fragment: fields in
|
|
112
113
|
* declaration order, each carrying its law-computed class name as the
|
|
113
114
|
* `newtype` (`classes` — the schema's class record for this relation;
|
|
114
|
-
* bare fields carry `undefined`), `
|
|
115
|
-
*
|
|
115
|
+
* bare fields carry `undefined`), `closed: undefined` (the option is the
|
|
116
|
+
* kind — one sum, R7).
|
|
116
117
|
*/
|
|
117
118
|
function lowerRelation(relation: AnyRelation, classes: RelationClasses): RelationSpec {
|
|
118
119
|
const fields: FieldSpec[] = relation.data.fields.map(function lowerDeclared(declared) {
|
|
119
120
|
return lowerField(declared.name, declared.field, classes[declared.name])
|
|
120
121
|
})
|
|
121
|
-
return { name: relation.name,
|
|
122
|
+
return { name: relation.name, fields, closed: undefined }
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* Lowers one closed relation to its `RelationSpec` fragment: declared
|
|
126
|
-
* intrinsic columns only (the engine materializes the synthetic `id`)
|
|
127
|
-
* the
|
|
128
|
-
* class
|
|
129
|
-
* every referencing field shares by law
|
|
130
|
-
* literal back to its roster)
|
|
131
|
-
* order (row id = index); the
|
|
132
|
-
* construction.
|
|
127
|
+
* intrinsic columns only (the engine materializes the synthetic `id`) and
|
|
128
|
+
* the fused closedness sum (R7) — the handle newtype (the COMPUTED class
|
|
129
|
+
* name of the id's generator class, `"Kind.id"`, always present: a closed
|
|
130
|
+
* id is a generator; every referencing field shares it by law, which is
|
|
131
|
+
* how the engine resolves a handle literal back to its roster) together
|
|
132
|
+
* with the ground axioms in declaration order (row id = index); the
|
|
133
|
+
* literals were already lowered at `closed()` construction.
|
|
133
134
|
*/
|
|
134
135
|
function lowerClosed(member: AnyClosed, classes: RelationClasses): RelationSpec {
|
|
135
136
|
const fields: FieldSpec[] = member.data.columns.map(function lowerColumn(column) {
|
|
136
137
|
return lowerField(column.name, column.field, classes[column.name])
|
|
137
138
|
})
|
|
138
|
-
const
|
|
139
|
+
const rows = member.data.rows.map(function lowerRow(row) {
|
|
139
140
|
return { handle: row.handle, values: row.values }
|
|
140
141
|
})
|
|
141
|
-
|
|
142
|
+
const newtype = classes.id
|
|
143
|
+
if (newtype === undefined) {
|
|
144
|
+
throw errors.new(`closed relation ${member.name}: the id's generator class is missing from the class map`)
|
|
145
|
+
}
|
|
146
|
+
return { name: member.name, fields, closed: { newtype, rows } }
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
/** The frozen empty class record a relation outside the schema's map lowers under (nothing classed). */
|
package/src/marshal.ts
CHANGED
|
@@ -70,10 +70,31 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
|
|
|
70
70
|
/**
|
|
71
71
|
* Reprojects any host object to a string-indexed record — the boundary
|
|
72
72
|
* through which generic fact objects (whose type parameters carry no index
|
|
73
|
-
* signature) enter the name-directed marshaling below, without a cast.
|
|
73
|
+
* signature) enter the name-directed marshaling below, without a cast. An
|
|
74
|
+
* ALLOCATION-FREE IDENTITY (the admission predicate is the type
|
|
75
|
+
* reprojection; the value passes through untouched): every consumer
|
|
76
|
+
* downstream — `rowOf`, `keyRowOf`, the query param marshal — only READS
|
|
77
|
+
* properties, so no copy is warranted. The one mutating consumer
|
|
78
|
+
* (`mintFreshCells` on the insert path) takes its own spread copy at the
|
|
79
|
+
* call site, so the caller's fact object is never written through this
|
|
80
|
+
* seam.
|
|
74
81
|
*/
|
|
75
|
-
function recordOf(fact: object): Record<string, unknown
|
|
76
|
-
|
|
82
|
+
function recordOf(fact: object): Readonly<Record<string, unknown>> {
|
|
83
|
+
if (!isStringIndexed(fact)) {
|
|
84
|
+
throw errors.new("fact object is not string-indexable")
|
|
85
|
+
}
|
|
86
|
+
return fact
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The trusted admission seam of {@link recordOf}: every JS object IS
|
|
91
|
+
* string-indexable (property reads on absent names yield `undefined`,
|
|
92
|
+
* which every consumer already guards), so the predicate verifies the one
|
|
93
|
+
* checkable fact — objecthood — and admits the value at the indexed type
|
|
94
|
+
* without a copy or a cast.
|
|
95
|
+
*/
|
|
96
|
+
function isStringIndexed(value: object): value is Readonly<Record<string, unknown>> {
|
|
97
|
+
return typeof value === "object" || typeof value === "function"
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
/**
|
|
@@ -240,18 +261,22 @@ function isCompleteFact<R extends AnyRelation>(
|
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
/**
|
|
243
|
-
* The insert-return trusted seam:
|
|
244
|
-
*
|
|
245
|
-
*
|
|
264
|
+
* The insert-return trusted seam (R11): one insert's return carries the
|
|
265
|
+
* engine's changed-state report beside the collected fresh cells (minted by
|
|
266
|
+
* the engine or resupplied by the caller) — the bit is verified boolean and
|
|
267
|
+
* the fresh ids present, same presence-only direction as
|
|
246
268
|
* {@link isCompleteFact}.
|
|
247
269
|
*/
|
|
248
|
-
function
|
|
270
|
+
function isInserted<R extends AnyRelation>(
|
|
249
271
|
relation: R,
|
|
250
|
-
|
|
251
|
-
):
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
value: Readonly<Record<string, FactValue | boolean>>
|
|
273
|
+
): value is Readonly<Record<string, FactValue | boolean>> & { readonly changed: boolean } & Minted<R> {
|
|
274
|
+
return (
|
|
275
|
+
typeof value.changed === "boolean" &&
|
|
276
|
+
relation.data.fields.every(function presentWhenFresh(declared) {
|
|
277
|
+
return !isFreshField(declared.field) || value[declared.name] !== undefined
|
|
278
|
+
})
|
|
279
|
+
)
|
|
255
280
|
}
|
|
256
281
|
|
|
257
282
|
/**
|
|
@@ -286,4 +311,4 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
|
|
|
286
311
|
}
|
|
287
312
|
|
|
288
313
|
export type { KeyFact, Minted }
|
|
289
|
-
export { cellOf, factOf, handleOf, isFreshField,
|
|
314
|
+
export { cellOf, factOf, handleOf, isFreshField, isInserted, keyRowOf, recordOf, rowOf }
|
package/src/native.ts
CHANGED
|
@@ -27,9 +27,11 @@ type SnapshotHandle = { readonly __brand: "bumbledb.snapshot" }
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* One exhumed store — the read-only, theory-less open (engine 70-api.md
|
|
30
|
-
* § exhume).
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* § exhume). Lifetimes are disposables (ruled 2026-07-23, R12):
|
|
31
|
+
* `exhumeClose` is the deterministic teardown the SDK's `Symbol.dispose`
|
|
32
|
+
* rides — releasing the environment (and the store's exclusive lock)
|
|
33
|
+
* scope-shaped, never a GC race; the engine-side drop remains the
|
|
34
|
+
* reclamation-only backstop for a collected-but-undisposed handle.
|
|
33
35
|
*/
|
|
34
36
|
type ExhumeHandle = { readonly __brand: "bumbledb.exhume" }
|
|
35
37
|
|
|
@@ -320,6 +322,33 @@ interface Staleness {
|
|
|
320
322
|
readonly maxRatio: number
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
/**
|
|
326
|
+
* `dbSnapshot`'s reply: the live handle WITH its witnessed generation —
|
|
327
|
+
* one crossing carries both (read inside the snapshot's own transaction,
|
|
328
|
+
* the race-closing rule of 50-storage.md), so no second `dbGeneration`
|
|
329
|
+
* call exists to pay or defend (finding 016's bridge shape).
|
|
330
|
+
*/
|
|
331
|
+
type SnapshotOpened = { readonly ok: true; readonly snapshot: SnapshotHandle; readonly generation: bigint }
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The plan-as-data report (ruled 2026-07-23, R13): the engine's
|
|
335
|
+
* `ExecutionStats` rendered to plain objects — camelCase keys, u64
|
|
336
|
+
* counters as `bigint`. A diagnostic surface, EXPLICITLY UNFROZEN: the
|
|
337
|
+
* shape follows the plan representation wherever it goes and no
|
|
338
|
+
* compatibility claim attaches, so this typing names the stable spine
|
|
339
|
+
* (version, emits, the plan sections) and leaves each section's leaves
|
|
340
|
+
* open for the host to introspect.
|
|
341
|
+
*/
|
|
342
|
+
interface Explain {
|
|
343
|
+
readonly introspectionVersion: number
|
|
344
|
+
readonly emits: bigint
|
|
345
|
+
readonly disjointRules?: Readonly<Record<string, unknown>>
|
|
346
|
+
readonly subsumed: ReadonlyArray<Readonly<Record<string, unknown>>>
|
|
347
|
+
readonly dead: ReadonlyArray<Readonly<Record<string, unknown>>>
|
|
348
|
+
readonly rules: ReadonlyArray<Readonly<Record<string, unknown>>>
|
|
349
|
+
readonly strata: ReadonlyArray<Readonly<Record<string, unknown>>>
|
|
350
|
+
}
|
|
351
|
+
|
|
323
352
|
interface Native {
|
|
324
353
|
/**
|
|
325
354
|
* Proof-of-life export (PRD-03): a non-empty string naming the bridge
|
|
@@ -367,11 +396,17 @@ interface Native {
|
|
|
367
396
|
* Opens a store FROM ITS OWN PERSISTED DESCRIPTOR (the read-only,
|
|
368
397
|
* theory-less open; engine 70-api.md § exhume) — no schema crosses in.
|
|
369
398
|
* The three adoption-era refusals return as data ({@link ExhumeResult});
|
|
370
|
-
* genuine failures throw. The handle
|
|
371
|
-
*
|
|
372
|
-
* store's exclusive lock — reclamation only, never correctness.
|
|
399
|
+
* genuine failures throw. The handle's deterministic teardown is
|
|
400
|
+
* `exhumeClose` (R12); GC reclamation remains the backstop only.
|
|
373
401
|
*/
|
|
374
402
|
dbExhume(path: string): ExhumeResult
|
|
403
|
+
/**
|
|
404
|
+
* Closes the exhume handle, releasing its environment (and the store's
|
|
405
|
+
* exclusive lock) deterministically — the native teardown under the
|
|
406
|
+
* SDK's `Symbol.dispose` (ruled 2026-07-23, R12: lifetimes are
|
|
407
|
+
* disposables, never `close()` methods to remember).
|
|
408
|
+
*/
|
|
409
|
+
exhumeClose(exhume: ExhumeHandle): void
|
|
375
410
|
/**
|
|
376
411
|
* The exhumed store's persisted schema as manifest-shaped data — the
|
|
377
412
|
* engine's own manifest rendering of the STORED descriptor: relations
|
|
@@ -388,8 +423,13 @@ interface Native {
|
|
|
388
423
|
*/
|
|
389
424
|
exhumeScan(exhume: ExhumeHandle, relationName: string): FactValue[][]
|
|
390
425
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Opens one MVCC read snapshot as a live handle, returned WITH its
|
|
428
|
+
* witnessed generation — one crossing carries both (finding 016), so
|
|
429
|
+
* no separate `dbGeneration` call (with its own transient read
|
|
430
|
+
* transaction and fault-pairing close branch) exists on this path.
|
|
431
|
+
*/
|
|
432
|
+
dbSnapshot(db: DbHandle): SnapshotOpened
|
|
393
433
|
/** Closes the snapshot, releasing its LMDB reader slot. */
|
|
394
434
|
snapshotClose(snap: SnapshotHandle): void
|
|
395
435
|
/** Full-relation export in row-id order (one row per fact). */
|
|
@@ -461,6 +501,15 @@ interface Native {
|
|
|
461
501
|
* — the host sorts.
|
|
462
502
|
*/
|
|
463
503
|
preparedExecute(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): FactValue[][]
|
|
504
|
+
/**
|
|
505
|
+
* Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
|
|
506
|
+
* query against the snapshot with counting instrumentation (the
|
|
507
|
+
* engine's `Snapshot::profile`, ANALYZE semantics) and returns the
|
|
508
|
+
* structured stats — plan sections and counters as plain values.
|
|
509
|
+
* Scalar params only (the engine's profile entry has no param-set
|
|
510
|
+
* spelling).
|
|
511
|
+
*/
|
|
512
|
+
preparedExplain(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): Explain
|
|
464
513
|
/** The pull-based plan-drift signal against a snapshot. */
|
|
465
514
|
preparedStaleness(prepared: PreparedHandle, snap: SnapshotHandle): Staleness
|
|
466
515
|
/** Releases the prepared query. */
|
|
@@ -566,6 +615,7 @@ export type {
|
|
|
566
615
|
DbOpenResult,
|
|
567
616
|
ExhumeHandle,
|
|
568
617
|
ExhumeResult,
|
|
618
|
+
Explain,
|
|
569
619
|
FactValue,
|
|
570
620
|
FindTermIr,
|
|
571
621
|
HeadOpIr,
|
|
@@ -586,6 +636,7 @@ export type {
|
|
|
586
636
|
QueryParam,
|
|
587
637
|
RuleIr,
|
|
588
638
|
SnapshotHandle,
|
|
639
|
+
SnapshotOpened,
|
|
589
640
|
Staleness,
|
|
590
641
|
StatementKindTag,
|
|
591
642
|
TaggedValue,
|