@bjornpagen/bumbledb 1.3.0 → 2.0.0-rc.1

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 (290) hide show
  1. package/COOKBOOK.md +224 -95
  2. package/README.md +114 -45
  3. package/dist/alternatives.d.ts +5 -6
  4. package/dist/alternatives.d.ts.map +1 -1
  5. package/dist/alternatives.js +5 -6
  6. package/dist/alternatives.js.map +1 -1
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +85 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/capacity.d.ts +14 -17
  12. package/dist/capacity.d.ts.map +1 -1
  13. package/dist/capacity.js +7 -7
  14. package/dist/capacity.js.map +1 -1
  15. package/dist/changes.d.ts +32 -41
  16. package/dist/changes.d.ts.map +1 -1
  17. package/dist/changes.js +86 -110
  18. package/dist/changes.js.map +1 -1
  19. package/dist/closed.d.ts +2 -2
  20. package/dist/closed.d.ts.map +1 -1
  21. package/dist/closed.js +5 -5
  22. package/dist/closed.js.map +1 -1
  23. package/dist/codec.d.ts +20 -27
  24. package/dist/codec.d.ts.map +1 -1
  25. package/dist/codec.js +48 -22
  26. package/dist/codec.js.map +1 -1
  27. package/dist/compile.d.ts +26 -50
  28. package/dist/compile.d.ts.map +1 -1
  29. package/dist/compile.js +75 -64
  30. package/dist/compile.js.map +1 -1
  31. package/dist/database/database.d.ts +98 -0
  32. package/dist/database/database.d.ts.map +1 -0
  33. package/dist/database/database.js +249 -0
  34. package/dist/database/database.js.map +1 -0
  35. package/dist/database/driver.d.ts +40 -0
  36. package/dist/database/driver.d.ts.map +1 -0
  37. package/dist/database/driver.js +68 -0
  38. package/dist/database/driver.js.map +1 -0
  39. package/dist/database/fs.d.ts +12 -0
  40. package/dist/database/fs.d.ts.map +1 -0
  41. package/dist/database/fs.js +120 -0
  42. package/dist/database/fs.js.map +1 -0
  43. package/dist/database/generate.d.ts +26 -0
  44. package/dist/database/generate.d.ts.map +1 -0
  45. package/dist/database/generate.js +102 -0
  46. package/dist/database/generate.js.map +1 -0
  47. package/dist/database/hosted.d.ts +32 -0
  48. package/dist/database/hosted.d.ts.map +1 -0
  49. package/dist/database/hosted.js +44 -0
  50. package/dist/database/hosted.js.map +1 -0
  51. package/dist/database/io.d.ts +94 -0
  52. package/dist/database/io.d.ts.map +1 -0
  53. package/dist/database/io.js +35 -0
  54. package/dist/database/io.js.map +1 -0
  55. package/dist/database/mem.d.ts +26 -0
  56. package/dist/database/mem.d.ts.map +1 -0
  57. package/dist/database/mem.js +67 -0
  58. package/dist/database/mem.js.map +1 -0
  59. package/dist/database/migration.d.ts +55 -0
  60. package/dist/database/migration.d.ts.map +1 -0
  61. package/dist/database/migration.js +46 -0
  62. package/dist/database/migration.js.map +1 -0
  63. package/dist/database/s3.d.ts +36 -0
  64. package/dist/database/s3.d.ts.map +1 -0
  65. package/dist/database/s3.js +141 -0
  66. package/dist/database/s3.js.map +1 -0
  67. package/dist/db.d.ts +44 -128
  68. package/dist/db.d.ts.map +1 -1
  69. package/dist/db.js +163 -287
  70. package/dist/db.js.map +1 -1
  71. package/dist/engine.d.ts +7 -0
  72. package/dist/engine.d.ts.map +1 -0
  73. package/dist/engine.js +2 -0
  74. package/dist/engine.js.map +1 -0
  75. package/dist/errors.d.ts +152 -46
  76. package/dist/errors.d.ts.map +1 -1
  77. package/dist/errors.js +107 -19
  78. package/dist/errors.js.map +1 -1
  79. package/dist/face.d.ts +5 -5
  80. package/dist/face.d.ts.map +1 -1
  81. package/dist/face.js +6 -6
  82. package/dist/face.js.map +1 -1
  83. package/dist/fields.d.ts +4 -5
  84. package/dist/fields.d.ts.map +1 -1
  85. package/dist/fields.js +3 -3
  86. package/dist/fields.js.map +1 -1
  87. package/dist/index.d.ts +60 -58
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +27 -29
  90. package/dist/index.js.map +1 -1
  91. package/dist/judgment.d.ts +1 -1
  92. package/dist/judgment.js +1 -1
  93. package/dist/law.d.ts +16 -68
  94. package/dist/law.d.ts.map +1 -1
  95. package/dist/law.js +3 -3
  96. package/dist/law.js.map +1 -1
  97. package/dist/lower.d.ts +3 -3
  98. package/dist/lower.d.ts.map +1 -1
  99. package/dist/lower.js +35 -39
  100. package/dist/lower.js.map +1 -1
  101. package/dist/native/addon.d.ts +23 -0
  102. package/dist/native/addon.d.ts.map +1 -0
  103. package/dist/native/addon.js +15 -0
  104. package/dist/native/addon.js.map +1 -0
  105. package/dist/native/binding.d.ts +996 -0
  106. package/dist/native/json.d.ts +14 -0
  107. package/dist/native/json.d.ts.map +1 -0
  108. package/dist/native/json.js +73 -0
  109. package/dist/native/json.js.map +1 -0
  110. package/dist/native/load.d.ts +9 -0
  111. package/dist/native/load.d.ts.map +1 -0
  112. package/dist/native/load.js +30 -0
  113. package/dist/native/load.js.map +1 -0
  114. package/dist/native/op.d.ts +29 -0
  115. package/dist/native/op.d.ts.map +1 -0
  116. package/dist/native/op.js +50 -0
  117. package/dist/native/op.js.map +1 -0
  118. package/dist/query/atom.d.ts +13 -14
  119. package/dist/query/atom.d.ts.map +1 -1
  120. package/dist/query/atom.js +2 -2
  121. package/dist/query/atom.js.map +1 -1
  122. package/dist/query/compute.d.ts +7 -7
  123. package/dist/query/compute.d.ts.map +1 -1
  124. package/dist/query/compute.js +7 -16
  125. package/dist/query/compute.js.map +1 -1
  126. package/dist/query/find.d.ts +8 -8
  127. package/dist/query/find.d.ts.map +1 -1
  128. package/dist/query/find.js +1 -1
  129. package/dist/query/lower.d.ts +31 -17
  130. package/dist/query/lower.d.ts.map +1 -1
  131. package/dist/query/lower.js +260 -368
  132. package/dist/query/lower.js.map +1 -1
  133. package/dist/query/run.d.ts +5 -5
  134. package/dist/query/run.d.ts.map +1 -1
  135. package/dist/query/run.js +32 -15
  136. package/dist/query/run.js.map +1 -1
  137. package/dist/query/scope.d.ts +20 -28
  138. package/dist/query/scope.d.ts.map +1 -1
  139. package/dist/query/scope.js +13 -8
  140. package/dist/query/scope.js.map +1 -1
  141. package/dist/query/segments.d.ts +2 -2
  142. package/dist/query/segments.d.ts.map +1 -1
  143. package/dist/query/segments.js +3 -3
  144. package/dist/query/segments.js.map +1 -1
  145. package/dist/relation.d.ts +1 -1
  146. package/dist/relation.d.ts.map +1 -1
  147. package/dist/relation.js +4 -4
  148. package/dist/relation.js.map +1 -1
  149. package/dist/result.d.ts +15 -22
  150. package/dist/result.d.ts.map +1 -1
  151. package/dist/result.js +30 -39
  152. package/dist/result.js.map +1 -1
  153. package/dist/rows.d.ts +9 -33
  154. package/dist/rows.d.ts.map +1 -1
  155. package/dist/rows.js +8 -12
  156. package/dist/rows.js.map +1 -1
  157. package/dist/runtime.d.ts +17 -43
  158. package/dist/runtime.d.ts.map +1 -1
  159. package/dist/runtime.js +41 -241
  160. package/dist/runtime.js.map +1 -1
  161. package/dist/scalar.d.ts +36 -106
  162. package/dist/scalar.d.ts.map +1 -1
  163. package/dist/scalar.js +36 -185
  164. package/dist/scalar.js.map +1 -1
  165. package/dist/schema.d.ts +7 -18
  166. package/dist/schema.d.ts.map +1 -1
  167. package/dist/schema.js +18 -228
  168. package/dist/schema.js.map +1 -1
  169. package/dist/selection.d.ts +4 -4
  170. package/dist/selection.d.ts.map +1 -1
  171. package/dist/selection.js +5 -5
  172. package/dist/selection.js.map +1 -1
  173. package/dist/shape.d.ts +5 -5
  174. package/dist/shape.d.ts.map +1 -1
  175. package/dist/spec.d.ts +7 -79
  176. package/dist/spec.d.ts.map +1 -1
  177. package/dist/spec.js.map +1 -1
  178. package/dist/statements.d.ts +3 -3
  179. package/dist/statements.d.ts.map +1 -1
  180. package/dist/statements.js +10 -10
  181. package/dist/statements.js.map +1 -1
  182. package/dist/uuid.d.ts +1 -1
  183. package/dist/uuid.d.ts.map +1 -1
  184. package/dist/uuid.js +1 -1
  185. package/dist/uuid.js.map +1 -1
  186. package/dist/values.d.ts +2 -2
  187. package/dist/values.d.ts.map +1 -1
  188. package/dist/values.js +2 -2
  189. package/dist/values.js.map +1 -1
  190. package/package.json +18 -18
  191. package/src/alternatives.ts +7 -8
  192. package/src/bin.ts +82 -0
  193. package/src/capacity.ts +20 -23
  194. package/src/changes.ts +134 -197
  195. package/src/closed.ts +5 -5
  196. package/src/codec.ts +77 -51
  197. package/src/compile.ts +91 -113
  198. package/src/database/database.ts +413 -0
  199. package/src/database/driver.ts +134 -0
  200. package/src/database/fs.ts +119 -0
  201. package/src/database/generate.ts +116 -0
  202. package/src/database/hosted.ts +102 -0
  203. package/src/database/io.ts +134 -0
  204. package/src/database/mem.ts +113 -0
  205. package/src/database/migration.ts +107 -0
  206. package/src/database/s3.ts +231 -0
  207. package/src/db.ts +290 -547
  208. package/src/engine.ts +14 -0
  209. package/src/errors.ts +118 -39
  210. package/src/face.ts +9 -9
  211. package/src/fields.ts +6 -7
  212. package/src/index.ts +102 -109
  213. package/src/judgment.ts +1 -1
  214. package/src/law.ts +18 -70
  215. package/src/lower.ts +61 -72
  216. package/src/native/addon.ts +54 -0
  217. package/src/native/binding.d.ts +996 -0
  218. package/src/native/json.ts +100 -0
  219. package/src/native/load.ts +31 -0
  220. package/src/native/op.ts +73 -0
  221. package/src/query/atom.ts +15 -16
  222. package/src/query/compute.ts +13 -22
  223. package/src/query/find.ts +8 -8
  224. package/src/query/lower.ts +323 -414
  225. package/src/query/run.ts +42 -21
  226. package/src/query/scope.ts +37 -38
  227. package/src/query/segments.ts +4 -4
  228. package/src/relation.ts +4 -4
  229. package/src/result.ts +53 -80
  230. package/src/rows.ts +18 -46
  231. package/src/runtime.ts +50 -346
  232. package/src/scalar.ts +76 -336
  233. package/src/schema.ts +23 -284
  234. package/src/selection.ts +6 -6
  235. package/src/shape.ts +5 -5
  236. package/src/spec.ts +7 -85
  237. package/src/statements.ts +11 -11
  238. package/src/uuid.ts +1 -1
  239. package/src/values.ts +4 -4
  240. package/dist/close.d.ts +0 -24
  241. package/dist/close.d.ts.map +0 -1
  242. package/dist/close.js +0 -34
  243. package/dist/close.js.map +0 -1
  244. package/dist/db-native.d.ts +0 -196
  245. package/dist/db-native.d.ts.map +0 -1
  246. package/dist/db-native.js +0 -13
  247. package/dist/db-native.js.map +0 -1
  248. package/dist/internal/log.d.ts +0 -26
  249. package/dist/internal/log.d.ts.map +0 -1
  250. package/dist/internal/log.js +0 -12
  251. package/dist/internal/log.js.map +0 -1
  252. package/dist/migration.d.ts +0 -31
  253. package/dist/migration.d.ts.map +0 -1
  254. package/dist/migration.js +0 -47
  255. package/dist/migration.js.map +0 -1
  256. package/dist/native.d.ts +0 -562
  257. package/dist/native.d.ts.map +0 -1
  258. package/dist/native.js +0 -123
  259. package/dist/native.js.map +0 -1
  260. package/dist/query/description.d.ts +0 -41
  261. package/dist/query/description.d.ts.map +0 -1
  262. package/dist/query/description.js +0 -311
  263. package/dist/query/description.js.map +0 -1
  264. package/dist/query/parse-ir.d.ts +0 -10
  265. package/dist/query/parse-ir.d.ts.map +0 -1
  266. package/dist/query/parse-ir.js +0 -280
  267. package/dist/query/parse-ir.js.map +0 -1
  268. package/dist/runtime-codes.d.ts +0 -2
  269. package/dist/runtime-codes.d.ts.map +0 -1
  270. package/dist/runtime-codes.js +0 -18
  271. package/dist/runtime-codes.js.map +0 -1
  272. package/dist/runtime-errors.d.ts +0 -118
  273. package/dist/runtime-errors.d.ts.map +0 -1
  274. package/dist/runtime-errors.js +0 -98
  275. package/dist/runtime-errors.js.map +0 -1
  276. package/dist/runtime-native.d.ts +0 -141
  277. package/dist/runtime-native.d.ts.map +0 -1
  278. package/dist/runtime-native.js +0 -5
  279. package/dist/runtime-native.js.map +0 -1
  280. package/pack-provenance.json +0 -7
  281. package/src/close.ts +0 -53
  282. package/src/db-native.ts +0 -243
  283. package/src/internal/log.ts +0 -48
  284. package/src/migration.ts +0 -61
  285. package/src/native.ts +0 -736
  286. package/src/query/description.ts +0 -424
  287. package/src/query/parse-ir.ts +0 -291
  288. package/src/runtime-codes.ts +0 -17
  289. package/src/runtime-errors.ts +0 -109
  290. package/src/runtime-native.ts +0 -189
package/COOKBOOK.md CHANGED
@@ -3,7 +3,9 @@
3
3
  Worked recipes for the Effect-native surface: typed schema values,
4
4
  declared keys and laws, application-owned `Uuid` identity, one bounded
5
5
  native runtime, scoped resources, immutable final-state changes, `Option`
6
- reads, sealed complete results, and one-shot page streams.
6
+ reads, sealed complete results, one-shot page streams, and durable
7
+ `Database`s over an object store. Recipes 1 to 18 use the embedded `Db` from
8
+ `@bjornpagen/bumbledb/engine`; recipes 19 and 20 use `Database`.
7
9
 
8
10
  Every `ts` fence below is extracted and type-checked against the real
9
11
  package surface by `test/cookbook-doc.test.ts`; the imports fence here is
@@ -11,7 +13,7 @@ prepended to every recipe. The examples are lazy Effect programs — nothing
11
13
  below runs a database at import time.
12
14
 
13
15
  ```ts
14
- import { Effect, Option, Result, Stream } from "effect"
16
+ import { Effect, Layer, Option, Result, Schema as EffectSchema, Stream } from "effect"
15
17
  import {
16
18
  alternatives,
17
19
  bool,
@@ -21,23 +23,25 @@ import {
21
23
  closedId,
22
24
  Compute,
23
25
  contained,
24
- Db,
25
- describeQuery,
26
+ Database,
27
+ decodeBoundaryField,
26
28
  duration,
29
+ encodeBoundaryField,
27
30
  f64,
31
+ fieldSchema,
32
+ FsStore,
28
33
  i64,
29
34
  uuid,
30
35
  Uuid,
31
36
  interval,
32
37
  key,
33
38
  mirrors,
34
- NativeRuntime,
39
+ Bumble,
35
40
  on,
36
41
  query,
37
- queryFromDescription,
38
42
  ref,
39
43
  relation,
40
- Scalar,
44
+ S3Store,
41
45
  schema,
42
46
  select,
43
47
  str,
@@ -46,14 +50,16 @@ import {
46
50
  weigh,
47
51
  within
48
52
  } from "@bjornpagen/bumbledb"
53
+ import { Db } from "@bjornpagen/bumbledb/engine"
49
54
  import type {
50
- ApplyOutcome,
51
55
  CompleteResult,
52
56
  Fact,
53
57
  FloatIntervalValue,
54
58
  IntervalValue,
55
- QueryReader
59
+ Migrations,
60
+ S3Sender
56
61
  } from "@bjornpagen/bumbledb"
62
+ import type { ApplyOutcome, QueryReader } from "@bjornpagen/bumbledb/engine"
57
63
 
58
64
  declare const localPath: string
59
65
  ```
