@bjornpagen/bumbledb 0.1.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 (108) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +84 -0
  3. package/dist/brand.d.ts +59 -0
  4. package/dist/brand.d.ts.map +1 -0
  5. package/dist/brand.js +47 -0
  6. package/dist/brand.js.map +1 -0
  7. package/dist/closed.d.ts +97 -0
  8. package/dist/closed.d.ts.map +1 -0
  9. package/dist/closed.js +107 -0
  10. package/dist/closed.js.map +1 -0
  11. package/dist/count.d.ts +55 -0
  12. package/dist/count.d.ts.map +1 -0
  13. package/dist/count.js +92 -0
  14. package/dist/count.js.map +1 -0
  15. package/dist/db.d.ts +341 -0
  16. package/dist/db.d.ts.map +1 -0
  17. package/dist/db.js +1016 -0
  18. package/dist/db.js.map +1 -0
  19. package/dist/exhume.d.ts +130 -0
  20. package/dist/exhume.d.ts.map +1 -0
  21. package/dist/exhume.js +145 -0
  22. package/dist/exhume.js.map +1 -0
  23. package/dist/face.d.ts +91 -0
  24. package/dist/face.d.ts.map +1 -0
  25. package/dist/face.js +69 -0
  26. package/dist/face.js.map +1 -0
  27. package/dist/fields.d.ts +187 -0
  28. package/dist/fields.d.ts.map +1 -0
  29. package/dist/fields.js +204 -0
  30. package/dist/fields.js.map +1 -0
  31. package/dist/index.d.ts +49 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lower.d.ts +34 -0
  36. package/dist/lower.d.ts.map +1 -0
  37. package/dist/lower.js +105 -0
  38. package/dist/lower.js.map +1 -0
  39. package/dist/marshal.d.ts +79 -0
  40. package/dist/marshal.d.ts.map +1 -0
  41. package/dist/marshal.js +167 -0
  42. package/dist/marshal.js.map +1 -0
  43. package/dist/native.d.ts +532 -0
  44. package/dist/native.d.ts.map +1 -0
  45. package/dist/native.js +64 -0
  46. package/dist/native.js.map +1 -0
  47. package/dist/query/atom.d.ts +285 -0
  48. package/dist/query/atom.d.ts.map +1 -0
  49. package/dist/query/atom.js +281 -0
  50. package/dist/query/atom.js.map +1 -0
  51. package/dist/query/lower.d.ts +145 -0
  52. package/dist/query/lower.d.ts.map +1 -0
  53. package/dist/query/lower.js +604 -0
  54. package/dist/query/lower.js.map +1 -0
  55. package/dist/query/predicate.d.ts +101 -0
  56. package/dist/query/predicate.d.ts.map +1 -0
  57. package/dist/query/predicate.js +85 -0
  58. package/dist/query/predicate.js.map +1 -0
  59. package/dist/query/run.d.ts +30 -0
  60. package/dist/query/run.d.ts.map +1 -0
  61. package/dist/query/run.js +94 -0
  62. package/dist/query/run.js.map +1 -0
  63. package/dist/query/scope.d.ts +168 -0
  64. package/dist/query/scope.d.ts.map +1 -0
  65. package/dist/query/scope.js +134 -0
  66. package/dist/query/scope.js.map +1 -0
  67. package/dist/query/select.d.ts +106 -0
  68. package/dist/query/select.d.ts.map +1 -0
  69. package/dist/query/select.js +69 -0
  70. package/dist/query/select.js.map +1 -0
  71. package/dist/relation.d.ts +120 -0
  72. package/dist/relation.d.ts.map +1 -0
  73. package/dist/relation.js +108 -0
  74. package/dist/relation.js.map +1 -0
  75. package/dist/schema.d.ts +50 -0
  76. package/dist/schema.d.ts.map +1 -0
  77. package/dist/schema.js +235 -0
  78. package/dist/schema.js.map +1 -0
  79. package/dist/spec.d.ts +218 -0
  80. package/dist/spec.d.ts.map +1 -0
  81. package/dist/spec.js +154 -0
  82. package/dist/spec.js.map +1 -0
  83. package/dist/statements.d.ts +91 -0
  84. package/dist/statements.d.ts.map +1 -0
  85. package/dist/statements.js +101 -0
  86. package/dist/statements.js.map +1 -0
  87. package/package.json +66 -0
  88. package/src/brand.ts +82 -0
  89. package/src/closed.ts +228 -0
  90. package/src/count.ts +117 -0
  91. package/src/db.ts +1519 -0
  92. package/src/exhume.ts +243 -0
  93. package/src/face.ts +161 -0
  94. package/src/fields.ts +385 -0
  95. package/src/index.ts +185 -0
  96. package/src/lower.ts +118 -0
  97. package/src/marshal.ts +220 -0
  98. package/src/native.ts +576 -0
  99. package/src/query/atom.ts +556 -0
  100. package/src/query/lower.ts +855 -0
  101. package/src/query/predicate.ts +195 -0
  102. package/src/query/run.ts +106 -0
  103. package/src/query/scope.ts +301 -0
  104. package/src/query/select.ts +140 -0
  105. package/src/relation.ts +252 -0
  106. package/src/schema.ts +297 -0
  107. package/src/spec.ts +325 -0
  108. package/src/statements.ts +148 -0
