@bjornpagen/bumbledb 1.1.0 → 1.2.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 (148) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +26 -3
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +89 -44
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +4 -9
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +108 -183
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +16 -16
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +36 -15
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +39 -0
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +28 -8
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +91 -38
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +14 -10
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +2 -0
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +2 -0
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +10 -10
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/native.d.ts +1 -1
  63. package/dist/native.d.ts.map +1 -1
  64. package/dist/native.js.map +1 -1
  65. package/dist/query/atom.d.ts +1 -1
  66. package/dist/query/atom.d.ts.map +1 -1
  67. package/dist/query/atom.js.map +1 -1
  68. package/dist/query/description.d.ts +33 -0
  69. package/dist/query/description.d.ts.map +1 -0
  70. package/dist/query/description.js +302 -0
  71. package/dist/query/description.js.map +1 -0
  72. package/dist/query/lower.d.ts +60 -4
  73. package/dist/query/lower.d.ts.map +1 -1
  74. package/dist/query/lower.js +126 -161
  75. package/dist/query/lower.js.map +1 -1
  76. package/dist/query/parse-ir.d.ts +7 -2
  77. package/dist/query/parse-ir.d.ts.map +1 -1
  78. package/dist/query/parse-ir.js +240 -53
  79. package/dist/query/parse-ir.js.map +1 -1
  80. package/dist/query/run.d.ts.map +1 -1
  81. package/dist/query/run.js +8 -10
  82. package/dist/query/run.js.map +1 -1
  83. package/dist/query/scope.d.ts +3 -4
  84. package/dist/query/scope.d.ts.map +1 -1
  85. package/dist/query/scope.js +5 -10
  86. package/dist/query/scope.js.map +1 -1
  87. package/dist/relation.d.ts +22 -48
  88. package/dist/relation.d.ts.map +1 -1
  89. package/dist/relation.js +30 -82
  90. package/dist/relation.js.map +1 -1
  91. package/dist/rows.d.ts +7 -4
  92. package/dist/rows.d.ts.map +1 -1
  93. package/dist/rows.js +44 -198
  94. package/dist/rows.js.map +1 -1
  95. package/dist/runtime-errors.d.ts +26 -2
  96. package/dist/runtime-errors.d.ts.map +1 -1
  97. package/dist/runtime-errors.js +43 -3
  98. package/dist/runtime-errors.js.map +1 -1
  99. package/dist/schema.d.ts +9 -1
  100. package/dist/schema.d.ts.map +1 -1
  101. package/dist/schema.js +85 -24
  102. package/dist/schema.js.map +1 -1
  103. package/dist/selection.d.ts +27 -0
  104. package/dist/selection.d.ts.map +1 -0
  105. package/dist/selection.js +104 -0
  106. package/dist/selection.js.map +1 -0
  107. package/dist/shape.d.ts +6 -9
  108. package/dist/shape.d.ts.map +1 -1
  109. package/dist/statements.d.ts +17 -52
  110. package/dist/statements.d.ts.map +1 -1
  111. package/dist/statements.js +92 -111
  112. package/dist/statements.js.map +1 -1
  113. package/dist/values.d.ts +25 -0
  114. package/dist/values.d.ts.map +1 -0
  115. package/dist/values.js +203 -0
  116. package/dist/values.js.map +1 -0
  117. package/pack-provenance.json +3 -3
  118. package/package.json +4 -4
  119. package/src/capacity.ts +86 -76
  120. package/src/changes.ts +183 -54
  121. package/src/closed.ts +161 -314
  122. package/src/codec.ts +116 -190
  123. package/src/compile.ts +56 -32
  124. package/src/db-native.ts +42 -0
  125. package/src/db.ts +134 -52
  126. package/src/errors.ts +16 -1
  127. package/src/face.ts +44 -74
  128. package/src/fields.ts +100 -146
  129. package/src/immutable.ts +76 -0
  130. package/src/index.ts +34 -28
  131. package/src/internal/log.ts +2 -0
  132. package/src/law.ts +13 -13
  133. package/src/lower.ts +13 -10
  134. package/src/native.ts +2 -0
  135. package/src/query/atom.ts +1 -0
  136. package/src/query/description.ts +394 -0
  137. package/src/query/lower.ts +139 -173
  138. package/src/query/parse-ir.ts +247 -54
  139. package/src/query/run.ts +15 -9
  140. package/src/query/scope.ts +10 -18
  141. package/src/relation.ts +58 -142
  142. package/src/rows.ts +62 -205
  143. package/src/runtime-errors.ts +47 -3
  144. package/src/schema.ts +99 -29
  145. package/src/selection.ts +124 -0
  146. package/src/shape.ts +9 -9
  147. package/src/statements.ts +138 -189
  148. package/src/values.ts +223 -0