@@ -97,7 +103,7 @@ void [Learning, scoreIsNumber, unitsAreExact]
97
103
 
98
104
  ## 2. One runtime layer; explicit create and open
99
105
 
100
- `NativeRuntime.layer()` acquires the single native runtime
106
+ `Bumble.layer()` acquires the single native runtime
101
107
  with scope; reuse ONE layer value so Effect's memoization shares it.
102
108
  Optional configuration controls workers, outstanding jobs/handles, and cleanup
103
109
  reporting. Defaults are up to four workers, 128 queued jobs, 128 cleanup reports,
@@ -105,10 +111,8 @@ reporting. Defaults are up to four workers, 128 queued jobs, 128 cleanup reports
105
111
  Operations use ordinary allocation without byte/row/work quotas or execution
106
112
  deadlines. Effect interruption requests cooperative cancellation. Runtime
107
113
  inspection reports outstanding work, not memory usage.
108
- Database `inspect().storage` reports virtual map extent, populated file length,
109
- non-free LMDB pages, and allocated disk blocks (null when unavailable).
110
- None of these measures process heap usage or resident RAM. Log history
111
- inspection uses the same `StorageInspection` fields.
114
+ Database `inspect()` reports the generation, the database's bytes on disk and
115
+ the native operations it retains; none of these measures heap or resident RAM.
112
116
  `Db.open` never creates a missing database; `Db.create` refuses existing
