@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.
Files changed (53) hide show
  1. package/COOKBOOK.md +10 -8
  2. package/README.md +5 -4
  3. package/dist/capacity.d.ts +2 -2
  4. package/dist/closed.d.ts +4 -4
  5. package/dist/db.d.ts +109 -60
  6. package/dist/db.d.ts.map +1 -1
  7. package/dist/db.js +90 -71
  8. package/dist/db.js.map +1 -1
  9. package/dist/exhume.d.ts +2 -2
  10. package/dist/face.d.ts +3 -3
  11. package/dist/fields.d.ts +1 -1
  12. package/dist/index.d.ts +32 -32
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/law.d.ts +5 -5
  16. package/dist/lower.d.ts +5 -5
  17. package/dist/marshal.d.ts +8 -30
  18. package/dist/marshal.d.ts.map +1 -1
  19. package/dist/marshal.js +4 -21
  20. package/dist/marshal.js.map +1 -1
  21. package/dist/native.d.ts +38 -21
  22. package/dist/native.d.ts.map +1 -1
  23. package/dist/native.js.map +1 -1
  24. package/dist/query/atom.d.ts +4 -4
  25. package/dist/query/find.d.ts +18 -13
  26. package/dist/query/find.d.ts.map +1 -1
  27. package/dist/query/find.js +2 -2
  28. package/dist/query/find.js.map +1 -1
  29. package/dist/query/lower.d.ts +10 -10
  30. package/dist/query/lower.d.ts.map +1 -1
  31. package/dist/query/lower.js +5 -4
  32. package/dist/query/lower.js.map +1 -1
  33. package/dist/query/parse-ir.d.ts +1 -1
  34. package/dist/query/parse-ir.d.ts.map +1 -1
  35. package/dist/query/parse-ir.js +11 -9
  36. package/dist/query/parse-ir.js.map +1 -1
  37. package/dist/query/run.d.ts +3 -3
  38. package/dist/query/scope.d.ts +5 -5
  39. package/dist/relation.d.ts +6 -17
  40. package/dist/relation.d.ts.map +1 -1
  41. package/dist/relation.js +3 -4
  42. package/dist/relation.js.map +1 -1
  43. package/dist/schema.d.ts +4 -4
  44. package/dist/statements.d.ts +4 -4
  45. package/package.json +4 -3
  46. package/src/db.ts +248 -168
  47. package/src/index.ts +8 -2
  48. package/src/marshal.ts +5 -35
  49. package/src/native.ts +34 -16
  50. package/src/query/find.ts +19 -14
  51. package/src/query/lower.ts +7 -6
  52. package/src/query/parse-ir.ts +11 -9
  53. 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, InsertFact } from "#relation.ts"
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
- * One violated statement of a rejected commit, as a typed value.
104
- * `statement` is the IDENTICAL SDK statement value the schema declared —
105
- * consumers `===`-match it against their own constants; it is `undefined`
106
- * exactly for the engine-materialized fresh-implied and closed auto-keys,
107
- * which have no declared spelling (`schema()` rejects an explicit
108
- * duplicate of them). `canonical` is the ENGINE's rendering of the
109
- * violated materialized statement — for a `mirrors` statement BOTH
110
- * materialized slots render as the one `==` utterance in the written
111
- * orientation (identical strings; the engine's `render.rs` renders each
112
- * partner of a mirrored pair as the `==` spelling, never a bare `<=`
113
- * direction). `direction` (`sourceUnsatisfied` | `targetRequired`) and
114
- * `measure` are the containment/capacity form payloads, passed through
115
- * from the engine VERBATIM (`measure` the capacity form's witnessed group
116
- * total — u128-wide, crossing whole as bigint, C3) — `direction` is
117
- * relative to the violated SLOT's
118
- * own orientation, so for a `mirrors` statement it alone cannot say which
119
- * side of the `==` was violated: the slot identity is carried by
120
- * `orientation`, present exactly for `mirrors` slots — `written` is the
121
- * `source <= target` slot as the statement was spelled, `mirrored` the
122
- * engine-materialized `target <= source` partner.
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
- readonly kind: "functionality"
127
- readonly statement?: Statement
128
- readonly canonical: string
129
- readonly facts: readonly OffendingFact<Rels>[]
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 one insert. Omitted fresh fields are MINTED through the
269
- * engine's alloc lane and returned as bare bigints; supplying them instead
270
- * preserves identity (the resupply idiom). Returns `{ changed, ...fresh }`
271
- * (ruled 2026-07-23, R11): the engine's changed-state report — the Rust
272
- * surface's `insert(&fact) -> bool` bijection `delete` always honored —
273
- * beside the relation's fresh cells, minted or resupplied. The
274
- * idempotent-replay lane reads the bit from the insert itself; no extra
275
- * `contains` round trip exists. The flattened shape cannot carry a FRESH
276
- * cell literally named `changed` beside the report, so admission refuses
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
- insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): { readonly changed: boolean } & Minted<R>
281
- /** Records one delete; `true` iff the final state changed. */
282
- delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
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: the form tag, the
455
- * SDK statement value that lowered to it (`undefined` for the
456
- * engine-materialized implied keys), and — for functionality forms — the
457
- * key's owner and projection (what keyed point reads resolve through).
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
- readonly kind: "functionality"
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
- return Object.freeze({ kind: "functionality", statement, canonical, facts })
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({ kind: "capacity", statement, canonical, measure: wire.measure, facts })
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({ kind: "containment", statement, canonical, direction: wire.direction, facts })
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
- /** The one spread copy of the write path: `mintFreshCells` writes minted cells in place, and they must never land in the caller's own fact object. */
1170
- const values: Record<string, unknown> = { ...recordOf(fact) }
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
- const inserted: Readonly<Record<string, FactValue | boolean>> = Object.freeze({ changed, ...fresh })
1177
- if (!isInserted(relation, inserted)) {
1178
- throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
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
- return inserted
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 remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
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 row = rowOf(relation.data, recordOf(fact))
1187
- return bridged("bumbledb tx delete", function record() {
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, Minted } from "#marshal.ts"
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. The one mutating consumer
78
- * (`mintFreshCells` on the insert path) takes its own spread copy at the
79
- * call site, so the caller's fact object is never written through this
80
- * seam.
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
- * fresh minting happens BEFORE this point (the transaction fills omitted
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, Minted }
314
- export { cellOf, factOf, handleOf, isFreshField, isInserted, keyRowOf, recordOf, rowOf }
283
+ export type { KeyFact }
284
+ export { cellOf, factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf }