@bjornpagen/bumbledb 0.2.0 → 0.4.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 +503 -427
- package/README.md +84 -36
- package/dist/closed.d.ts +111 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +94 -99
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +16 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +46 -8
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +114 -69
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +38 -21
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +72 -87
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +35 -67
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +224 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +224 -0
- package/dist/law.js.map +1 -0
- package/dist/lower.d.ts +17 -10
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +34 -23
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +67 -6
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +6 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +139 -56
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +7 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +71 -56
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +341 -69
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +10 -9
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +2 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -6
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +100 -43
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +89 -30
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +5 -5
- package/dist/query/select.d.ts.map +1 -1
- package/dist/relation.d.ts +29 -15
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +45 -17
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +41 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +16 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +10 -8
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +67 -31
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +97 -20
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +214 -146
- package/src/db.ts +65 -10
- package/src/face.ts +169 -102
- package/src/fields.ts +97 -164
- package/src/index.ts +43 -18
- package/src/law.ts +519 -0
- package/src/lower.ts +36 -23
- package/src/marshal.ts +74 -7
- package/src/native.ts +6 -2
- package/src/query/atom.ts +155 -65
- package/src/query/lower.ts +572 -167
- package/src/query/predicate.ts +43 -33
- package/src/query/run.ts +26 -6
- package/src/query/scope.ts +161 -51
- package/src/query/select.ts +5 -5
- package/src/relation.ts +60 -26
- package/src/schema.ts +48 -7
- package/src/spec.ts +10 -8
- package/src/statements.ts +165 -46
package/src/closed.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Closed relations (`docs/architecture/10-data-model.md` § closed
|
|
3
3
|
* relations): a vocabulary whose extension is declared in the schema — two
|
|
4
|
-
* tiers, one function.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* "
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* tiers, one function. Bare tier: `closed("Kind", ["Checking",
|
|
5
|
+
* "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
|
|
6
|
+
* { pages: true }, ... })` — one call, three arguments (the curried tier-2
|
|
7
|
+
* spelling is DELETED — canonical utterance): the axioms record IS the
|
|
8
|
+
* handle declaration, every handle carrying every column exactly once
|
|
9
|
+
* (type-enforced). At the host surface a handle is its NAME — a string
|
|
10
|
+
* literal of the roster's union (the drizzle law: translation, not
|
|
11
|
+
* abstraction; dispatch over a vocabulary is native `switch` narrowing, so
|
|
12
|
+
* no match operator is minted and no handle constants exist — the literal
|
|
13
|
+
* `"Checking"` is the ONE spelling). Handles are pure DATA, not properties
|
|
14
|
+
* of the value, so NO handle name is reserved: a vocabulary may legally
|
|
15
|
+
* contain handles named `match`, `where`, or `id` — the axioms record and
|
|
16
|
+
* the roster are their own namespaces. The value's whole surface: `name`;
|
|
17
|
+
* `id` — the field descriptor carrying the CLOSED LINKAGE (the roster —
|
|
18
|
+
* pure structure, no declared domain: the laws type the columns, and
|
|
19
|
+
* `schema()` names the id's generator class `"Kind.id"`) for other
|
|
20
|
+
* relations' field blocks (`kind: Kind.id`); `data` (the lowering
|
|
21
|
+
* carrier); `axioms` (payload readback, `Kind.axioms`); `columns` (the
|
|
22
|
+
* runtime twin of the `Cols` type parameter, which the face layer's
|
|
23
|
+
* structural wall reads); and — exactly when payload columns exist —
|
|
24
|
+
* `where()`, the ψ-selection surface (`Kind.where({ mastered: true })` as a
|
|
25
|
+
* face source), resolved through the ONE selection machine
|
|
26
|
+
* (`relation.ts::resolveSelection`); the bare tier has no payload columns
|
|
27
|
+
* to select on, so `.where` is absent there, at the type AND on the value.
|
|
28
|
+
* No fact type and no insert surface exist — closed relations are
|
|
29
|
+
* unwritable by construction: the value simply lacks the writable relation
|
|
30
|
+
* shape.
|
|
18
31
|
*/
|
|
19
32
|
|
|
20
33
|
import * as errors from "@superbuilders/errors"
|
|
@@ -26,31 +39,27 @@ import {
|
|
|
26
39
|
type Infer,
|
|
27
40
|
literalOf
|
|
28
41
|
} from "#fields.ts"
|
|
42
|
+
import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
|
|
29
43
|
import type { LiteralSpec } from "#spec.ts"
|
|
30
44
|
|
|
31
|
-
/**
|
|
32
|
-
* The value-surface property names a handle may not shadow — the macro's
|
|
33
|
-
* name-collision diagnostic, here over the closed value's own properties
|
|
34
|
-
* (`relation`/`selection` are reserved so a closed value can never be
|
|
35
|
-
* mistaken for a selected relation by `on()`'s discriminant).
|
|
36
|
-
*/
|
|
37
|
-
const reservedHandleNames: readonly string[] = Object.freeze([
|
|
38
|
-
"name",
|
|
39
|
-
"id",
|
|
40
|
-
"data",
|
|
41
|
-
"axioms",
|
|
42
|
-
"columns",
|
|
43
|
-
"fromId",
|
|
44
|
-
"relation",
|
|
45
|
-
"selection"
|
|
46
|
-
])
|
|
47
|
-
|
|
48
45
|
/**
|
|
49
46
|
* A payload column of a closed relation: any field descriptor except a
|
|
50
47
|
* fresh-marked one (a vocabulary's rows are ground axioms, never minted).
|
|
51
48
|
*/
|
|
52
49
|
type PayloadField = Exclude<AnyField, { readonly fresh: true }>
|
|
53
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A declared payload column BLOCK: name → descriptor, with `id`
|
|
53
|
+
* unspellable — the sealed shape mints the synthetic `id` itself (ordinal
|
|
54
|
+
* 0 of the matchable fields), so a declared column named `id` would be
|
|
55
|
+
* shadowed by the synthetic slot everywhere the shape resolves by name
|
|
56
|
+
* (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
|
|
57
|
+
* typed here and judged again at construction in {@link mintClosed} — the
|
|
58
|
+
* runtime twin for untyped callers, warmer and earlier than the engine's
|
|
59
|
+
* `DuplicateFieldName` at `Db.create`.
|
|
60
|
+
*/
|
|
61
|
+
type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
|
|
62
|
+
|
|
54
63
|
/** One declared payload column: name plus its field descriptor. */
|
|
55
64
|
interface ClosedColumn {
|
|
56
65
|
readonly name: string
|
|
@@ -96,39 +105,84 @@ type Axioms<Handles extends string, Cols extends Record<string, PayloadField>> =
|
|
|
96
105
|
interface ClosedCore<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
97
106
|
readonly name: Name
|
|
98
107
|
/**
|
|
99
|
-
* The
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
108
|
+
* The closed reference descriptor: `kind: Kind.id` in another relation's
|
|
109
|
+
* field block is the reference through which handle literals become
|
|
110
|
+
* legal in that relation's selections. Pure structure plus the PRECISE
|
|
111
|
+
* roster (`ClosedIdField<Handles>` — the handle union is the field's
|
|
112
|
+
* value type under `Infer`); the referencing field's domain is law-born:
|
|
113
|
+
* `schema()` computes it from the declared containment (`"Kind.id"`, the
|
|
114
|
+
* generator class).
|
|
103
115
|
*/
|
|
104
|
-
readonly id: ClosedIdField
|
|
116
|
+
readonly id: ClosedIdField<Handles>
|
|
105
117
|
readonly data: ClosedData
|
|
106
118
|
/** Payload readback: handle to its declared column values, bare and structural. */
|
|
107
119
|
readonly axioms: Axioms<Handles, Cols>
|
|
108
120
|
/**
|
|
109
|
-
* The declared payload columns, name →
|
|
121
|
+
* The declared payload columns, name → field descriptor — an HONEST
|
|
110
122
|
* frozen runtime record (the descriptors themselves, by identity), and
|
|
111
|
-
* the typed carrier a projected payload column's
|
|
123
|
+
* the typed carrier a projected payload column's structural shape is
|
|
112
124
|
* recovered through off the schema type (the face layer's
|
|
113
|
-
* `
|
|
125
|
+
* `ProjectedShape` reads it; `data.columns` carries the same
|
|
114
126
|
* descriptors in declaration order for the lowering).
|
|
115
127
|
*/
|
|
116
128
|
readonly columns: Cols
|
|
117
|
-
/** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
|
|
118
|
-
fromId(id: bigint): Handles | undefined
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
132
|
+
* The `where()` argument of a closed relation: EXACTLY the relation
|
|
133
|
+
* surface's {@link SelectionInput}, over the declared payload columns — the
|
|
134
|
+
* ONE selection vocabulary, so a spelling change there (H3's membership
|
|
135
|
+
* arrays) flows through with no local change here. The synthetic `id` is
|
|
136
|
+
* deliberately unspellable ({@link PayloadColumns} refuses an `id` column):
|
|
137
|
+
* an id selection is spelled only as handle literals on the REFERENCING
|
|
138
|
+
* side (the canonical-utterance law).
|
|
139
|
+
*/
|
|
140
|
+
type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = SelectionInput<Cols>
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A closed relation with a ψ selection applied — what `on()` consumes as a
|
|
144
|
+
* σ-carrying closed source (`on(Kind.where({ mastered: true }), "id")`).
|
|
145
|
+
* Deliberately the SAME discriminant shape as the ordinary `Selected`
|
|
146
|
+
* (`relation`/`selection` — `face.ts::faceParts` splits both by `"relation"
|
|
147
|
+
* in source`), and structurally UNMISTAKABLE for one: an `AnyClosed` lacks
|
|
148
|
+
* the relation shape (no `fields` record, no `RelationData`).
|
|
149
|
+
*/
|
|
150
|
+
interface SelectedClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
151
|
+
readonly relation: Closed<Name, Handles, Cols>
|
|
152
|
+
readonly selection: readonly SelectionBinding[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Any ψ-selected closed relation value. */
|
|
156
|
+
interface AnySelectedClosed {
|
|
157
|
+
readonly relation: AnyClosed
|
|
158
|
+
readonly selection: readonly SelectionBinding[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The ψ-selection surface of a payload-tier closed value. The selection is
|
|
163
|
+
* resolved EAGERLY against the declared columns and lowered as-is — the SDK
|
|
164
|
+
* never pre-folds ψ into an id set: pass-through lowering is what the macro
|
|
165
|
+
* does, and the ENGINE folds against the sealed extension at validate
|
|
166
|
+
* (`compile_member_set`).
|
|
167
|
+
*/
|
|
168
|
+
interface ClosedSelectable<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
169
|
+
where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A closed relation value: the core surface plus — exactly when payload
|
|
174
|
+
* columns exist — `where()` (the bare tier has nothing to select on, so
|
|
175
|
+
* the method is ABSENT there, not merely uncallable). NOTHING else:
|
|
176
|
+
* handles are data on the roster, never properties of the value (the
|
|
177
|
+
* handle constants, the match operator, and the id-to-handle weld died
|
|
178
|
+
* with the bigint era — dispatch is native `switch` narrowing over the
|
|
179
|
+
* handle union).
|
|
126
180
|
*/
|
|
127
|
-
type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Cols
|
|
131
|
-
|
|
181
|
+
type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = [
|
|
182
|
+
keyof Cols
|
|
183
|
+
] extends [never]
|
|
184
|
+
? ClosedCore<Name, Handles, Cols>
|
|
185
|
+
: ClosedCore<Name, Handles, Cols> & ClosedSelectable<Name, Handles, Cols>
|
|
132
186
|
|
|
133
187
|
/** Any closed relation value, whatever its roster and columns. */
|
|
134
188
|
interface AnyClosed {
|
|
@@ -183,21 +237,6 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
|
|
|
183
237
|
})
|
|
184
238
|
}
|
|
185
239
|
|
|
186
|
-
/**
|
|
187
|
-
* The trusted seam of the handle-constant mint: every handle reads back as
|
|
188
|
-
* an own bigint — verified before the record is admitted at the constants
|
|
189
|
-
* type (a "__proto__"-named handle riding the object-protocol accessor
|
|
190
|
-
* would fail exactly this check).
|
|
191
|
-
*/
|
|
192
|
-
function constantsMinted<Handles extends string>(
|
|
193
|
-
record: Readonly<Record<string, bigint>>,
|
|
194
|
-
handles: readonly Handles[]
|
|
195
|
-
): record is Readonly<Record<string, bigint>> & { readonly [H in Handles]: bigint } {
|
|
196
|
-
return handles.every(function constantMinted(handle) {
|
|
197
|
-
return typeof record[handle] === "bigint"
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
240
|
/**
|
|
202
241
|
* Reads one handle's ground axiom row for lowering. The typed payload
|
|
203
242
|
* surface makes absence unrepresentable ({@link Axioms} carries every
|
|
@@ -239,22 +278,6 @@ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadF
|
|
|
239
278
|
return out
|
|
240
279
|
}
|
|
241
280
|
|
|
242
|
-
/** Mints the handle constants: one own bigint per handle, ids = declaration order. */
|
|
243
|
-
function mintHandleConstants<Handles extends string>(
|
|
244
|
-
name: string,
|
|
245
|
-
handles: readonly Handles[]
|
|
246
|
-
): { readonly [H in Handles]: bigint } {
|
|
247
|
-
const out: Record<string, bigint> = {}
|
|
248
|
-
handles.forEach(function mintHandleConstant(handle, index) {
|
|
249
|
-
Object.defineProperty(out, handle, { value: BigInt(index), enumerable: true })
|
|
250
|
-
})
|
|
251
|
-
Object.freeze(out)
|
|
252
|
-
if (!constantsMinted(out, handles)) {
|
|
253
|
-
throw errors.new(`closed relation ${name}: handle-constant minting incomplete`)
|
|
254
|
-
}
|
|
255
|
-
return out
|
|
256
|
-
}
|
|
257
|
-
|
|
258
281
|
/** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
|
|
259
282
|
function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
|
|
260
283
|
name: Name,
|
|
@@ -262,29 +285,40 @@ function closed<const Name extends string, const Handles extends readonly [strin
|
|
|
262
285
|
): Closed<Name, Handles[number], Record<never, never>>
|
|
263
286
|
|
|
264
287
|
/**
|
|
265
|
-
* Payload tier: declared columns
|
|
266
|
-
* { mastered: bool }
|
|
267
|
-
* false } })`. The
|
|
268
|
-
*
|
|
269
|
-
*
|
|
288
|
+
* Payload tier: declared columns AND ground axioms, one call — `closed(
|
|
289
|
+
* "Grade", { mastered: bool }, { DirectPass: { mastered: true }, Failed:
|
|
290
|
+
* { mastered: false } })`. The curried tier-2 spelling is DELETED
|
|
291
|
+
* (canonical utterance): `Cols` infers from the column block, the handle
|
|
292
|
+
* set from the axioms record's keys (reverse mapped-type inference), and
|
|
293
|
+
* every row is contextually checked against the declared columns — a
|
|
294
|
+
* wrong-typed value errors ON its property. The axioms record's keys ARE
|
|
295
|
+
* the handles (declaration order = key order, integer-index names
|
|
296
|
+
* rejected); every row carries every column exactly once (type-enforced by
|
|
297
|
+
* {@link Axioms}).
|
|
270
298
|
*/
|
|
271
|
-
function closed<const Name extends string, const Cols extends
|
|
299
|
+
function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
|
|
272
300
|
name: Name,
|
|
273
|
-
columns: Cols
|
|
274
|
-
|
|
301
|
+
columns: Cols,
|
|
302
|
+
axioms: Axioms<Handles, Cols>
|
|
303
|
+
): Closed<Name, Handles, Cols>
|
|
275
304
|
|
|
276
|
-
function closed<const Name extends string>(
|
|
305
|
+
function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
|
|
277
306
|
name: Name,
|
|
278
|
-
shape: readonly [string, ...string[]] |
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
| (<Handles extends string>(
|
|
282
|
-
axioms: Axioms<Handles, Record<string, PayloadField>>
|
|
283
|
-
) => Closed<Name, Handles, Record<string, PayloadField>>) {
|
|
307
|
+
shape: readonly [string, ...string[]] | Cols,
|
|
308
|
+
axioms?: Axioms<Handles, Cols>
|
|
309
|
+
): Closed<Name, string, Record<never, never>> | Closed<Name, Handles, Cols> {
|
|
284
310
|
if (isHandleTuple(shape)) {
|
|
311
|
+
if (axioms !== undefined) {
|
|
312
|
+
throw errors.new(`closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`)
|
|
313
|
+
}
|
|
285
314
|
return closedBare(name, shape)
|
|
286
315
|
}
|
|
287
|
-
|
|
316
|
+
if (axioms === undefined) {
|
|
317
|
+
throw errors.new(
|
|
318
|
+
`closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, columns, axioms) (the curried spelling is deleted)`
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
return closedPayload(name, shape, axioms)
|
|
288
322
|
}
|
|
289
323
|
|
|
290
324
|
/** The bare tier's precisely-typed builder: no columns, no axioms. */
|
|
@@ -297,35 +331,49 @@ function closedBare<Name extends string, Handles extends string>(
|
|
|
297
331
|
|
|
298
332
|
/**
|
|
299
333
|
* The payload tier's precisely-typed builder: column names are judged
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* set off the axioms record's own keys.
|
|
334
|
+
* first (the macro-expansion analog), then the handle set is read off the
|
|
335
|
+
* axioms record's own keys.
|
|
303
336
|
*/
|
|
304
|
-
function closedPayload<Name extends string,
|
|
337
|
+
function closedPayload<Name extends string, Handles extends string, Cols extends PayloadColumns>(
|
|
305
338
|
name: Name,
|
|
306
|
-
columns: Cols
|
|
307
|
-
|
|
339
|
+
columns: Cols,
|
|
340
|
+
axioms: Axioms<Handles, Cols>
|
|
341
|
+
): Closed<Name, Handles, Cols> {
|
|
308
342
|
for (const columnName of Object.keys(columns)) {
|
|
309
343
|
assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
|
|
310
344
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
|
|
315
|
-
}
|
|
316
|
-
if (!handleKeysOwn(axioms, handles)) {
|
|
317
|
-
throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
|
|
318
|
-
}
|
|
319
|
-
return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
|
|
345
|
+
const handles = Object.keys(axioms)
|
|
346
|
+
for (const handle of handles) {
|
|
347
|
+
assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
|
|
320
348
|
}
|
|
349
|
+
if (!handleKeysOwn(axioms, handles)) {
|
|
350
|
+
throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
|
|
351
|
+
}
|
|
352
|
+
return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The trusted seam of the ergonomic-surface mint: `where` reads back as an
|
|
357
|
+
* own function exactly when payload columns exist, and is ABSENT otherwise
|
|
358
|
+
* — the runtime twin of the {@link Closed} type's conditional arm
|
|
359
|
+
* (`ClosedCore` alone vs `ClosedCore & ClosedSelectable`), verified before
|
|
360
|
+
* the minted value is admitted at the conditional type.
|
|
361
|
+
*/
|
|
362
|
+
function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
363
|
+
value: ClosedCore<Name, Handles, Cols>,
|
|
364
|
+
cols: readonly ClosedColumn[]
|
|
365
|
+
): value is ClosedCore<Name, Handles, Cols> & Closed<Name, Handles, Cols> {
|
|
366
|
+
const selectable = "where" in value && typeof value.where === "function"
|
|
367
|
+
return cols.length > 0 ? selectable : !selectable
|
|
321
368
|
}
|
|
322
369
|
|
|
323
370
|
/**
|
|
324
371
|
* Mints one closed relation value — the shared seam of both tiers, HONESTLY
|
|
325
372
|
* typed end to end (a wrong-shaped mint is a compile error here, not a
|
|
326
373
|
* laundered `unknown`): roster checks, eager axiom lowering, the
|
|
327
|
-
*
|
|
328
|
-
* twin of the `Cols` type parameter), and the
|
|
374
|
+
* roster-carrying `id` descriptor, the frozen `columns` carrier (the runtime
|
|
375
|
+
* twin of the `Cols` type parameter), and — on the payload tier only — the
|
|
376
|
+
* ψ-selection `where()`.
|
|
329
377
|
*/
|
|
330
378
|
function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
331
379
|
name: Name,
|
|
@@ -342,17 +390,17 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
342
390
|
throw errors.new(`closed relation ${name}: duplicate handle ${handle}`)
|
|
343
391
|
}
|
|
344
392
|
seen.add(handle)
|
|
345
|
-
if (reservedHandleNames.includes(handle)) {
|
|
346
|
-
throw errors.new(
|
|
347
|
-
`closed relation ${name}: handle ${handle} collides with the closed value's own surface (${reservedHandleNames.join(", ")})`
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
393
|
}
|
|
351
394
|
const handleList: readonly Handles[] = Object.freeze([...handles])
|
|
352
|
-
const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
|
|
395
|
+
const roster: ClosedRoster<Handles> = Object.freeze({ name, handles: handleList })
|
|
353
396
|
const cols: ClosedColumn[] = []
|
|
354
397
|
for (const [columnName, field] of Object.entries(columns)) {
|
|
355
398
|
assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
|
|
399
|
+
if (columnName === "id") {
|
|
400
|
+
throw errors.new(
|
|
401
|
+
`closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
|
|
402
|
+
)
|
|
403
|
+
}
|
|
356
404
|
cols.push(Object.freeze({ name: columnName, field }))
|
|
357
405
|
}
|
|
358
406
|
Object.freeze(cols)
|
|
@@ -369,38 +417,58 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
369
417
|
columns: cols,
|
|
370
418
|
rows: Object.freeze(rows)
|
|
371
419
|
})
|
|
372
|
-
const id: ClosedIdField
|
|
373
|
-
kind: "u64",
|
|
374
|
-
domain: `${name}Id`,
|
|
375
|
-
closed: roster
|
|
376
|
-
})
|
|
420
|
+
const id: ClosedIdField<Handles> = Object.freeze({ kind: "u64", closed: roster })
|
|
377
421
|
/**
|
|
378
|
-
* Handle names are arbitrary identifiers, so rows
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* value whose type claims a bigint constant but reads back an object.
|
|
385
|
-
* (Object SPREAD is CreateDataProperty by spec, so the copies below are
|
|
386
|
-
* own-property safe for column names too.)
|
|
422
|
+
* Handle names are arbitrary identifiers, so axiom rows are minted with
|
|
423
|
+
* OWN-property definition (inside {@link mintAxioms}), never assignment:
|
|
424
|
+
* a handle named "__proto__" would otherwise ride the Object.prototype
|
|
425
|
+
* accessor — silently swapping the record's prototype instead of
|
|
426
|
+
* creating the row. (Object SPREAD is CreateDataProperty by spec, so the
|
|
427
|
+
* copies below are own-property safe for column names too.)
|
|
387
428
|
*/
|
|
388
429
|
const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
|
|
389
|
-
const constants = mintHandleConstants(name, handleList)
|
|
390
430
|
const columnsOut: Cols = { ...columns }
|
|
391
431
|
Object.freeze(columnsOut)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
432
|
+
const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
|
|
433
|
+
/**
|
|
434
|
+
* The ψ selection: resolved against the declared payload columns through
|
|
435
|
+
* the ONE selection machine (`relation.ts::resolveSelection` — a
|
|
436
|
+
* `ClosedColumn` is structurally a `RelationField`), never pre-folded
|
|
437
|
+
* into an id set (the engine folds at validate).
|
|
438
|
+
*/
|
|
439
|
+
function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
|
|
440
|
+
const owner = holder.value
|
|
441
|
+
if (owner === undefined) {
|
|
442
|
+
throw errors.new(`closed relation ${name}: self-reference read before construction completed`)
|
|
401
443
|
}
|
|
402
|
-
|
|
444
|
+
return Object.freeze({
|
|
445
|
+
relation: owner,
|
|
446
|
+
selection: resolveSelection(name, cols, Object.entries(selection))
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
const core = { name, id, data, axioms: axiomsOut, columns: columnsOut }
|
|
450
|
+
const value: ClosedCore<Name, Handles, Cols> =
|
|
451
|
+
cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
|
|
452
|
+
if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
|
|
453
|
+
throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
|
|
454
|
+
}
|
|
455
|
+
holder.value = value
|
|
456
|
+
return value
|
|
403
457
|
}
|
|
404
458
|
|
|
405
|
-
export type {
|
|
459
|
+
export type {
|
|
460
|
+
AnyClosed,
|
|
461
|
+
AnySelectedClosed,
|
|
462
|
+
AxiomRow,
|
|
463
|
+
Axioms,
|
|
464
|
+
Closed,
|
|
465
|
+
ClosedColumn,
|
|
466
|
+
ClosedCore,
|
|
467
|
+
ClosedData,
|
|
468
|
+
ClosedRow,
|
|
469
|
+
ClosedSelectable,
|
|
470
|
+
ClosedSelectionInput,
|
|
471
|
+
PayloadField,
|
|
472
|
+
SelectedClosed
|
|
473
|
+
}
|
|
406
474
|
export { closed }
|
package/src/db.ts
CHANGED
|
@@ -35,7 +35,17 @@ import * as errors from "@superbuilders/errors"
|
|
|
35
35
|
import type { Exhumed } from "#exhume.ts"
|
|
36
36
|
import { exhumeStore } from "#exhume.ts"
|
|
37
37
|
import { lower } from "#lower.ts"
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
factOf,
|
|
40
|
+
handleOf,
|
|
41
|
+
isFreshField,
|
|
42
|
+
isMintedFresh,
|
|
43
|
+
type KeyFact,
|
|
44
|
+
keyRowOf,
|
|
45
|
+
type Minted,
|
|
46
|
+
recordOf,
|
|
47
|
+
rowOf
|
|
48
|
+
} from "#marshal.ts"
|
|
39
49
|
|
|
40
50
|
import type {
|
|
41
51
|
DbHandle,
|
|
@@ -55,7 +65,7 @@ import type { Query } from "#query/lower.ts"
|
|
|
55
65
|
import { lowerQuery } from "#query/lower.ts"
|
|
56
66
|
import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
57
67
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
58
|
-
import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
|
|
68
|
+
import type { AnyRelation, Fact, InsertFact, RelationField } from "#relation.ts"
|
|
59
69
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
60
70
|
import type { KeyStatement, Statement } from "#statements.ts"
|
|
61
71
|
|
|
@@ -79,7 +89,10 @@ type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]
|
|
|
79
89
|
/**
|
|
80
90
|
* One offending fact of a violation: the cited relation's name (a member
|
|
81
91
|
* of the schema's record) and the fact decoded to a named natural-value
|
|
82
|
-
* object — partial exactly as the engine cites it.
|
|
92
|
+
* object — partial exactly as the engine cites it. Closed-referencing
|
|
93
|
+
* cells arrive as handle NAMES (the marshal bijection's read half), so the
|
|
94
|
+
* record and the violation's `canonical` string — which the engine already
|
|
95
|
+
* renders with handle names — agree on the one spelling.
|
|
83
96
|
*/
|
|
84
97
|
interface OffendingFact<Rels extends SchemaRelations> {
|
|
85
98
|
readonly relation: keyof Rels & string
|
|
@@ -525,6 +538,22 @@ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" |
|
|
|
525
538
|
return "written"
|
|
526
539
|
}
|
|
527
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The declared field descriptors a violation's offending cells decode
|
|
543
|
+
* through: an ordinary relation's declared fields; a closed relation's
|
|
544
|
+
* SEALED shape — the roster-carrying synthetic `id` (the member's own
|
|
545
|
+
* reference descriptor) plus its payload columns (a `ClosedColumn` is
|
|
546
|
+
* structurally a `RelationField`). This is how {@link openDb}'s
|
|
547
|
+
* `offendingFactOf` reaches the open-time descriptors: the cited relation
|
|
548
|
+
* name resolves to the schema member, and the member carries them.
|
|
549
|
+
*/
|
|
550
|
+
function declaredFieldsOf(member: SchemaRelation): readonly RelationField[] {
|
|
551
|
+
if ("axioms" in member) {
|
|
552
|
+
return [{ name: "id", field: member.id }, ...member.data.columns]
|
|
553
|
+
}
|
|
554
|
+
return member.data.fields
|
|
555
|
+
}
|
|
556
|
+
|
|
528
557
|
/** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
|
|
529
558
|
interface Tables {
|
|
530
559
|
readonly relations: ReadonlyMap<string, RelationEntry>
|
|
@@ -705,12 +734,20 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
705
734
|
}
|
|
706
735
|
|
|
707
736
|
function offendingFactOf(fact: WireViolationFact): OffendingFact<Rels> {
|
|
708
|
-
|
|
737
|
+
const entry = tables.relations.get(fact.relation)
|
|
738
|
+
if (entry === undefined || !isMemberName(fact.relation)) {
|
|
709
739
|
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
710
740
|
}
|
|
741
|
+
const declared = declaredFieldsOf(entry.member)
|
|
711
742
|
const decoded: Record<string, FactValue> = {}
|
|
712
743
|
for (const cell of fact.fields) {
|
|
713
|
-
|
|
744
|
+
const cited = declared.find(function byName(candidate) {
|
|
745
|
+
return candidate.name === cell.name
|
|
746
|
+
})
|
|
747
|
+
decoded[cell.name] =
|
|
748
|
+
cited !== undefined && "closed" in cited.field
|
|
749
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, cited.field.closed, cell.value)
|
|
750
|
+
: cell.value
|
|
714
751
|
}
|
|
715
752
|
return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
|
|
716
753
|
}
|
|
@@ -1419,16 +1456,31 @@ process.once("exit", function closeCachedStores() {
|
|
|
1419
1456
|
}
|
|
1420
1457
|
})
|
|
1421
1458
|
|
|
1459
|
+
/**
|
|
1460
|
+
* The engine twin of the schema-level class wall, as a matchable value
|
|
1461
|
+
* (`errors.is`): the shared lowering rejected a spec whose statement pairs
|
|
1462
|
+
* faces with disagreeing newtype labels — the faces of a dependency agree
|
|
1463
|
+
* on their newtype, or neither carries one. UNREACHABLE through the typed
|
|
1464
|
+
* builder (the SDK computes every label from the laws, so its lowered
|
|
1465
|
+
* specs cohere by construction); a raw spec handed to the bridge is the
|
|
1466
|
+
* one road here, and the runtime referee that proves the engine judges
|
|
1467
|
+
* what the types claim.
|
|
1468
|
+
*/
|
|
1469
|
+
const ErrNewtypeMismatch = errors.new(
|
|
1470
|
+
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1422
1473
|
/**
|
|
1423
1474
|
* The one admission path both verbs share: canonical-path cache lookup
|
|
1424
1475
|
* first (a hit returns the SAME `Db` value for the identical theory, a
|
|
1425
1476
|
* typed fingerprint error for a different one, and a typed refusal for
|
|
1426
1477
|
* `create` — the store a cache entry proves initialized is exactly what
|
|
1427
1478
|
* create refuses). On a miss: lower the theory, run one bridge call, and
|
|
1428
|
-
* wrap the
|
|
1429
|
-
* validation, every issue in one message)
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1479
|
+
* wrap the domain refusals — `schemaError` (spec resolution + schema
|
|
1480
|
+
* validation, every issue in one message), `newtypeMismatch` (the
|
|
1481
|
+
* coherence wall, {@link ErrNewtypeMismatch}), and `fingerprintMismatch`
|
|
1482
|
+
* (a different theory cannot open the store) — into typed errors carrying
|
|
1483
|
+
* the engine's message intact.
|
|
1432
1484
|
*/
|
|
1433
1485
|
function admit<Rels extends SchemaRelations>(
|
|
1434
1486
|
verb: "create" | "open",
|
|
@@ -1458,6 +1510,9 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1458
1510
|
return native.dbOpen(canonical, spec)
|
|
1459
1511
|
})
|
|
1460
1512
|
if (!opened.ok) {
|
|
1513
|
+
if (opened.kind === "newtypeMismatch") {
|
|
1514
|
+
throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`)
|
|
1515
|
+
}
|
|
1461
1516
|
throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
|
|
1462
1517
|
}
|
|
1463
1518
|
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
@@ -1526,4 +1581,4 @@ export type {
|
|
|
1526
1581
|
WitnessedWriteResult,
|
|
1527
1582
|
WriteResult
|
|
1528
1583
|
}
|
|
1529
|
-
export { abandon, Db }
|
|
1584
|
+
export { abandon, Db, ErrNewtypeMismatch }
|