113
117
  authority. Both are scoped acquisitions.
114
118
 
@@ -131,7 +135,7 @@ const createOnce = Effect.scoped(
131
135
  )
132
136
 
133
137
  // One boundary; an Effect app provides the layer in its own graph instead.
134
- const layer = NativeRuntime.layer()
138
+ const layer = Bumble.layer()
135
139
  void [openExisting.pipe(Effect.provide(layer)), createOnce.pipe(Effect.provide(layer))]
136
140
  ```
137
141
 
@@ -155,15 +159,14 @@ const applyOnce = Effect.scoped(
155
159
  const draft = yield* ChangeSet.builder(Tasks)
156
160
  yield* draft.insert(Task, [{ id: taskId, title: "write the cookbook", done: 0n }])
157
161
  const changes = yield* draft.finish()
158
- const outcome: ApplyOutcome = yield* db.apply(changes, { expected: { kind: "any" } })
159
- switch (outcome.kind) {
160
- case "accepted":
161
- case "no-change":
162
- return outcome.witness
163
- case "invariant-rejected":
162
+ const outcome: ApplyOutcome = yield* db.apply(changes)
163
+ switch (outcome._tag) {
164
+ case "Committed":
165
+ return outcome.generation
166
+ case "Rejected":
164
167
  // Complete statement diagnostics, typed data — never a throw.
165
168
  return yield* Effect.fail(outcome.violations)
166
- case "moved":
169
+ case "Moved":
167
170
  return outcome.current
168
171
  }
169
172
  })
@@ -173,9 +176,9 @@ void applyOnce
173
176
 
174
177
  For a noncommitting check, `db.judge` runs the same admission procedure and
175
178
  aborts its private candidate on the native worker. It briefly takes the
176
- writer; it does not hold a writer session open in JavaScript. Admitted and
177
- rejected judgments include the actual base and net proposed fact counts.
178
- Operational errors stay in the Effect error channel.
179
+ writer; it does not hold a writer session open in JavaScript. A judgment
180
+ carries the generation it judged against, and an admitted one also carries the
181
+ net proposed fact counts. Operational errors stay in the Effect error channel.
179
182
 
180
183
  ```ts