package/src/capacity.ts CHANGED
@@ -1,55 +1,15 @@
1
1
  import { AuthoringError } from "#errors.ts"
2
- /**
3
- * Capacity-law mints — the window, weight, and dependent-bound vocabulary
4
- * the one `capacity` statement constructor consumes
5
- * § the canonical-utterance law, restated
6
- * around the aggregate form). `within` is the ONE window spelling —
7
- * `within(n)` exact (`within(0n)` IS the exclusion's one spelling),
8
- * `within(lo, hi)` range (`within(0n, hi)` the canonical ceiling),
9
- * `within(lo, "*")` floor — `weigh` names the measure on the SOURCE row
10
- * (`weigh("watts")` a u64 field, `weigh(duration("booked"))` an interval's
11
- * measure), and `ref`/`duration` read a dependent bound from the
12
- * TARGET row (hi slot only — ruled 2026-07-24, C6). The ban table is
13
- * enforced REPRESENTATIONALLY in two tiers, split per-aggregate where
14
- * weight-sensitive (design § 6: a ban is canonical-utterance policing when
15
- * it is weight-independent, semantic deduplication when it is not):
16
- *
17
- * - **The type tier**: a banned spelling written as a LITERAL does not
18
- * compile — every negative bound, `within(n, n)`, `within(0n, 0n)`, and
19
- * `within(0n, "*")` are type errors naming the canonical form. The
20
- * weight-SENSITIVE row rides the `capacity` overloads themselves:
21
- * `within(1n, "*")` is banned on the unit overload only (`{1..*}` on the
22
- * count instance is the bare containment respelled —
23
- * `window_floor_containment`), and LEGAL on the weighted one ("positive
24
- * total" is not an existence claim over rows).
25
- * - **The construction tier**: a bound the type level cannot judge — a
26
- * COMPUTED `bigint`, or an inverted `within(lo, hi)` order — is judged at
27
- * construction with the same canonical-naming errors; and past both
28
- * tiers the engine's own spec validation remains the law for a hostile
29
- * FFI caller (the standing two-tier ban enforcement).
30
- *
31
- * The weight vocabulary is closed at the row (ruled 2026-07-24, ruling 6):
32
- * a path weight (`weigh("model.watts")`) is a typed refusal at BOTH tiers
33
- * whose diagnostic names the pinned-column idiom — the two-column
34
- * containment IS the join, stated as a law. Bounds are `bigint` (u64
35
- * crosses as bigint always, PRD-04's law); the witnessed measure comes
36
- * back as `bigint` too (u128-wide engine accumulator, C3).
37
- */
2
+ /** Structural capacity measures and windows. Constructors and generated
3
+ * descriptions share the same checked representation. Bounds are u64;
4
+ * dependent bounds read the target row, measures read the source row. */
38
5
 
39
6
  import type { AnyFace, FaceFields, FaceSource, ProjectedShape } from "#face.ts"
40
7
  import type { CapacityBoundSpec, CapacityWindowSpec, WeightSpec } from "#spec.ts"
8
+ import { integerValue, recordValue } from "#values.ts"
41
9
 
42
- const admitted: unique symbol = Symbol("bumbledb.capacity.admitted")
43
-
44
- interface CapacityWindow<S extends CapacityWindowSpec = CapacityWindowSpec> {
45
- readonly window: S
46
- readonly [admitted]: true
47
- }
10
+ type CapacityWindow<S extends CapacityWindowSpec = CapacityWindowSpec> = S
48
11
 
49
- interface CapacityWeight<S extends WeightSpec = WeightSpec> {
50
- readonly weight: S
51
- readonly [admitted]: true
52
- }
12
+ type CapacityWeight<S extends WeightSpec = WeightSpec> = S
53
13
 
