@bjornpagen/bumbledb 0.4.0 → 0.5.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 +181 -47
- package/README.md +4 -2
- 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 +87 -70
- 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 +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- 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 +18 -15
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +9 -16
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +5 -8
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +87 -76
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +34 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +1 -2
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +2 -16
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +7 -47
- 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 +122 -84
- package/src/exhume.ts +1 -15
- package/src/face.ts +4 -3
- package/src/fields.ts +58 -16
- package/src/index.ts +10 -8
- 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 +13 -21
- package/src/query/lower.ts +90 -91
- package/src/query/predicate.ts +39 -4
- package/src/query/run.ts +1 -3
- package/src/query/scope.ts +7 -59
- package/src/relation.ts +9 -51
- package/src/schema.ts +7 -33
- package/src/statements.ts +33 -38
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"
|
|
64
|
+
import { bridged, native } from "#native.ts"
|
|
63
65
|
import type { SelectColumn } 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>
|
|
@@ -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
|
}
|
|
@@ -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(
|
|
@@ -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
|
package/src/fields.ts
CHANGED
|
@@ -172,15 +172,31 @@ type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
|
|
|
172
172
|
: never
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* The typed shape refusal
|
|
175
|
+
* The typed shape refusal shared by every literal machine — the selection
|
|
176
|
+
* lowering here, the row marshaler (`marshal.ts`), and the query-literal
|
|
177
|
+
* tagger (`query/lower.ts`) all throw through this ONE voice; reached only
|
|
176
178
|
* through ill-typed input (the well-typed surfaces make it unrepresentable).
|
|
177
179
|
*/
|
|
178
|
-
function literalShapeError(expected: string, value: unknown): Error {
|
|
179
|
-
return errors.new(
|
|
180
|
+
function literalShapeError(context: string, expected: string, value: unknown): Error {
|
|
181
|
+
return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The roster a field descriptor carries — THE one reader: present exactly
|
|
186
|
+
* on a closed-reference descriptor (the structural `closed` property of
|
|
187
|
+
* {@link ClosedIdField}), absent on every other field kind. Tolerates
|
|
188
|
+
* `undefined` so name-lookup misses flow through without a re-spelled
|
|
189
|
+
* probe at every call site.
|
|
190
|
+
*/
|
|
191
|
+
function rosterOf(field: AnyField | undefined): ClosedRoster | undefined {
|
|
192
|
+
if (field !== undefined && "closed" in field) {
|
|
193
|
+
return field.closed
|
|
194
|
+
}
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Narrows an interval-shaped value: a plain object with bigint start/end — THE one interval predicate. */
|
|
199
|
+
function isIntervalValue(value: unknown): value is IntervalValue {
|
|
184
200
|
return (
|
|
185
201
|
typeof value === "object" &&
|
|
186
202
|
value !== null &&
|
|
@@ -202,7 +218,7 @@ function isIntervalLiteral(value: unknown): value is IntervalValue {
|
|
|
202
218
|
*/
|
|
203
219
|
function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
|
|
204
220
|
if (typeof value !== "string") {
|
|
205
|
-
throw literalShapeError(`a ${closed.name} handle name (string)`, value)
|
|
221
|
+
throw literalShapeError("selection literal", `a ${closed.name} handle name (string)`, value)
|
|
206
222
|
}
|
|
207
223
|
if (!closed.handles.includes(value)) {
|
|
208
224
|
throw errors.new(`"${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`)
|
|
@@ -212,8 +228,8 @@ function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
|
|
|
212
228
|
|
|
213
229
|
/** Lowers one interval literal at its element type. */
|
|
214
230
|
function intervalLiteral(element: "u64" | "i64", value: unknown): LiteralSpec {
|
|
215
|
-
if (!
|
|
216
|
-
throw literalShapeError("interval ({ start, end } bigints)", value)
|
|
231
|
+
if (!isIntervalValue(value)) {
|
|
232
|
+
throw literalShapeError("selection literal", "interval ({ start, end } bigints)", value)
|
|
217
233
|
}
|
|
218
234
|
if (element === "u64") {
|
|
219
235
|
return { kind: "value", value: { kind: "intervalU64", start: value.start, end: value.end } }
|
|
@@ -306,37 +322,50 @@ function interval(element: U64Field | I64Field, width?: bigint): IntervalField<"
|
|
|
306
322
|
* else lowers to a plain value tagged by the field's structural kind.
|
|
307
323
|
*/
|
|
308
324
|
function literalOf(field: AnyField, value: unknown): LiteralSpec {
|
|
309
|
-
|
|
310
|
-
|
|
325
|
+
const roster = rosterOf(field)
|
|
326
|
+
if (roster !== undefined) {
|
|
327
|
+
return handleLiteral(roster, value)
|
|
311
328
|
}
|
|
312
329
|
switch (field.kind) {
|
|
313
330
|
case "bool": {
|
|
314
331
|
if (typeof value !== "boolean") {
|
|
315
|
-
throw literalShapeError("boolean", value)
|
|
332
|
+
throw literalShapeError("selection literal", "boolean", value)
|
|
316
333
|
}
|
|
317
334
|
return { kind: "value", value: { kind: "bool", value } }
|
|
318
335
|
}
|
|
319
336
|
case "u64": {
|
|
320
337
|
if (typeof value !== "bigint") {
|
|
321
|
-
throw literalShapeError("bigint", value)
|
|
338
|
+
throw literalShapeError("selection literal", "bigint", value)
|
|
322
339
|
}
|
|
323
340
|
return { kind: "value", value: { kind: "u64", value } }
|
|
324
341
|
}
|
|
325
342
|
case "i64": {
|
|
326
343
|
if (typeof value !== "bigint") {
|
|
327
|
-
throw literalShapeError("bigint", value)
|
|
344
|
+
throw literalShapeError("selection literal", "bigint", value)
|
|
328
345
|
}
|
|
329
346
|
return { kind: "value", value: { kind: "i64", value } }
|
|
330
347
|
}
|
|
331
348
|
case "str": {
|
|
332
349
|
if (typeof value !== "string") {
|
|
333
|
-
throw literalShapeError("string", value)
|
|
350
|
+
throw literalShapeError("selection literal", "string", value)
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The marshal's bijection law at the schema-literal seam
|
|
354
|
+
* (`marshal.ts` cellOf): a lone surrogate would cross dbCreate
|
|
355
|
+
* lossily (stored as U+FFFD engine-side), collapsing two
|
|
356
|
+
* distinct TS schema values into one stored theory/fingerprint
|
|
357
|
+
* and splitting the canonical statement rendering from the
|
|
358
|
+
* SDK's. All three string-admission seams — fact row, query
|
|
359
|
+
* literal/param, schema literal — enforce the one law.
|
|
360
|
+
*/
|
|
361
|
+
if (!value.isWellFormed()) {
|
|
362
|
+
throw literalShapeError("selection literal", "well-formed string", value)
|
|
334
363
|
}
|
|
335
364
|
return { kind: "value", value: { kind: "string", value } }
|
|
336
365
|
}
|
|
337
366
|
case "bytes": {
|
|
338
367
|
if (!(value instanceof Uint8Array)) {
|
|
339
|
-
throw literalShapeError("Uint8Array", value)
|
|
368
|
+
throw literalShapeError("selection literal", "Uint8Array", value)
|
|
340
369
|
}
|
|
341
370
|
return { kind: "value", value: { kind: "fixedBytes", value } }
|
|
342
371
|
}
|
|
@@ -359,4 +388,17 @@ export type {
|
|
|
359
388
|
StrField,
|
|
360
389
|
U64Field
|
|
361
390
|
}
|
|
362
|
-
export {
|
|
391
|
+
export {
|
|
392
|
+
assertDeclarationOrderKey,
|
|
393
|
+
bool,
|
|
394
|
+
bytes,
|
|
395
|
+
i64,
|
|
396
|
+
interval,
|
|
397
|
+
isIntervalValue,
|
|
398
|
+
literalOf,
|
|
399
|
+
literalShapeError,
|
|
400
|
+
rosterOf,
|
|
401
|
+
span,
|
|
402
|
+
str,
|
|
403
|
+
u64
|
|
404
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
* `db.prepare` as a plain value; the comparison/connective builders are
|
|
15
15
|
* also free exports, and the free names `eq`/`not`/`and`/`or` collide with
|
|
16
16
|
* common host identifiers — import aliasing is the answer; the SDK does
|
|
17
|
-
* not rename for collision-avoidance),
|
|
17
|
+
* not rename for collision-avoidance), the exhume surface
|
|
18
18
|
* (`Db.exhume` — the one schema-independent read path: the store's
|
|
19
19
|
* self-described shapes and raw facts by name, typed at bare structural
|
|
20
|
-
* values, deliberately schema-free)
|
|
20
|
+
* values, deliberately schema-free), and the answer-ordering helpers
|
|
21
|
+
* (`by`/`desc` — sort keys as data for the language's own `.sort`; answers
|
|
22
|
+
* are sets, the engine never orders, and limit is the language's own
|
|
23
|
+
* `.slice`). The raw native bridge is not exported.
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
26
|
export type {
|
|
@@ -50,7 +53,7 @@ export type {
|
|
|
50
53
|
WitnessedWriteResult,
|
|
51
54
|
WriteResult
|
|
52
55
|
} from "#db.ts"
|
|
53
|
-
export { abandon, Db, ErrNewtypeMismatch } from "#db.ts"
|
|
56
|
+
export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP } from "#db.ts"
|
|
54
57
|
export type {
|
|
55
58
|
Exhumed,
|
|
56
59
|
ExhumedAxiom,
|
|
@@ -105,6 +108,8 @@ export type {
|
|
|
105
108
|
Staleness,
|
|
106
109
|
StatementKindTag
|
|
107
110
|
} from "#native.ts"
|
|
111
|
+
export type { Desc, SortKey } from "#order.ts"
|
|
112
|
+
export { by, desc } from "#order.ts"
|
|
108
113
|
|
|
109
114
|
export type {
|
|
110
115
|
AnyCond,
|
|
@@ -119,7 +124,7 @@ export type {
|
|
|
119
124
|
SelectColumn,
|
|
120
125
|
Tree
|
|
121
126
|
} from "#query/atom.ts"
|
|
122
|
-
export { ALLEN, allen, and,
|
|
127
|
+
export { ALLEN, allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
|
|
123
128
|
export type {
|
|
124
129
|
AnyQuery,
|
|
125
130
|
AnyRuleValue,
|
|
@@ -150,16 +155,13 @@ export type {
|
|
|
150
155
|
ParamEntry,
|
|
151
156
|
ParamsRecord,
|
|
152
157
|
SetParam,
|
|
153
|
-
Var
|
|
154
|
-
VarsRecord
|
|
158
|
+
Var
|
|
155
159
|
} from "#query/scope.ts"
|
|
156
160
|
export type { Agg, SelectEntry } from "#query/select.ts"
|
|
157
161
|
export type {
|
|
158
162
|
AnyRelation,
|
|
159
163
|
AnySelected,
|
|
160
164
|
Fact,
|
|
161
|
-
FieldRef,
|
|
162
|
-
FieldRefs,
|
|
163
165
|
FieldsShape,
|
|
164
166
|
FreshKeys,
|
|
165
167
|
InsertFact,
|
package/src/law.ts
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
import * as errors from "@superbuilders/errors"
|
|
54
54
|
import type { AnyClosed } from "#closed.ts"
|
|
55
|
+
import { isClosedMember, sealedFieldsOf } from "#closed.ts"
|
|
55
56
|
import type { FaceData } from "#face.ts"
|
|
56
57
|
import type { AnyRelation, RelationFields } from "#relation.ts"
|
|
57
58
|
import type { SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
@@ -328,22 +329,21 @@ interface MemberCoords {
|
|
|
328
329
|
readonly fields: ReadonlyArray<{ readonly name: string; readonly generator: boolean }>
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Reads every member's coordinates off the relation record, declaration
|
|
334
|
+
* order throughout — the sealed shape through THE one reader
|
|
335
|
+
* (`sealedFieldsOf`): a closed member's generator is its synthetic `id`
|
|
336
|
+
* (ordinal 0), an ordinary member's generators are its fresh-marked fields.
|
|
337
|
+
*/
|
|
332
338
|
function memberCoords(relations: SchemaRelations): MemberCoords[] {
|
|
333
339
|
const out: MemberCoords[] = []
|
|
334
340
|
for (const [relationName, member] of Object.entries(relations)) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
]
|
|
342
|
-
out.push({ relation: relationName, fields })
|
|
343
|
-
continue
|
|
344
|
-
}
|
|
345
|
-
const fields = member.data.fields.map(function fieldCoord(declared) {
|
|
346
|
-
return { name: declared.name, generator: "fresh" in declared.field && declared.field.fresh === true }
|
|
341
|
+
const closed = isClosedMember(member)
|
|
342
|
+
const fields = sealedFieldsOf(member).map(function fieldCoord(declared) {
|
|
343
|
+
return {
|
|
344
|
+
name: declared.name,
|
|
345
|
+
generator: closed ? declared.name === "id" : "fresh" in declared.field && declared.field.fresh === true
|
|
346
|
+
}
|
|
347
347
|
})
|
|
348
348
|
out.push({ relation: relationName, fields })
|
|
349
349
|
}
|
|
@@ -490,7 +490,8 @@ function computeClasses(name: string, relations: SchemaRelations, statements: re
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
|
-
* The trusted seam of the class-map mint (the
|
|
493
|
+
* The trusted admission seam of the class-map mint (the pattern's home is
|
|
494
|
+
* `isTypedScope` in query/lower.ts): the
|
|
494
495
|
* checkable facts — one own record per declared relation, one own entry
|
|
495
496
|
* per declared field (the closed sealed shape's `id` included), everything
|
|
496
497
|
* frozen — are verified before the runtime map is admitted at the computed
|
package/src/lower.ts
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { AnyClosed } from "#closed.ts"
|
|
12
|
+
import { isClosedMember } from "#closed.ts"
|
|
12
13
|
import type { FaceData } from "#face.ts"
|
|
13
14
|
import type { AnyField } from "#fields.ts"
|
|
14
15
|
import type { RelationClasses } from "#law.ts"
|
|
15
16
|
import type { AnyRelation } from "#relation.ts"
|
|
16
|
-
import type { AnySchema
|
|
17
|
+
import type { AnySchema } from "#schema.ts"
|
|
17
18
|
import type {
|
|
18
19
|
FieldSpec,
|
|
19
20
|
LiteralSetSpec,
|
|
@@ -25,14 +26,6 @@ import type {
|
|
|
25
26
|
} from "#spec.ts"
|
|
26
27
|
import type { Statement } from "#statements.ts"
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* The relation-kind discriminant: a closed relation's runtime description
|
|
30
|
-
* carries its handle roster, an ordinary relation's never does.
|
|
31
|
-
*/
|
|
32
|
-
function isClosedMember(member: SchemaRelation): member is AnyClosed {
|
|
33
|
-
return "handles" in member.data
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
/**
|
|
37
30
|
* Lowers one field descriptor's structural type to the wire
|
|
38
31
|
* {@link ValueTypeSpec}: the S1 kind tags map 1:1 onto the `ValueType`
|