@bjornpagen/bumbledb 0.4.0 → 0.6.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 +191 -92
- package/README.md +9 -7
- package/dist/closed.d.ts +30 -2
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +66 -20
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +33 -6
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +89 -72
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +1 -14
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +1 -1
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +2 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +18 -1
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +44 -16
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +2 -1
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +15 -14
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +1 -7
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +15 -27
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +21 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +20 -3
- package/dist/native.js.map +1 -1
- package/dist/order.d.ts +36 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +135 -0
- package/dist/order.js.map +1 -0
- package/dist/query/atom.d.ts +140 -206
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +33 -52
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +116 -0
- package/dist/query/find.d.ts.map +1 -0
- package/dist/query/{select.js → find.js} +22 -22
- package/dist/query/find.js.map +1 -0
- package/dist/query/lower.d.ts +124 -162
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +462 -507
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +22 -14
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +45 -20
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +3 -3
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +10 -11
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +124 -83
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +77 -70
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +9 -22
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +9 -28
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -31
- package/dist/schema.js.map +1 -1
- package/dist/statements.d.ts +5 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +30 -35
- package/dist/statements.js.map +1 -1
- package/package.json +2 -5
- package/src/closed.ts +73 -28
- package/src/db.ts +126 -88
- package/src/exhume.ts +1 -15
- package/src/face.ts +4 -3
- package/src/fields.ts +58 -16
- package/src/index.ts +19 -13
- package/src/law.ts +15 -14
- package/src/lower.ts +2 -9
- package/src/marshal.ts +15 -31
- package/src/native.ts +22 -4
- package/src/order.ts +156 -0
- package/src/query/atom.ts +186 -276
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +614 -749
- package/src/query/predicate.ts +47 -20
- package/src/query/run.ts +11 -13
- package/src/query/scope.ts +165 -132
- package/src/relation.ts +9 -51
- package/src/schema.ts +7 -33
- package/src/statements.ts +33 -38
- package/dist/query/select.d.ts +0 -128
- package/dist/query/select.d.ts.map +0 -1
- package/dist/query/select.js.map +0 -1
- package/src/query/select.ts +0 -215
package/src/closed.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type Infer,
|
|
40
40
|
literalOf
|
|
41
41
|
} from "#fields.ts"
|
|
42
|
+
import type { AnyRelation, RelationField } from "#relation.ts"
|
|
42
43
|
import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
|
|
43
44
|
import type { LiteralSpec } from "#spec.ts"
|
|
44
45
|
|
|
@@ -53,7 +54,7 @@ type PayloadField = Exclude<AnyField, { readonly fresh: true }>
|
|
|
53
54
|
* unspellable — the sealed shape mints the synthetic `id` itself (ordinal
|
|
54
55
|
* 0 of the matchable fields), so a declared column named `id` would be
|
|
55
56
|
* shadowed by the synthetic slot everywhere the shape resolves by name
|
|
56
|
-
* (`
|
|
57
|
+
* (`sealedFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
|
|
57
58
|
* typed here and judged again at construction in {@link mintClosed} — the
|
|
58
59
|
* runtime twin for untyped callers, warmer and earlier than the engine's
|
|
59
60
|
* `DuplicateFieldName` at `Db.create`.
|
|
@@ -193,6 +194,48 @@ interface AnyClosed {
|
|
|
193
194
|
readonly columns: Readonly<Record<string, PayloadField>>
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
/**
|
|
198
|
+
* THE relation-kind discriminant — the ONE spelling of "is this schema
|
|
199
|
+
* member closed?" (the type tier's twin is the `AnyClosed` conditional
|
|
200
|
+
* arms). A closed relation's runtime description carries its handle
|
|
201
|
+
* roster; an ordinary relation's never does. Every runtime closed/ordinary
|
|
202
|
+
* fork in the SDK judges through this predicate — never a re-spelled
|
|
203
|
+
* structural probe.
|
|
204
|
+
*/
|
|
205
|
+
function isClosedMember(member: AnyRelation | AnyClosed): member is AnyClosed {
|
|
206
|
+
return "handles" in member.data
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The SEALED field list of a schema member — THE one reader of "what
|
|
211
|
+
* fields does this owner expose": an ordinary relation's declared fields;
|
|
212
|
+
* a closed relation's sealed shape — the synthetic `id` (the value's own
|
|
213
|
+
* roster-carrying descriptor, by identity) at ordinal 0, then the declared
|
|
214
|
+
* payload columns at declared index + 1 (the sealed shift, mirroring the
|
|
215
|
+
* engine's `SchemaDescriptor::sealed_fields`). A `ClosedColumn` is
|
|
216
|
+
* structurally a `RelationField`, so both kinds read uniformly.
|
|
217
|
+
*/
|
|
218
|
+
function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
|
|
219
|
+
if (isClosedMember(member)) {
|
|
220
|
+
return Object.freeze([Object.freeze({ name: "id", field: member.id }), ...member.data.columns])
|
|
221
|
+
}
|
|
222
|
+
return member.data.fields
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* One sealed field by name — derived from {@link sealedFieldsOf}, so the
|
|
227
|
+
* closed synthetic `id` resolves everywhere a name is looked up (no reader
|
|
228
|
+
* can silently lack the `id` arm). `undefined` when the name is foreign
|
|
229
|
+
* (the type tiers make that unwritable; the engine re-judges at
|
|
230
|
+
* `Db.create`).
|
|
231
|
+
*/
|
|
232
|
+
function sealedFieldOf(member: AnyRelation | AnyClosed, fieldName: string): AnyField | undefined {
|
|
233
|
+
const declared = sealedFieldsOf(member).find(function byName(candidate) {
|
|
234
|
+
return candidate.name === fieldName
|
|
235
|
+
})
|
|
236
|
+
return declared?.field
|
|
237
|
+
}
|
|
238
|
+
|
|
196
239
|
/** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
|
|
197
240
|
function isHandleTuple(
|
|
198
241
|
shape: readonly [string, ...string[]] | Record<string, PayloadField>
|
|
@@ -219,7 +262,8 @@ function handleKeysOwn<Handles extends string>(
|
|
|
219
262
|
* The trusted seam of the axiom-readback mint: every handle carries an own
|
|
220
263
|
* frozen row and every row carries every declared column as an own
|
|
221
264
|
* property — verified before the record is admitted as the typed
|
|
222
|
-
* {@link Axioms} (the
|
|
265
|
+
* {@link Axioms} (the trusted-admission-seam pattern — its home is
|
|
266
|
+
* `isTypedScope` in query/lower.ts).
|
|
223
267
|
*/
|
|
224
268
|
function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
225
269
|
record: Readonly<Record<string, object>>,
|
|
@@ -237,38 +281,22 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
|
|
|
237
281
|
})
|
|
238
282
|
}
|
|
239
283
|
|
|
240
|
-
/**
|
|
241
|
-
* Reads one handle's ground axiom row for lowering. The typed payload
|
|
242
|
-
* surface makes absence unrepresentable ({@link Axioms} carries every
|
|
243
|
-
* handle's row); the refusal below guards the one ill-typed path — payload
|
|
244
|
-
* columns with the bare tier's absent axioms — which no public spelling
|
|
245
|
-
* reaches.
|
|
246
|
-
*/
|
|
247
|
-
function groundRow<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
248
|
-
name: string,
|
|
249
|
-
axioms: Axioms<Handles, Cols> | undefined,
|
|
250
|
-
handle: Handles
|
|
251
|
-
): Readonly<Record<string, unknown>> {
|
|
252
|
-
if (axioms === undefined) {
|
|
253
|
-
throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
|
|
254
|
-
}
|
|
255
|
-
return axioms[handle]
|
|
256
|
-
}
|
|
257
|
-
|
|
258
284
|
/**
|
|
259
285
|
* Mints the axiom-readback record: one own frozen row per handle (the bare
|
|
260
286
|
* tier's rows are empty — it declares no columns), each row a fresh copy of
|
|
261
|
-
* its ground axiom.
|
|
287
|
+
* its ground axiom. Both tiers supply a REAL axioms record ({@link
|
|
288
|
+
* closedBare} mints its empty rows), so the columns-without-axioms state is
|
|
289
|
+
* unrepresentable here — no undefined arm exists to guard.
|
|
262
290
|
*/
|
|
263
291
|
function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
264
292
|
name: string,
|
|
265
293
|
handles: readonly Handles[],
|
|
266
294
|
cols: readonly ClosedColumn[],
|
|
267
|
-
axioms: Axioms<Handles, Cols>
|
|
295
|
+
axioms: Axioms<Handles, Cols>
|
|
268
296
|
): Axioms<Handles, Cols> {
|
|
269
297
|
const out: Record<string, object> = {}
|
|
270
298
|
for (const handle of handles) {
|
|
271
|
-
const row =
|
|
299
|
+
const row = Object.freeze({ ...axioms[handle] })
|
|
272
300
|
Object.defineProperty(out, handle, { value: row, enumerable: true })
|
|
273
301
|
}
|
|
274
302
|
Object.freeze(out)
|
|
@@ -321,12 +349,29 @@ function closed<const Name extends string, const Cols extends PayloadColumns, Ha
|
|
|
321
349
|
return closedPayload(name, shape, axioms)
|
|
322
350
|
}
|
|
323
351
|
|
|
324
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The bare tier's precisely-typed builder: no columns, and the axioms
|
|
354
|
+
* record is the EMPTY-ROW record over the handle roster (one own frozen
|
|
355
|
+
* `{}` per handle, `__proto__`-safe own-property definition) — the same
|
|
356
|
+
* representation the payload tier carries, so `mintClosed` never sees a
|
|
357
|
+
* tier fork and the columns-without-axioms state stops being spellable.
|
|
358
|
+
*/
|
|
325
359
|
function closedBare<Name extends string, Handles extends string>(
|
|
326
360
|
name: Name,
|
|
327
361
|
handles: readonly [Handles, ...Handles[]]
|
|
328
362
|
): Closed<Name, Handles, Record<never, never>> {
|
|
329
|
-
|
|
363
|
+
const empty: Record<string, object> = {}
|
|
364
|
+
for (const handle of handles) {
|
|
365
|
+
/** A duplicated name mints one row; the roster's own duplicate refusal in {@link mintClosed} stays the judge. */
|
|
366
|
+
if (!Object.hasOwn(empty, handle)) {
|
|
367
|
+
Object.defineProperty(empty, handle, { value: Object.freeze({}), enumerable: true })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
Object.freeze(empty)
|
|
371
|
+
if (!axiomsMinted<Handles, Record<never, never>>(empty, handles, [])) {
|
|
372
|
+
throw errors.new(`closed relation ${name}: bare-tier axiom-row minting incomplete`)
|
|
373
|
+
}
|
|
374
|
+
return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, empty)
|
|
330
375
|
}
|
|
331
376
|
|
|
332
377
|
/**
|
|
@@ -379,7 +424,7 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
379
424
|
name: Name,
|
|
380
425
|
handles: readonly Handles[],
|
|
381
426
|
columns: Cols,
|
|
382
|
-
axioms: Axioms<Handles, Cols>
|
|
427
|
+
axioms: Axioms<Handles, Cols>
|
|
383
428
|
): Closed<Name, Handles, Cols> {
|
|
384
429
|
if (handles.length === 0) {
|
|
385
430
|
throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
|
|
@@ -405,8 +450,8 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
405
450
|
}
|
|
406
451
|
Object.freeze(cols)
|
|
407
452
|
const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
|
|
453
|
+
const row: Readonly<Record<string, unknown>> = axioms[handle]
|
|
408
454
|
const values = cols.map(function lowerAxiomLiteral(column) {
|
|
409
|
-
const row = groundRow(name, axioms, handle)
|
|
410
455
|
return Object.freeze(literalOf(column.field, row[column.name]))
|
|
411
456
|
})
|
|
412
457
|
return Object.freeze({ handle, values: Object.freeze(values) })
|
|
@@ -471,4 +516,4 @@ export type {
|
|
|
471
516
|
PayloadField,
|
|
472
517
|
SelectedClosed
|
|
473
518
|
}
|
|
474
|
-
export { closed }
|
|
519
|
+
export { closed, isClosedMember, sealedFieldOf, sealedFieldsOf }
|
package/src/db.ts
CHANGED
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
import * as path from "node:path"
|
|
34
34
|
import * as errors from "@superbuilders/errors"
|
|
35
|
+
import { isClosedMember, sealedFieldsOf } from "#closed.ts"
|
|
35
36
|
import type { Exhumed } from "#exhume.ts"
|
|
36
37
|
import { exhumeStore } from "#exhume.ts"
|
|
38
|
+
import { rosterOf } from "#fields.ts"
|
|
37
39
|
import { lower } from "#lower.ts"
|
|
38
40
|
import {
|
|
39
41
|
factOf,
|
|
@@ -59,13 +61,13 @@ import type {
|
|
|
59
61
|
Violation as WireViolation,
|
|
60
62
|
ViolationFact as WireViolationFact
|
|
61
63
|
} from "#native.ts"
|
|
62
|
-
import { native } from "#native.ts"
|
|
63
|
-
import type {
|
|
64
|
+
import { bridged, native } from "#native.ts"
|
|
65
|
+
import type { FindColumn } from "#query/atom.ts"
|
|
64
66
|
import type { Query } from "#query/lower.ts"
|
|
65
67
|
import { lowerQuery } from "#query/lower.ts"
|
|
66
68
|
import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
67
69
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
68
|
-
import type { AnyRelation, Fact, InsertFact
|
|
70
|
+
import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
|
|
69
71
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
70
72
|
import type { KeyStatement, Statement } from "#statements.ts"
|
|
71
73
|
|
|
@@ -348,11 +350,16 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
348
350
|
* The ONE witnessed-write form: snapshot → `fn` (premise reads via
|
|
349
351
|
* `snap`, delta via `tx`) → witnessed commit, which lands only if no
|
|
350
352
|
* state-changing commit intervened since the snapshot. On a moved
|
|
351
|
-
* generation the WHOLE `fn` reruns on a fresh snapshot
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
353
|
+
* generation the WHOLE `fn` reruns on a fresh snapshot: this process
|
|
354
|
+
* holds the store's only write handle, so every generation move is
|
|
355
|
+
* self-inflicted by the host's own interleaved writes, and each rerun
|
|
356
|
+
* witnesses a strictly newer generation — the benign race converges.
|
|
357
|
+
* The loop's honesty bound is {@link WITNESSED_ATTEMPT_CAP}: a callback
|
|
358
|
+
* that moves the generation on EVERY attempt (a plain `db.write` before
|
|
359
|
+
* its first tx verb) would spin forever, so past the cap the typed
|
|
360
|
+
* {@link ErrWitnessedLivelock} is thrown instead of a silent loop (the
|
|
361
|
+
* engine's ruling: the error, never a loop — retry is host policy, and
|
|
362
|
+
* the cap is that policy's own diagnostic). `fn` may decline to commit
|
|
356
363
|
* by returning {@link abandon}`(payload)` — the outcome is then
|
|
357
364
|
* `{ ok: false, abandoned: payload }` and NO commit (not even an empty
|
|
358
365
|
* one) is issued.
|
|
@@ -368,19 +375,6 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
368
375
|
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
369
376
|
}
|
|
370
377
|
|
|
371
|
-
/**
|
|
372
|
-
* The bridge guard: runs one native call and wraps anything it throws —
|
|
373
|
-
* marshal-shape refusals and handle-lifecycle refusals cross as genuine
|
|
374
|
-
* typed failures, never bare foreign errors.
|
|
375
|
-
*/
|
|
376
|
-
function bridged<T>(context: string, run: () => T): T {
|
|
377
|
-
const result = errors.trySync(run)
|
|
378
|
-
if (result.error) {
|
|
379
|
-
throw errors.wrap(result.error, context)
|
|
380
|
-
}
|
|
381
|
-
return result.data
|
|
382
|
-
}
|
|
383
|
-
|
|
384
378
|
/** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
|
|
385
379
|
interface RelationEntry {
|
|
386
380
|
readonly id: number
|
|
@@ -443,7 +437,7 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
|
|
|
443
437
|
function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
444
438
|
const entries: StatementEntry[] = []
|
|
445
439
|
for (const member of Object.values(theory.relations)) {
|
|
446
|
-
if (
|
|
440
|
+
if (isClosedMember(member)) {
|
|
447
441
|
continue
|
|
448
442
|
}
|
|
449
443
|
for (const declared of member.data.fields) {
|
|
@@ -457,7 +451,7 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
|
457
451
|
}
|
|
458
452
|
}
|
|
459
453
|
for (const member of Object.values(theory.relations)) {
|
|
460
|
-
if (
|
|
454
|
+
if (isClosedMember(member)) {
|
|
461
455
|
entries.push({
|
|
462
456
|
kind: "functionality",
|
|
463
457
|
statement: undefined,
|
|
@@ -527,6 +521,33 @@ function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
|
|
|
527
521
|
return typeof data === "object" && data !== null && "kind" in data
|
|
528
522
|
}
|
|
529
523
|
|
|
524
|
+
/**
|
|
525
|
+
* THE one selector dispatch of the `get` overload pair (primary-key vs
|
|
526
|
+
* key-statement, `docs/architecture/70-api.md` § the freeze): judges the
|
|
527
|
+
* middle argument once and hands the narrowed pieces to the chosen
|
|
528
|
+
* continuation. `Db.get` and the read scope's `get` both dispatch through
|
|
529
|
+
* here, so the two mismatch refusals speak with one voice and the symmetry
|
|
530
|
+
* rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
|
|
531
|
+
* construction.
|
|
532
|
+
*/
|
|
533
|
+
function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
|
|
534
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
535
|
+
declaredKey: DeclaredKeyFact<R, P> | undefined,
|
|
536
|
+
byStatement: (statement: KeyStatement<R, P>, key: DeclaredKeyFact<R, P>) => T,
|
|
537
|
+
byPrimary: (key: KeyFact<R>) => T
|
|
538
|
+
): T {
|
|
539
|
+
if (declaredKey !== undefined) {
|
|
540
|
+
if (!isStatementValue(keyOrStatement)) {
|
|
541
|
+
throw errors.new("keyed get takes a key() statement value as its second argument")
|
|
542
|
+
}
|
|
543
|
+
return byStatement(keyOrStatement, declaredKey)
|
|
544
|
+
}
|
|
545
|
+
if (isStatementValue(keyOrStatement)) {
|
|
546
|
+
throw errors.new("keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)")
|
|
547
|
+
}
|
|
548
|
+
return byPrimary(keyOrStatement)
|
|
549
|
+
}
|
|
550
|
+
|
|
530
551
|
/** Maps a slot's reversal flag to the violation's `orientation` payload. */
|
|
531
552
|
function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
|
|
532
553
|
if (reversed === undefined) {
|
|
@@ -538,22 +559,6 @@ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" |
|
|
|
538
559
|
return "written"
|
|
539
560
|
}
|
|
540
561
|
|
|
541
|
-
/**
|
|
542
|
-
* The declared field descriptors a violation's offending cells decode
|
|
543
|
-
* through: an ordinary relation's declared fields; a closed relation's
|
|
544
|
-
* SEALED shape — the roster-carrying synthetic `id` (the member's own
|
|
545
|
-
* reference descriptor) plus its payload columns (a `ClosedColumn` is
|
|
546
|
-
* structurally a `RelationField`). This is how {@link openDb}'s
|
|
547
|
-
* `offendingFactOf` reaches the open-time descriptors: the cited relation
|
|
548
|
-
* name resolves to the schema member, and the member carries them.
|
|
549
|
-
*/
|
|
550
|
-
function declaredFieldsOf(member: SchemaRelation): readonly RelationField[] {
|
|
551
|
-
if ("axioms" in member) {
|
|
552
|
-
return [{ name: "id", field: member.id }, ...member.data.columns]
|
|
553
|
-
}
|
|
554
|
-
return member.data.fields
|
|
555
|
-
}
|
|
556
|
-
|
|
557
562
|
/** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
|
|
558
563
|
interface Tables {
|
|
559
564
|
readonly relations: ReadonlyMap<string, RelationEntry>
|
|
@@ -638,7 +643,7 @@ interface PreparedPlan {
|
|
|
638
643
|
readonly handle: PreparedHandle
|
|
639
644
|
readonly owner: object
|
|
640
645
|
readonly params: readonly ParamEntry[]
|
|
641
|
-
readonly
|
|
646
|
+
readonly finds: readonly FindColumn[]
|
|
642
647
|
}
|
|
643
648
|
|
|
644
649
|
/** The private engine halves of this module's prepared values. */
|
|
@@ -667,6 +672,32 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
|
|
|
667
672
|
*/
|
|
668
673
|
const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
|
|
669
674
|
|
|
675
|
+
/**
|
|
676
|
+
* The witnessed loop's attempt cap — a generous power of two. Benign
|
|
677
|
+
* self-inflicted contention (the host's own commits landing between an
|
|
678
|
+
* attempt's snapshot and its witnessed begin) converges in a handful of
|
|
679
|
+
* retries because each rerun reads a FRESHER snapshot; a workload that moves
|
|
680
|
+
* the generation on EVERY one of this many consecutive attempts is not
|
|
681
|
+
* converging and never will (see {@link ErrWitnessedLivelock}).
|
|
682
|
+
*/
|
|
683
|
+
const WITNESSED_ATTEMPT_CAP = 64
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* The typed livelock refusal `writeWitnessed` throws past
|
|
687
|
+
* {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
|
|
688
|
+
* moved, which is only sustainable when the callback ITSELF (even
|
|
689
|
+
* indirectly) issues an interleaved plain `db.write` before its first tx
|
|
690
|
+
* verb on every attempt — each rerun then re-moves the generation it is
|
|
691
|
+
* about to witness, forever. That is host-policy pathology, not engine
|
|
692
|
+
* judgment (the engine ships the error, never a loop), so it THROWS rather
|
|
693
|
+
* than returning a result arm. Match with `errors.is`; the remedy is to
|
|
694
|
+
* move the interleaved write out of the callback (or make it first-attempt
|
|
695
|
+
* only — the delta belongs on `tx`, premise reads on `snap`).
|
|
696
|
+
*/
|
|
697
|
+
const ErrWitnessedLivelock = errors.new(
|
|
698
|
+
"bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current"
|
|
699
|
+
)
|
|
700
|
+
|
|
670
701
|
/**
|
|
671
702
|
* Fills one insert's omitted fresh cells through the engine's
|
|
672
703
|
* alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
|
|
@@ -725,7 +756,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
725
756
|
if (entry === undefined || entry.member !== relation) {
|
|
726
757
|
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
727
758
|
}
|
|
728
|
-
if (
|
|
759
|
+
if (isClosedMember(relation)) {
|
|
729
760
|
throw errors.new(
|
|
730
761
|
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
731
762
|
)
|
|
@@ -738,15 +769,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
738
769
|
if (entry === undefined || !isMemberName(fact.relation)) {
|
|
739
770
|
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
740
771
|
}
|
|
741
|
-
const declared =
|
|
772
|
+
const declared = sealedFieldsOf(entry.member)
|
|
742
773
|
const decoded: Record<string, FactValue> = {}
|
|
743
774
|
for (const cell of fact.fields) {
|
|
744
775
|
const cited = declared.find(function byName(candidate) {
|
|
745
776
|
return candidate.name === cell.name
|
|
746
777
|
})
|
|
778
|
+
const roster = rosterOf(cited?.field)
|
|
747
779
|
decoded[cell.name] =
|
|
748
|
-
|
|
749
|
-
? handleOf(`violation fact ${fact.relation} field ${cell.name}`,
|
|
780
|
+
roster !== undefined
|
|
781
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
750
782
|
: cell.value
|
|
751
783
|
}
|
|
752
784
|
return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
|
|
@@ -828,25 +860,22 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
828
860
|
): Fact<R> | undefined {
|
|
829
861
|
assertLive()
|
|
830
862
|
const entry = resolveOrdinary(relation)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
863
|
+
return selectKeyRead(
|
|
864
|
+
keyOrStatement,
|
|
865
|
+
declaredKey,
|
|
866
|
+
function byStatement(statement, key) {
|
|
867
|
+
return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
|
|
868
|
+
},
|
|
869
|
+
function byPrimary(key) {
|
|
870
|
+
const primaryKey = entry.primaryKey
|
|
871
|
+
if (primaryKey === undefined) {
|
|
872
|
+
throw errors.new(
|
|
873
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
return readThroughKey(relation, entry, primaryKey, recordOf(key))
|
|
834
877
|
}
|
|
835
|
-
|
|
836
|
-
return readThroughKey(relation, entry, selected, recordOf(declaredKey))
|
|
837
|
-
}
|
|
838
|
-
if (isStatementValue(keyOrStatement)) {
|
|
839
|
-
throw errors.new(
|
|
840
|
-
"keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
|
|
841
|
-
)
|
|
842
|
-
}
|
|
843
|
-
const primaryKey = entry.primaryKey
|
|
844
|
-
if (primaryKey === undefined) {
|
|
845
|
-
throw errors.new(
|
|
846
|
-
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
847
|
-
)
|
|
848
|
-
}
|
|
849
|
-
return readThroughKey(relation, entry, primaryKey, recordOf(keyOrStatement))
|
|
878
|
+
)
|
|
850
879
|
}
|
|
851
880
|
return { contains, get }
|
|
852
881
|
}
|
|
@@ -909,7 +938,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
909
938
|
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
910
939
|
return native.preparedExecute(plan.handle, state.handle, wire)
|
|
911
940
|
})
|
|
912
|
-
return decodeAnswers<Row>(plan.
|
|
941
|
+
return decodeAnswers<Row>(plan.finds, rows)
|
|
913
942
|
}
|
|
914
943
|
const scope: ReadScope<Rels> = Object.freeze({
|
|
915
944
|
generation,
|
|
@@ -1003,18 +1032,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1003
1032
|
declaredKey?: DeclaredKeyFact<R, P>
|
|
1004
1033
|
): Fact<R> | undefined {
|
|
1005
1034
|
return read(function getInScope(snap) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1035
|
+
return selectKeyRead(
|
|
1036
|
+
keyOrStatement,
|
|
1037
|
+
declaredKey,
|
|
1038
|
+
function byStatement(statement, key) {
|
|
1039
|
+
return snap.get(relation, statement, key)
|
|
1040
|
+
},
|
|
1041
|
+
function byPrimary(key) {
|
|
1042
|
+
return snap.get(relation, key)
|
|
1009
1043
|
}
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
if (isStatementValue(keyOrStatement)) {
|
|
1013
|
-
throw errors.new(
|
|
1014
|
-
"keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
|
|
1015
|
-
)
|
|
1016
|
-
}
|
|
1017
|
-
return snap.get(relation, keyOrStatement)
|
|
1044
|
+
)
|
|
1018
1045
|
})
|
|
1019
1046
|
}
|
|
1020
1047
|
|
|
@@ -1298,13 +1325,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1298
1325
|
return commitWitnessed(state, txHandle)
|
|
1299
1326
|
}
|
|
1300
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* The witnessed retry loop. What it retries: the benign race — the
|
|
1330
|
+
* host's OWN interleaved commit landing between an attempt's snapshot
|
|
1331
|
+
* and its witnessed begin (every writer shares this handle, so a move
|
|
1332
|
+
* is always self-inflicted) — by rerunning the WHOLE callback on a
|
|
1333
|
+
* fresh snapshot, which converges because each rerun witnesses a
|
|
1334
|
+
* strictly newer generation. What it refuses: the pathology where the
|
|
1335
|
+
* callback itself (even indirectly) issues a plain `db.write` before
|
|
1336
|
+
* its first tx verb, moving the generation on EVERY attempt — an
|
|
1337
|
+
* unbounded loop would spin forever with no diagnostic, so past
|
|
1338
|
+
* {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
|
|
1339
|
+
* {@link ErrWitnessedLivelock} instead (the engine's ruling: the
|
|
1340
|
+
* error, never a loop — retry is host policy, and this is the host
|
|
1341
|
+
* policy's own honesty bound).
|
|
1342
|
+
*/
|
|
1301
1343
|
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
|
|
1302
|
-
for (
|
|
1344
|
+
for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
|
|
1303
1345
|
const attempt = witnessedAttempt(fn)
|
|
1304
1346
|
if (attempt !== undefined) {
|
|
1305
1347
|
return attempt
|
|
1306
1348
|
}
|
|
1307
1349
|
}
|
|
1350
|
+
throw errors.wrap(
|
|
1351
|
+
ErrWitnessedLivelock,
|
|
1352
|
+
`writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
|
|
1353
|
+
)
|
|
1308
1354
|
}
|
|
1309
1355
|
|
|
1310
1356
|
/**
|
|
@@ -1326,17 +1372,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1326
1372
|
if (member === undefined) {
|
|
1327
1373
|
throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
|
|
1328
1374
|
}
|
|
1329
|
-
const sealed =
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
"id",
|
|
1333
|
-
...member.data.columns.map(function columnName(column) {
|
|
1334
|
-
return column.name
|
|
1335
|
-
})
|
|
1336
|
-
]
|
|
1337
|
-
: member.data.fields.map(function fieldName(declared) {
|
|
1338
|
-
return declared.name
|
|
1339
|
-
})
|
|
1375
|
+
const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
|
|
1376
|
+
return declared.name
|
|
1377
|
+
})
|
|
1340
1378
|
sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
|
|
1341
1379
|
if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
|
|
1342
1380
|
throw errors.new(
|
|
@@ -1386,7 +1424,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1386
1424
|
handle: preparedHandle,
|
|
1387
1425
|
owner,
|
|
1388
1426
|
params: q.data.params,
|
|
1389
|
-
|
|
1427
|
+
finds: q.data.finds
|
|
1390
1428
|
})
|
|
1391
1429
|
)
|
|
1392
1430
|
planReclaimer.register(prepared, preparedHandle)
|
|
@@ -1581,4 +1619,4 @@ export type {
|
|
|
1581
1619
|
WitnessedWriteResult,
|
|
1582
1620
|
WriteResult
|
|
1583
1621
|
}
|
|
1584
|
-
export { abandon, Db, ErrNewtypeMismatch }
|
|
1622
|
+
export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
|
package/src/exhume.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import * as errors from "@superbuilders/errors"
|
|
28
28
|
import type { FactValue, Manifest } from "#native.ts"
|
|
29
|
-
import { native } from "#native.ts"
|
|
29
|
+
import { bridged, native } from "#native.ts"
|
|
30
30
|
import type { ValueTypeSpec } from "#spec.ts"
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -132,20 +132,6 @@ interface Exhumed {
|
|
|
132
132
|
scan(relation: string): readonly ExhumedFact[]
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/**
|
|
136
|
-
* The bridge guard (db.ts's twin over this module's calls): runs one
|
|
137
|
-
* native call and wraps anything it throws, so marshal-shape and
|
|
138
|
-
* handle-lifecycle refusals cross as genuine typed failures, never bare
|
|
139
|
-
* foreign errors.
|
|
140
|
-
*/
|
|
141
|
-
function bridged<T>(context: string, run: () => T): T {
|
|
142
|
-
const result = errors.trySync(run)
|
|
143
|
-
if (result.error) {
|
|
144
|
-
throw errors.wrap(result.error, context)
|
|
145
|
-
}
|
|
146
|
-
return result.data
|
|
147
|
-
}
|
|
148
|
-
|
|
149
135
|
/**
|
|
150
136
|
* Shapes the bridge's manifest rendering of the stored descriptor into the
|
|
151
137
|
* SDK's {@link ExhumedDescriptor}: same relations in the same engine-id
|
package/src/face.ts
CHANGED
|
@@ -101,11 +101,11 @@ type FaceSource = AnyRelation | AnyClosed | AnySelected | AnySelectedClosed
|
|
|
101
101
|
* names address the sealed shape).
|
|
102
102
|
*/
|
|
103
103
|
type FaceFields<S extends FaceSource> = S extends AnySelected
|
|
104
|
-
? keyof S["relation"]
|
|
104
|
+
? keyof RelationFields<S["relation"]> & string
|
|
105
105
|
: S extends AnySelectedClosed
|
|
106
106
|
? "id" | (keyof S["relation"]["columns"] & string)
|
|
107
107
|
: S extends AnyRelation
|
|
108
|
-
? keyof S
|
|
108
|
+
? keyof RelationFields<S> & string
|
|
109
109
|
: S extends { readonly axioms: Readonly<Record<string, infer Row>> }
|
|
110
110
|
? "id" | (keyof Row & string)
|
|
111
111
|
: never
|
|
@@ -263,7 +263,8 @@ function on<S extends FaceSource>(source: S, fields: string | readonly string[])
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
|
-
* The trusted seam of the face mint (the
|
|
266
|
+
* The trusted admission seam of the face mint (the pattern's home is
|
|
267
|
+
* `isTypedScope` in query/lower.ts): the
|
|
267
268
|
* checkable facts — the value carries exactly the source and projection it
|
|
268
269
|
* was built from, and `data.owner` is exactly the owner {@link faceParts}
|
|
269
270
|
* resolves for that source — are verified before the wide construction is
|