181
184
  const judgeOnce = Effect.scoped(
@@ -184,15 +187,15 @@ const judgeOnce = Effect.scoped(
184
187
  const draft = yield* ChangeSet.builder(Tasks)
185
188
  const id = yield* Effect.sync(() => crypto.randomUUID())
186
189
  yield* draft.insert(Task, [{ id, title: "inspect a candidate", done: 0n }])
187
- return yield* db.judge(yield* draft.finish(), { expected: { kind: "any" } })
190
+ return yield* db.judge(yield* draft.finish())
188
191
  })
189
192
  )
190
193
  void judgeOnce
191
194
  ```
192
195
 
193
196
  Judgment is optional: `apply` always judges for itself. To act on a judgment,
194
- use its base as an exact expected witness and handle `moved`; do not assume
195
- another writer could not change the database between these operations.
197
+ judge and apply with the same snapshot's witness and handle `Moved`; do not
198
+ assume another writer could not change the database between these operations.
196
199
 
197
200
  Change sets also work as inspectable values without a database. Their
198
201
  `counts` report distinct requested additions/removals, not the net change
@@ -318,39 +321,6 @@ const readTwoAuthors = (reader: QueryReader<typeof Library>) =>
318
321
  void readTwoAuthors
319
322
  ```
320
323
 
321
- Generated queries use the same checked rule, scalar, and execution machinery.
322
- `describeQuery` exposes the logical IR and its names as owned data. Its relation
323
- ordinals refer to the supplied schema's declaration order; variables are local
324
- to each rule. Intermediate names are local labels. The result-field record
325
- passed to `queryFromDescription` must match the actual derived head, including
326
- closed vocabularies, and determines the returned row type. Parameters supplied
327
- to a generated query are checked against their uses during execution.
328
-
329
- ```ts
330
- const Event = relation("Event", { id: uuid, amount: u64 })
331
- const Events = schema("Events", { Event }, [key(Event, ["id"])])
332
- const countEvents = query(Events).rule((r) =>
333
- r.match(Event, {}).find({ count: r.count() })
334
- )
335
- const description = describeQuery(countEvents)
336
- const totalEvents = queryFromDescription(
337
- Events,
338
- { ...description, columns: ["total"] },
339
- { total: u64 }
340
- )
341
- const readTotal = (reader: QueryReader<typeof Events>) =>
342
- Effect.scoped(Effect.gen(function* () {
343
- const result = yield* reader.execute(totalEvents, {})
344
- const rows: readonly { readonly total: bigint }[] = yield* result.collect()
345
- return rows
346
- }))
347
- void readTotal
348
- ```
349
-
350
- Descriptions contain bigint and byte values, so they are not a JSON wire format.
351
- An accepted description is an authored query; native preparation still checks
352
- engine semantics. It carries no snapshot, prepared handle, or result rows.
353
-
354
324
  ## 6. Grouped exact aggregates
355
325
 
356
326
  Aggregates fold the group's distinct full bindings: keep the identity-bearing
@@ -458,9 +428,9 @@ void [okOrRefused, mintOnce]
458
428
 
459
429
  ## 10. Witnessed correction: exact expected state
