@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.
- package/COOKBOOK.md +176 -36
- package/README.md +70 -26
- package/dist/capacity.d.ts +15 -55
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +63 -12
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +28 -6
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +119 -71
- package/dist/changes.js.map +1 -1
- package/dist/closed.d.ts +23 -51
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +110 -168
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +8 -14
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +113 -188
- package/dist/codec.js.map +1 -1
- package/dist/compile.d.ts +17 -18
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +41 -20
- package/dist/compile.js.map +1 -1
- package/dist/db-native.d.ts +76 -41
- package/dist/db-native.d.ts.map +1 -1
- package/dist/db-native.js +8 -0
- package/dist/db-native.js.map +1 -1
- package/dist/db.d.ts +61 -26
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +152 -85
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +11 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -1
- package/dist/errors.js.map +1 -1
- package/dist/face.d.ts +19 -25
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +30 -24
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +13 -26
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +89 -116
- package/dist/fields.js.map +1 -1
- package/dist/immutable.d.ts +10 -0
- package/dist/immutable.d.ts.map +1 -0
- package/dist/immutable.js +75 -0
- package/dist/immutable.js.map +1 -0
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/log.d.ts +6 -4
- package/dist/internal/log.d.ts.map +1 -1
- package/dist/internal/log.js +3 -1
- package/dist/internal/log.js.map +1 -1
- package/dist/law.d.ts +11 -11
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +1 -1
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +11 -6
- package/dist/lower.js.map +1 -1
- package/dist/migration.d.ts +2 -3
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +5 -5
- package/dist/migration.js.map +1 -1
- package/dist/native.d.ts +2 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +1 -1
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/description.d.ts +33 -0
- package/dist/query/description.d.ts.map +1 -0
- package/dist/query/description.js +302 -0
- package/dist/query/description.js.map +1 -0
- package/dist/query/lower.d.ts +60 -4
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +126 -161
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +7 -2
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +240 -53
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +8 -10
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +3 -4
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +5 -10
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +22 -48
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +30 -82
- package/dist/relation.js.map +1 -1
- package/dist/result.d.ts +10 -25
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +7 -12
- package/dist/result.js.map +1 -1
- package/dist/rows.d.ts +11 -14
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +49 -237
- package/dist/rows.js.map +1 -1
- package/dist/runtime-codes.d.ts +1 -1
- package/dist/runtime-codes.d.ts.map +1 -1
- package/dist/runtime-codes.js +1 -2
- package/dist/runtime-codes.js.map +1 -1
- package/dist/runtime-errors.d.ts +27 -15
- package/dist/runtime-errors.d.ts.map +1 -1
- package/dist/runtime-errors.js +44 -8
- package/dist/runtime-errors.js.map +1 -1
- package/dist/runtime-native.d.ts +16 -34
- package/dist/runtime-native.d.ts.map +1 -1
- package/dist/runtime-native.js.map +1 -1
- package/dist/runtime.d.ts +11 -28
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +14 -54
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +9 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +85 -24
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +27 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +104 -0
- package/dist/selection.js.map +1 -0
- package/dist/shape.d.ts +6 -9
- package/dist/shape.d.ts.map +1 -1
- package/dist/statements.d.ts +17 -52
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +92 -111
- package/dist/statements.js.map +1 -1
- package/dist/values.d.ts +25 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +203 -0
- package/dist/values.js.map +1 -0
- package/pack-provenance.json +3 -3
- package/package.json +4 -4
- package/src/capacity.ts +86 -76
- package/src/changes.ts +225 -94
- package/src/closed.ts +161 -314
- package/src/codec.ts +123 -213
- package/src/compile.ts +61 -38
- package/src/db-native.ts +73 -68
- package/src/db.ts +268 -165
- package/src/errors.ts +16 -1
- package/src/face.ts +44 -74
- package/src/fields.ts +100 -146
- package/src/immutable.ts +76 -0
- package/src/index.ts +38 -31
- package/src/internal/log.ts +4 -5
- package/src/law.ts +14 -14
- package/src/lower.ts +13 -10
- package/src/migration.ts +5 -12
- package/src/native.ts +2 -1
- package/src/query/atom.ts +1 -0
- package/src/query/description.ts +394 -0
- package/src/query/lower.ts +139 -173
- package/src/query/parse-ir.ts +247 -54
- package/src/query/run.ts +15 -9
- package/src/query/scope.ts +10 -18
- package/src/relation.ts +58 -142
- package/src/result.ts +16 -45
- package/src/rows.ts +57 -250
- package/src/runtime-codes.ts +1 -2
- package/src/runtime-errors.ts +48 -8
- package/src/runtime-native.ts +13 -46
- package/src/runtime.ts +51 -119
- package/src/schema.ts +99 -29
- package/src/selection.ts +124 -0
- package/src/shape.ts +9 -9
- package/src/statements.ts +138 -189
- 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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
148
|
+
const db = yield* Db.open(localPath, Tasks)
|
|
140
149
|
const taskId = yield* Effect.sync(() => crypto.randomUUID())
|
|
141
|
-
const draft = yield* ChangeSet.builder(Tasks
|
|
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, {
|
|
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`
|
|
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
|
|
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(
|
|
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 }
|
|
207
|
-
return yield* result.collect(
|
|
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.
|
|
402
|
+
## 8. Completed results: collect or consume delivery batches
|
|
267
403
|
|
|
268
|
-
`collect(
|
|
269
|
-
leaves the result available. `pages(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
333
|
-
const previous = yield* snapshot.get(
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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 =
|
|
362
|
-
const dense =
|
|
363
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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(
|
|
130
|
-
const found = yield* snapshot.get(
|
|
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 }
|
|
135
|
-
const rows = yield* result.collect(
|
|
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(
|
|
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)
|
|
155
|
-
checked
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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
|
-
|
|
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`.
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|