@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.
- package/LICENSE +12 -0
- package/README.md +84 -0
- package/dist/brand.d.ts +59 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +47 -0
- package/dist/brand.js.map +1 -0
- package/dist/closed.d.ts +97 -0
- package/dist/closed.d.ts.map +1 -0
- package/dist/closed.js +107 -0
- package/dist/closed.js.map +1 -0
- package/dist/count.d.ts +55 -0
- package/dist/count.d.ts.map +1 -0
- package/dist/count.js +92 -0
- package/dist/count.js.map +1 -0
- package/dist/db.d.ts +341 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +1016 -0
- package/dist/db.js.map +1 -0
- package/dist/exhume.d.ts +130 -0
- package/dist/exhume.d.ts.map +1 -0
- package/dist/exhume.js +145 -0
- package/dist/exhume.js.map +1 -0
- package/dist/face.d.ts +91 -0
- package/dist/face.d.ts.map +1 -0
- package/dist/face.js +69 -0
- package/dist/face.js.map +1 -0
- package/dist/fields.d.ts +187 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +204 -0
- package/dist/fields.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lower.d.ts +34 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +105 -0
- package/dist/lower.js.map +1 -0
- package/dist/marshal.d.ts +79 -0
- package/dist/marshal.d.ts.map +1 -0
- package/dist/marshal.js +167 -0
- package/dist/marshal.js.map +1 -0
- package/dist/native.d.ts +532 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +64 -0
- package/dist/native.js.map +1 -0
- package/dist/query/atom.d.ts +285 -0
- package/dist/query/atom.d.ts.map +1 -0
- package/dist/query/atom.js +281 -0
- package/dist/query/atom.js.map +1 -0
- package/dist/query/lower.d.ts +145 -0
- package/dist/query/lower.d.ts.map +1 -0
- package/dist/query/lower.js +604 -0
- package/dist/query/lower.js.map +1 -0
- package/dist/query/predicate.d.ts +101 -0
- package/dist/query/predicate.d.ts.map +1 -0
- package/dist/query/predicate.js +85 -0
- package/dist/query/predicate.js.map +1 -0
- package/dist/query/run.d.ts +30 -0
- package/dist/query/run.d.ts.map +1 -0
- package/dist/query/run.js +94 -0
- package/dist/query/run.js.map +1 -0
- package/dist/query/scope.d.ts +168 -0
- package/dist/query/scope.d.ts.map +1 -0
- package/dist/query/scope.js +134 -0
- package/dist/query/scope.js.map +1 -0
- package/dist/query/select.d.ts +106 -0
- package/dist/query/select.d.ts.map +1 -0
- package/dist/query/select.js +69 -0
- package/dist/query/select.js.map +1 -0
- package/dist/relation.d.ts +120 -0
- package/dist/relation.d.ts.map +1 -0
- package/dist/relation.js +108 -0
- package/dist/relation.js.map +1 -0
- package/dist/schema.d.ts +50 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +235 -0
- package/dist/schema.js.map +1 -0
- package/dist/spec.d.ts +218 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +154 -0
- package/dist/spec.js.map +1 -0
- package/dist/statements.d.ts +91 -0
- package/dist/statements.d.ts.map +1 -0
- package/dist/statements.js +101 -0
- package/dist/statements.js.map +1 -0
- package/package.json +66 -0
- package/src/brand.ts +82 -0
- package/src/closed.ts +228 -0
- package/src/count.ts +117 -0
- package/src/db.ts +1519 -0
- package/src/exhume.ts +243 -0
- package/src/face.ts +161 -0
- package/src/fields.ts +385 -0
- package/src/index.ts +185 -0
- package/src/lower.ts +118 -0
- package/src/marshal.ts +220 -0
- package/src/native.ts +576 -0
- package/src/query/atom.ts +556 -0
- package/src/query/lower.ts +855 -0
- package/src/query/predicate.ts +195 -0
- package/src/query/run.ts +106 -0
- package/src/query/scope.ts +301 -0
- package/src/query/select.ts +140 -0
- package/src/relation.ts +252 -0
- package/src/schema.ts +297 -0
- package/src/spec.ts +325 -0
- package/src/statements.ts +148 -0
package/src/marshal.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marshal layer (PRD-07): fact object ⇄ positional `FactValue[]` by
|
|
3
|
+
* field ordinal, schema-directed, in ONE place only. The write side lowers
|
|
4
|
+
* named host objects to rows in the relation's field-declaration order
|
|
5
|
+
* (declaration order = ordinal ids, the macro's law); the read side decodes
|
|
6
|
+
* rows back to named objects with brands applied by assertion — the store
|
|
7
|
+
* is the proof carrier: a row the engine admitted IS a legal fact of its
|
|
8
|
+
* relation, so readback asserts the brand instead of re-judging the value
|
|
9
|
+
* (the same trust direction as Rust's typed readback). Shape mismatches
|
|
10
|
+
* here are genuine failures and THROW typed; they are never domain data.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as errors from "@superbuilders/errors"
|
|
14
|
+
import type { FieldData } from "#fields.ts"
|
|
15
|
+
import type { FactValue } from "#native.ts"
|
|
16
|
+
import type { AnyRelation, Fact, FreshKeys, RelationData } from "#relation.ts"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The inferred object type `tx.insert` returns: one property per
|
|
20
|
+
* fresh-marked field of `R`, carrying the minted (or resupplied) branded
|
|
21
|
+
* id. A relation with no fresh field returns the empty object.
|
|
22
|
+
*/
|
|
23
|
+
type Minted<R extends AnyRelation> = { [K in FreshKeys<R>]: Fact<R>[K] }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The key object `get` reads through. THE PRIMARY-KEY RULE: `get` always
|
|
27
|
+
* reads through the PRIMARY candidate key — the first-declared one in the
|
|
28
|
+
* engine's materialized statement order (fresh-implied keys first, closed
|
|
29
|
+
* auto-keys second, declared `key()` statements last), so a fresh-bearing
|
|
30
|
+
* relation's primary key is always its fresh field. When `R` carries a
|
|
31
|
+
* fresh field the type demands exactly that field; otherwise the primary
|
|
32
|
+
* key lives in the schema's statement list, which the type system cannot
|
|
33
|
+
* see (statements are untyped values), so the type admits any partial fact
|
|
34
|
+
* and the projection is verified at runtime — a missing key field throws
|
|
35
|
+
* naming the projection.
|
|
36
|
+
*/
|
|
37
|
+
type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
|
|
38
|
+
? Partial<Fact<R>>
|
|
39
|
+
: { [K in FreshKeys<R>]: Fact<R>[K] }
|
|
40
|
+
|
|
41
|
+
/** The typed shape refusal of the row marshaler — a genuine failure, never data. */
|
|
42
|
+
function cellShapeError(context: string, expected: string, value: unknown): Error {
|
|
43
|
+
return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Narrows an interval cell: a plain object with bigint start/end. */
|
|
47
|
+
function isIntervalCell(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
|
|
48
|
+
return (
|
|
49
|
+
typeof value === "object" &&
|
|
50
|
+
value !== null &&
|
|
51
|
+
"start" in value &&
|
|
52
|
+
"end" in value &&
|
|
53
|
+
typeof value.start === "bigint" &&
|
|
54
|
+
typeof value.end === "bigint"
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reprojects any host object to a string-indexed record — the boundary
|
|
60
|
+
* through which generic fact objects (whose type parameters carry no index
|
|
61
|
+
* signature) enter the name-directed marshaling below, without a cast.
|
|
62
|
+
*/
|
|
63
|
+
function recordOf(fact: object): Record<string, unknown> {
|
|
64
|
+
return Object.fromEntries(Object.entries(fact))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Marshals one host cell at its field position to the natural wire value,
|
|
69
|
+
* schema-directed by the field's structural type (never guessed). Brands
|
|
70
|
+
* are phantoms, so the runtime values are exactly the wire's natural JS
|
|
71
|
+
* values; only widths and domains are left to the engine's own judge.
|
|
72
|
+
*/
|
|
73
|
+
function cellOf(context: string, field: FieldData, value: unknown): FactValue {
|
|
74
|
+
switch (field.type.kind) {
|
|
75
|
+
case "bool": {
|
|
76
|
+
if (typeof value !== "boolean") {
|
|
77
|
+
throw cellShapeError(context, "boolean", value)
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
case "u64":
|
|
82
|
+
case "i64": {
|
|
83
|
+
if (typeof value !== "bigint") {
|
|
84
|
+
throw cellShapeError(context, "bigint", value)
|
|
85
|
+
}
|
|
86
|
+
return value
|
|
87
|
+
}
|
|
88
|
+
case "string": {
|
|
89
|
+
if (typeof value !== "string") {
|
|
90
|
+
throw cellShapeError(context, "string", value)
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A lone surrogate would be lossily replaced with U+FFFD at the
|
|
94
|
+
* bridge's UTF-8 crossing — the stored fact would differ from the
|
|
95
|
+
* written one, and distinct JS strings would collapse to one fact.
|
|
96
|
+
* The bijection law refuses it here, the one seam every write and
|
|
97
|
+
* lookup lowers through.
|
|
98
|
+
*/
|
|
99
|
+
if (!value.isWellFormed()) {
|
|
100
|
+
throw cellShapeError(context, "well-formed string", value)
|
|
101
|
+
}
|
|
102
|
+
return value
|
|
103
|
+
}
|
|
104
|
+
case "fixedBytes": {
|
|
105
|
+
if (!(value instanceof Uint8Array)) {
|
|
106
|
+
throw cellShapeError(context, "Uint8Array", value)
|
|
107
|
+
}
|
|
108
|
+
return value
|
|
109
|
+
}
|
|
110
|
+
case "interval": {
|
|
111
|
+
if (!isIntervalCell(value)) {
|
|
112
|
+
throw cellShapeError(context, "interval ({ start, end } bigints)", value)
|
|
113
|
+
}
|
|
114
|
+
return { start: value.start, end: value.end }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Marshals one complete fact object to its positional row, in field
|
|
121
|
+
* declaration order (= ordinal ids). Every declared field must be present;
|
|
122
|
+
* fresh minting happens BEFORE this point (the transaction fills omitted
|
|
123
|
+
* fresh cells via the engine's alloc lane).
|
|
124
|
+
*/
|
|
125
|
+
function rowOf(relation: RelationData, fact: Readonly<Record<string, unknown>>): FactValue[] {
|
|
126
|
+
return relation.fields.map(function marshalCell(declared) {
|
|
127
|
+
const value = fact[declared.name]
|
|
128
|
+
if (value === undefined) {
|
|
129
|
+
throw errors.new(`relation ${relation.name}: fact is missing field ${declared.name}`)
|
|
130
|
+
}
|
|
131
|
+
return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Marshals a key object through a key statement's projection, in the
|
|
137
|
+
* statement's projection order (what the engine's keyed point reads take).
|
|
138
|
+
* A key field absent from the object throws naming the primary projection
|
|
139
|
+
* (the {@link KeyFact} rule's runtime half).
|
|
140
|
+
*/
|
|
141
|
+
function keyRowOf(
|
|
142
|
+
relation: RelationData,
|
|
143
|
+
projection: readonly string[],
|
|
144
|
+
key: Readonly<Record<string, unknown>>
|
|
145
|
+
): FactValue[] {
|
|
146
|
+
return projection.map(function marshalKeyCell(fieldName) {
|
|
147
|
+
const declared = relation.fields.find(function byName(candidate) {
|
|
148
|
+
return candidate.name === fieldName
|
|
149
|
+
})
|
|
150
|
+
if (declared === undefined) {
|
|
151
|
+
throw errors.new(`relation ${relation.name}: key projection cites unknown field ${fieldName}`)
|
|
152
|
+
}
|
|
153
|
+
const value = key[fieldName]
|
|
154
|
+
if (value === undefined) {
|
|
155
|
+
throw errors.new(
|
|
156
|
+
`relation ${relation.name}: key object is missing field ${fieldName} — get reads through the primary (first-declared) key, whose projection is (${projection.join(", ")})`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return cellOf(`relation ${relation.name} key field ${fieldName}`, declared.field, value)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The read-side trusted seam: a decoded row carrying every declared field
|
|
165
|
+
* IS a fact of its relation — the engine admitted it, so the brand is
|
|
166
|
+
* asserted, not re-derived. The checkable half (completeness) is verified;
|
|
167
|
+
* the phantom half (brands) is carried by the store's own judgment.
|
|
168
|
+
*/
|
|
169
|
+
function isBrandedFact<R extends AnyRelation>(
|
|
170
|
+
relation: R,
|
|
171
|
+
decoded: Readonly<Record<string, FactValue>>
|
|
172
|
+
): decoded is Readonly<Record<string, FactValue>> & Fact<R> {
|
|
173
|
+
return relation.data.fields.every(function present(declared) {
|
|
174
|
+
return decoded[declared.name] !== undefined
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The insert-return trusted seam: the collected fresh cells of one insert
|
|
180
|
+
* (minted by the engine or resupplied by the caller) are the relation's
|
|
181
|
+
* branded fresh ids — same assertion direction as {@link isBrandedFact}.
|
|
182
|
+
*/
|
|
183
|
+
function isMintedFresh<R extends AnyRelation>(
|
|
184
|
+
relation: R,
|
|
185
|
+
minted: Readonly<Record<string, FactValue>>
|
|
186
|
+
): minted is Readonly<Record<string, FactValue>> & Minted<R> {
|
|
187
|
+
return relation.data.fields.every(function presentWhenFresh(declared) {
|
|
188
|
+
return !declared.field.minted || minted[declared.name] !== undefined
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Unmarshals one positional row to the relation's named, branded, frozen
|
|
194
|
+
* fact object — the inverse of {@link rowOf}, ordinal-directed by the same
|
|
195
|
+
* declaration order.
|
|
196
|
+
*/
|
|
197
|
+
function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]): Fact<R> {
|
|
198
|
+
const data = relation.data
|
|
199
|
+
if (row.length !== data.fields.length) {
|
|
200
|
+
throw errors.new(
|
|
201
|
+
`relation ${data.name}: row arity ${row.length} does not match the ${data.fields.length} declared fields`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
const decoded: Record<string, FactValue> = {}
|
|
205
|
+
data.fields.forEach(function decodeCell(declared, ordinal) {
|
|
206
|
+
const cell = row[ordinal]
|
|
207
|
+
if (cell === undefined) {
|
|
208
|
+
throw errors.new(`relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`)
|
|
209
|
+
}
|
|
210
|
+
decoded[declared.name] = cell
|
|
211
|
+
})
|
|
212
|
+
Object.freeze(decoded)
|
|
213
|
+
if (!isBrandedFact(relation, decoded)) {
|
|
214
|
+
throw errors.new(`relation ${data.name}: decoded row is not a complete fact`)
|
|
215
|
+
}
|
|
216
|
+
return decoded
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type { KeyFact, Minted }
|
|
220
|
+
export { cellOf, factOf, isMintedFresh, keyRowOf, recordOf, rowOf }
|