460
430
 
461
- Read under a short scope, keep the copied witness, and apply with
462
- `expected: { kind: "exact", at: witness }`. An intervening net change moves
463
- the apply instead of silently overwriting.
431
+ Read under a short scope, keep the snapshot's witness (an opaque value that
432
+ outlives the snapshot), and pass it to `apply` as the expected state. An intervening net change moves the apply instead of
433
+ silently overwriting.
464
434
 
465
435
  ```ts
466
436
  const Account = relation("Account", { id: uuid, balance: i64 })
@@ -485,7 +455,7 @@ const correct = (accountId: Uuid) =>
485
455
  yield* draft.delete(Account, [observed.previous])
486
456
  yield* draft.insert(Account, [{ ...observed.previous, balance: observed.previous.balance + 1n }])
487
457
  const changes = yield* draft.finish()
488
- return yield* db.apply(changes, { expected: { kind: "exact", at: observed.at } })
458
+ return yield* db.apply(changes, observed.at)
489
459
  })
490
460
  )
491
461
  void correct
@@ -509,44 +479,28 @@ const dense: FloatIntervalValue = { start: 0.25, end: 1.5 }
509
479
  void [Windows, discrete, dense]
510
480
  ```
511
481
 
512
- ## 12. Scoped ownership and honest close
482
+ ## 12. Scoped ownership
513
483
 
514
- Every native resource is scoped; early `close()` is itself an Effect
515
- returning the honest `CloseReport`. A scope finalizer that cannot complete
516
- teardown surfaces a structured `CloseFailure` DEFECT in the Cause — never a
517
- silently swallowed failure, never falsely reclaimed resources.
484
+ Every native resource belongs to the scope that acquired it: the database, snapshots, prepared
485
+ queries, results and change sets are released when their scope closes, innermost first. There is
486
+ no `close()`; to release something early, acquire it in a smaller scope. A release that cannot
487
+ drain cleanly is a `CloseFailure` defect in the Cause, never a swallowed failure.
518
488
 
519
489
  ```ts
520
490
  const Item = relation("Item", { id: uuid, label: str })
521
491
  const Items = schema("Items", { Item }, [key(Item, ["id"])])
522
492
 
523
- const explicitClose = Effect.scoped(
493
+ const shortLived = Effect.scoped(
524
494
  Effect.gen(function* () {
525
495
  const db = yield* Db.open(localPath, Items)
526
- const report = yield* db.close()
527
- // `closed` releases this capability's obligations; `incomplete` and
528
- // `failed` retain native Closing accounting — they are never
529
- // counted as reclaimed.
530
- return report.kind
496
+ const generation = yield* Effect.scoped(Effect.map(db.snapshot(), (snapshot) => snapshot.generation))
497
+ return generation
531
498
  })
532
499
  )
533
- void explicitClose
500
+ void shortLived
534
501
  ```
535
502
 
536
- ## 13. Unresolved field arithmetic is authoring metadata
537
-
538
- `Scalar.field("units")` is not a typed program. Builders accept it inside
539
- arithmetic. Native compilation binds it against the verified source
540
- schema — including empty input — before any manifest write or freeze.
541
-
542
- ```ts
543
- const incrementUnits = Scalar.add(Scalar.field("units"), Scalar.u64(1n))
544
- const asFloat = Scalar.toF64(Scalar.add(Scalar.field("units"), Scalar.u64(1n)))
545
- void [incrementUnits, asFloat]
546
- ```
547
-
548
-
549
- ## 14. Derive slices, measure them, and round the total
503
+ ## 13. Derive slices, measure them, and round the total
550
504
 
551
505
  An earning range crosses two rate bands. Intersection produces a relation of
552
506
  nonempty segments, measurement produces exact integer widths, and ordinary
@@ -586,7 +540,7 @@ const calculate = Effect.scoped(Effect.gen(function* () {
586
540
  { id: 1n, schedule: 1n, span: { start: 0n, end: 100n }, numerator: 1n },
587
541
  { id: 2n, schedule: 1n, span: { start: 100n, end: 200n }, numerator: 2n }
588
542
  ])
589
- yield* db.apply(yield* change.finish(), { expected: { kind: "any" } })
543
+ yield* db.apply(yield* change.finish())
590
544
  const snapshot = yield* db.snapshot()
591
545
  return yield* (yield* snapshot.execute(amounts, {})).collect()
592
546
  }))
@@ -601,7 +555,7 @@ calculation; summing separately rounded contributions can give a different resul
601
555
  Checked multiplication and aggregate result ranges still apply before the final
602
556
  `mulDiv`. The query creates no stored slice or copied-width facts.
603
557
 
604
- ## 15. Subtract a window, coalesce coverage, and measure
558
+ ## 14. Subtract a window, coalesce coverage, and measure
605
559
 
606
560
  Binary difference returns zero, one, or two maximal nonempty pieces. A subsequent