54
14
  interface FieldRef<F extends string = string> {
55
15
  readonly kind: "field"
@@ -61,14 +21,6 @@ interface DurationRef<F extends string = string> {
61
21
  readonly field: F
62
22
  }
63
23
 
64
- function isCapacityWindow(value: unknown): value is CapacityWindow {
65
- return typeof value === "object" && value !== null && admitted in value && "window" in value
66
- }
67
-
68
- function isCapacityWeight(value: unknown): value is CapacityWeight {
69
- return typeof value === "object" && value !== null && admitted in value && "weight" in value
70
- }
71
-
72
24
  interface BannedWindow<Canonical extends string> {
73
25
  readonly "banned window spelling — the canonical-utterance law names the one legal form": Canonical
74
26
  }
@@ -129,7 +81,7 @@ type RangeBan<Lo extends bigint, Hi extends bigint> = bigint extends Lo
129
81
  * only: Duration weights pair with Duration-capable bounds, so the
130
82
  * weighted overload takes the same window freely.
131
83
  */
132
- type UnitDimensionBan<W extends CapacityWindow> = W["window"] extends {
84
+ type UnitDimensionBan<W extends CapacityWindow> = W extends {
133
85
  readonly hi: { readonly kind: "durationField" }
134
86
  }
135
87
  ? BannedWindow<"a count of facts bounded by a span of time mixes dimensions (C18) — weigh the source with weigh(duration(field)), or bound by a u64 field or literal">
@@ -154,17 +106,17 @@ interface BoundKindMismatch<K, Want> {
154
106
 
155
107
  type BoundOnTarget<K extends string, Want extends "u64" | "interval", B extends AnyFace> = string extends K
156
108
  ? unknown
157
- : K extends FaceFields<B["source"]>
158
- ? KindAt<B["source"], K> extends Want
109
+ : K extends FaceFields<B["owner"]>
110
+ ? KindAt<B["owner"], K> extends Want
159
111
  ? unknown
160
112
  : BoundKindMismatch<K, Want>
161
- : BoundOffTargetRoster<K, FaceFields<B["source"]>>
113
+ : BoundOffTargetRoster<K, FaceFields<B["owner"]>>
162
114
 
163
- type BoundsOnTarget<W extends CapacityWindow, B extends AnyFace> = W["window"] extends {
115
+ type BoundsOnTarget<W extends CapacityWindow, B extends AnyFace> = W extends {
164
116
  readonly hi: FieldRef<infer K>
165
117
  }
166
118
  ? BoundOnTarget<K, "u64", B>
167
- : W["window"] extends { readonly hi: DurationRef<infer K> }
119
+ : W extends { readonly hi: DurationRef<infer K> }
168
120
  ? BoundOnTarget<K, "interval", B>
169
121
  : unknown
170
122
 
@@ -189,30 +141,32 @@ interface WeightKindMismatch<K, Want> {
189
141
  * position. Checked at the `capacity` call where the source face is
190
142
  * inferred — the proven constrain-after-inference pattern.
191
143
  */
192
- type WeightOnSource<M extends CapacityWeight, A extends AnyFace> = M["weight"] extends {
144
+ type WeightOnSource<M extends CapacityWeight, A extends AnyFace> = M extends {
193
145
  readonly kind: "field"
194
146
  readonly field: infer K extends string
195
147
  }
196
148
  ? string extends K
197
149
  ? unknown
198
- : K extends FaceFields<A["source"]>
199
- ? KindAt<A["source"], K> extends "u64"
150
+ : K extends FaceFields<A["owner"]>
151
+ ? KindAt<A["owner"], K> extends "u64"
200
152
  ? unknown
201
153
  : WeightKindMismatch<K, "u64">
202
- : WeightOffSourceRoster<K, FaceFields<A["source"]>>
203
- : M["weight"] extends { readonly kind: "durationField"; readonly field: infer K extends string }
154
+ : WeightOffSourceRoster<K, FaceFields<A["owner"]>>
155
+ : M extends { readonly kind: "durationField"; readonly field: infer K extends string }
204
156
  ? string extends K
205
157
  ? unknown
206
- : K extends FaceFields<A["source"]>
207
- ? KindAt<A["source"], K> extends "interval"
158
+ : K extends FaceFields<A["owner"]>
159
+ ? KindAt<A["owner"], K> extends "interval"
208
160
  ? unknown
209
161
  : WeightKindMismatch<K, "interval">
210
- : WeightOffSourceRoster<K, FaceFields<A["source"]>>
162
+ : WeightOffSourceRoster<K, FaceFields<A["owner"]>>
211
163
  : unknown
212
164
 
213
165
  const unitWeight: WeightSpec = Object.freeze({ kind: "unit" })
214
166
 
215
- function assertRowLocal(field: string, role: string): string {
167
+ function assertRowLocal<F extends string>(field: F, role: string): F {
168
+ if (typeof field !== "string" || !field.isWellFormed())
169
+ throw new AuthoringError({ message: `${role}: expected a well-formed field name` })
216
170
  if (field.includes(".")) {
217
171
  throw new AuthoringError({
218
172
  message: `${role} \`${field}\` walks a reference — the vocabulary is closed at the row (ruling 6): pin the column with a two-column containment (Source(ref, f) <= Catalog(id, f)) and name the local field`
@@ -222,8 +176,8 @@ function assertRowLocal(field: string, role: string): string {
222
176
  }
223
177
 
224
178
  function admitWindow<S extends CapacityWindowSpec>(window: S): CapacityWindow<S> {
225
- const value: CapacityWindow<S> = { window, [admitted]: true }
226
- return Object.freeze(value)
179
+ capacityWindow(window)
180
+ return Object.freeze(window)
227
181
  }
228
182
 
229
183
  function lit(value: bigint): CapacityBoundSpec {
@@ -265,6 +219,8 @@ function within<const Lo extends bigint, const Hi extends bigint>(
265
219
  | { readonly kind: "exact"; readonly n: { readonly kind: "lit"; readonly value: Lo } }
266
220
  >
267
221
  function within(lo: bigint, hi?: bigint | "*" | FieldRef | DurationRef): CapacityWindow {
222
+ integerValue("capacity lower bound", "u64", lo)
223
+ if (typeof hi === "bigint") integerValue("capacity upper bound", "u64", hi)
268
224
  if (lo < 0n) {
269
225
  throw new AuthoringError({
270
226
  message: `capacity bounds are u64: within(${lo}${hi === undefined ? "" : ", …"}) is out of domain`
@@ -302,8 +258,8 @@ function within(lo: bigint, hi?: bigint | "*" | FieldRef | DurationRef): Capacit
302
258
  }
303
259
 
304
260
  function admitWeight<S extends WeightSpec>(weight: S): CapacityWeight<S> {
305
- const value: CapacityWeight<S> = { weight, [admitted]: true }
306
- return Object.freeze(value)
261
+ capacityWeight(weight)
262
+ return Object.freeze(weight)
307
263
  }
308
264
 
309
265
  /**
@@ -326,7 +282,7 @@ function weigh(measure: string | DurationRef): CapacityWeight {
326
282
  }
327
283
 
328
284
  function ref<const F extends string>(field: F & PathBan<F>): FieldRef<F> {
329
- return Object.freeze({ kind: "field", field: assertRowLocal(field, "dependent bound") }) as FieldRef<F>
285
+ return Object.freeze({ kind: "field", field: assertRowLocal(field, "dependent bound") })
330
286
  }
331
287
 
332
288
  /**
@@ -336,8 +292,62 @@ function ref<const F extends string>(field: F & PathBan<F>): FieldRef<F> {
336
292
  * (Duration weights pair with Duration-capable bounds — C18).
337
293
  */
338
294
  function duration<const F extends string>(field: F & PathBan<F>): DurationRef<F> {
339
- return Object.freeze({ kind: "durationField", field: assertRowLocal(field, "Duration measure") }) as DurationRef<F>
295
+ return Object.freeze({ kind: "durationField", field: assertRowLocal(field, "Duration measure") })
340
296
  }
341
297
 
342
298
  export type { BoundsOnTarget, CapacityWeight, CapacityWindow, DurationRef, FieldRef, UnitDimensionBan, WeightOnSource }
343
- export { duration, isCapacityWeight, isCapacityWindow, ref, unitWeight, weigh, within }
299
+
300
+ /** Read a checked bound without retaining a caller-owned record. */
301
+ function capacityBound(input: unknown): CapacityBoundSpec {
302
+ const kind = typeof input === "object" && input !== null && "kind" in input ? input.kind : undefined
303
+ if (kind === "lit") {
304
+ const bound = recordValue("capacity bound", input, ["kind", "value"])
305
+ return Object.freeze({ kind, value: integerValue("capacity bound", "u64", bound.value) })
306
+ }
307
+ if (kind === "field" || kind === "durationField") {
308
+ const bound = recordValue("capacity bound", input, ["kind", "field"])
309
+ if (typeof bound.field !== "string") throw new AuthoringError({ message: "capacity bound: expected a field name" })
310
+ return Object.freeze({ kind, field: assertRowLocal(bound.field, "dependent bound") })
311
+ }
312
+ throw new AuthoringError({ message: "capacity bound: unknown kind" })
313
+ }
314
+
315
+ function literalBound(input: unknown): CapacityBoundSpec & { readonly kind: "lit" } {
316
+ const bound = capacityBound(input)
317
+ if (bound.kind !== "lit")
318
+ throw new AuthoringError({ message: "dependent bounds are allowed only at a range's upper bound" })
319
+ return bound
320
+ }
321
+
322
+ function capacityWindow(input: unknown): CapacityWindow {
323
+ const kind = typeof input === "object" && input !== null && "kind" in input ? input.kind : undefined
324
+ if (kind === "exact") {
325
+ const window = recordValue("capacity window", input, ["kind", "n"])
326
+ return Object.freeze({ kind, n: literalBound(window.n) })
327
+ }
328
+ if (kind === "floor") {
329
+ const window = recordValue("capacity window", input, ["kind", "lo"])
330
+ return Object.freeze({ kind, lo: literalBound(window.lo) })
331
+ }
332
+ if (kind === "range") {
333
+ const window = recordValue("capacity window", input, ["kind", "lo", "hi"])
334
+ const lo = literalBound(window.lo)
335
+ const hi = capacityBound(window.hi)
336
+ if (hi.kind === "lit" && hi.value < lo.value) throw new AuthoringError({ message: "capacity window is inverted" })
337
+ return Object.freeze({ kind, lo, hi })
338
+ }
339
+ throw new AuthoringError({ message: "capacity window: unknown kind" })
340
+ }
341
+
342
+ function capacityWeight(input: unknown): CapacityWeight {
343
+ const kind = typeof input === "object" && input !== null && "kind" in input ? input.kind : undefined
344
+ if (kind === "unit") {
345
+ recordValue("capacity weight", input, ["kind"])
346
+ return unitWeight
347
+ }
348
+ const weight = capacityBound(input)
349
+ if (weight.kind === "lit") throw new AuthoringError({ message: "capacity weight: expected unit or a source field" })
350
+ return weight
351
+ }
352
+
353
+ export { capacityWeight, capacityWindow, duration, ref, unitWeight, weigh, within }
package/src/changes.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  import type { Scope } from "effect"
2
- import { Effect, Exit } from "effect"
2
+ import { Effect, Exit, Option, Stream } from "effect"
3
3
  import { drainClose, releaseOwner } from "#close.ts"
4
+ import { isClosedMember, membersAgree } from "#closed.ts"
4
5
  import type { SchemaId } from "#compile.ts"
5
6
  import { Schema as CoreSchema, schemaTables } from "#compile.ts"
6
- import type { ChangesHandle, DraftHandle } from "#db-native.ts"
7
+ import type { ChangeRecordWire, ChangesHandle, ChangesWire, DraftHandle } from "#db-native.ts"
7
8
  import { dbNative } from "#db-native.ts"
9
+ import { SdkInvariantError } from "#errors.ts"
8
10
  import { lower } from "#lower.ts"
9
- import type { AnyRelation, Fact } from "#relation.ts"
11
+ import { type AnyRelation, type Fact, relationFields } from "#relation.ts"
10
12
  import type { CellValue } from "#rows.ts"
11
- import { cellOf, hostCellCharge, recordOf } from "#rows.ts"
13
+ import { factCellsOf, factOfCells, hostCellCharge } from "#rows.ts"
12
14
  import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
13
15
  import type { CloseReport } from "#runtime-errors.ts"
14
- import { DbError } from "#runtime-errors.ts"
15
- import type { AnySchema } from "#schema.ts"
16
+ import { argumentError, DbError } from "#runtime-errors.ts"
17
+ import type { AnySchema, SchemaRelation } from "#schema.ts"
16
18
  import type { Rel } from "#shape.ts"
19
+ import { bytesValue, recordValue } from "#values.ts"
17
20
 
18
21
  /**
19
22
  * `ChangeSet` — the engine's checked immutable delta:
@@ -22,10 +25,32 @@ import type { Rel } from "#shape.ts"
22
25
  * add-wins. Immutable and reusable while open; sealing/submitting it later
23
26
  * retains the SAME native value — no second JS row walk ever happens.
24
27
  */
28
+ /** Counts of distinct fact additions and removals, never input events. */
29
+ interface ChangeCounts {
30
+ readonly added: bigint
31
+ readonly removed: bigint
32
+ }
33
+
34
+ /** A relation-name-discriminated union of plain, fully typed facts. */
35
+ type ChangeRecord<S extends AnySchema> = {
36
+ [N in keyof S["relations"]]: S["relations"][N] extends AnyRelation
37
+ ? { readonly relation: N; readonly kind: "add" | "remove"; readonly fact: Fact<S["relations"][N]> }
38
+ : never
39
+ }[keyof S["relations"]]
40
+
25
41
  interface ChangeSet<S extends AnySchema> {
26
42
  readonly schemaId: SchemaId
43
+ /** Distinct requested actions; use judge for effective counts against a store. */
44
+ readonly counts: ChangeCounts
45
+ readonly byteLength: bigint
27
46
  /** Phantom schema brand: a ChangeSet is only ever applied to its own S. */
28
47
  readonly schema?: S
48
+ /** Independent scoped traversal, delivered in bounded native batches. Reusable. */
49
+ records(): Stream.Stream<ChangeRecord<S>, DbError>
50
+ /** Explicit full byte materialization. Mutating returned bytes cannot alter this value. */
51
+ toBytes(): Effect.Effect<Uint8Array, DbError>
52
+ /** Commutative add-wins composition into one command, not sequential replay. */
53
+ compose(other: ChangeSet<S>): Effect.Effect<ChangeSet<S>, DbError, Scope.Scope>
29
54
  close(): Effect.Effect<CloseReport>
30
55
  }
31
56
 
@@ -97,31 +122,15 @@ function eventLoopTurn(): Effect.Effect<void> {
97
122
  }
98
123
 
99
124
  function hostFactCharge(relation: AnyRelation, record: Readonly<Record<string, unknown>>): bigint {
100
- const data = relation.data
125
+ const fields = relationFields(relation)
101
126
  let bytes = 0n
102
- for (const declared of data.fields) {
127
+ for (const declared of fields) {
103
128
  const value = record[declared.name]
104
- if (value === undefined) {
105
- throw refusal("ChangeDraft.ingest", "InvalidArgument")
106
- }
107
129
  bytes += hostCellCharge(value)
108
130
  }
109
131
  return bytes
110
132
  }
111
133
 
112
- function projectFact(relation: AnyRelation, record: Readonly<Record<string, unknown>>): readonly CellValue[] {
113
- const data = relation.data
114
- const cells: CellValue[] = []
115
- for (const declared of data.fields) {
116
- const value = record[declared.name]
117
- if (value === undefined) {
118
- throw refusal("ChangeDraft.ingest", "InvalidArgument")
119
- }
120
- cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value))
121
- }
122
- return cells
123
- }
124
-
125
134
  /**
126
135
  * Pulls one bounded chunk off the caller's iterator. Host length is judged
127
136
  * before any string scan or byte copy. A leftover fact that does not fit
@@ -141,13 +150,17 @@ function pullChunk(relation: AnyRelation, iterator: Iterator<object>, pending: o
141
150
  }
142
151
  current = next.value
143
152
  }
144
- const record = recordOf(current)
153
+ const record = recordValue(
154
+ `relation ${relation.name}`,
155
+ current,
156
+ relationFields(relation).map((field) => field.name)
157
+ )
145
158
  const charge = hostFactCharge(relation, record)
146
159
  if (rows > 0n && bytes + charge > CHUNK_BYTES) {
147
160
  leftover = current
148
161
  break
149
162
  }
150
- for (const cell of projectFact(relation, record)) cells.push(cell)
163
+ for (const cell of factCellsOf(relation, record)) cells.push(cell)
151
164
  bytes += charge
152
165
  rows += 1n
153
166
  current = undefined
@@ -185,7 +198,7 @@ function ingest(
185
198
  yield* spendAndDrain(state, operation)
186
199
  return yield* Effect.fail(refusal(operation, "SpentHandle"))
187
200
  }
188
- if (state.theory.relations[relation.name] !== relation) {
201
+ if (!membersAgree(state.theory.relations[relation.name], relation)) {
189
202
  return yield* Effect.fail(refusal(operation, "InvalidArgument"))
190
203
  }
191
204
  const tables = schemaTables(state.theory)
@@ -197,7 +210,7 @@ function ingest(
197
210
  const body = Effect.gen(function* () {
198
211
  const iterator = yield* Effect.try({
199
212
  try: () => rows[Symbol.iterator](),
200
- catch: () => refusal(operation, "InvalidArgument")
213
+ catch: (cause) => argumentError(operation, cause)
201
214
  })
202
215
  let leftover: object | undefined
203
216
  let done = false
@@ -205,7 +218,7 @@ function ingest(
205
218
  while (!done) {
206
219
  const chunk = yield* Effect.try({
207
220
  try: () => pullChunk(relation, iterator, leftover),
208
- catch: (cause) => (cause instanceof DbError ? cause : refusal(operation, "InvalidArgument"))
221
+ catch: (cause) => argumentError(operation, cause)
209
222
  }).pipe(Effect.catch((error) => spendAndDrain(state, operation).pipe(Effect.andThen(Effect.fail(error)))))
210
223
  leftover = chunk.leftover
211
224
  done = chunk.done && leftover === undefined
@@ -244,19 +257,126 @@ function ingest(
244
257
  })
245
258
  }
246
259
 
247
- function makeChangeSet<S extends AnySchema>(handle: ChangesHandle, schemaId: SchemaId): ChangeSet<S> {
260
+ function decodeChangeRecord<S extends AnySchema>(
261
+ relations: readonly SchemaRelation[],
262
+ record: ChangeRecordWire
263
+ ): ChangeRecord<S> {
264
+ const relation = relations[record.relation]
265
+ if (relation === undefined || isClosedMember(relation) || (record.kind !== "add" && record.kind !== "remove")) {
266
+ throw new SdkInvariantError({ message: "ChangeSet.records: invalid native record descriptor" })
267
+ }
268
+ // Relation id resolves through this schema's own ordered roster. The
269
+ // shared projector validates all field values before this union seam.
270
+ return Object.freeze({
271
+ relation: relation.name,
272
+ kind: record.kind,
273
+ fact: factOfCells(relation, record.values)
274
+ }) as ChangeRecord<S>
275
+ }
276
+
277
+ function changeRecords<S extends AnySchema>(
278
+ handle: ChangesHandle,
279
+ relations: readonly SchemaRelation[]
280
+ ): Stream.Stream<ChangeRecord<S>, DbError> {
281
+ return Stream.unwrap(
282
+ Effect.gen(function* () {
283
+ const cursor = yield* Effect.acquireRelease(
284
+ nativeOperationWith(
285
+ "ChangeSet.records",
286
+ (callback) => dbNative.runtimeChangesCursor(handle, callback),
287
+ dbNative.runtimeChangesCursorTake,
288
+ (value) => value
289
+ ),
290
+ (value) =>
291
+ releaseOwner("ChangeCursor.close", (callback) => dbNative.runtimeChangesCursorClose(value, callback)),
292
+ { interruptible: true }
293
+ )
294
+ return Stream.paginate(undefined, () =>
295
+ nativeOperationWith(
296
+ "ChangeSet.records",
297
+ (callback) => dbNative.runtimeChangesCursorNext(cursor, callback),
298
+ dbNative.runtimeChangePageTake,
299
+ (page) => page
300
+ ).pipe(
301
+ Effect.map((page) =>
302
+ page === null
303
+ ? ([[], Option.none<undefined>()] as const)
304
+ : ([page.map((record) => decodeChangeRecord<S>(relations, record)), Option.some(undefined)] as const)
305
+ )
306
+ )
307
+ )
308
+ })
309
+ )
310
+ }
311
+
312
+ function acquireChanges<S extends AnySchema>(
313
+ theory: S,
314
+ schemaId: SchemaId,
315
+ acquire: Effect.Effect<ChangesWire, DbError>
316
+ ): Effect.Effect<ChangeSet<S>, DbError, Scope.Scope> {
317
+ return Effect.acquireRelease(
318
+ acquire.pipe(Effect.map((wire) => makeChangeSet(theory, wire, schemaId))),
319
+ (changes) =>
320
+ Effect.suspend(() => {
321
+ const internal = changesInternals.get(changes)
322
+ if (internal === undefined) return Effect.void
323
+ internal.closed = true
324
+ return releaseOwner("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(internal.handle, callback))
325
+ }),
326
+ { interruptible: true }
327
+ )
328
+ }
329
+
330
+ function makeChangeSet<S extends AnySchema>(theory: S, wire: ChangesWire, schemaId: SchemaId): ChangeSet<S> {
331
+ const handle = wire.changes
332
+ const relations = Object.values(theory.relations)
333
+ const internal: ChangesInternal = { handle, schemaId, closed: false }
248
334
  const value: ChangeSet<S> = {
249
335
  schemaId,
336
+ counts: Object.freeze({ ...wire.counts }),
337
+ byteLength: wire.byteLength,
338
+ records() {
339
+ return changeRecords<S>(handle, relations)
340
+ },
341
+ toBytes() {
342
+ return nativeOperationWith(
343
+ "ChangeSet.toBytes",
344
+ (callback) => dbNative.runtimeChangesBytes(handle, callback),
345
+ dbNative.runtimeBytesTake,
346
+ (bytes) => bytes
347
+ )
348
+ },
349
+ compose(other) {
350
+ return Effect.suspend(() => {
351
+ const right = internalChanges(other)
352
+ if (right === undefined || right.schemaId !== schemaId)
353
+ return Effect.fail(refusal("ChangeSet.compose", "InvalidArgument"))
354
+ if (internal.closed || right.closed) return Effect.fail(refusal("ChangeSet.compose", "ClosedHandle"))
355
+ return acquireChanges(
356
+ theory,
357
+ schemaId,
358
+ nativeOperationWith(
359
+ "ChangeSet.compose",
360
+ (callback) => dbNative.runtimeChangesCompose(handle, right.handle, callback),
361
+ dbNative.runtimeChangesTake,
362
+ (result) => result
363
+ )
364
+ )
365
+ })
366
+ },
250
367
  close() {
251
- return drainClose("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(handle, callback))
368
+ return Effect.suspend(() => {
369
+ internal.closed = true
370
+ return drainClose("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(handle, callback))
371
+ })
252
372
  }
253
373
  }
254
374
  Object.freeze(value)
255
- changesInternals.set(value, { handle, schemaId, closed: false })
375
+ changesInternals.set(value, internal)
256
376
  return value
257
377
  }
258
378
 
259
- function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId): ChangeDraft<S> {
379
+ function makeDraft<S extends AnySchema>(theory: S, state: DraftState, schemaId: SchemaId): ChangeDraft<S> {
260
380
  const draft: ChangeDraft<S> = {
261
381
  insert(relation, rows) {
262
382
  return ingest(state, "ChangeDraft.insert", relation, rows)
@@ -265,7 +385,9 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
265
385
  return ingest(state, "ChangeDraft.delete", relation, rows)
266
386
  },
267
387
  finish() {
268
- return Effect.acquireRelease(
388
+ return acquireChanges(
389
+ theory,
390
+ schemaId,
269
391
  Effect.gen(function* () {
270
392
  if (state.spent) {
271
393
  return yield* Effect.fail(refusal("ChangeDraft.finish", "SpentHandle"))
@@ -276,26 +398,13 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
276
398
  }
277
399
  // Finish CONSUMES the draft, success or failure.
278
400
  state.spent = true
279
- const wire = yield* nativeOperationWith(
401
+ return yield* nativeOperationWith(
280
402
  "ChangeDraft.finish",
281
403
  (callback) => dbNative.runtimeDraftFinish(state.handle, callback),
282
404
  dbNative.runtimeChangesTake,
283
405
  (value) => value
284
406
  )
285
- return makeChangeSet<S>(wire.changes, schemaId)
286
- }),
287
- (changes) =>
288
- Effect.suspend(() => {
289
- const internal = changesInternals.get(changes)
290
- if (internal === undefined || internal.closed) {
291
- return Effect.void
292
- }
293
- internal.closed = true
294
- return releaseOwner("ChangeSet.close", (callback) =>
295
- dbNative.runtimeChangesClose(internal.handle, callback)
296
- )
297
- }),
298
- { interruptible: true }
407
+ })
299
408
  )
300
409
  },
301
410
  close() {
@@ -318,7 +427,7 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
318
427
  const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(schema: S) {
319
428
  const handle = yield* runtimeHandle()
320
429
  const compiled = yield* CoreSchema.compile(schema)
321
- const spec = lower(schema)
430
+ const spec = lower(compiled.schema)
322
431
  return yield* Effect.acquireRelease(
323
432
  Effect.gen(function* () {
324
433
  const draftHandle = yield* nativeOperationWith(
@@ -329,11 +438,11 @@ const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(s
329
438
  )
330
439
  const state: DraftState = {
331
440
  handle: draftHandle,
332
- theory: schema,
441
+ theory: compiled.schema,
333
442
  spent: false,
334
443
  inFlight: false
335
444
  }
336
- const draft = makeDraft<S>(state, compiled.schemaId)
445
+ const draft = makeDraft(compiled.schema, state, compiled.schemaId)
337
446
  draftStates.set(draft, state)
338
447
  return draft
339
448
  }),
@@ -357,7 +466,27 @@ const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(s
357
466
  // native handle without exposing it on the public capability.
358
467
  const draftStates = new WeakMap<object, DraftState>()
359
468
 
360
- const ChangeSet = Object.freeze({ builder })
469
+ /** Parse canonical native bytes, owning the input when the Effect starts. */
470
+ const fromBytes = Effect.fn("ChangeSet.fromBytes")(function* <S extends AnySchema>(schema: S, bytes: Uint8Array) {
471
+ const ownedBytes = yield* Effect.try({
472
+ try: () => bytesValue("ChangeSet.fromBytes", bytes),
473
+ catch: (cause) => argumentError("ChangeSet.fromBytes", cause)
474
+ })
475
+ const handle = yield* runtimeHandle()
476
+ const compiled = yield* CoreSchema.compile(schema)
477
+ return yield* acquireChanges(
478
+ compiled.schema,
479
+ compiled.schemaId,
480
+ nativeOperationWith(
481
+ "ChangeSet.fromBytes",
482
+ (callback) => dbNative.runtimeChangesParse(handle, lower(compiled.schema), ownedBytes, callback),
483
+ dbNative.runtimeChangesTake,
484
+ (wire) => wire
485
+ )
486
+ )
487
+ })
488
+
489
+ const ChangeSet = Object.freeze({ builder, fromBytes })
361
490
 
362
- export type { ChangeDraft }
491
+ export type { ChangeCounts, ChangeDraft, ChangeRecord }
363
492
  export { ChangeSet, internalChanges }