@bjornpagen/bumbledb 1.0.1 → 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 (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  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 +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  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 +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  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 +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  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 +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  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/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
package/COOKBOOK.md CHANGED
@@ -8,7 +8,7 @@ reads, sealed complete results, and one-shot page streams.
8
8
  Every `ts` fence below is extracted and type-checked against the real
9
9
  package surface by `test/cookbook-doc.test.ts`; the imports fence here is
10
10
  prepended to every recipe. The examples are lazy Effect programs — nothing
11
- below runs a database at import time, and measured policies are inputs.
11
+ below runs a database at import time.
12
12
 
13
13
  ```ts
14
14
  import { Effect, Option, Result, Stream } from "effect"
@@ -18,6 +18,7 @@ import {
18
18
  closed,
19
19
  contained,
20
20
  Db,
21
+ describeQuery,
21
22
  duration,
22
23
  f64,
23
24
  i64,
@@ -29,11 +30,11 @@ import {
29
30
  NativeRuntime,
30
31
  on,
31
32
  query,
33
+ queryFromDescription,
32
34
  ref,
33
35
  relation,
34
36
  Scalar,
35
37
  schema,
36
- span,
37
38
  str,
38
39
  u64,
39
40
  v,
@@ -43,14 +44,12 @@ import {
43
44
  import type {
44
45
  ApplyOutcome,
45
46
  CompleteResult,
46
- ExecutionPolicy,
47
47
  Fact,
48
- NativeRuntimeOptions,
48
+ FloatIntervalValue,
49
+ IntervalValue,
49
50
  QueryReader
50
51
  } from "@bjornpagen/bumbledb"
51
52
 
52
- declare const work: ExecutionPolicy
53
- declare const runtimePolicy: NativeRuntimeOptions
54
53
  declare const localPath: string
55
54
  ```
56
55
 
@@ -93,8 +92,18 @@ void [Learning, scoreIsNumber, unitsAreExact]
93
92
 
94
93
  ## 2. One runtime layer; explicit create and open
95
94
 
96
- `NativeRuntime.layer(options)` acquires the single bounded native runtime
95
+ `NativeRuntime.layer()` acquires the single native runtime
97
96
  with scope; reuse ONE layer value so Effect's memoization shares it.
97
+ Optional configuration controls workers, outstanding jobs/handles, and cleanup
98
+ reporting. Defaults are up to four workers, 128 queued jobs, 128 cleanup reports,
99
+ 64 directory owners, 1,024 native handles, and a five-second cleanup report window.
100
+ Operations use ordinary allocation without byte/row/work quotas or execution
101
+ deadlines. Effect interruption requests cooperative cancellation. Runtime
102
+ inspection reports outstanding work, not memory usage.
103
+ Database `inspect().storage` reports virtual map extent, populated file length,
104
+ non-free LMDB pages, and allocated disk blocks (null when unavailable).
105
+ None of these measures process heap usage or resident RAM. Log history
106
+ inspection uses the same `StorageInspection` fields.
98
107
  `Db.open` never creates a missing database; `Db.create` refuses existing
99
108
  authority. Both are scoped acquisitions.
100
109
 
@@ -104,20 +113,20 @@ const Docs = schema("Docs", { Doc }, [key(Doc, ["id"])])
104
113
 
105
114
  const openExisting = Effect.scoped(
106
115
  Effect.gen(function* () {
107
- const db = yield* Db.open(localPath, Docs, work)
116
+ const db = yield* Db.open(localPath, Docs)
108
117
  return db.schemaId
109
118
  })
110
119
  )
111
120
 
112
121
  const createOnce = Effect.scoped(
113
122
  Effect.gen(function* () {
114
- const db = yield* Db.create(localPath, Docs, work)
123
+ const db = yield* Db.create(localPath, Docs)
115
124
  return db.schemaId
116
125
  })
117
126
  )
118
127
 
119
128
  // One boundary; an Effect app provides the layer in its own graph instead.
120
- const layer = NativeRuntime.layer(runtimePolicy)
129
+ const layer = NativeRuntime.layer()
121
130
  void [openExisting.pipe(Effect.provide(layer)), createOnce.pipe(Effect.provide(layer))]
122
131
  ```
123
132
 
@@ -125,7 +134,7 @@ void [openExisting.pipe(Effect.provide(layer)), createOnce.pipe(Effect.provide(l
125
134
 
126
135
  `ChangeSet.builder` acquires a scoped database-free draft. Ingestion effects
127
136
  are lazy and re-runnable while the draft is building — each execution reads
128
- the then-current iterable and charges work again. Within ONE change set the
137
+ the then-current iterable. Within ONE change set the
129
138
  normalization is `(add, remove ∖ add)`: the identical fact's add wins
130
139
  independent of call order. `finish()` consumes the draft into an immutable,
131
140
  reusable `ChangeSet`.
@@ -136,12 +145,12 @@ const Tasks = schema("Tasks", { Task }, [key(Task, ["id"])])
136
145
 
137
146
  const applyOnce = Effect.scoped(
138
147
  Effect.gen(function* () {
139
- const db = yield* Db.open(localPath, Tasks, work)
148
+ const db = yield* Db.open(localPath, Tasks)
140
149
  const taskId = yield* Effect.sync(() => crypto.randomUUID())
141
- const draft = yield* ChangeSet.builder(Tasks, work)
150
+ const draft = yield* ChangeSet.builder(Tasks)
142
151
  yield* draft.insert(Task, [{ id: taskId, title: "write the cookbook", done: 0n }])
143
152
  const changes = yield* draft.finish()
144
- const outcome: ApplyOutcome = yield* db.apply(changes, { ...work, expected: { kind: "any" } })
153
+ const outcome: ApplyOutcome = yield* db.apply(changes, { expected: { kind: "any" } })
145
154
  switch (outcome.kind) {
146
155
  case "accepted":
147
156
  case "no-change":
@@ -157,18 +166,83 @@ const applyOnce = Effect.scoped(
157
166
  void applyOnce
158
167
  ```
159
168
 
169
+ For a noncommitting check, `db.judge` runs the same admission procedure and
170
+ aborts its private candidate on the native worker. It briefly takes the
171
+ writer; it does not hold a writer session open in JavaScript. Admitted and
172
+ rejected judgments include the actual base and net proposed fact counts.
173
+ Operational errors stay in the Effect error channel.
174
+
175
+ ```ts
176
+ const judgeOnce = Effect.scoped(
177
+ Effect.gen(function* () {
178
+ const db = yield* Db.open(localPath, Tasks)
179
+ const draft = yield* ChangeSet.builder(Tasks)
180
+ const id = yield* Effect.sync(() => crypto.randomUUID())
181
+ yield* draft.insert(Task, [{ id, title: "inspect a candidate", done: 0n }])
182
+ return yield* db.judge(yield* draft.finish(), { expected: { kind: "any" } })
183
+ })
184
+ )
185
+ void judgeOnce
186
+ ```
187
+
188
+ Judgment is optional: `apply` always judges for itself. To act on a judgment,
189
+ use its base as an exact expected witness and handle `moved`; do not assume
190
+ another writer could not change the database between these operations.
191
+
192
+ Change sets also work as inspectable values without a database. Their
193
+ `counts` report distinct requested additions/removals, not the net change
194
+ against any store. `records()` yields `{ relation, kind, fact }` records;
195
+ the relation name narrows the fact type. Every traversal opens an independent
196
+ cursor over the shared native bytes. Early termination, failure and interruption
197
+ close it, without consuming the change set. Delivery is bounded to 256 records
198
+ and a 64 KiB canonical-byte target per batch; a larger individual row is allowed.
199
+
200
+ `toBytes()` explicitly copies the complete canonical payload. `fromBytes`
201
+ checks its schema fingerprint, framing, scalar values, order and uniqueness;
202
+ it refuses malformed bytes instead of silently normalizing them. Input bytes
203
+ must remain stable until the Effect exits, and returned bytes are independently
204
+ owned. Native byte order is canonical encoding order, not insertion order.
205
+
206
+ ```ts
207
+ const inspectAndCompose = Effect.scoped(
208
+ Effect.gen(function* () {
209
+ const draft = yield* ChangeSet.builder(Tasks)
210
+ const id = yield* Effect.sync(() => crypto.randomUUID())
211
+ yield* draft.insert(Task, [{ id, title: "inspect typed changes", done: 0n }])
212
+ const changes = yield* draft.finish()
213
+ const bytes = yield* changes.toBytes()
214
+ const decoded = yield* ChangeSet.fromBytes(Tasks, bytes)
215
+ const combined = yield* changes.compose(decoded)
216
+ const preview = yield* Stream.runCollect(combined.records().pipe(Stream.take(10)))
217
+ return { counts: combined.counts, byteLength: combined.byteLength,
218
+ titles: preview.map((record) => record.fact.title) }
219
+ })
220
+ )
221
+ void inspectAndCompose
222
+ ```
223
+
224
+ `compose` performs a native linear merge without decoding/re-encoding rows.
225
+ It is commutative, associative and idempotent: the identical fact's add wins
226
+ over its removal within the combined command. Two distinct rows sharing a key
227
+ both survive composition and can still be rejected by admission. Applying two
228
+ commands sequentially has different semantics: a later removal removes a
229
+ previously added fact. Neither input is consumed by composition.
230
+
160
231
  ## 4. Keyed reads are Options
161
232
 
162
- `get` reads through the relation's primary declared key. A missing key is
233
+ `get` takes the exact key descriptor used in the schema and its complete
234
+ projected value. Every declared key is usable; there is no implicit primary key.
235
+ A missing key is
163
236
  `Option.none` — never a fake I/O error, never a nullable row.
164
237
 
165
238
  ```ts
166
239
  const Person = relation("Person", { id: uuid, name: str })
167
- const People = schema("People", { Person }, [key(Person, ["id"])])
240
+ const PersonById = key(Person, ["id"])
241
+ const People = schema("People", { Person }, [PersonById])
168
242
 
169
243
  const lookup = (reader: QueryReader<typeof People>, personId: Uuid) =>
170
244
  Effect.gen(function* () {
171
- const found = yield* reader.get(Person, { id: personId }, work)
245
+ const found = yield* reader.get(PersonById, { id: personId })
172
246
  return Option.isSome(found) ? found.value.name : "unknown"
173
247
  })
174
248
  void lookup
@@ -203,13 +277,75 @@ const booksBy = query(Library).rule((r) => {
203
277
  const readBooks = (reader: QueryReader<typeof Library>, name: string) =>
204
278
  Effect.scoped(
205
279
  Effect.gen(function* () {
206
- const result = yield* reader.execute(booksBy, { name }, work)
207
- return yield* result.collect({ maxBytes: work.resultBytes }, work)
280
+ const result = yield* reader.execute(booksBy, { name })
281
+ return yield* result.collect()
208
282
  })
209
283
  )
210
284
  void readBooks
211
285
  ```
212
286
 
287
+ For repeated calls on one snapshot, prepare once. The scoped handle keeps
288
+ one compiled plan and reuses its execution buffers; only parameters cross
289
+ the native boundary on subsequent calls. One-shot `execute` does not retain
290
+ a plan. Preparations share the snapshot's pinned version but close
291
+ independently; completed results also remain independent.
292
+
293
+ Ordinary execution keeps buffers for reuse. If a prepared query will sit idle
294
+ after a large operation, `yield* prepared.releaseMemory()` drops its
295
+ execution buffers but keeps the compiled query and pinned snapshot. Shared
296
+ query caches belong to the database: `yield* db.clearCache()` clears those
297
+ separately. Neither operation invalidates live results. Both are optional;
298
+ scope closure releases ownership as usual.
299
+
300
+ ```ts
301
+ const readTwoAuthors = (reader: QueryReader<typeof Library>) =>
302
+ Effect.scoped(
303
+ Effect.gen(function* () {
304
+ const prepared = yield* reader.prepare(booksBy)
305
+ const first = yield* prepared.execute({ name: "Ursula Le Guin" })
306
+ const second = yield* prepared.execute({ name: "Octavia Butler" })
307
+ return [
308
+ yield* first.collect(),
309
+ yield* second.collect()
310
+ ]
311
+ })
312
+ )
313
+ void readTwoAuthors
314
+ ```
315
+
316
+ Generated queries use the same checked rule, scalar, and execution machinery.
317
+ `describeQuery` exposes the logical IR and its names as owned data. Its relation
318
+ ordinals refer to the supplied schema's declaration order; variables are local
319
+ to each rule. Intermediate names are local labels. The result-field record
320
+ passed to `queryFromDescription` must match the actual derived head, including
321
+ closed vocabularies, and determines the returned row type. Parameters supplied
322
+ to a generated query are checked against their uses during execution.
323
+
324
+ ```ts
325
+ const Event = relation("Event", { id: uuid, amount: u64 })
326
+ const Events = schema("Events", { Event }, [key(Event, ["id"])])
327
+ const countEvents = query(Events).rule((r) =>
328
+ r.match(Event, {}).find({ count: r.count() })
329
+ )
330
+ const description = describeQuery(countEvents)
331
+ const totalEvents = queryFromDescription(
332
+ Events,
333
+ { ...description, columns: ["total"] },
334
+ { total: u64 }
335
+ )
336
+ const readTotal = (reader: QueryReader<typeof Events>) =>
337
+ Effect.scoped(Effect.gen(function* () {
338
+ const result = yield* reader.execute(totalEvents, {})
339
+ const rows: readonly { readonly total: bigint }[] = yield* result.collect()
340
+ return rows
341
+ }))
342
+ void readTotal
343
+ ```
344
+
345
+ Descriptions contain bigint and byte values, so they are not a JSON wire format.
346
+ An accepted description is an authored query; native preparation still checks
347
+ engine semantics. It carries no snapshot, prepared handle, or result rows.
348
+
213
349
  ## 6. Grouped exact aggregates
214
350
 
215
351
  Aggregates fold the group's distinct full bindings: keep the identity-bearing
@@ -263,13 +399,15 @@ const labeled = query(Telemetry).rule((r) => {
263
399
  void labeled
264
400
  ```
265
401
 
266
- ## 8. Bounded results: capped collect, one-shot page stream
402
+ ## 8. Completed results: collect or consume delivery batches
267
403
 
268
- `collect({ maxBytes }, work)` is database-enforced total materialization and
269
- leaves the result available. `pages({ pageBytes }, work)` is a ONE-SHOT consuming
404
+ `collect()` explicitly materializes all rows into JavaScript and
405
+ leaves the result available. `pages()` is a ONE-SHOT consuming
270
406
  stream over the completed result: the first run moves the backing into a
271
407
  private scoped cursor; a second run refuses. Every element is one owned page
272
- array — pages, not rows — delivered after complete evaluation.
408
+ array of up to 256 rows — pages, not rows — delivered after complete evaluation.
409
+ Paging bounds delivery, not query execution or total result storage. An empty
410
+ result emits one empty page; interruption and early termination close the cursor.
273
411
 
274
412
  ```ts
275
413
  const Event = relation("Event", { id: uuid, at: i64 })
@@ -281,7 +419,7 @@ const everything = query(Feed).rule((r) => {
281
419
  })
282
420
 
283
421
  const drain = (result: CompleteResult<{ readonly id: Uuid; readonly at: bigint }>) =>
284
- result.pages({ pageBytes: 65536n }, work).pipe(
422
+ result.pages().pipe(
285
423
  Stream.runForEach((page) =>
286
424
  Effect.sync(() => {
287
425
  // One owned page array; caller mutation cannot reach native
@@ -321,27 +459,28 @@ the apply instead of silently overwriting.
321
459
 
322
460
  ```ts
323
461
  const Account = relation("Account", { id: uuid, balance: i64 })
324
- const Bank = schema("Bank", { Account }, [key(Account, ["id"])])
462
+ const AccountById = key(Account, ["id"])
463
+ const Bank = schema("Bank", { Account }, [AccountById])
325
464
 
326
465
  const correct = (accountId: Uuid) =>
327
466
  Effect.scoped(
328
467
  Effect.gen(function* () {
329
- const db = yield* Db.open(localPath, Bank, work)
468
+ const db = yield* Db.open(localPath, Bank)
330
469
  const observed = yield* Effect.scoped(
331
470
  Effect.gen(function* () {
332
- const snapshot = yield* db.snapshot(work)
333
- const previous = yield* snapshot.get(Account, { id: accountId }, work)
471
+ const snapshot = yield* db.snapshot()
472
+ const previous = yield* snapshot.get(AccountById, { id: accountId })
334
473
  if (Option.isNone(previous)) {
335
474
  return yield* Effect.fail({ missing: accountId })
336
475
  }
337
476
  return { previous: previous.value, at: snapshot.witness }
338
477
  })
339
478
  )
340
- const draft = yield* ChangeSet.builder(Bank, work)
479
+ const draft = yield* ChangeSet.builder(Bank)
341
480
  yield* draft.delete(Account, [observed.previous])
342
481
  yield* draft.insert(Account, [{ ...observed.previous, balance: observed.previous.balance + 1n }])
343
482
  const changes = yield* draft.finish()
344
- return yield* db.apply(changes, { ...work, expected: { kind: "exact", at: observed.at } })
483
+ return yield* db.apply(changes, { expected: { kind: "exact", at: observed.at } })
345
484
  })
346
485
  )
347
486
  void correct
@@ -352,16 +491,17 @@ void correct
352
491
  `f64` is a real schema scalar: NaN canonicalizes to the one quiet NaN,
353
492
  `-0` to `+0`, and the relational order is total. `interval(f64)` is the
354
493
  parameterized dense interval — half-open, NaN-free, strictly ordered.
355
- `span` builds checked interval values as `Result`s.
494
+ Interval values are structural records. Their field descriptor validates
495
+ endpoint ranges, nonemptiness and fixed width at the boundary. Integer rays
496
+ end at the element's maximum; fixed-width intervals cannot be rays.
356
497
 
357
498
  ```ts
358
499
  const Window = relation("Window", { id: uuid, confidence: interval(f64), during: interval(i64) })
359
500
  const Windows = schema("Windows", { Window }, [key(Window, ["id"])])
360
501
 
361
- const discrete = span(0n, 60n)
362
- const dense = span(0.25, 1.5)
363
- const bothChecked: boolean = Result.isSuccess(discrete) && Result.isSuccess(dense)
364
- void [Windows, bothChecked]
502
+ const discrete: IntervalValue = { start: 0n, end: 60n }
503
+ const dense: FloatIntervalValue = { start: 0.25, end: 1.5 }
504
+ void [Windows, discrete, dense]
365
505
  ```
366
506
 
367
507
  ## 12. Scoped ownership and honest close
@@ -377,7 +517,7 @@ const Items = schema("Items", { Item }, [key(Item, ["id"])])
377
517
 
378
518
  const explicitClose = Effect.scoped(
379
519
  Effect.gen(function* () {
380
- const db = yield* Db.open(localPath, Items, work)
520
+ const db = yield* Db.open(localPath, Items)
381
521
  const report = yield* db.close()
382
522
  // `closed` releases this capability's obligations; `incomplete` and
383
523
  // `failed` retain native Closing accounting — they are never
package/README.md CHANGED
@@ -19,6 +19,11 @@ ordinary `bigint`, `number` (for `f64`), `string`, boolean, byte, `Uuid`,
19
19
  and interval values; queries infer their parameter and result types from how
20
20
  those values are used.
21
21
 
22
+ Relations, closed rosters, keys, selections, and constraints are checked
23
+ structural descriptions. Schema construction owns copies of declarations.
24
+ Independently constructed equivalent declarations work in queries, codecs,
25
+ writes, and key lookups; ordered fields and enum handles must agree.
26
+
22
27
  ## Platform support
23
28
 
24
29
  The TypeScript package ships native binaries for **darwin-arm64**
@@ -33,8 +38,12 @@ correctness CI is distinct from the Apple Silicon performance measurements.
33
38
 
34
39
  ## Install
35
40
 
41
+ This guide follows the 1.2.0 source API. Use the guide from the Git tag matching
42
+ your installed package. GitHub release tarballs and npm publication are separate;
43
+ the npm command below applies once that version is published.
44
+
36
45
  ```sh
37
- pnpm add @bjornpagen/bumbledb@1.0.1 effect@4.0.0-rc.112
46
+ pnpm add @bjornpagen/bumbledb@1.2.0 effect@4.0.0-rc.112
38
47
  ```
39
48
 
40
49
  ## Quick start
@@ -68,7 +77,6 @@ import {
68
77
  weigh,
69
78
  within
70
79
  } from "@bjornpagen/bumbledb"
71
- import type { ExecutionPolicy, NativeRuntimeOptions } from "@bjornpagen/bumbledb"
72
80
 
73
81
  // Relations describe stored records. Identity fields are ordinary
74
82
  // application-owned Uuid values — the database issues no identity.
@@ -82,8 +90,9 @@ const Attempt = relation("Attempt", {
82
90
  })
83
91
 
84
92
  // Keys are declared statements; references and capacity are laws.
93
+ const StudentById = key(Student, ["id"])
85
94
  const Learning = schema("Learning", { Student, Attempt }, [
86
- key(Student, ["id"]),
95
+ StudentById,
87
96
  key(Attempt, ["id"]),
88
97
  contained(on(Attempt, "student"), on(Student, "id")),
89
98
  capacity(on(Student, "id"), {
@@ -93,9 +102,6 @@ const Learning = schema("Learning", { Student, Attempt }, [
93
102
  })
94
103
  ])
95
104
 
96
- // Measured policies are inputs — the library invents no limits.
97
- declare const runtimePolicy: NativeRuntimeOptions
98
- declare const work: ExecutionPolicy
99
105
  declare const localPath: string
100
106
 
101
107
  // Queries are reusable typed values: reusing a v(R) variable is the join,
@@ -111,35 +117,35 @@ const attemptsFor = query(Learning).rule((r) => {
111
117
  const program = Effect.scoped(
112
118
  Effect.gen(function* () {
113
119
  // First use: create explicitly. Use Db.open for an existing store.
114
- const db = yield* Db.create(localPath, Learning, work)
120
+ const db = yield* Db.create(localPath, Learning)
115
121
  const studentId = yield* Effect.sync(() => crypto.randomUUID())
116
122
  const attemptId = yield* Effect.sync(() => crypto.randomUUID())
117
123
 
118
- const draft = yield* ChangeSet.builder(Learning, work)
124
+ const draft = yield* ChangeSet.builder(Learning)
119
125
  yield* draft.insert(Student, [{ id: studentId, name: "Ada", budget: 10n }])
120
126
  yield* draft.insert(Attempt, [
121
127
  { id: attemptId, student: studentId, score: 0.9, units: 1n, active: { start: 0n, end: 60n } }
122
128
  ])
123
129
  const changes = yield* draft.finish()
124
130
 
125
- const outcome = yield* db.apply(changes, { ...work, expected: { kind: "any" } })
131
+ const outcome = yield* db.apply(changes, { expected: { kind: "any" } })
126
132
  if (outcome.kind !== "accepted" && outcome.kind !== "no-change") {
127
133
  return outcome
128
134
  }
129
- const snapshot = yield* db.snapshot(work)
130
- const found = yield* snapshot.get(Student, { id: studentId }, work)
135
+ const snapshot = yield* db.snapshot()
136
+ const found = yield* snapshot.get(StudentById, { id: studentId })
131
137
  if (Option.isNone(found)) {
132
138
  return outcome
133
139
  }
134
- const result = yield* snapshot.execute(attemptsFor, { student: studentId }, work)
135
- const rows = yield* result.collect({ maxBytes: work.resultBytes }, work)
140
+ const result = yield* snapshot.execute(attemptsFor, { student: studentId })
141
+ const rows = yield* result.collect()
136
142
  return { outcome, rows }
137
143
  })
138
144
  )
139
145
 
140
146
  // One boundary for this script; an Effect app supplies the layer in its
141
147
  // own application graph instead.
142
- void Effect.runPromise(program.pipe(Effect.provide(NativeRuntime.layer(runtimePolicy))))
148
+ void Effect.runPromise(program.pipe(Effect.provide(NativeRuntime.layer())))
143
149
  ```
144
150
 
145
151
  Every `ts` fence in this README is extracted and type-checked against the
@@ -151,8 +157,8 @@ The SDK translates TypeScript values directly into the engine's shared schema
151
157
  and query representations.
152
158
 
153
159
  - Fields use `bool`, `bytes`, `f64`, `i64`, `uuid`, `u64`, `str`, and
154
- `interval` (`interval(f64)` is the dense float interval); `span` builds
155
- checked interval values. `relation()` declares stored records, while
160
+ `interval` (`interval(f64)` is the dense float interval). Interval values
161
+ are plain `{ start, end }` records checked against their field. `relation()` declares stored records, while
156
162
  `closed()` declares a fixed enum-like set whose values may carry typed
157
163
  columns. `Infer` exposes the resulting TypeScript value type. `Uuid` is
158
164
  a structural template-literal string, not a nominal brand or a cast helper.
@@ -162,24 +168,41 @@ and query representations.
162
168
  - `schema()` accepts `key`, `contained`, `mirrors`, and `capacity`
163
169
  statements. Keys are declared statements — there is no minted identity.
164
170
  `capacity(target, { from, weight?, within })` takes named options;
165
- `.where` makes a reference conditional, `within` sets a count or
171
+ `select(relation, { field: value })` makes a reference conditional,
172
+ `closedId(roster)` supplies a field that accepts named enum handles,
173
+ `within` sets a count or
166
174
  measurement range, and `weigh` chooses a numeric field or interval
167
175
  duration. Harmless equivalent window spellings lower to one canonical
168
176
  law; genuinely different meanings still refuse.
169
- - `NativeRuntime.layer(options)` owns the one bounded native runtime;
177
+ - `NativeRuntime.layer()` owns the shared native runtime with sensible defaults;
170
178
  provide it once in the app graph. `Db.create` and `Db.open` are scoped
171
179
  Effects over that runtime; `open` never creates and `create` refuses
172
- existing authority. `db.apply(changes, { ...work, expected })` judges one
180
+ existing authority. `db.apply(changes, { expected })` judges one
173
181
  immutable final-state change: `accepted`, `no-change`,
174
182
  `invariant-rejected` (complete statement diagnostics), or `moved`.
175
- - `ChangeSet.builder(schema, work)` acquires a scoped database-free draft;
183
+ `db.judge(changes, { expected })` uses the same admission path but aborts
184
+ the private candidate. It returns `admitted` or `invariant-rejected` with
185
+ the actual base witness and net additions/removals, or `moved` without
186
+ judging. Both use `WriteOptions`; judgment never guarantees a later apply.
187
+ - `ChangeSet.builder(schema)` acquires a scoped database-free draft;
176
188
  `insert`/`delete` are lazy bounded ingestion effects, `finish()` seals the
177
- immutable `ChangeSet`. Snapshots satisfy the shared `QueryReader`: typed
178
- `get` returns `Option`, `execute` returns a sealed `CompleteResult` whose
179
- `collect({ maxBytes }, work)` is capped materialization and whose
180
- `pages({ pageBytes }, work)` is a one-shot consuming `Stream` of owned page
181
- arrays after complete evaluation. Delivery work is fresh: it does not
182
- inherit the snapshot or execution deadline.
189
+ immutable `ChangeSet`. It exposes `schemaId`, `counts`, and `byteLength`.
190
+ `records()` is a reusable, bounded stream of relation-name-discriminated
191
+ facts; each traversal owns an independent scoped cursor. `toBytes()`
192
+ explicitly materializes canonical bytes; `ChangeSet.fromBytes(schema, bytes)`
193
+ checks them without normalizing malformed input. `left.compose(right)`
194
+ merges native records into one add-wins command without decoding rows.
195
+ Composition is commutative, associative, and idempotent, not sequential
196
+ replay. Its counts describe requested distinct actions; only `judge`
197
+ measures their effect against a store.
198
+ - Snapshots satisfy the shared `QueryReader`: typed
199
+ `get(keyDescriptor, keyValues)` chooses an explicit declared key and returns
200
+ `Option`; `execute` returns a sealed `CompleteResult` whose
201
+ `collect()` explicitly materializes all rows and whose
202
+ `pages()` is a one-shot consuming `Stream` of owned page
203
+ arrays after complete evaluation. Each page contains up to 256 rows; this
204
+ bounds delivery rather than streaming execution. Effect scope closes the
205
+ cursor on completion, failure, or interruption.
183
206
  - `query(S).rule(...)` builds typed queries. Reusing a variable created by
184
207
  `v(R)` joins records through that value. The builder supports named result
185
208
  rows, typed parameters, negation, comparisons, boolean conditions, set
@@ -187,6 +210,21 @@ and query representations.
187
210
  `f64` are deterministic with one final rounding), named intermediate
188
211
  results, nonrecursive composition of query templates, and linear
189
212
  recursive reachability.
213
+ - `describeQuery(q)` returns owned logical IR, result names, intermediate
214
+ table names, and parameter names. `queryFromDescription(S, description,
215
+ resultFields)` checks a generated description through the same rule builder,
216
+ scalar grammar, lowering, and execution paths. The result-field record is
217
+ checked against the derived head and infers the returned row type; it is
218
+ not an unchecked cast. Generated parameters are validated by their uses at
219
+ execution. Ordinals refer to the supplied schema's ordered declarations;
220
+ variable ordinals are local to each rule. Native preparation still checks
221
+ engine semantics. The description contains plain values, including bigint
222
+ and byte arrays, rather than native resources or a JSON encoding.
223
+ Query `data` and `schema`, and compiled-schema inspection properties,
224
+ return detached snapshots: editing their byte buffers cannot change the
225
+ query or compilation. Checked immutable branches may be shared. Structural
226
+ declarations with byte payloads are copied when consumed; JavaScript byte
227
+ buffers themselves are mutable and are never treated as immutable cache keys.
190
228
  - `Scalar.field("units")` is an unresolved source-field leaf.
191
229
  `Scalar.add(Scalar.field("units"), Scalar.u64(1n))` authors synchronously
192
230
  without native loading. Native schema binding typechecks it, including
@@ -195,6 +233,12 @@ and query representations.
195
233
  Effect error channel; interruption and finalizer problems stay in
196
234
  `Cause`. Resource owners are scoped and report honest `CloseReport`s;
197
235
  incomplete teardown surfaces as a structured `CloseFailure` defect.
236
+ `error.message` contains the operation and reason code for safe default
237
+ display. Inspect `error.reason` explicitly for engine details, or
238
+ `InvalidArgument.detail` for authoring text and available structured
239
+ diagnostics such as `MissingField`, `UnknownField`, and `InvalidValue`.
240
+ Those details can contain application names or values and should not be
241
+ copied into public logs indiscriminately.
198
242
 
199
243
  ## Cookbook
200
244