@bjornpagen/bumbledb 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COOKBOOK.md +150 -9
- package/README.md +18 -5
- package/dist/capacity.d.ts +310 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +138 -0
- package/dist/capacity.js.map +1 -0
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +115 -76
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +178 -122
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +11 -7
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +3 -2
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +1 -1
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +20 -14
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +66 -12
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +72 -7
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -9
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +8 -0
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +80 -13
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +5 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +7 -12
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +15 -5
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +95 -32
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +39 -7
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +62 -25
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +105 -22
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/capacity.ts +454 -0
- package/src/closed.ts +13 -7
- package/src/db.ts +302 -184
- package/src/exhume.ts +48 -11
- package/src/face.ts +14 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +20 -9
- package/src/law.ts +3 -2
- package/src/lower.ts +20 -14
- package/src/marshal.ts +38 -13
- package/src/native.ts +65 -11
- package/src/query/atom.ts +119 -15
- package/src/query/lower.ts +117 -15
- package/src/query/run.ts +7 -12
- package/src/query/scope.ts +15 -6
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +112 -34
- package/src/statements.ts +188 -38
- package/dist/count.d.ts +0 -102
- package/dist/count.d.ts.map +0 -1
- package/dist/count.js +0 -115
- package/dist/count.js.map +0 -1
- package/src/count.ts +0 -211
package/dist/db.js
CHANGED
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
* keyed to statements, read through scoped snapshots, and run the witnessed
|
|
6
6
|
* read-compute-write loop — all typed by the schema's relations record.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
|
|
9
|
+
* value this module returns carries a close spelling — release is
|
|
10
|
+
* deterministic and scope-shaped in the language's own syntax. `Db` values
|
|
11
|
+
* are CACHED per canonical path for the life of the process (a best-effort
|
|
12
|
+
* exit hook closes the cached environments; correctness never depends on it
|
|
13
|
+
* — the engine fsyncs every commit, so a process that dies without the hook
|
|
14
|
+
* loses nothing that was committed). Snapshots are scope-shaped both ways:
|
|
15
|
+
* `read(fn)` opens one before `fn` and closes it after unconditionally
|
|
16
|
+
* (the {@link ReadScope} handed to `fn` is invalidated the moment `fn`
|
|
17
|
+
* returns), and `using snap = db.read()` hands the caller the lifetime,
|
|
18
|
+
* released by the scope's own `Symbol.dispose` at scope exit. Prepared
|
|
19
|
+
* plans are plain values whose engine-side half is reclaimed by a GC
|
|
20
|
+
* finalizer — reclamation only, never correctness.
|
|
18
21
|
*
|
|
19
22
|
* PROCESS MODEL: one process, one exclusive-lock handle per store. The
|
|
20
23
|
* cached `Db` value owns the LMDB environment's exclusive lock until
|
|
@@ -35,10 +38,11 @@ import { isClosedMember, sealedFieldsOf } from "#closed.ts";
|
|
|
35
38
|
import { exhumeStore } from "#exhume.ts";
|
|
36
39
|
import { rosterOf } from "#fields.ts";
|
|
37
40
|
import { lower } from "#lower.ts";
|
|
38
|
-
import { factOf, handleOf, isFreshField,
|
|
41
|
+
import { factOf, handleOf, isFreshField, isInserted, keyRowOf, recordOf, rowOf } from "#marshal.ts";
|
|
39
42
|
import { bridged, native } from "#native.ts";
|
|
40
43
|
import { lowerQuery } from "#query/lower.ts";
|
|
41
44
|
import { decodeAnswers, wireParams } from "#query/run.ts";
|
|
45
|
+
import { isStatement } from "#statements.ts";
|
|
42
46
|
/**
|
|
43
47
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
44
48
|
* how `writeWitnessed` distinguishes "abort without committing" from an
|
|
@@ -46,23 +50,43 @@ import { decodeAnswers, wireParams } from "#query/run.ts";
|
|
|
46
50
|
*/
|
|
47
51
|
const abandonMark = Symbol("bumbledb.abandon");
|
|
48
52
|
/**
|
|
49
|
-
* Wraps a payload in the {@link Abandon} sentinel — the one way a
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
+
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
54
|
+
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
55
|
+
* (nothing is committed, not even an empty commit) and the write resolves
|
|
56
|
+
* to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
|
|
57
|
+
* alike (R10).
|
|
53
58
|
*/
|
|
54
59
|
function abandon(payload) {
|
|
55
60
|
return Object.freeze({ [abandonMark]: true, payload });
|
|
56
61
|
}
|
|
57
62
|
/**
|
|
58
|
-
* Narrows a
|
|
59
|
-
*
|
|
60
|
-
*
|
|
63
|
+
* Narrows a write callback result to the abandon sentinel. The probe is
|
|
64
|
+
* the private {@link abandonMark} symbol only {@link abandon} sets, and
|
|
65
|
+
* `R`'s `Abandon` arm is the only way a sentinel can flow out of the
|
|
61
66
|
* callback — so the narrowed payload type is sound by construction.
|
|
62
67
|
*/
|
|
63
68
|
function isAbandon(value) {
|
|
64
69
|
return typeof value === "object" && value !== null && abandonMark in value;
|
|
65
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The abandon outcome's trusted admission seam: the value's shape is the
|
|
73
|
+
* checkable half (the sentinel mark only {@link abandon} mints, and the
|
|
74
|
+
* outcome carrying that sentinel's own payload), and the sentinel's
|
|
75
|
+
* existence IS the proof `R` carries an `Abandon` arm — so the outcome is
|
|
76
|
+
* admitted at the conditional {@link AbandonedArm} face the type tier
|
|
77
|
+
* cannot resolve over an open `R`.
|
|
78
|
+
*/
|
|
79
|
+
function isAbandonedOutcome(outcome, sentinel) {
|
|
80
|
+
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload;
|
|
81
|
+
}
|
|
82
|
+
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
83
|
+
function abandonedOutcome(sentinel) {
|
|
84
|
+
const outcome = Object.freeze({ ok: false, abandoned: sentinel.payload });
|
|
85
|
+
if (!isAbandonedOutcome(outcome, sentinel)) {
|
|
86
|
+
throw errors.new("bumbledb abandon outcome construction incomplete");
|
|
87
|
+
}
|
|
88
|
+
return outcome;
|
|
89
|
+
}
|
|
66
90
|
/**
|
|
67
91
|
* The module-private inference slot of {@link Prepared}: an optional symbol
|
|
68
92
|
* property (never set at runtime) that keeps the prepared value's `Row` and
|
|
@@ -125,10 +149,10 @@ function impliedKeyEntries(theory) {
|
|
|
125
149
|
return entries;
|
|
126
150
|
}
|
|
127
151
|
/**
|
|
128
|
-
* One declared statement's materialized slots: a key or
|
|
129
|
-
* one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
130
|
-
* two containments, `source <= target` first), both owned by the
|
|
131
|
-
* value.
|
|
152
|
+
* One declared statement's materialized slots: a key or capacity statement
|
|
153
|
+
* occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
154
|
+
* `==` to two containments, `source <= target` first), both owned by the
|
|
155
|
+
* one SDK value.
|
|
132
156
|
*/
|
|
133
157
|
function declaredEntries(statement) {
|
|
134
158
|
const data = statement.data;
|
|
@@ -151,8 +175,8 @@ function declaredEntries(statement) {
|
|
|
151
175
|
}
|
|
152
176
|
return [{ kind: "containment", statement, key: undefined }];
|
|
153
177
|
}
|
|
154
|
-
case "
|
|
155
|
-
return [{ kind: "
|
|
178
|
+
case "capacity": {
|
|
179
|
+
return [{ kind: "capacity", statement, key: undefined }];
|
|
156
180
|
}
|
|
157
181
|
}
|
|
158
182
|
}
|
|
@@ -167,16 +191,14 @@ function isThenable(value) {
|
|
|
167
191
|
}
|
|
168
192
|
/**
|
|
169
193
|
* Narrows a keyed-get middle argument to a statement value (vs a key
|
|
170
|
-
* object)
|
|
171
|
-
*
|
|
172
|
-
*
|
|
194
|
+
* object) through the statement module's admission brand — a
|
|
195
|
+
* REPRESENTATION, never a shape probe: fact cell shapes are structurally
|
|
196
|
+
* OPEN (an interval value carrying an excess `kind` property is a legal
|
|
197
|
+
* cell), so no property probe could ever be sound here, but no host-built
|
|
198
|
+
* key object can spell the module-private brand symbol.
|
|
173
199
|
*/
|
|
174
200
|
function isStatementValue(value) {
|
|
175
|
-
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
const data = value.data;
|
|
179
|
-
return typeof data === "object" && data !== null && "kind" in data;
|
|
201
|
+
return isStatement(value);
|
|
180
202
|
}
|
|
181
203
|
/**
|
|
182
204
|
* THE one selector dispatch of the `get` overload pair (primary-key vs
|
|
@@ -213,7 +235,11 @@ function orientationOf(reversed) {
|
|
|
213
235
|
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
214
236
|
* positional mirror against the engine's reported order — any drift
|
|
215
237
|
* (count, kind, id, or membership) is a construction-time failure, never a
|
|
216
|
-
* silent misattribution of a violation to the wrong statement value.
|
|
238
|
+
* silent misattribution of a violation to the wrong statement value. The
|
|
239
|
+
* declaration-ordinal law the query lowering leans on is verified in the
|
|
240
|
+
* same walks: relation ids and sealed field ids both equal declaration
|
|
241
|
+
* order, so a constructed `Tables` IS the proof and `prepare` inherits it
|
|
242
|
+
* structurally — never a silently misaddressed query.
|
|
217
243
|
*/
|
|
218
244
|
function tablesOf(theory, manifest) {
|
|
219
245
|
const entries = materializedEntries(theory);
|
|
@@ -236,6 +262,11 @@ function tablesOf(theory, manifest) {
|
|
|
236
262
|
for (const field of relation.fields) {
|
|
237
263
|
fieldIds.set(field.name, field.id);
|
|
238
264
|
}
|
|
265
|
+
sealedFieldsOf(member).forEach(function verifyField(declared, fieldOrdinal) {
|
|
266
|
+
if (fieldIds.get(declared.name) !== fieldOrdinal) {
|
|
267
|
+
throw errors.new(`bumbledb manifest drift: ${relation.name}.${declared.name} has engine field id ${fieldIds.get(declared.name)}, its sealed ordinal is ${fieldOrdinal}`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
239
270
|
let primaryKey;
|
|
240
271
|
entries.forEach(function firstOwnedKey(entry, index) {
|
|
241
272
|
if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
|
|
@@ -244,11 +275,15 @@ function tablesOf(theory, manifest) {
|
|
|
244
275
|
});
|
|
245
276
|
relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }));
|
|
246
277
|
}
|
|
247
|
-
|
|
248
|
-
|
|
278
|
+
Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
|
|
279
|
+
const entry = relations.get(name);
|
|
280
|
+
if (entry === undefined) {
|
|
249
281
|
throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`);
|
|
250
282
|
}
|
|
251
|
-
|
|
283
|
+
if (entry.id !== ordinal) {
|
|
284
|
+
throw errors.new(`bumbledb manifest drift: relation ${name} has engine id ${entry.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
252
287
|
return Object.freeze({ relations, statements: Object.freeze(entries) });
|
|
253
288
|
}
|
|
254
289
|
/** The private lifetime records of this module's read scopes. */
|
|
@@ -381,7 +416,7 @@ function openDb(handle, theory, manifest) {
|
|
|
381
416
|
canonical: wire.canonical,
|
|
382
417
|
direction: wire.direction,
|
|
383
418
|
orientation: orientationOf(entry.reversed),
|
|
384
|
-
|
|
419
|
+
measure: wire.measure,
|
|
385
420
|
facts: Object.freeze(wire.facts.map(offendingFactOf))
|
|
386
421
|
});
|
|
387
422
|
}
|
|
@@ -401,7 +436,7 @@ function openDb(handle, theory, manifest) {
|
|
|
401
436
|
throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
|
|
402
437
|
}
|
|
403
438
|
if (entry.kind !== "functionality" || entry.key === undefined) {
|
|
404
|
-
throw errors.new("keyed get takes a key() statement — containments and
|
|
439
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
|
|
405
440
|
}
|
|
406
441
|
if (entry.key.owner !== relation.name) {
|
|
407
442
|
throw errors.new(`keyed get statement keys ${entry.key.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
|
|
@@ -454,10 +489,11 @@ function openDb(handle, theory, manifest) {
|
|
|
454
489
|
/**
|
|
455
490
|
* Builds one {@link ReadScope} over a live scope state. Every verb
|
|
456
491
|
* asserts liveness first: the owning call flips `state.live` the moment
|
|
457
|
-
* its callback returns
|
|
492
|
+
* its callback returns (or the scope's own `Symbol.dispose` does, for a
|
|
493
|
+
* `using`-acquired scope), so a leaked scope is a typed refusal forever
|
|
458
494
|
* after.
|
|
459
495
|
*/
|
|
460
|
-
function makeScope(state
|
|
496
|
+
function makeScope(state) {
|
|
461
497
|
function assertLive() {
|
|
462
498
|
if (!state.live) {
|
|
463
499
|
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned");
|
|
@@ -494,12 +530,27 @@ function openDb(handle, theory, manifest) {
|
|
|
494
530
|
});
|
|
495
531
|
return decodeAnswers(plan.finds, rows);
|
|
496
532
|
}
|
|
533
|
+
function explain(prepared, params) {
|
|
534
|
+
assertLive();
|
|
535
|
+
const plan = planOf(prepared);
|
|
536
|
+
const wire = wireParams(plan.params, recordOf(params));
|
|
537
|
+
return bridged("explain bumbledb prepared query", function callExplain() {
|
|
538
|
+
return native.preparedExplain(plan.handle, state.handle, wire);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
542
|
+
function dispose() {
|
|
543
|
+
state.live = false;
|
|
544
|
+
closeScopeState(state);
|
|
545
|
+
}
|
|
497
546
|
const scope = Object.freeze({
|
|
498
|
-
generation,
|
|
547
|
+
generation: state.generation,
|
|
499
548
|
scan,
|
|
500
549
|
get: reads.get,
|
|
501
550
|
contains: reads.contains,
|
|
502
|
-
execute
|
|
551
|
+
execute,
|
|
552
|
+
explain,
|
|
553
|
+
[Symbol.dispose]: dispose
|
|
503
554
|
});
|
|
504
555
|
scopeStates.set(scope, state);
|
|
505
556
|
return scope;
|
|
@@ -511,47 +562,48 @@ function openDb(handle, theory, manifest) {
|
|
|
511
562
|
* until the exact moment it matters, so the failure carries the census.
|
|
512
563
|
*/
|
|
513
564
|
let liveSnapshots = 0;
|
|
514
|
-
/**
|
|
565
|
+
/**
|
|
566
|
+
* Opens one snapshot and its scope state (live until the owner flips
|
|
567
|
+
* it). The witnessed generation rides the snapshot open itself — one
|
|
568
|
+
* crossing carries both (finding 016), so the fault-pairing close
|
|
569
|
+
* branch a second `dbGeneration` call needed is structurally gone.
|
|
570
|
+
*/
|
|
515
571
|
function openScopeState() {
|
|
516
|
-
const
|
|
572
|
+
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
517
573
|
return native.dbSnapshot(handle);
|
|
518
574
|
});
|
|
519
575
|
liveSnapshots += 1;
|
|
520
|
-
return { handle:
|
|
576
|
+
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner };
|
|
521
577
|
}
|
|
522
|
-
/**
|
|
578
|
+
/**
|
|
579
|
+
* Closes a scope's snapshot after the owner invalidated it — a LATCH:
|
|
580
|
+
* the snapshot closes exactly once, whichever of the owning call and
|
|
581
|
+
* the scope's own `Symbol.dispose` gets there first, so an early
|
|
582
|
+
* in-callback disposal never double-closes (and never double-counts
|
|
583
|
+
* the census).
|
|
584
|
+
*/
|
|
523
585
|
function closeScopeState(state) {
|
|
586
|
+
if (state.closed) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
state.closed = true;
|
|
524
590
|
bridged("close bumbledb snapshot", function closeSnapshot() {
|
|
525
591
|
native.snapshotClose(state.handle);
|
|
526
592
|
});
|
|
527
593
|
liveSnapshots -= 1;
|
|
528
594
|
}
|
|
529
|
-
/**
|
|
530
|
-
* Reads the committed generation for a just-opened scope, closing the
|
|
531
|
-
* scope's snapshot when the read faults: `dbGeneration` opens a transient
|
|
532
|
-
* engine read txn, so reader-table exhaustion is precisely the state in
|
|
533
|
-
* which it throws — with one snapshot already open. An unpaired fault
|
|
534
|
-
* here would park a snapshot worker and consume one of the engine's
|
|
535
|
-
* reader slots FOREVER (and undercount the liveSnapshots census), each
|
|
536
|
-
* fault ratcheting toward ReadersFull-for-the-process's-lifetime.
|
|
537
|
-
*/
|
|
538
|
-
function generationForScope(state) {
|
|
539
|
-
const generation = errors.trySync(function readGeneration() {
|
|
540
|
-
return bridged("read bumbledb generation", function callGeneration() {
|
|
541
|
-
return native.dbGeneration(handle);
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
if (generation.error) {
|
|
545
|
-
state.live = false;
|
|
546
|
-
closeScopeState(state);
|
|
547
|
-
throw generation.error;
|
|
548
|
-
}
|
|
549
|
-
return generation.data;
|
|
550
|
-
}
|
|
551
595
|
function read(fn) {
|
|
552
596
|
const state = openScopeState();
|
|
553
|
-
const
|
|
554
|
-
|
|
597
|
+
const scope = makeScope(state);
|
|
598
|
+
if (fn === undefined) {
|
|
599
|
+
/**
|
|
600
|
+
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
601
|
+
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
602
|
+
* is the deterministic release, scope-shaped in the language's
|
|
603
|
+
* own syntax.
|
|
604
|
+
*/
|
|
605
|
+
return scope;
|
|
606
|
+
}
|
|
555
607
|
const result = errors.trySync(function runRead() {
|
|
556
608
|
return fn(scope);
|
|
557
609
|
});
|
|
@@ -586,6 +638,11 @@ function openDb(handle, theory, manifest) {
|
|
|
586
638
|
return snap.execute(prepared, params);
|
|
587
639
|
});
|
|
588
640
|
}
|
|
641
|
+
function explain(prepared, params) {
|
|
642
|
+
return read(function explainInScope(snap) {
|
|
643
|
+
return snap.explain(prepared, params);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
589
646
|
/**
|
|
590
647
|
* Builds one {@link Tx} over a transaction-handle thunk: `write` passes
|
|
591
648
|
* an already-begun handle; `writeWitnessed` passes a LAZY thunk that
|
|
@@ -618,17 +675,18 @@ function openDb(handle, theory, manifest) {
|
|
|
618
675
|
assertLive();
|
|
619
676
|
const entry = resolveOrdinary(relation);
|
|
620
677
|
const txHandle = resolveTx();
|
|
621
|
-
|
|
678
|
+
/** The one spread copy of the write path: `mintFreshCells` writes minted cells in place, and they must never land in the caller's own fact object. */
|
|
679
|
+
const values = { ...recordOf(fact) };
|
|
622
680
|
const fresh = mintFreshCells(txHandle, entry, relation, values);
|
|
623
681
|
const row = rowOf(relation.data, values);
|
|
624
|
-
bridged("bumbledb tx insert", function record() {
|
|
625
|
-
native.txInsert(txHandle, entry.id, row);
|
|
682
|
+
const changed = bridged("bumbledb tx insert", function record() {
|
|
683
|
+
return native.txInsert(txHandle, entry.id, row);
|
|
626
684
|
});
|
|
627
|
-
Object.freeze(fresh);
|
|
628
|
-
if (!
|
|
629
|
-
throw errors.new(`relation ${relation.name}:
|
|
685
|
+
const inserted = Object.freeze({ changed, ...fresh });
|
|
686
|
+
if (!isInserted(relation, inserted)) {
|
|
687
|
+
throw errors.new(`relation ${relation.name}: insert return record is incomplete`);
|
|
630
688
|
}
|
|
631
|
-
return
|
|
689
|
+
return inserted;
|
|
632
690
|
}
|
|
633
691
|
function remove(relation, fact) {
|
|
634
692
|
assertLive();
|
|
@@ -678,6 +736,18 @@ function openDb(handle, theory, manifest) {
|
|
|
678
736
|
});
|
|
679
737
|
throw errors.new("bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed");
|
|
680
738
|
}
|
|
739
|
+
if (isAbandon(built.data)) {
|
|
740
|
+
/**
|
|
741
|
+
* The caller's explicit decline to commit (R10): the sentinel's
|
|
742
|
+
* contract is unconditional — roll back, nothing committed, not
|
|
743
|
+
* even an empty commit; commit is unreachable for a sentinel
|
|
744
|
+
* result.
|
|
745
|
+
*/
|
|
746
|
+
bridged("abort bumbledb write transaction", function abort() {
|
|
747
|
+
native.txAbort(txHandle);
|
|
748
|
+
});
|
|
749
|
+
return abandonedOutcome(built.data);
|
|
750
|
+
}
|
|
681
751
|
const committed = errors.trySync(function commitDelta() {
|
|
682
752
|
return bridged("commit bumbledb write transaction", function commit() {
|
|
683
753
|
return native.txCommit(txHandle);
|
|
@@ -708,15 +778,10 @@ function openDb(handle, theory, manifest) {
|
|
|
708
778
|
});
|
|
709
779
|
}
|
|
710
780
|
function write(fn) {
|
|
711
|
-
const
|
|
712
|
-
return
|
|
713
|
-
return native.dbWriteBegin(handle);
|
|
714
|
-
});
|
|
781
|
+
const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
|
|
782
|
+
return native.dbWriteBegin(handle);
|
|
715
783
|
});
|
|
716
|
-
|
|
717
|
-
throw errors.wrap(begun.error, `begin bumbledb write transaction (live snapshots at fault: ${liveSnapshots})`);
|
|
718
|
-
}
|
|
719
|
-
return runDelta(begun.data, fn);
|
|
784
|
+
return runDelta(txHandle, fn);
|
|
720
785
|
}
|
|
721
786
|
/**
|
|
722
787
|
* Commits an already-begun witnessed transaction and closes the
|
|
@@ -760,8 +825,7 @@ function openDb(handle, theory, manifest) {
|
|
|
760
825
|
*/
|
|
761
826
|
function witnessedAttempt(fn) {
|
|
762
827
|
const state = openScopeState();
|
|
763
|
-
const
|
|
764
|
-
const scope = makeScope(state, generation);
|
|
828
|
+
const scope = makeScope(state);
|
|
765
829
|
const pending = { tx: undefined };
|
|
766
830
|
function beginWitnessed() {
|
|
767
831
|
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
@@ -823,7 +887,7 @@ function openDb(handle, theory, manifest) {
|
|
|
823
887
|
if (isAbandon(built.data)) {
|
|
824
888
|
abortPending();
|
|
825
889
|
closeScopeState(state);
|
|
826
|
-
return
|
|
890
|
+
return abandonedOutcome(built.data);
|
|
827
891
|
}
|
|
828
892
|
const late = errors.trySync(function resolveCommitTx() {
|
|
829
893
|
if (pending.tx === undefined) {
|
|
@@ -867,38 +931,10 @@ function openDb(handle, theory, manifest) {
|
|
|
867
931
|
}
|
|
868
932
|
throw errors.wrap(ErrWitnessedLivelock, `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`);
|
|
869
933
|
}
|
|
870
|
-
/**
|
|
871
|
-
* Verifies the declaration-ordinal law the query lowering leans on
|
|
872
|
-
* against the live manifest tables: relation ids and sealed field ids
|
|
873
|
-
* both equal declaration order (`dbManifest` is the engine's own
|
|
874
|
-
* pinning of it). Any drift is a construction-time failure here, never
|
|
875
|
-
* a silently misaddressed query.
|
|
876
|
-
*/
|
|
877
|
-
function assertOrdinalAlignment() {
|
|
878
|
-
Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
|
|
879
|
-
const entry = tables.relations.get(name);
|
|
880
|
-
if (entry === undefined || entry.id !== ordinal) {
|
|
881
|
-
throw errors.new(`bumbledb manifest drift: relation ${name} has engine id ${entry?.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`);
|
|
882
|
-
}
|
|
883
|
-
const member = theory.relations[name];
|
|
884
|
-
if (member === undefined) {
|
|
885
|
-
throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`);
|
|
886
|
-
}
|
|
887
|
-
const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
|
|
888
|
-
return declared.name;
|
|
889
|
-
});
|
|
890
|
-
sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
|
|
891
|
-
if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
|
|
892
|
-
throw errors.new(`bumbledb manifest drift: ${name}.${fieldName} has engine field id ${entry.fieldIds.get(fieldName)}, its sealed ordinal is ${fieldOrdinal}`);
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
934
|
function prepare(q) {
|
|
898
935
|
if (q.schema !== theory) {
|
|
899
936
|
throw errors.new(`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`);
|
|
900
937
|
}
|
|
901
|
-
assertOrdinalAlignment();
|
|
902
938
|
const program = lowerQuery(q);
|
|
903
939
|
const outcome = bridged("prepare bumbledb program", function callPrepare() {
|
|
904
940
|
return native.dbPrepare(handle, program);
|
|
@@ -939,6 +975,7 @@ function openDb(handle, theory, manifest) {
|
|
|
939
975
|
get,
|
|
940
976
|
contains,
|
|
941
977
|
execute,
|
|
978
|
+
explain,
|
|
942
979
|
write,
|
|
943
980
|
writeWitnessed,
|
|
944
981
|
prepare
|
|
@@ -987,6 +1024,24 @@ process.once("exit", function closeCachedStores() {
|
|
|
987
1024
|
* what the types claim.
|
|
988
1025
|
*/
|
|
989
1026
|
const ErrNewtypeMismatch = errors.new("bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one");
|
|
1027
|
+
/**
|
|
1028
|
+
* `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
|
|
1029
|
+
* where the spread wins: a FRESH field literally named `changed` would
|
|
1030
|
+
* shadow the engine's changed-state report on every insert of its
|
|
1031
|
+
* relation. No field name is reserved SILENTLY — the one unspeakable
|
|
1032
|
+
* spelling is refused here at admission, before any store is touched.
|
|
1033
|
+
* Supplied (non-fresh) fields named `changed` never enter the return
|
|
1034
|
+
* record and stay legal.
|
|
1035
|
+
*/
|
|
1036
|
+
function refuseShadowedChanged(theory) {
|
|
1037
|
+
for (const [name, member] of Object.entries(theory.relations)) {
|
|
1038
|
+
for (const declared of sealedFieldsOf(member)) {
|
|
1039
|
+
if (declared.name === "changed" && isFreshField(declared.field)) {
|
|
1040
|
+
throw errors.new(`relation ${name}: a fresh field named "changed" would shadow tx.insert's changed-state report in its { changed, ...fresh } return (R11) — rename the fresh field; a supplied field named "changed" stays legal (only fresh cells ride the return)`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
990
1045
|
/**
|
|
991
1046
|
* The one admission path both verbs share: canonical-path cache lookup
|
|
992
1047
|
* first (a hit returns the SAME `Db` value for the identical theory, a
|
|
@@ -1000,6 +1055,7 @@ const ErrNewtypeMismatch = errors.new("bumbledb newtypeMismatch: a statement pai
|
|
|
1000
1055
|
* the engine's message intact.
|
|
1001
1056
|
*/
|
|
1002
1057
|
function admit(verb, storePath, theory) {
|
|
1058
|
+
refuseShadowedChanged(theory);
|
|
1003
1059
|
const canonical = path.resolve(storePath);
|
|
1004
1060
|
const cached = openStores.get(canonical);
|
|
1005
1061
|
if (cached !== undefined) {
|
|
@@ -1064,12 +1120,12 @@ const Db = Object.freeze({
|
|
|
1064
1120
|
* one schema-independent read path (no theory, no fingerprint check; the
|
|
1065
1121
|
* store rebirth tool's entry). Lives beside `open`/`create` so the path
|
|
1066
1122
|
* law stays in one place: the same `node:path.resolve` canonicalization,
|
|
1067
|
-
* applied here. The value is NOT cached and
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
* the descriptor).
|
|
1123
|
+
* applied here. The value is NOT cached and is a DISPOSABLE lifetime
|
|
1124
|
+
* (R12) — `using exhumed = await Db.exhume(path)` releases the engine
|
|
1125
|
+
* handle and the store's exclusive lock at scope exit, so a same-path
|
|
1126
|
+
* reopen never waits on GC. A store not yet adopted rejects with the
|
|
1127
|
+
* typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
|
|
1128
|
+
* `Db.open` under the creating schema back-fills the descriptor).
|
|
1073
1129
|
*/
|
|
1074
1130
|
async exhume(storePath) {
|
|
1075
1131
|
return exhumeStore(path.resolve(storePath));
|