607
561
  `pack` stage coalesces overlapping or adjacent coverage per owner; measurement
@@ -656,7 +610,7 @@ segments, not arbitrary sums of already-rounded floating lengths.
656
610
  Each difference subtracts one interval. Unioning `A minus B1` with `A minus B2`
657
611
  does not subtract their combined coverage from `A`.
658
612
 
659
- ## 16. Exhaustive alternatives with ordinary laws
613
+ ## 15. Exhaustive alternatives with ordinary laws
660
614
 
661
615
  Declare each key once. The helper expands a closed roster into ordinary
662
616
  containment and mirrors statements in roster order. Equivalent independently
@@ -691,7 +645,7 @@ Each payload keeps its own inferred fields and ordinary relation representation.
691
645
  Evidence and ownership still need their own laws. The expansion has the same
692
646
  native descriptor, fingerprint, and admission costs as the identical manual laws.
693
647
 
694
- ## 17. Exact integer quotients and explicit rounding
648
+ ## 16. Exact integer quotients and explicit rounding
695
649
 
696
650
  `mulDiv` requires matching integer operands and a strictly positive divisor.
697
651
  It computes the product exactly in a wider native integer and checks the public
@@ -717,3 +671,178 @@ returns `-4`. Exact quotients do not change. `u64::MAX * 2 / 2` succeeds;
717
671
  Existing checked multiply/divide retain their intermediate-overflow behavior,
718
672
  so replacing them with `mulDiv` is an explicit semantic choice. All three modes
719
673
  perform bounded work without an additional scan, index, or JavaScript evaluator.