package/src/exhume.ts ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * The exhume surface — the SDK's ONE schema-independent read path
3
+ * (docs/course-serialization/prd-02-sdk-exhume-surface.md; engine
4
+ * 70-api.md § exhume): a read-only, theory-less open returning the store's
5
+ * SELF-DESCRIBED relation shapes and raw facts by relation name. The
6
+ * sighting it exists for: a run store whose creating schema has since
7
+ * evolved — the record outlives the schema, and exhume is how the record
8
+ * is read back for rebirth (exhume the old store, create the successor
9
+ * under the new theory, copy by NAME, re-derive).
10
+ *
11
+ * DELIBERATELY UNTYPED: no branded type appears anywhere on this surface.
12
+ * The caller's schema is the wrong theory for an exhumed store BY
13
+ * DEFINITION (a store the current theory could open would never need
14
+ * exhuming), so every value crosses as its plain natural JS form and every
15
+ * fact is keyed by field NAME. The SDK never reconstructs a `Schema` value
16
+ * from the descriptor — that inverse mapping is deliberately out of scope;
17
+ * the rebirth tool keys by name.
18
+ *
19
+ * ZERO CLOSABLES: no value here carries a close, dispose, or release
20
+ * spelling. The engine-side handle (and the store's exclusive advisory
21
+ * lock) is reclaimed when the `Exhumed` value is garbage-collected —
22
+ * reclamation only, never correctness: the store is never written through
23
+ * this surface, so there is nothing to flush.
24
+ */
25
+
26
+ import * as errors from "@superbuilders/errors"
27
+ import type { FactValue, Manifest } from "#native.ts"
28
+ import { native } from "#native.ts"
29
+ import type { ValueTypeSpec } from "#spec.ts"
30
+
31
+ /**
32
+ * The typed `descriptorMissing` refusal: the store predates self-describing
33
+ * stores and has not been adopted. The remedy is in the message — one
34
+ * fingerprint-matching `Db.open` under the creating schema back-fills the
35
+ * descriptor (engine 50-storage.md § the `_meta` block) and the store is
36
+ * self-describing forever. Match with `errors.is`.
37
+ */
38
+ const ErrExhumeNoDescriptor = errors.new(
39
+ "bumbledb exhume: the store carries no schema descriptor (not yet adopted) — open it once under its creating schema (one fingerprint-matching Db.open back-fills the descriptor; engine 50-storage.md)"
40
+ )
41
+
42
+ /**
43
+ * The typed `formatMismatch` refusal: the store's on-disk format version is
44
+ * not this engine's (no migration path exists, as everywhere). Match with
45
+ * `errors.is`.
46
+ */
47
+ const ErrExhumeFormatMismatch = errors.new(
48
+ "bumbledb exhume: storage format version mismatch — the store was written by a different engine format"
49
+ )
50
+
51
+ /**
52
+ * The typed `corruption` refusal: the persisted descriptor fails its
53
+ * integrity gates (the stored bytes hash to something other than the stored
54
+ * fingerprint, or the bytes do not decode and re-encode faithfully). Match
55
+ * with `errors.is`.
56
+ */
57
+ const ErrExhumeCorruption = errors.new("bumbledb exhume: the persisted schema descriptor fails its integrity gates")
58
+
59
+ /**
60
+ * One exhumed fact as a plain name-keyed record of natural JS values —
61
+ * deliberately untyped (module doc): `bigint` for u64/i64, `string` for
62
+ * str, `boolean` for bool, `Uint8Array` for bytes<N>, a `{ start, end }`
63
+ * bigint pair for intervals. Closed-relation rows open with the synthetic
64
+ * `id` field, exactly as the descriptor's sealed field list declares.
65
+ */
66
+ type ExhumedFact = Readonly<Record<string, FactValue>>
67
+
68
+ /**
69
+ * One field of an exhumed relation, exactly as the store's creator declared
70
+ * it: the name and the structural type tag (byte width on `fixedBytes`,
71
+ * element and optional width on `interval`).
72
+ */
73
+ interface ExhumedField {
74
+ readonly name: string
75
+ readonly valueType: ValueTypeSpec
76
+ }
77
+
78
+ /**
79
+ * One ground axiom of an exhumed closed relation: the handle (the row's
80
+ * identity, never a column), the declaration-order row id, and the declared
81
+ * payload columns by name.
82
+ */
83
+ interface ExhumedAxiom {
84
+ readonly handle: string
85
+ readonly id: bigint
86
+ readonly values: ExhumedFact
87
+ }
88
+
89
+ /**
90
+ * One relation of an exhumed store: the name, the SEALED field list in
91
+ * declaration order (a closed relation opens with the synthetic (`id`,
92
+ * u64) handle field), and — exactly on closed relations — the roster of
93
+ * ground axioms. Scan rows come back in this field order, so pairing a
94
+ * row's positions against `fields` is the name-keyed reading `scan`
95
+ * performs.
96
+ */
97
+ interface ExhumedRelation {
98
+ readonly name: string
99
+ readonly fields: readonly ExhumedField[]
100
+ readonly roster: readonly ExhumedAxiom[] | undefined
101
+ }
102
+
103
+ /**
104
+ * The exhumed store's schema as declared, decoded from the store's own
105
+ * persisted descriptor: relations in engine-id order (declaration order
106
+ * mints every id), each with its ordered field descriptions and closed
107
+ * roster — enough for a caller to key facts by name and re-insert them
108
+ * into a differently-fingerprinted successor store. No branded type
109
+ * appears here (module doc: exhumed data is deliberately untyped).
110
+ */
111
+ interface ExhumedDescriptor {
112
+ readonly relations: readonly ExhumedRelation[]
113
+ }
114
+
115
+ /**
116
+ * One exhumed store: the self-described relation shapes and the raw facts
117
+ * by relation name. Read-only by construction — no write verb, no prepare
118
+ * verb, no close (zero closables; the engine handle is GC-reclaimed).
119
+ */
120
+ interface Exhumed {
121
+ /** The store's own persisted schema, as its creator declared it. */
122
+ readonly descriptor: ExhumedDescriptor
123
+ /**
124
+ * Full-relation export in row-id order, each row decoded per the
125
+ * STORED descriptor to a name-keyed record of natural values (str
126
+ * resolved through the engine's `_dict` before crossing; a closed
127
+ * relation scans its sealed roster). Each call reads one consistent
128
+ * snapshot. An unknown relation name is a typed error — the
129
+ * descriptor is the caller's roster.
130
+ */
131
+ scan(relation: string): readonly ExhumedFact[]
132
+ }
133
+
134
+ /**
135
+ * The bridge guard (db.ts's twin over this module's calls): runs one
136
+ * native call and wraps anything it throws, so marshal-shape and
137
+ * handle-lifecycle refusals cross as genuine typed failures, never bare
138
+ * foreign errors.
139
+ */
140
+ function bridged<T>(context: string, run: () => T): T {
141
+ const result = errors.trySync(run)
142
+ if (result.error) {
143
+ throw errors.wrap(result.error, context)
144
+ }
145
+ return result.data
146
+ }
147
+
148
+ /**
149
+ * Shapes the bridge's manifest rendering of the stored descriptor into the
150
+ * SDK's {@link ExhumedDescriptor}: same relations in the same engine-id
151
+ * order, extension rows re-keyed by column name. Pure reshaping — no
152
+ * re-validation of engine-decoded values happens here (the bridge stays
153
+ * dumb and so does this).
154
+ */
155
+ function descriptorOf(manifest: Manifest): ExhumedDescriptor {
156
+ const relations = manifest.relations.map(function relationOf(relation): ExhumedRelation {
157
+ const fields = relation.fields.map(function fieldOf(field): ExhumedField {
158
+ return Object.freeze({ name: field.name, valueType: field.valueType })
159
+ })
160
+ let roster: readonly ExhumedAxiom[] | undefined
161
+ if (relation.extension !== undefined) {
162
+ roster = Object.freeze(
163
+ relation.extension.map(function axiomOf(row): ExhumedAxiom {
164
+ const values: Record<string, FactValue> = {}
165
+ for (const cell of row.values) {
166
+ values[cell.name] = cell.value
167
+ }
168
+ return Object.freeze({ handle: row.handle, id: row.id, values: Object.freeze(values) })
169
+ })
170
+ )
171
+ }
172
+ return Object.freeze({ name: relation.name, fields: Object.freeze(fields), roster })
173
+ })
174
+ return Object.freeze({ relations: Object.freeze(relations) })
175
+ }
176
+
177
+ /**
178
+ * Opens one exhumed store at an ALREADY-CANONICAL path (`Db.exhume` is the
179
+ * public spelling — the path law lives in db.ts beside `open`/`create`).
180
+ * The bridge's three domain refusals become the typed error constants
181
+ * ({@link ErrExhumeNoDescriptor}, {@link ErrExhumeFormatMismatch},
182
+ * {@link ErrExhumeCorruption}), each carrying the engine's message in its
183
+ * wrap. The returned value is NOT cached: exhume is a forensic read, and
184
+ * caching would pin the store's exclusive lock for the process's life —
185
+ * GC reclamation is the whole lifecycle.
186
+ */
187
+ function exhumeStore(canonical: string): Exhumed {
188
+ const outcome = bridged(`exhume bumbledb store at ${canonical}`, function callExhume() {
189
+ return native.dbExhume(canonical)
190
+ })
191
+ if (!outcome.ok) {
192
+ switch (outcome.kind) {
193
+ case "descriptorMissing":
194
+ throw errors.wrap(ErrExhumeNoDescriptor, `exhume ${canonical}: ${outcome.message}`)
195
+ case "formatMismatch":
196
+ throw errors.wrap(ErrExhumeFormatMismatch, `exhume ${canonical}: ${outcome.message}`)
197
+ case "corruption":
198
+ throw errors.wrap(ErrExhumeCorruption, `exhume ${canonical}: ${outcome.message}`)
199
+ }
200
+ }
201
+ const handle = outcome.exhume
202
+ const manifest = bridged("read bumbledb exhumed descriptor", function callDescriptor() {
203
+ return native.exhumeDescriptor(handle)
204
+ })
205
+ const descriptor = descriptorOf(manifest)
206
+ const fieldNames = new Map<string, readonly string[]>()
207
+ for (const relation of descriptor.relations) {
208
+ fieldNames.set(
209
+ relation.name,
210
+ relation.fields.map(function nameOf(field) {
211
+ return field.name
212
+ })
213
+ )
214
+ }
215
+ function scan(relation: string): readonly ExhumedFact[] {
216
+ const names = fieldNames.get(relation)
217
+ if (names === undefined) {
218
+ throw errors.new(`bumbledb exhume: the store's descriptor declares no relation ${relation}`)
219
+ }
220
+ const rows = bridged(`scan bumbledb exhumed relation ${relation}`, function callScan() {
221
+ return native.exhumeScan(handle, relation)
222
+ })
223
+ return Object.freeze(
224
+ rows.map(function factOf(row): ExhumedFact {
225
+ const fact: Record<string, FactValue> = {}
226
+ names.forEach(function pair(name, index) {
227
+ const cell = row[index]
228
+ if (cell === undefined) {
229
+ throw errors.new(
230
+ `bumbledb exhume drift: relation ${relation} row has no value at position ${index} (${name})`
231
+ )
232
+ }
233
+ fact[name] = cell
234
+ })
235
+ return Object.freeze(fact)
236
+ })
237
+ )
238
+ }
239
+ return Object.freeze({ descriptor, scan })
240
+ }
241
+
242
+ export type { Exhumed, ExhumedAxiom, ExhumedDescriptor, ExhumedFact, ExhumedField, ExhumedRelation }
243
+ export { ErrExhumeCorruption, ErrExhumeFormatMismatch, ErrExhumeNoDescriptor, exhumeStore }
package/src/face.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Faces — the projection-with-selection value both containments and
3
+ * windows consume: `on(Account, "holder")`, `on(Account.where({ kind:
4
+ * Kind.Savings }), "id")`, `on(Kind, "id")` (a closed relation's sealed
5
+ * shape opens with its synthetic `id`). Projection is positional: tuple
6
+ * order is preserved in the type, and the statement constructors pair the
7
+ * two sides' tuples by arity (`SameArity`).
8
+ */
9
+
10
+ import type { AnyClosed } from "#closed.ts"
11
+ import type { AnyRelation, AnySelected, SelectionBinding } from "#relation.ts"
12
+ import { renderLiteralSet } from "#spec.ts"
13
+
14
+ /** The empty σ of a selection-free face, shared by every bare projection. */
15
+ const emptySelection: readonly SelectionBinding[] = Object.freeze([])
16
+
17
+ /**
18
+ * Splits a face source into its owner and σ: a selected relation carries
19
+ * its own bindings (`relation` is the discriminant — the property exists on
20
+ * no relation or closed value, and `closed()` reserves the name against
21
+ * handle collisions); a bare relation or closed relation carries none.
22
+ */
23
+ function faceParts(source: FaceSource): {
24
+ readonly owner: FaceOwner
25
+ readonly selection: readonly SelectionBinding[]
26
+ } {
27
+ if ("relation" in source) {
28
+ return { owner: source.relation, selection: source.selection }
29
+ }
30
+ return { owner: source, selection: emptySelection }
31
+ }
32
+
33
+ /**
34
+ * A disjunctive literal set for a selection binding — `field == {A, B}`.
35
+ * The signature of {@link oneOf} demands two leading literals, so the
36
+ * one-element set (banned: it is the bare literal) and the empty set
37
+ * (banned: it selects nothing) are unwritable.
38
+ */
39
+ interface OneOf<V> {
40
+ readonly literals: readonly [V, V, ...V[]]
41
+ }
42
+
43
+ /**
44
+ * Constructs a literal set (read disjunctively) for a `where()` binding.
45
+ * Two leading arguments by signature: the degenerate sets have no spelling
46
+ * (the canonical-utterance law, `docs/architecture/70-api.md`).
47
+ */
48
+ function oneOf<V>(first: V, second: V, ...rest: V[]): OneOf<V> {
49
+ const literals: readonly [V, V, ...V[]] = [first, second, ...rest]
50
+ Object.freeze(literals)
51
+ return Object.freeze({ literals })
52
+ }
53
+
54
+ /** The relation a face projects from — ordinary or closed. */
55
+ type FaceOwner = AnyRelation | AnyClosed
56
+
57
+ /** A face's runtime description: owner, π (written order), σ (resolved bindings). */
58
+ interface FaceData {
59
+ readonly owner: FaceOwner
60
+ readonly projection: readonly string[]
61
+ readonly selection: readonly SelectionBinding[]
62
+ }
63
+
64
+ /**
65
+ * A face value. `P` is the projection tuple as written — the statement
66
+ * constructors read its length for positional-arity pairing.
67
+ */
68
+ interface Face<P extends readonly string[]> {
69
+ readonly projection: P
70
+ readonly data: FaceData
71
+ }
72
+
73
+ /** Any face value, whatever its projection. */
74
+ type AnyFace = Face<readonly string[]>
75
+
76
+ /** What `on()` accepts: a relation, a selected relation, or a closed relation. */
77
+ type FaceSource = AnyRelation | AnyClosed | AnySelected
78
+
79
+ /**
80
+ * The field names a face over `S` may project: a relation's declared
81
+ * fields; a selected relation's underlying fields; a closed relation's
82
+ * SEALED shape — the synthetic `id` plus its declared payload columns
83
+ * (`docs/architecture/70-api.md`: statement field names address the sealed
84
+ * shape).
85
+ */
86
+ type FaceFields<S extends FaceSource> = S extends AnySelected
87
+ ? keyof S["relation"]["fields"] & string
88
+ : S extends AnyRelation
89
+ ? keyof S["fields"] & string
90
+ : S extends { readonly axioms: Readonly<Record<string, infer Row>> }
91
+ ? "id" | (keyof Row & string)
92
+ : never
93
+
94
+ /** The projection arity of a face. */
95
+ type Arity<F extends AnyFace> = F["projection"]["length"]
96
+
97
+ /**
98
+ * The legible arity-mismatch verdict: when the two faces of a containment,
99
+ * bijection, or window project different numbers of fields, this type is
100
+ * intersected into the second face's parameter and names both arities.
101
+ */
102
+ interface FaceArityMismatch<Left, Right> {
103
+ readonly "face arity mismatch — positional pairing requires both sides to project equally many fields": readonly [
104
+ Left,
105
+ Right
106
+ ]
107
+ }
108
+
109
+ /**
110
+ * Resolves to `unknown` (a no-op intersection) when the two faces project
111
+ * equally many fields, and to {@link FaceArityMismatch} otherwise — the
112
+ * named helper the statement constructors constrain with.
113
+ */
114
+ type SameArity<A extends AnyFace, B extends AnyFace> =
115
+ Arity<A> extends Arity<B>
116
+ ? Arity<B> extends Arity<A>
117
+ ? unknown
118
+ : FaceArityMismatch<Arity<A>, Arity<B>>
119
+ : FaceArityMismatch<Arity<A>, Arity<B>>
120
+
121
+ /**
122
+ * Projects a face: `on(Account, "holder")`, `on(Account.where({...}),
123
+ * "id")`. Field names are typechecked against the source; tuple order is
124
+ * preserved (positional pairing with the other side, macro parity). At
125
+ * least one field by signature — an empty projection has no meaning in the
126
+ * statement grammar.
127
+ */
128
+ function on<S extends FaceSource, const P extends readonly [FaceFields<S>, ...FaceFields<S>[]]>(
129
+ source: S,
130
+ ...projection: P
131
+ ): Face<P> {
132
+ const parts = faceParts(source)
133
+ Object.freeze(projection)
134
+ const data: FaceData = Object.freeze({
135
+ owner: parts.owner,
136
+ projection,
137
+ selection: parts.selection
138
+ })
139
+ return Object.freeze({ projection, data })
140
+ }
141
+
142
+ /**
143
+ * Renders one face in the exact macro notation — `Name(p1, p2 | f == lit,
144
+ * g == {a, b})`, the selection block only when σ is nonempty (the engine
145
+ * renderer's own shape, `schema/render.rs`).
146
+ */
147
+ function renderFace(face: FaceData): string {
148
+ const projection = face.projection.join(", ")
149
+ if (face.selection.length === 0) {
150
+ return `${face.owner.name}(${projection})`
151
+ }
152
+ const bindings = face.selection
153
+ .map(function renderBinding(binding) {
154
+ return `${binding.field} == ${renderLiteralSet(binding.set)}`
155
+ })
156
+ .join(", ")
157
+ return `${face.owner.name}(${projection} | ${bindings})`
158
+ }
159
+
160
+ export type { AnyFace, Arity, Face, FaceArityMismatch, FaceData, FaceFields, FaceOwner, FaceSource, OneOf, SameArity }
161
+ export { on, oneOf, renderFace }