@bjornpagen/bumbledb 0.12.0 → 0.14.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 +10 -8
- package/README.md +5 -4
- package/dist/capacity.d.ts +2 -2
- package/dist/closed.d.ts +4 -4
- package/dist/db.d.ts +109 -60
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +90 -71
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +2 -2
- package/dist/face.d.ts +3 -3
- package/dist/fields.d.ts +1 -1
- package/dist/index.d.ts +32 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +5 -5
- package/dist/lower.d.ts +5 -5
- package/dist/marshal.d.ts +8 -30
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +4 -21
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +38 -21
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +4 -4
- package/dist/query/find.d.ts +18 -13
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +2 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +10 -10
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +5 -4
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +11 -9
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +3 -3
- package/dist/query/scope.d.ts +5 -5
- package/dist/relation.d.ts +6 -17
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -4
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +4 -4
- package/dist/statements.d.ts +4 -4
- package/package.json +4 -3
- package/src/db.ts +248 -168
- package/src/index.ts +8 -2
- package/src/marshal.ts +5 -35
- package/src/native.ts +34 -16
- package/src/query/find.ts +19 -14
- package/src/query/lower.ts +7 -6
- package/src/query/parse-ir.ts +11 -9
- package/src/relation.ts +3 -15
package/src/db.ts
CHANGED
|
@@ -37,17 +37,7 @@ import type { Exhumed } from "#exhume.ts"
|
|
|
37
37
|
import { exhumeStore } from "#exhume.ts"
|
|
38
38
|
import { rosterOf } from "#fields.ts"
|
|
39
39
|
import { lower } from "#lower.ts"
|
|
40
|
-
import {
|
|
41
|
-
factOf,
|
|
42
|
-
handleOf,
|
|
43
|
-
isFreshField,
|
|
44
|
-
isInserted,
|
|
45
|
-
type KeyFact,
|
|
46
|
-
keyRowOf,
|
|
47
|
-
type Minted,
|
|
48
|
-
recordOf,
|
|
49
|
-
rowOf
|
|
50
|
-
} from "#marshal.ts"
|
|
40
|
+
import { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
51
41
|
|
|
52
42
|
import type {
|
|
53
43
|
DbHandle,
|
|
@@ -56,6 +46,7 @@ import type {
|
|
|
56
46
|
PreparedHandle,
|
|
57
47
|
SnapshotHandle,
|
|
58
48
|
TxHandle,
|
|
49
|
+
WireFreshRange,
|
|
59
50
|
Violation as WireViolation,
|
|
60
51
|
ViolationFact as WireViolationFact
|
|
61
52
|
} from "#native.ts"
|
|
@@ -65,7 +56,7 @@ import type { Query } from "#query/lower.ts"
|
|
|
65
56
|
import { lowerQuery } from "#query/lower.ts"
|
|
66
57
|
import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
67
58
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
68
|
-
import type { AnyRelation, Fact,
|
|
59
|
+
import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
|
|
69
60
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
70
61
|
import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
71
62
|
|
|
@@ -76,6 +67,70 @@ import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
|
76
67
|
*/
|
|
77
68
|
type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
|
|
78
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Facts consumed vs facts that changed the in-memory final-state view.
|
|
72
|
+
* The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
|
|
73
|
+
*/
|
|
74
|
+
interface MutationReport {
|
|
75
|
+
readonly submitted: bigint
|
|
76
|
+
readonly changed: bigint
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Half-open fresh-id range from one `reserve`. Empty cannot yield a
|
|
81
|
+
* minted id — `start` exists only on the nonempty arm.
|
|
82
|
+
*/
|
|
83
|
+
type FreshRange =
|
|
84
|
+
| {
|
|
85
|
+
readonly empty: true
|
|
86
|
+
readonly count: 0n
|
|
87
|
+
at(index: bigint): undefined
|
|
88
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
readonly empty: false
|
|
92
|
+
readonly start: bigint
|
|
93
|
+
readonly endExclusive: bigint
|
|
94
|
+
readonly count: bigint
|
|
95
|
+
at(index: bigint): bigint | undefined
|
|
96
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function freshRangeOf(wire: WireFreshRange): FreshRange {
|
|
100
|
+
if (wire.empty) {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
empty: true,
|
|
103
|
+
count: 0n,
|
|
104
|
+
at(_index: bigint) {
|
|
105
|
+
return undefined
|
|
106
|
+
},
|
|
107
|
+
*[Symbol.iterator](): IterableIterator<bigint> {}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
const start = wire.start
|
|
111
|
+
const endExclusive = wire.endExclusive
|
|
112
|
+
const count = endExclusive - start
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
empty: false,
|
|
115
|
+
start,
|
|
116
|
+
endExclusive,
|
|
117
|
+
get count() {
|
|
118
|
+
return count
|
|
119
|
+
},
|
|
120
|
+
at(index: bigint) {
|
|
121
|
+
if (index < 0n || index >= count) {
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
return start + index
|
|
125
|
+
},
|
|
126
|
+
*[Symbol.iterator](): IterableIterator<bigint> {
|
|
127
|
+
for (let id = start; id < endExclusive; id++) {
|
|
128
|
+
yield id
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
79
134
|
/**
|
|
80
135
|
* The key object of a key-statement-selected `get`: exactly the selected
|
|
81
136
|
* `key()` statement's projection fields, each at the relation's own BARE
|
|
@@ -100,56 +155,88 @@ interface OffendingFact<Rels extends SchemaRelations> {
|
|
|
100
155
|
}
|
|
101
156
|
|
|
102
157
|
/**
|
|
103
|
-
*
|
|
104
|
-
* `statement` is
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
* Shared body of every violation arm: the engine's canonical rendering and
|
|
159
|
+
* the cited facts. `statement` is NOT here — its presence is the
|
|
160
|
+
* discriminant. Implied auto-keys have no SDK spelling (`statement` is
|
|
161
|
+
* the value `undefined`); every declared form carries the IDENTICAL
|
|
162
|
+
* statement value the schema declared (consumers `===`-match it).
|
|
163
|
+
*/
|
|
164
|
+
type ViolationBody<Rels extends SchemaRelations> = {
|
|
165
|
+
readonly canonical: string
|
|
166
|
+
readonly facts: readonly OffendingFact<Rels>[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A functionality violation of an engine-materialized fresh-implied or
|
|
171
|
+
* closed auto-key. These slots have no declared spelling (`schema()`
|
|
172
|
+
* rejects an explicit duplicate); `statement` is present and `undefined`.
|
|
173
|
+
*/
|
|
174
|
+
type ImpliedKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
175
|
+
readonly kind: "functionality"
|
|
176
|
+
readonly statement: undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A functionality violation of a declared `key()` statement. `statement`
|
|
181
|
+
* is the IDENTICAL SDK value the schema declared.
|
|
182
|
+
*/
|
|
183
|
+
type DeclaredKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
184
|
+
readonly kind: "functionality"
|
|
185
|
+
readonly statement: Statement
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A containment violation of a declared `contained()` statement (no
|
|
190
|
+
* `orientation` — that property exists exactly on {@link MirrorViolation}).
|
|
191
|
+
*/
|
|
192
|
+
type ContainmentViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
193
|
+
readonly kind: "containment"
|
|
194
|
+
readonly statement: Statement
|
|
195
|
+
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A containment violation of one slot of a declared `mirrors()` statement.
|
|
200
|
+
* BOTH materialized slots render as the one `==` utterance in the written
|
|
201
|
+
* orientation (identical `canonical` strings; the engine's `render.rs`
|
|
202
|
+
* never emits a bare `<=` for a mirrored pair). `direction` is relative
|
|
203
|
+
* to the violated SLOT's own orientation, so it alone cannot say which
|
|
204
|
+
* side of the `==` was violated: `written` is the `source <= target` slot
|
|
205
|
+
* as the statement was spelled, `mirrored` the engine-materialized
|
|
206
|
+
* `target <= source` partner.
|
|
207
|
+
*/
|
|
208
|
+
type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
209
|
+
readonly kind: "containment"
|
|
210
|
+
readonly statement: Statement
|
|
211
|
+
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
212
|
+
readonly orientation: "written" | "mirrored"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A capacity violation of a declared `capacity()` statement. `measure` is
|
|
217
|
+
* the engine's witnessed group total — u128-wide, crossing whole as
|
|
218
|
+
* bigint (C3: truncation is unrepresentable).
|
|
219
|
+
*/
|
|
220
|
+
type CapacityViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
|
|
221
|
+
readonly kind: "capacity"
|
|
222
|
+
readonly statement: Statement
|
|
223
|
+
readonly measure: bigint
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* One violated statement of a rejected commit, as a typed value. The
|
|
228
|
+
* arms are a true discriminant: `statement === undefined` is exactly the
|
|
229
|
+
* implied-auto-key arm; every declared form carries `Statement` (not
|
|
230
|
+
* `Statement | undefined`, not an omit-optional). `canonical` is the
|
|
231
|
+
* ENGINE's rendering. `direction` / `measure` pass through from the
|
|
232
|
+
* engine VERBATIM.
|
|
123
233
|
*/
|
|
124
234
|
type Violation<Rels extends SchemaRelations> =
|
|
125
|
-
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
| {
|
|
132
|
-
readonly kind: "containment"
|
|
133
|
-
readonly statement?: Statement
|
|
134
|
-
readonly canonical: string
|
|
135
|
-
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
136
|
-
readonly facts: readonly OffendingFact<Rels>[]
|
|
137
|
-
}
|
|
138
|
-
| {
|
|
139
|
-
readonly kind: "containment"
|
|
140
|
-
readonly statement?: Statement
|
|
141
|
-
readonly canonical: string
|
|
142
|
-
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
143
|
-
readonly orientation: "written" | "mirrored"
|
|
144
|
-
readonly facts: readonly OffendingFact<Rels>[]
|
|
145
|
-
}
|
|
146
|
-
| {
|
|
147
|
-
readonly kind: "capacity"
|
|
148
|
-
readonly statement?: Statement
|
|
149
|
-
readonly canonical: string
|
|
150
|
-
readonly measure: bigint
|
|
151
|
-
readonly facts: readonly OffendingFact<Rels>[]
|
|
152
|
-
}
|
|
235
|
+
| ImpliedKeyViolation<Rels>
|
|
236
|
+
| DeclaredKeyViolation<Rels>
|
|
237
|
+
| ContainmentViolation<Rels>
|
|
238
|
+
| MirrorViolation<Rels>
|
|
239
|
+
| CapacityViolation<Rels>
|
|
153
240
|
|
|
154
241
|
/**
|
|
155
242
|
* The abandoned arm of a write result (ruled 2026-07-23, R10): present in
|
|
@@ -265,21 +352,22 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
|
265
352
|
*/
|
|
266
353
|
interface Tx<Rels extends SchemaRelations> {
|
|
267
354
|
/**
|
|
268
|
-
* Records
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* that one spelling ({@link refuseShadowedChanged}) — never a silent
|
|
278
|
-
* shadow here.
|
|
355
|
+
* Records a collection of inserts. Singleton is `[fact]`. Empty is
|
|
356
|
+
* lawful. Returns how many facts were consumed and how many changed
|
|
357
|
+
* the in-memory final-state view. Every fact is complete — omitted
|
|
358
|
+
* fresh cells are a type error; mint first with {@link Tx.reserve}.
|
|
359
|
+
*/
|
|
360
|
+
insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
361
|
+
/**
|
|
362
|
+
* Records a collection of deletes. Singleton is `[fact]`. Returns
|
|
363
|
+
* how many facts were consumed and how many changed the view.
|
|
279
364
|
*/
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
|
|
365
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
366
|
+
/**
|
|
367
|
+
* Mints `count` consecutive fresh values for a `.fresh` field.
|
|
368
|
+
* `count === 0n` is empty and does not yield a start.
|
|
369
|
+
*/
|
|
370
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
283
371
|
/** Final-state membership of one complete fact. */
|
|
284
372
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
285
373
|
/**
|
|
@@ -451,18 +539,28 @@ interface PrimaryKey {
|
|
|
451
539
|
}
|
|
452
540
|
|
|
453
541
|
/**
|
|
454
|
-
* One materialized-statement slot as the SDK mirrors it
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* key's owner and projection (what keyed point reads resolve
|
|
542
|
+
* One materialized-statement slot as the SDK mirrors it. Implied auto-keys
|
|
543
|
+
* omit `statement` (the engine owns those slots); every declared form
|
|
544
|
+
* carries the SDK value that lowered to it. Functionality forms also
|
|
545
|
+
* carry the key's owner and projection (what keyed point reads resolve
|
|
546
|
+
* through).
|
|
458
547
|
*/
|
|
548
|
+
type ImpliedKeyEntry = {
|
|
549
|
+
readonly kind: "functionality"
|
|
550
|
+
readonly owner: string
|
|
551
|
+
readonly projection: readonly string[]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
type DeclaredKeyEntry = {
|
|
555
|
+
readonly kind: "functionality"
|
|
556
|
+
readonly statement: Statement
|
|
557
|
+
readonly owner: string
|
|
558
|
+
readonly projection: readonly string[]
|
|
559
|
+
}
|
|
560
|
+
|
|
459
561
|
type StatementEntry =
|
|
460
|
-
|
|
|
461
|
-
|
|
462
|
-
readonly statement?: Statement
|
|
463
|
-
readonly owner: string
|
|
464
|
-
readonly projection: readonly string[]
|
|
465
|
-
}
|
|
562
|
+
| ImpliedKeyEntry
|
|
563
|
+
| DeclaredKeyEntry
|
|
466
564
|
| { readonly kind: "containment"; readonly statement: Statement }
|
|
467
565
|
| { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
|
|
468
566
|
| { readonly kind: "capacity"; readonly statement: Statement }
|
|
@@ -749,44 +847,6 @@ const ErrGenerationMoved = errors.new(
|
|
|
749
847
|
"bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
|
|
750
848
|
)
|
|
751
849
|
|
|
752
|
-
/**
|
|
753
|
-
* Fills one insert's omitted fresh cells through the engine's
|
|
754
|
-
* alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
|
|
755
|
-
* spelling) and collects every fresh cell — minted or resupplied — for the
|
|
756
|
-
* insert's return. Mutates `values` in place with the minted cells.
|
|
757
|
-
*/
|
|
758
|
-
function mintFreshCells(
|
|
759
|
-
txHandle: TxHandle,
|
|
760
|
-
entry: RelationEntry,
|
|
761
|
-
relation: AnyRelation,
|
|
762
|
-
values: Record<string, unknown>
|
|
763
|
-
): Record<string, FactValue> {
|
|
764
|
-
const fresh: Record<string, FactValue> = {}
|
|
765
|
-
for (const declared of relation.data.fields) {
|
|
766
|
-
if (!isFreshField(declared.field)) {
|
|
767
|
-
continue
|
|
768
|
-
}
|
|
769
|
-
let cell = values[declared.name]
|
|
770
|
-
if (cell === undefined) {
|
|
771
|
-
const fieldId = entry.fieldIds.get(declared.name)
|
|
772
|
-
if (fieldId === undefined) {
|
|
773
|
-
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${declared.name}`)
|
|
774
|
-
}
|
|
775
|
-
cell = bridged("bumbledb tx alloc", function mint() {
|
|
776
|
-
return native.txAlloc(txHandle, entry.id, fieldId)
|
|
777
|
-
})
|
|
778
|
-
values[declared.name] = cell
|
|
779
|
-
}
|
|
780
|
-
if (typeof cell !== "bigint") {
|
|
781
|
-
throw errors.new(
|
|
782
|
-
`relation ${relation.name} field ${declared.name}: a fresh cell is a u64 bigint, got ${typeof cell}`
|
|
783
|
-
)
|
|
784
|
-
}
|
|
785
|
-
fresh[declared.name] = cell
|
|
786
|
-
}
|
|
787
|
-
return fresh
|
|
788
|
-
}
|
|
789
|
-
|
|
790
850
|
/**
|
|
791
851
|
* Constructs one open `Db` over an already-admitted handle: builds the
|
|
792
852
|
* id-resolution tables once and closes over them — the `Db` owns handle
|
|
@@ -841,16 +901,24 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
841
901
|
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
842
902
|
}
|
|
843
903
|
const facts = Object.freeze(wire.facts.map(offendingFactOf))
|
|
844
|
-
const statement = entry.statement
|
|
845
904
|
const canonical = wire.canonical
|
|
846
905
|
if (entry.kind === "functionality") {
|
|
847
|
-
|
|
906
|
+
if (!("statement" in entry)) {
|
|
907
|
+
return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
|
|
908
|
+
}
|
|
909
|
+
return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
|
|
848
910
|
}
|
|
849
911
|
if (entry.kind === "capacity") {
|
|
850
912
|
if (wire.kind !== "capacity") {
|
|
851
913
|
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
|
|
852
914
|
}
|
|
853
|
-
return Object.freeze({
|
|
915
|
+
return Object.freeze({
|
|
916
|
+
kind: "capacity",
|
|
917
|
+
statement: entry.statement,
|
|
918
|
+
canonical,
|
|
919
|
+
measure: wire.measure,
|
|
920
|
+
facts
|
|
921
|
+
})
|
|
854
922
|
}
|
|
855
923
|
if (wire.kind !== "containment") {
|
|
856
924
|
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
|
|
@@ -858,14 +926,20 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
858
926
|
if (entry.kind === "mirrors") {
|
|
859
927
|
return Object.freeze({
|
|
860
928
|
kind: "containment",
|
|
861
|
-
statement,
|
|
929
|
+
statement: entry.statement,
|
|
862
930
|
canonical,
|
|
863
931
|
direction: wire.direction,
|
|
864
932
|
orientation: entry.orientation,
|
|
865
933
|
facts
|
|
866
934
|
})
|
|
867
935
|
}
|
|
868
|
-
return Object.freeze({
|
|
936
|
+
return Object.freeze({
|
|
937
|
+
kind: "containment",
|
|
938
|
+
statement: entry.statement,
|
|
939
|
+
canonical,
|
|
940
|
+
direction: wire.direction,
|
|
941
|
+
facts
|
|
942
|
+
})
|
|
869
943
|
}
|
|
870
944
|
|
|
871
945
|
/**
|
|
@@ -877,7 +951,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
877
951
|
*/
|
|
878
952
|
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
879
953
|
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
880
|
-
return candidate.statement === statement
|
|
954
|
+
return "statement" in candidate && candidate.statement === statement
|
|
881
955
|
})
|
|
882
956
|
const entry = tables.statements[statementId]
|
|
883
957
|
if (entry === undefined) {
|
|
@@ -1159,38 +1233,59 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1159
1233
|
})
|
|
1160
1234
|
}
|
|
1161
1235
|
})
|
|
1162
|
-
function insert<R extends MemberRelation<Rels>>(
|
|
1163
|
-
relation: R,
|
|
1164
|
-
fact: InsertFact<R>
|
|
1165
|
-
): { readonly changed: boolean } & Minted<R> {
|
|
1236
|
+
function insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1166
1237
|
assertLive()
|
|
1238
|
+
const rows: FactValue[][] = []
|
|
1239
|
+
for (const fact of facts) {
|
|
1240
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1241
|
+
}
|
|
1167
1242
|
const entry = resolveOrdinary(relation)
|
|
1168
1243
|
const txHandle = resolveTx()
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const fresh = mintFreshCells(txHandle, entry, relation, values)
|
|
1172
|
-
const row = rowOf(relation.data, values)
|
|
1173
|
-
const changed = bridged("bumbledb tx insert", function record() {
|
|
1174
|
-
return native.txInsert(txHandle, entry.id, row)
|
|
1244
|
+
const report = bridged("bumbledb tx insert", function record() {
|
|
1245
|
+
return native.txInsert(txHandle, entry.id, rows)
|
|
1175
1246
|
})
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1247
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1248
|
+
}
|
|
1249
|
+
function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1250
|
+
assertLive()
|
|
1251
|
+
const rows: FactValue[][] = []
|
|
1252
|
+
for (const fact of facts) {
|
|
1253
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1179
1254
|
}
|
|
1180
|
-
|
|
1255
|
+
const entry = resolveOrdinary(relation)
|
|
1256
|
+
const txHandle = resolveTx()
|
|
1257
|
+
const report = bridged("bumbledb tx delete", function record() {
|
|
1258
|
+
return native.txDelete(txHandle, entry.id, rows)
|
|
1259
|
+
})
|
|
1260
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1181
1261
|
}
|
|
1182
|
-
function
|
|
1262
|
+
function reserve<R extends MemberRelation<Rels>>(
|
|
1263
|
+
relation: R,
|
|
1264
|
+
field: FreshKeys<R> & string,
|
|
1265
|
+
count: bigint
|
|
1266
|
+
): FreshRange {
|
|
1183
1267
|
assertLive()
|
|
1184
1268
|
const entry = resolveOrdinary(relation)
|
|
1269
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
1270
|
+
return candidate.name === field
|
|
1271
|
+
})
|
|
1272
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
1273
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
1274
|
+
}
|
|
1275
|
+
const fieldId = entry.fieldIds.get(field)
|
|
1276
|
+
if (fieldId === undefined) {
|
|
1277
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
1278
|
+
}
|
|
1185
1279
|
const txHandle = resolveTx()
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
return native.txDelete(txHandle, entry.id, row)
|
|
1280
|
+
const range = bridged("bumbledb tx reserve", function mint() {
|
|
1281
|
+
return native.txReserve(txHandle, entry.id, fieldId, count)
|
|
1189
1282
|
})
|
|
1283
|
+
return freshRangeOf(range)
|
|
1190
1284
|
}
|
|
1191
1285
|
const tx: Tx<Rels> = Object.freeze({
|
|
1192
1286
|
insert,
|
|
1193
1287
|
delete: remove,
|
|
1288
|
+
reserve,
|
|
1194
1289
|
contains: reads.contains,
|
|
1195
1290
|
get: reads.get
|
|
1196
1291
|
})
|
|
@@ -1362,27 +1457,6 @@ const ErrNewtypeMismatch = errors.new(
|
|
|
1362
1457
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1363
1458
|
)
|
|
1364
1459
|
|
|
1365
|
-
/**
|
|
1366
|
-
* `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
|
|
1367
|
-
* where the spread wins: a FRESH field literally named `changed` would
|
|
1368
|
-
* shadow the engine's changed-state report on every insert of its
|
|
1369
|
-
* relation. No field name is reserved SILENTLY — the one unspeakable
|
|
1370
|
-
* spelling is refused here at admission, before any store is touched.
|
|
1371
|
-
* Supplied (non-fresh) fields named `changed` never enter the return
|
|
1372
|
-
* record and stay legal.
|
|
1373
|
-
*/
|
|
1374
|
-
function refuseShadowedChanged(theory: AnySchema): void {
|
|
1375
|
-
for (const [name, member] of Object.entries(theory.relations)) {
|
|
1376
|
-
for (const declared of sealedFieldsOf(member)) {
|
|
1377
|
-
if (declared.name === "changed" && isFreshField(declared.field)) {
|
|
1378
|
-
throw errors.new(
|
|
1379
|
-
`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)`
|
|
1380
|
-
)
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
1460
|
/**
|
|
1387
1461
|
* The one admission path both verbs share: lower the theory, run one
|
|
1388
1462
|
* bridge call, and wrap the domain refusals — `schemaError` (spec
|
|
@@ -1399,7 +1473,6 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1399
1473
|
storePath: string,
|
|
1400
1474
|
theory: Schema<Rels>
|
|
1401
1475
|
): Db<Rels> {
|
|
1402
|
-
refuseShadowedChanged(theory)
|
|
1403
1476
|
const canonical = path.resolve(storePath)
|
|
1404
1477
|
const spec = lower(theory)
|
|
1405
1478
|
const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
|
|
@@ -1466,9 +1539,16 @@ const Db = Object.freeze({
|
|
|
1466
1539
|
export type {
|
|
1467
1540
|
Abandon,
|
|
1468
1541
|
AbandonedArm,
|
|
1542
|
+
CapacityViolation,
|
|
1543
|
+
ContainmentViolation,
|
|
1469
1544
|
DeclaredKeyFact,
|
|
1545
|
+
DeclaredKeyViolation,
|
|
1470
1546
|
DeltaBuild,
|
|
1547
|
+
FreshRange,
|
|
1548
|
+
ImpliedKeyViolation,
|
|
1471
1549
|
MemberRelation,
|
|
1550
|
+
MirrorViolation,
|
|
1551
|
+
MutationReport,
|
|
1472
1552
|
OffendingFact,
|
|
1473
1553
|
Prepared,
|
|
1474
1554
|
ReadScope,
|
package/src/index.ts
CHANGED
|
@@ -51,9 +51,16 @@ export { closed } from "#closed.ts"
|
|
|
51
51
|
export type {
|
|
52
52
|
Abandon,
|
|
53
53
|
AbandonedArm,
|
|
54
|
+
CapacityViolation,
|
|
55
|
+
ContainmentViolation,
|
|
54
56
|
DeclaredKeyFact,
|
|
57
|
+
DeclaredKeyViolation,
|
|
55
58
|
DeltaBuild,
|
|
59
|
+
FreshRange,
|
|
60
|
+
ImpliedKeyViolation,
|
|
56
61
|
MemberRelation,
|
|
62
|
+
MirrorViolation,
|
|
63
|
+
MutationReport,
|
|
57
64
|
OffendingFact,
|
|
58
65
|
Prepared,
|
|
59
66
|
ReadScope,
|
|
@@ -109,7 +116,7 @@ export type {
|
|
|
109
116
|
export { bool, bytes, i64, interval, span, str, u64 } from "#fields.ts"
|
|
110
117
|
export type { ClassesOf, ClassWall, LawfulStatements, RelationClasses, SchemaClasses } from "#law.ts"
|
|
111
118
|
export { lower, lowerClosed, lowerRelation } from "#lower.ts"
|
|
112
|
-
export type { KeyFact
|
|
119
|
+
export type { KeyFact } from "#marshal.ts"
|
|
113
120
|
export type { FactValue, ParsedQuery, QueryIr, StatementKindTag } from "#native.ts"
|
|
114
121
|
|
|
115
122
|
export type {
|
|
@@ -164,7 +171,6 @@ export type {
|
|
|
164
171
|
Fact,
|
|
165
172
|
FieldsShape,
|
|
166
173
|
FreshKeys,
|
|
167
|
-
InsertFact,
|
|
168
174
|
Relation,
|
|
169
175
|
RelationData,
|
|
170
176
|
RelationField,
|
package/src/marshal.ts
CHANGED
|
@@ -43,13 +43,6 @@ function isFreshField(field: AnyField): boolean {
|
|
|
43
43
|
return "fresh" in field && field.fresh === true
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* The inferred object type `tx.insert` returns: one property per
|
|
48
|
-
* fresh-marked field of `R`, carrying the minted (or resupplied) id as a
|
|
49
|
-
* bare `bigint`. A relation with no fresh field returns the empty object.
|
|
50
|
-
*/
|
|
51
|
-
type Minted<R extends AnyRelation> = { [K in FreshKeys<R>]: Fact<R>[K] }
|
|
52
|
-
|
|
53
46
|
/**
|
|
54
47
|
* The key object `get` reads through. THE PRIMARY-KEY RULE: `get` always
|
|
55
48
|
* reads through the PRIMARY candidate key — the first-declared one in the
|
|
@@ -74,10 +67,7 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
|
|
|
74
67
|
* ALLOCATION-FREE IDENTITY (the admission predicate is the type
|
|
75
68
|
* reprojection; the value passes through untouched): every consumer
|
|
76
69
|
* downstream — `rowOf`, `keyRowOf`, the query param marshal — only READS
|
|
77
|
-
* properties, so no copy is warranted.
|
|
78
|
-
* (`mintFreshCells` on the insert path) takes its own spread copy at the
|
|
79
|
-
* call site, so the caller's fact object is never written through this
|
|
80
|
-
* seam.
|
|
70
|
+
* properties, so no copy is warranted.
|
|
81
71
|
*/
|
|
82
72
|
function recordOf(fact: object): Readonly<Record<string, unknown>> {
|
|
83
73
|
if (!isStringIndexed(fact)) {
|
|
@@ -203,9 +193,8 @@ function cellOf(context: string, field: AnyField, value: unknown): FactValue {
|
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
195
|
* Marshals one complete fact object to its positional row, in field
|
|
206
|
-
* declaration order (= ordinal ids). Every declared field must be present
|
|
207
|
-
*
|
|
208
|
-
* fresh cells via the engine's alloc lane).
|
|
196
|
+
* declaration order (= ordinal ids). Every declared field must be present.
|
|
197
|
+
* Mint with `tx.reserve` first; insert takes complete facts.
|
|
209
198
|
*/
|
|
210
199
|
function rowOf(relation: RelationData, fact: Readonly<Record<string, unknown>>): FactValue[] {
|
|
211
200
|
return relation.fields.map(function marshalCell(declared) {
|
|
@@ -260,25 +249,6 @@ function isCompleteFact<R extends AnyRelation>(
|
|
|
260
249
|
})
|
|
261
250
|
}
|
|
262
251
|
|
|
263
|
-
/**
|
|
264
|
-
* The insert-return trusted seam (R11): one insert's return carries the
|
|
265
|
-
* engine's changed-state report beside the collected fresh cells (minted by
|
|
266
|
-
* the engine or resupplied by the caller) — the bit is verified boolean and
|
|
267
|
-
* the fresh ids present, same presence-only direction as
|
|
268
|
-
* {@link isCompleteFact}.
|
|
269
|
-
*/
|
|
270
|
-
function isInserted<R extends AnyRelation>(
|
|
271
|
-
relation: R,
|
|
272
|
-
value: Readonly<Record<string, FactValue | boolean>>
|
|
273
|
-
): value is Readonly<Record<string, FactValue | boolean>> & { readonly changed: boolean } & Minted<R> {
|
|
274
|
-
return (
|
|
275
|
-
typeof value.changed === "boolean" &&
|
|
276
|
-
relation.data.fields.every(function presentWhenFresh(declared) {
|
|
277
|
-
return !isFreshField(declared.field) || value[declared.name] !== undefined
|
|
278
|
-
})
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
252
|
/**
|
|
283
253
|
* Unmarshals one positional row to the relation's named, frozen fact object
|
|
284
254
|
* of bare structural values — the inverse of {@link rowOf},
|
|
@@ -310,5 +280,5 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
|
|
|
310
280
|
return decoded
|
|
311
281
|
}
|
|
312
282
|
|
|
313
|
-
export type { KeyFact
|
|
314
|
-
export { cellOf, factOf, handleOf, isFreshField,
|
|
283
|
+
export type { KeyFact }
|
|
284
|
+
export { cellOf, factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf }
|