674
+
675
+ ## 17. Use field codecs directly
676
+
677
+ Fields carry their host-value schema and strict JSON-boundary representation.
678
+ Use the same descriptors in relations, selected input records, and individual
679
+ values. Integer JSON values are canonical decimal strings; float JSON values
680
+ are canonical `$f64` bit images. Native row bytes remain a separate format.
681
+
682
+ ```ts
683
+ const Method = closed("Method", ["Electronic", "Postal"])
684
+ const input = EffectSchema.Struct({
685
+ amount: fieldSchema(i64),
686
+ method: fieldSchema(closedId(Method)),
687
+ window: fieldSchema(interval(i64))
688
+ })
689
+ const valid = EffectSchema.is(input)({
690
+ amount: -25n,
691
+ method: "Electronic",
692
+ window: { start: 0n, end: 60n }
693
+ })
694
+ const amount = Result.getOrThrow(decodeBoundaryField(i64, "-25"))
695
+ const json = Result.getOrThrow(encodeBoundaryField(i64, amount)) // "-25"
696
+ void [valid, json]
697
+ ```
698
+
699
+ The codecs enforce integer bounds, closed handles, byte widths, valid intervals,
700
+ and canonical floating-point values through the same interpreter used by rows.
701
+ Application parsing of dates, currency, or nonblank text composes with the field
702
+ schema using ordinary Effect Schema operations.
703
+
704
+ ## 18. Combine projections and computations in one query
705
+
706
+ Every arm supplies the same ordered output columns and compatible field types.
707
+ A stored scalar and a computed scalar can occupy the same projection column.
708
+
709
+ ```ts
710
+ const Entry = relation("Entry", { id: u64, amount: i64, previous: i64, correction: bool })
711
+ const Ledger = schema("Ledger", { Entry }, [])
712
+ const amounts = query(Ledger)
713
+ .rule((r) => {
714
+ const { id, amount } = v(Entry)
715
+ return r.match(Entry, { id, amount, correction: false }).find({ id, amount })
716
+ })
717
+ .rule((r) => {
718
+ const { id, amount, previous } = v(Entry)
719
+ return r.match(Entry, { id, amount, previous, correction: true })
720
+ .find({ id, amount: Compute.subtract(amount, previous) })
721
+ })
722
+ const totals = query(Ledger).rule((r) => {
723
+ const row = v(amounts)
724
+ return r.match(amounts, row).find({ id: row.id, total: r.sum(row.amount) })
725
+ })
726
+ void totals
727
+ ```
728
+
729
+ Identical output facts deduplicate before the following stage aggregates them.
730
+ An evidenced zero remains a row; an absent group has none. Interval projections
731
+ can combine with produced interval pieces of the same element kind; a width
732
+ refinement survives only when every arm preserves it. Aggregate operators must
733
+ agree across arms and cannot share a column with a projection.
734
+
735
+ Each expression retains its original failure boundary. Moving filters across
736
+ an imported stage can change which arithmetic executes; a later filter cannot
737
+ erase an upstream overflow.
738
+
739
+ ## 19. A durable database: idempotent submits and fresh reads
740
+
741
+ A `Database` is a log in an object store read through a disposable local
742
+ cache. Provide it once with `Database.layer` under a service key; in a Lambda,
743
+ build the layer during init. Production opens with `onOpen: "verify"` after
744
+ `bumbledb migrate` ran in the deploy pipeline.
745
+
746
+ Mint one request id per intent and reuse it for every retry of that intent: a
747
+ command is decided at most once. `Decided` carries the receipt (`Committed`,
748
+ `NoChange`, `PreconditionFailed` or `InvariantRejected`). Every refusal except
749
+ `Unknown` proves the command is not in the log; after `Unknown`, `resolve`
750
+ answers from the log. `precondition: reader.revision` decides the command only
751
+ if nothing committed since that read, which makes read-modify-write exact.
752
+
753
+ ```ts
754
+ const Task = relation("Task", { id: uuid, title: str, done: bool })
755
+ const Tasks = schema("Tasks", { Task }, [key(Task, ["id"])])
756
+ const openTasks = query(Tasks).rule((r) => {
757
+ const { id, title } = v(Task)
758
+ return r.match(Task, { id, title, done: false }).find({ id, title })
759
+ })
760
+
761
+ declare const migrations: Migrations
762
+ declare const s3: S3Sender
763
+
764
+ const TasksDb = Database.tag<typeof Tasks>("app/TasksDb")
765
+ const TasksLive = Database.layer(TasksDb, {
766
+ schema: Tasks,
767
+ migrations,
768
+ store: S3Store.make({
769
+ client: s3,
770
+ log: { bucket: "tasks--use1-az4--x-s3" },
771
+ checkpoints: { bucket: "tasks-checkpoints" },
772
+ prefix: "prod/"
773
+ }),
774
+ cache: { directory: "/tmp/tasks" },
775
+ onOpen: "verify"
776
+ }).pipe(Layer.provide(Bumble.layer()))
777
+
778
+ const completeFirst = Effect.scoped(
779
+ Effect.gen(function* () {
780
+ const db = yield* TasksDb
781
+ const reader = yield* db.read("latest")
782
+ const [first] = yield* (yield* reader.execute(openTasks, {})).collect()
783
+ if (first === undefined) {
784
+ return "nothing open"
785
+ }
786
+ const draft = yield* ChangeSet.builder(Tasks)
787
+ yield* draft.delete(Task, [{ ...first, done: false }])
788
+ yield* draft.insert(Task, [{ ...first, done: true }])
789
+ const requestId = Database.requestId()
790
+ const outcome = yield* db.submit(yield* draft.finish(), { requestId, precondition: reader.revision })
791
+ if (outcome._tag === "Decided") {
792
+ return outcome.receipt.outcome._tag
793
+ }
794
+ if (outcome.refusal._tag === "Unknown") {
795
+ const receipt = yield* db.resolve(requestId)
796
+ return Option.isSome(receipt) ? receipt.value.outcome._tag : "not decided"
797
+ }
798
+ return outcome.refusal._tag
799
+ })
800
+ )
801
+
802
+ void completeFirst.pipe(Effect.provide(TasksLive))
803
+ ```
804
+
805
+ `read` takes `"cached"` (the cache as it is), `"latest"` (one request to find
806
+ the log's tip) or `{ atLeast: seq }` (read-your-writes after a receipt's `seq`).
807
+
808
+ ## 20. One database per tenant
809
+
810
+ `Database.pool` opens a database per tenant on first use and closes it after
811
+ it has been idle for `idleTimeToLive`. Each tenant has its own log and cache;
812
+ `FsStore` runs the same protocol on a local directory, and `MemStore` is the
813
+ in-memory store for tests.
814
+
815
+ ```ts
816
+ const Note = relation("Note", { id: uuid, text: str })
817
+ const Notes = schema("Notes", { Note }, [key(Note, ["id"])])
818
+ const allNotes = query(Notes).rule((r) => {
819
+ const { id, text } = v(Note)
820
+ return r.match(Note, { id, text }).find({ id, text })
821
+ })
822
+
823
+ declare const noteMigrations: Migrations
824
+
825
+ const perTenant = Effect.scoped(
826
+ Effect.gen(function* () {
827
+ const pool = yield* Database.pool({
828
+ schema: Notes,
829
+ migrations: noteMigrations,
830
+ store: (tenant) => FsStore.make(`data/${tenant}/log`),
831
+ cache: (tenant) => ({ directory: `data/${tenant}/cache` }),
832
+ onOpen: "migrate",
833
+ idleTimeToLive: "10 minutes"
834
+ })
835
+ const count = (tenant: string) =>
836
+ Effect.scoped(
837
+ Effect.gen(function* () {
838
+ const db = yield* pool.get(tenant)
839
+ const reader = yield* db.read("cached")
840
+ return (yield* (yield* reader.execute(allNotes, {})).collect()).length
841
+ })
842
+ )
843
+ return yield* Effect.all([count("acme"), count("globex")])
844
+ })
845
+ )
846
+
847
+ void perTenant.pipe(Effect.provide(Bumble.layer()))
848
+ ```