@bjornpagen/bumbledb 0.2.0 → 0.3.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 +462 -406
- package/README.md +66 -31
- package/dist/closed.d.ts +121 -25
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +108 -42
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +12 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +20 -5
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +100 -55
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +36 -10
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +50 -79
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +20 -53
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +17 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- 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/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 +88 -50
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +4 -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 +90 -43
- 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/scope.d.ts +76 -41
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +77 -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 +21 -8
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +13 -7
- 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 +7 -6
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +61 -31
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +22 -17
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +243 -68
- package/src/db.ts +23 -5
- package/src/face.ts +162 -84
- package/src/fields.ts +57 -136
- package/src/index.ts +42 -16
- package/src/law.ts +519 -0
- package/src/lower.ts +36 -23
- package/src/native.ts +6 -2
- package/src/query/atom.ts +105 -58
- package/src/query/lower.ts +271 -139
- package/src/query/predicate.ts +43 -33
- package/src/query/scope.ts +125 -49
- package/src/query/select.ts +5 -5
- package/src/relation.ts +15 -9
- package/src/schema.ts +48 -7
- package/src/spec.ts +7 -6
- package/src/statements.ts +83 -43
package/src/law.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The law-typing engine (owner ruling 2026-07-18, "option 2, zero debate"):
|
|
3
|
+
* THE LAWS TYPE THE COLUMNS. Domains are declared nowhere — `schema()`
|
|
4
|
+
* computes every field's domain FROM the statement list, at BOTH the type
|
|
5
|
+
* level (this module's type machinery, reading the statements tuple type)
|
|
6
|
+
* and at runtime (a plain union-find over the same pairs), and the two
|
|
7
|
+
* tiers are the same computation by construction.
|
|
8
|
+
*
|
|
9
|
+
* The three class laws (ratified; implemented exactly):
|
|
10
|
+
*
|
|
11
|
+
* 1. GENERATORS — a `fresh` field is a generator and names its class by
|
|
12
|
+
* its declaration coordinate (`"Account.id"`); a closed relation's
|
|
13
|
+
* synthetic id is a generator named `"Kind.id"`.
|
|
14
|
+
* 2. GENERATOR-LESS classes are named by their least member coordinate
|
|
15
|
+
* in relation-declaration × field-declaration order (readable off the
|
|
16
|
+
* relation record and each member's frozen field list at the VALUE
|
|
17
|
+
* tier — deterministic, pinned forever; the wire reads only this
|
|
18
|
+
* tier). At the TYPE tier the same class is carried as its
|
|
19
|
+
* member-coordinate SET (see {@link ClassOfCoord}): TypeScript's
|
|
20
|
+
* union member order is not observably deterministic, so a type-level
|
|
21
|
+
* least-member pick would drift between compilations — the set is the
|
|
22
|
+
* canonical deterministic spelling, the runtime name is always a
|
|
23
|
+
* member of it, and the join judgment is identical at both tiers.
|
|
24
|
+
* 3. BARE — a field in no law has NO class and pairs only with bare in
|
|
25
|
+
* queries (the deliberate sum-domain pointer stays legal).
|
|
26
|
+
*
|
|
27
|
+
* THE WALL: a class containing more than one generator is a contradiction
|
|
28
|
+
* (two mints cannot share a carrier) — a schema-level COMPILE error (the
|
|
29
|
+
* named, self-locating {@link ClassWall}: which generator coordinates
|
|
30
|
+
* collided, through which paired slots) with a construction-time runtime
|
|
31
|
+
* twin (`computeClasses` throws with the same content, naming the exact
|
|
32
|
+
* statement).
|
|
33
|
+
*
|
|
34
|
+
* Every paired face of the statement tuple unions its positionwise field
|
|
35
|
+
* slots: containment (ψ-selected targets included — a selection changes
|
|
36
|
+
* pairing not at all), the `==` bijection, and window source/target pairs.
|
|
37
|
+
* `key()` statements pair nothing (an FD constrains one relation's own
|
|
38
|
+
* rows; it identifies no carriers).
|
|
39
|
+
*
|
|
40
|
+
* The type tier reads pairs off the statement types' exact face data, so
|
|
41
|
+
* spell the statement list INLINE in `schema()` (the `const` type
|
|
42
|
+
* parameter keeps the tuple precise). A widened `Statement[]` list
|
|
43
|
+
* degrades the TYPE tier to generators-only (no pair is readable off a
|
|
44
|
+
* widened type) — the runtime map stays complete and authoritative, and
|
|
45
|
+
* the wire lowering reads only the runtime map. Every loop below is
|
|
46
|
+
* tail-recursive with an accumulator, so the machinery rides TypeScript's
|
|
47
|
+
* tail-recursion elimination at primer scale (~40 relations, ~200 slots,
|
|
48
|
+
* ~123 statements); should a schema ever exceed the compiler's limits, tsc
|
|
49
|
+
* fails LOUDLY with its own instantiation-depth error — the map is never
|
|
50
|
+
* silently widened.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import * as errors from "@superbuilders/errors"
|
|
54
|
+
import type { AnyClosed } from "#closed.ts"
|
|
55
|
+
import type { FaceData } from "#face.ts"
|
|
56
|
+
import type { AnyRelation, RelationFields } from "#relation.ts"
|
|
57
|
+
import type { SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
58
|
+
import { renderStatement, type Statement } from "#statements.ts"
|
|
59
|
+
|
|
60
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
61
|
+
// The class-map shapes.
|
|
62
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
63
|
+
|
|
64
|
+
/** One relation's computed classes: field name → class name, `undefined` = bare. */
|
|
65
|
+
type RelationClasses = { readonly [field: string]: string | undefined }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The class map a schema carries — relation name → field name → the
|
|
69
|
+
* computed class name (`undefined` = bare). THE domain authority: queries
|
|
70
|
+
* and the wire lowering read domains from here and nowhere else. The wide
|
|
71
|
+
* shape is the default every `Schema`-generic surface accepts; a concrete
|
|
72
|
+
* schema's `classes` property carries the EXACT computed map
|
|
73
|
+
* ({@link ClassesOf}) at the type level and the frozen runtime twin
|
|
74
|
+
* (`computeClasses`) at the value level — one property, two tiers, one
|
|
75
|
+
* computation.
|
|
76
|
+
*/
|
|
77
|
+
type SchemaClasses = { readonly [relation: string]: RelationClasses }
|
|
78
|
+
|
|
79
|
+
/** Looks one relation's class record up in a schema's class map (absent relation = no classes). */
|
|
80
|
+
type ClassRecordOf<Classes extends SchemaClasses, N extends string> = N extends keyof Classes
|
|
81
|
+
? Classes[N]
|
|
82
|
+
: Record<never, never>
|
|
83
|
+
|
|
84
|
+
/** Looks one field's class up in a relation's class record (absent field = bare). */
|
|
85
|
+
type ClassLookup<CR, K> = K extends keyof CR ? CR[K] & (string | undefined) : undefined
|
|
86
|
+
|
|
87
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
88
|
+
// Coordinates.
|
|
89
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
90
|
+
|
|
91
|
+
/** A closed member's declared payload-column record (`never` on ordinary relations). */
|
|
92
|
+
type MemberColumns<M> = M extends AnyClosed ? M["columns"] : never
|
|
93
|
+
|
|
94
|
+
/** The field names of one schema member: a relation's declared fields; a closed relation's sealed `id` + columns. */
|
|
95
|
+
type MemberFieldNames<M extends SchemaRelation> = M extends AnyClosed
|
|
96
|
+
? "id" | (keyof MemberColumns<M> & string)
|
|
97
|
+
: M extends AnyRelation
|
|
98
|
+
? keyof RelationFields<M> & string
|
|
99
|
+
: never
|
|
100
|
+
|
|
101
|
+
/** The fresh-marked field names of one field block. */
|
|
102
|
+
type FreshFieldNames<Fields> = {
|
|
103
|
+
[F in keyof Fields & string]: Fields[F] extends { readonly fresh: true } ? F : never
|
|
104
|
+
}[keyof Fields & string]
|
|
105
|
+
|
|
106
|
+
/** One member's generator coordinates: fresh fields; a closed relation's synthetic id. */
|
|
107
|
+
type MemberGenerators<N extends string, M extends SchemaRelation> = M extends AnyClosed
|
|
108
|
+
? `${N}.id`
|
|
109
|
+
: M extends AnyRelation
|
|
110
|
+
? `${N}.${FreshFieldNames<RelationFields<M>>}`
|
|
111
|
+
: never
|
|
112
|
+
|
|
113
|
+
/** Every generator coordinate of a relation record (a union). */
|
|
114
|
+
type GeneratorsOf<Rels extends SchemaRelations> = {
|
|
115
|
+
[N in keyof Rels & string]: MemberGenerators<N, Rels[N]>
|
|
116
|
+
}[keyof Rels & string]
|
|
117
|
+
|
|
118
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
119
|
+
// Pairs: the positionwise slot pairs of every paired face.
|
|
120
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
121
|
+
|
|
122
|
+
/** One paired-slot pair of coordinates. */
|
|
123
|
+
type Pair = readonly [string, string]
|
|
124
|
+
|
|
125
|
+
/** A list of slot pairs. */
|
|
126
|
+
type PairList = readonly Pair[]
|
|
127
|
+
|
|
128
|
+
/** Zips two faces' projections into coordinate pairs, positionwise. */
|
|
129
|
+
type ZipCoords<
|
|
130
|
+
SN extends string,
|
|
131
|
+
SP extends readonly string[],
|
|
132
|
+
TN extends string,
|
|
133
|
+
TP extends readonly string[],
|
|
134
|
+
Acc extends PairList = []
|
|
135
|
+
> = SP extends readonly [infer SH extends string, ...infer ST extends readonly string[]]
|
|
136
|
+
? TP extends readonly [infer TH extends string, ...infer TT extends readonly string[]]
|
|
137
|
+
? ZipCoords<SN, ST, TN, TT, readonly [...Acc, readonly [`${SN}.${SH}`, `${TN}.${TH}`]]>
|
|
138
|
+
: Acc
|
|
139
|
+
: Acc
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One statement's slot pairs: containments (bidirectional included — pair
|
|
143
|
+
* unions are symmetric) and windows pair their two faces positionwise;
|
|
144
|
+
* `key()` pairs nothing. A widened face (owner name or projection no
|
|
145
|
+
* longer literal) contributes nothing — the runtime map stays complete.
|
|
146
|
+
*/
|
|
147
|
+
type StatementPairs<St extends Statement> = St["data"] extends {
|
|
148
|
+
readonly source: infer S extends FaceData
|
|
149
|
+
readonly target: infer T extends FaceData
|
|
150
|
+
}
|
|
151
|
+
? string extends S["owner"]["name"]
|
|
152
|
+
? []
|
|
153
|
+
: string extends T["owner"]["name"]
|
|
154
|
+
? []
|
|
155
|
+
: ZipCoords<S["owner"]["name"], S["projection"], T["owner"]["name"], T["projection"]>
|
|
156
|
+
: []
|
|
157
|
+
|
|
158
|
+
/** Every slot pair of a statements tuple, in written order. */
|
|
159
|
+
type PairsOf<Stmts extends readonly Statement[], Acc extends PairList = []> = Stmts extends readonly [
|
|
160
|
+
infer H extends Statement,
|
|
161
|
+
...infer T extends readonly Statement[]
|
|
162
|
+
]
|
|
163
|
+
? PairsOf<T, readonly [...Acc, ...StatementPairs<H>]>
|
|
164
|
+
: Acc
|
|
165
|
+
|
|
166
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
167
|
+
// Union-find over the pairs: connected components as coordinate unions.
|
|
168
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
169
|
+
|
|
170
|
+
/** The component (a union of coordinates) containing `X`, or `never` when `X` is in none. */
|
|
171
|
+
type CompOf<Comps extends readonly string[], X extends string> = Comps extends readonly [
|
|
172
|
+
infer H extends string,
|
|
173
|
+
...infer T extends readonly string[]
|
|
174
|
+
]
|
|
175
|
+
? [X] extends [H]
|
|
176
|
+
? H
|
|
177
|
+
: CompOf<T, X>
|
|
178
|
+
: never
|
|
179
|
+
|
|
180
|
+
/** Rebuilds the component list without the component `C` (components are disjoint; identity is mutual extension). */
|
|
181
|
+
type WithoutComp<
|
|
182
|
+
Comps extends readonly string[],
|
|
183
|
+
C extends string,
|
|
184
|
+
Acc extends readonly string[] = []
|
|
185
|
+
> = Comps extends readonly [infer H extends string, ...infer T extends readonly string[]]
|
|
186
|
+
? [H, C] extends [C, H]
|
|
187
|
+
? WithoutComp<T, C, Acc>
|
|
188
|
+
: WithoutComp<T, C, readonly [...Acc, H]>
|
|
189
|
+
: Acc
|
|
190
|
+
|
|
191
|
+
/** Unions one pair into the component list: create, extend, keep, or merge. */
|
|
192
|
+
type AddPair<Comps extends readonly string[], A extends string, B extends string> = [
|
|
193
|
+
CompOf<Comps, A>,
|
|
194
|
+
CompOf<Comps, B>
|
|
195
|
+
] extends [infer CA extends string, infer CB extends string]
|
|
196
|
+
? [CA] extends [never]
|
|
197
|
+
? [CB] extends [never]
|
|
198
|
+
? readonly [...Comps, A | B]
|
|
199
|
+
: readonly [...WithoutComp<Comps, CB>, CB | A]
|
|
200
|
+
: [CB] extends [never]
|
|
201
|
+
? readonly [...WithoutComp<Comps, CA>, CA | B]
|
|
202
|
+
: [CA, CB] extends [CB, CA]
|
|
203
|
+
? Comps
|
|
204
|
+
: readonly [...WithoutComp<WithoutComp<Comps, CA>, CB>, CA | CB]
|
|
205
|
+
: Comps
|
|
206
|
+
|
|
207
|
+
/** Folds every pair into connected components (tail-recursive — the whole walk is one loop). */
|
|
208
|
+
type BuildComps<Pairs extends PairList, Comps extends readonly string[] = readonly []> = Pairs extends readonly [
|
|
209
|
+
infer P extends Pair,
|
|
210
|
+
...infer T extends PairList
|
|
211
|
+
]
|
|
212
|
+
? BuildComps<T, AddPair<Comps, P[0], P[1]>>
|
|
213
|
+
: Comps
|
|
214
|
+
|
|
215
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
216
|
+
// The wall and the names.
|
|
217
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
218
|
+
|
|
219
|
+
/** Whether a union holds two or more members (`All` captures the whole union across distribution). */
|
|
220
|
+
type IsMulti<U, All = U> = [U] extends [never] ? false : U extends unknown ? ([All] extends [U] ? false : true) : never
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The named, self-locating compile verdict of the one-generator wall: the
|
|
224
|
+
* generator coordinates that collided and the paired slots (rendered
|
|
225
|
+
* `A.x ~ B.y`, statement order) whose chain unified them. Intersected into
|
|
226
|
+
* `schema()`'s statements parameter, so the error lands ON the statement
|
|
227
|
+
* list with this key naming the law. The runtime twin throws from
|
|
228
|
+
* `computeClasses` with the same content, naming the exact statement.
|
|
229
|
+
*/
|
|
230
|
+
interface ClassWall<Generators extends string, Chain extends readonly string[]> {
|
|
231
|
+
readonly "schema class wall — the statements unify two generators into one class (two mints cannot share a carrier)": {
|
|
232
|
+
readonly generators: Generators
|
|
233
|
+
readonly through: Chain
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The paired slots lying inside component `C`, rendered — the wall's self-locating chain. */
|
|
238
|
+
type ChainOf<Pairs extends PairList, C extends string, Acc extends readonly string[] = []> = Pairs extends readonly [
|
|
239
|
+
infer P extends Pair,
|
|
240
|
+
...infer T extends PairList
|
|
241
|
+
]
|
|
242
|
+
? [P[0]] extends [C]
|
|
243
|
+
? ChainOf<T, C, readonly [...Acc, `${P[0]} ~ ${P[1]}`]>
|
|
244
|
+
: ChainOf<T, C, Acc>
|
|
245
|
+
: Acc
|
|
246
|
+
|
|
247
|
+
/** Scans the components for a two-generator class: `unknown` (lawful) or the {@link ClassWall}. */
|
|
248
|
+
type WallScan<Comps extends readonly string[], Gens extends string, Pairs extends PairList> = Comps extends readonly [
|
|
249
|
+
infer H extends string,
|
|
250
|
+
...infer T extends readonly string[]
|
|
251
|
+
]
|
|
252
|
+
? true extends IsMulti<Extract<H, Gens>>
|
|
253
|
+
? ClassWall<Extract<H, Gens>, ChainOf<Pairs, H>>
|
|
254
|
+
: WallScan<T, Gens, Pairs>
|
|
255
|
+
: unknown
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The one-generator-per-class law as a constraint: resolves to `unknown`
|
|
259
|
+
* (a no-op intersection into the statements parameter) when the statement
|
|
260
|
+
* list is lawful, and to the named {@link ClassWall} otherwise.
|
|
261
|
+
*/
|
|
262
|
+
type LawfulStatements<Rels extends SchemaRelations, Stmts extends readonly Statement[]> = WallScan<
|
|
263
|
+
BuildComps<PairsOf<Stmts>>,
|
|
264
|
+
GeneratorsOf<Rels>,
|
|
265
|
+
PairsOf<Stmts>
|
|
266
|
+
>
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* One coordinate's class per the three laws, at the TYPE tier: its
|
|
270
|
+
* component's single generator (the exact literal — a generator names its
|
|
271
|
+
* class); a component-less generator is its own class; a component-less
|
|
272
|
+
* non-generator is bare (`undefined`); and a GENERATOR-LESS component is
|
|
273
|
+
* carried as its member-coordinate SET (the union of the component's
|
|
274
|
+
* coordinates — a canonical, deterministic type). The set REPRESENTS the
|
|
275
|
+
* runtime's least-member class name faithfully: the runtime name is by
|
|
276
|
+
* construction a member, two slots share a class exactly when their sets
|
|
277
|
+
* are identical (so `JoinOk` judges identically at both tiers), and bare
|
|
278
|
+
* never equals a set. The least-member PICK itself is deliberately not
|
|
279
|
+
* made at the type tier: TypeScript's union member order is not observably
|
|
280
|
+
* deterministic (the same key union tuples differently across checking
|
|
281
|
+
* contexts — measured, not conjectured), so any type-level "least in
|
|
282
|
+
* declaration order" would drift between compilations; the ratified
|
|
283
|
+
* declaration-order name lives at the VALUE tier (`computeClasses`), which
|
|
284
|
+
* is the only tier the wire reads.
|
|
285
|
+
*/
|
|
286
|
+
type ClassOfCoord<Comps extends readonly string[], Gens extends string, C extends string> = [CompOf<Comps, C>] extends [
|
|
287
|
+
infer M extends string
|
|
288
|
+
]
|
|
289
|
+
? [M] extends [never]
|
|
290
|
+
? [C] extends [Gens]
|
|
291
|
+
? C
|
|
292
|
+
: undefined
|
|
293
|
+
: [Extract<M, Gens>] extends [infer G extends string]
|
|
294
|
+
? [G] extends [never]
|
|
295
|
+
? M
|
|
296
|
+
: G
|
|
297
|
+
: never
|
|
298
|
+
: never
|
|
299
|
+
|
|
300
|
+
/** The computed class map over precomputed components/generators. */
|
|
301
|
+
type ComputedClasses<Rels extends SchemaRelations, Comps extends readonly string[], Gens extends string> = {
|
|
302
|
+
readonly [N in keyof Rels & string]: {
|
|
303
|
+
readonly [F in MemberFieldNames<Rels[N]>]: ClassOfCoord<Comps, Gens, `${N}.${F}`>
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* THE type-level class map of a schema: relation name → field name → the
|
|
309
|
+
* law-computed class (`undefined` = bare) — what `schema()` returns as the
|
|
310
|
+
* `classes` property's type, and what query joins compare. Generator
|
|
311
|
+
* classes are exact name literals; generator-less classes are their
|
|
312
|
+
* member-coordinate sets (see {@link ClassOfCoord} — the runtime map's
|
|
313
|
+
* least-member name is always a member, so the property type is honest).
|
|
314
|
+
*/
|
|
315
|
+
type ClassesOf<Rels extends SchemaRelations, Stmts extends readonly Statement[]> = ComputedClasses<
|
|
316
|
+
Rels,
|
|
317
|
+
BuildComps<PairsOf<Stmts>>,
|
|
318
|
+
GeneratorsOf<Rels>
|
|
319
|
+
>
|
|
320
|
+
|
|
321
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
322
|
+
// The runtime twin: the same computation as a plain union-find.
|
|
323
|
+
// ————————————————————————————————————————————————————————————————————————
|
|
324
|
+
|
|
325
|
+
/** One relation's declared coordinates and generator flags, in declaration order. */
|
|
326
|
+
interface MemberCoords {
|
|
327
|
+
readonly relation: string
|
|
328
|
+
readonly fields: ReadonlyArray<{ readonly name: string; readonly generator: boolean }>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Reads every member's coordinates off the relation record, declaration order throughout. */
|
|
332
|
+
function memberCoords(relations: SchemaRelations): MemberCoords[] {
|
|
333
|
+
const out: MemberCoords[] = []
|
|
334
|
+
for (const [relationName, member] of Object.entries(relations)) {
|
|
335
|
+
if ("handles" in member.data) {
|
|
336
|
+
const fields = [
|
|
337
|
+
{ name: "id", generator: true },
|
|
338
|
+
...member.data.columns.map(function columnCoord(column) {
|
|
339
|
+
return { name: column.name, generator: false }
|
|
340
|
+
})
|
|
341
|
+
]
|
|
342
|
+
out.push({ relation: relationName, fields })
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
const fields = member.data.fields.map(function fieldCoord(declared) {
|
|
346
|
+
return { name: declared.name, generator: "fresh" in declared.field && declared.field.fresh === true }
|
|
347
|
+
})
|
|
348
|
+
out.push({ relation: relationName, fields })
|
|
349
|
+
}
|
|
350
|
+
return out
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** A plain union-find over coordinate strings, with per-root generator rosters. */
|
|
354
|
+
interface UnionFind {
|
|
355
|
+
find(coord: string): string
|
|
356
|
+
union(a: string, b: string): string
|
|
357
|
+
generatorsOf(root: string): readonly string[]
|
|
358
|
+
markGenerator(coord: string): void
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Builds the union-find. */
|
|
362
|
+
function makeUnionFind(): UnionFind {
|
|
363
|
+
const parent = new Map<string, string>()
|
|
364
|
+
const generators = new Map<string, string[]>()
|
|
365
|
+
function find(coord: string): string {
|
|
366
|
+
const at = parent.get(coord)
|
|
367
|
+
if (at === undefined) {
|
|
368
|
+
parent.set(coord, coord)
|
|
369
|
+
return coord
|
|
370
|
+
}
|
|
371
|
+
if (at === coord) {
|
|
372
|
+
return coord
|
|
373
|
+
}
|
|
374
|
+
const root = find(at)
|
|
375
|
+
parent.set(coord, root)
|
|
376
|
+
return root
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
find,
|
|
380
|
+
union(a, b) {
|
|
381
|
+
const rootA = find(a)
|
|
382
|
+
const rootB = find(b)
|
|
383
|
+
if (rootA === rootB) {
|
|
384
|
+
return rootA
|
|
385
|
+
}
|
|
386
|
+
parent.set(rootB, rootA)
|
|
387
|
+
const merged = [...(generators.get(rootA) ?? []), ...(generators.get(rootB) ?? [])]
|
|
388
|
+
generators.delete(rootB)
|
|
389
|
+
if (merged.length > 0) {
|
|
390
|
+
generators.set(rootA, merged)
|
|
391
|
+
}
|
|
392
|
+
return rootA
|
|
393
|
+
},
|
|
394
|
+
generatorsOf(root) {
|
|
395
|
+
return generators.get(root) ?? []
|
|
396
|
+
},
|
|
397
|
+
markGenerator(coord) {
|
|
398
|
+
const root = find(coord)
|
|
399
|
+
generators.set(root, [...(generators.get(root) ?? []), coord])
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The paired faces of one statement, or undefined for a key (an FD pairs nothing). */
|
|
405
|
+
function statementFaces(statement: Statement): readonly [FaceData, FaceData] | undefined {
|
|
406
|
+
const data = statement.data
|
|
407
|
+
if (data.kind === "key") {
|
|
408
|
+
return undefined
|
|
409
|
+
}
|
|
410
|
+
return [data.source, data.target]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Computes the class map — the runtime twin of {@link ClassesOf}, the SAME
|
|
415
|
+
* computation as a plain union-find: every paired face's positionwise slot
|
|
416
|
+
* pairs union their coordinates; a fresh field or closed id is a generator
|
|
417
|
+
* naming its class; a generator-less class is named by its least member in
|
|
418
|
+
* relation-declaration × field-declaration order; a slot in no law is bare
|
|
419
|
+
* (`undefined`). The one-generator wall throws HERE, naming the two
|
|
420
|
+
* coordinates and the statement that unified them — the same content the
|
|
421
|
+
* compile-tier {@link ClassWall} carries. The returned map is frozen, own
|
|
422
|
+
* properties throughout (arbitrary field names ride own-property
|
|
423
|
+
* definition, never the object protocol).
|
|
424
|
+
*/
|
|
425
|
+
function computeClasses(name: string, relations: SchemaRelations, statements: readonly Statement[]): SchemaClasses {
|
|
426
|
+
const members = memberCoords(relations)
|
|
427
|
+
const uf = makeUnionFind()
|
|
428
|
+
const paired = new Set<string>()
|
|
429
|
+
const generatorSet = new Set<string>()
|
|
430
|
+
for (const member of members) {
|
|
431
|
+
for (const field of member.fields) {
|
|
432
|
+
const coord = `${member.relation}.${field.name}`
|
|
433
|
+
uf.find(coord)
|
|
434
|
+
if (field.generator) {
|
|
435
|
+
generatorSet.add(coord)
|
|
436
|
+
uf.markGenerator(coord)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const statement of statements) {
|
|
441
|
+
const faces = statementFaces(statement)
|
|
442
|
+
if (faces === undefined) {
|
|
443
|
+
continue
|
|
444
|
+
}
|
|
445
|
+
const [source, target] = faces
|
|
446
|
+
source.projection.forEach(function unionSlot(fieldName, position) {
|
|
447
|
+
const targetField = target.projection[position]
|
|
448
|
+
if (targetField === undefined) {
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
const coordA = `${source.owner.name}.${fieldName}`
|
|
452
|
+
const coordB = `${target.owner.name}.${targetField}`
|
|
453
|
+
paired.add(coordA)
|
|
454
|
+
paired.add(coordB)
|
|
455
|
+
const root = uf.union(coordA, coordB)
|
|
456
|
+
const gens = uf.generatorsOf(root)
|
|
457
|
+
if (gens.length > 1) {
|
|
458
|
+
throw errors.new(
|
|
459
|
+
`schema ${name}: the statements unify two generators into one class — ${gens.join(" and ")} (two mints cannot share a carrier) — ${renderStatement(statement)}`
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
const names = new Map<string, string>()
|
|
465
|
+
for (const member of members) {
|
|
466
|
+
for (const field of member.fields) {
|
|
467
|
+
const coord = `${member.relation}.${field.name}`
|
|
468
|
+
const root = uf.find(coord)
|
|
469
|
+
if (!names.has(root)) {
|
|
470
|
+
const gens = uf.generatorsOf(root)
|
|
471
|
+
names.set(root, gens[0] ?? coord)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const classes: Record<string, RelationClasses> = {}
|
|
476
|
+
for (const member of members) {
|
|
477
|
+
const record: Record<string, string | undefined> = {}
|
|
478
|
+
for (const field of member.fields) {
|
|
479
|
+
const coord = `${member.relation}.${field.name}`
|
|
480
|
+
const classed = paired.has(coord) || generatorSet.has(coord)
|
|
481
|
+
Object.defineProperty(record, field.name, {
|
|
482
|
+
value: classed ? names.get(uf.find(coord)) : undefined,
|
|
483
|
+
enumerable: true
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
Object.freeze(record)
|
|
487
|
+
Object.defineProperty(classes, member.relation, { value: record, enumerable: true })
|
|
488
|
+
}
|
|
489
|
+
return Object.freeze(classes)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The trusted seam of the class-map mint (the `refsComplete` pattern): the
|
|
494
|
+
* checkable facts — one own record per declared relation, one own entry
|
|
495
|
+
* per declared field (the closed sealed shape's `id` included), everything
|
|
496
|
+
* frozen — are verified before the runtime map is admitted at the computed
|
|
497
|
+
* {@link ClassesOf} type. The NAME agreement of the two tiers is pinned by
|
|
498
|
+
* the generated fixture probes (the runtime/type diff check).
|
|
499
|
+
*/
|
|
500
|
+
function classesComplete<Classes extends SchemaClasses>(
|
|
501
|
+
classes: SchemaClasses,
|
|
502
|
+
relations: SchemaRelations
|
|
503
|
+
): classes is Classes {
|
|
504
|
+
if (!Object.isFrozen(classes)) {
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
return memberCoords(relations).every(function relationMinted(member) {
|
|
508
|
+
const record = classes[member.relation]
|
|
509
|
+
if (record === undefined || !Object.isFrozen(record)) {
|
|
510
|
+
return false
|
|
511
|
+
}
|
|
512
|
+
return member.fields.every(function fieldMinted(field) {
|
|
513
|
+
return Object.hasOwn(record, field.name)
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export type { ClassesOf, ClassLookup, ClassRecordOf, ClassWall, LawfulStatements, RelationClasses, SchemaClasses }
|
|
519
|
+
export { classesComplete, computeClasses }
|
package/src/lower.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { AnyClosed } from "#closed.ts"
|
|
12
12
|
import type { FaceData } from "#face.ts"
|
|
13
13
|
import type { AnyField } from "#fields.ts"
|
|
14
|
+
import type { RelationClasses } from "#law.ts"
|
|
14
15
|
import type { AnyRelation } from "#relation.ts"
|
|
15
16
|
import type { AnySchema, SchemaRelation } from "#schema.ts"
|
|
16
17
|
import type {
|
|
@@ -57,17 +58,19 @@ function valueTypeOf(field: AnyField): ValueTypeSpec {
|
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* Lowers one field descriptor to its {@link FieldSpec}: the structural
|
|
60
|
-
* type, the
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* type, the structural fresh mark (`fresh` is the literal `true` exactly
|
|
62
|
+
* on a fresh-marked u64 — on an unmarked one the property holds the marked
|
|
63
|
+
* descriptor, never `true`), and the wire's `newtype` — the COMPUTED class
|
|
64
|
+
* name `schema()` derived from the statement list (the laws type the
|
|
65
|
+
* columns), `undefined` on a bare field. The engine reads newtypes for
|
|
66
|
+
* handle resolution and the coherence check only and DROPS them at
|
|
67
|
+
* descriptor lowering — class names are never fingerprinted.
|
|
65
68
|
*/
|
|
66
|
-
function lowerField(name: string, field: AnyField): FieldSpec {
|
|
69
|
+
function lowerField(name: string, field: AnyField, newtype: string | undefined): FieldSpec {
|
|
67
70
|
return {
|
|
68
71
|
name,
|
|
69
72
|
valueType: valueTypeOf(field),
|
|
70
|
-
newtype
|
|
73
|
+
newtype,
|
|
71
74
|
fresh: "fresh" in field && field.fresh === true
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -113,11 +116,14 @@ function lowerStatement(statement: Statement): StatementSpec {
|
|
|
113
116
|
|
|
114
117
|
/**
|
|
115
118
|
* Lowers one ordinary relation to its `RelationSpec` fragment: fields in
|
|
116
|
-
* declaration order,
|
|
119
|
+
* declaration order, each carrying its law-computed class name as the
|
|
120
|
+
* `newtype` (`classes` — the schema's class record for this relation;
|
|
121
|
+
* bare fields carry `undefined`), `extension: undefined` (the option is
|
|
122
|
+
* the kind).
|
|
117
123
|
*/
|
|
118
|
-
function lowerRelation(relation: AnyRelation): RelationSpec {
|
|
124
|
+
function lowerRelation(relation: AnyRelation, classes: RelationClasses): RelationSpec {
|
|
119
125
|
const fields: FieldSpec[] = relation.data.fields.map(function lowerDeclared(declared) {
|
|
120
|
-
return lowerField(declared.name, declared.field)
|
|
126
|
+
return lowerField(declared.name, declared.field, classes[declared.name])
|
|
121
127
|
})
|
|
122
128
|
return { name: relation.name, newtype: undefined, fields, extension: undefined }
|
|
123
129
|
}
|
|
@@ -125,34 +131,41 @@ function lowerRelation(relation: AnyRelation): RelationSpec {
|
|
|
125
131
|
/**
|
|
126
132
|
* Lowers one closed relation to its `RelationSpec` fragment: declared
|
|
127
133
|
* intrinsic columns only (the engine materializes the synthetic `id`),
|
|
128
|
-
* the
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
+
* the handle newtype — the COMPUTED class name of the id's generator
|
|
135
|
+
* class (`"Kind.id"`, always present: a closed id is a generator), which
|
|
136
|
+
* every referencing field shares by law (how the engine resolves a handle
|
|
137
|
+
* literal back to its roster) — and the ground axioms in declaration
|
|
138
|
+
* order (row id = index); the literals were already lowered at `closed()`
|
|
139
|
+
* construction.
|
|
134
140
|
*/
|
|
135
|
-
function lowerClosed(member: AnyClosed): RelationSpec {
|
|
141
|
+
function lowerClosed(member: AnyClosed, classes: RelationClasses): RelationSpec {
|
|
136
142
|
const fields: FieldSpec[] = member.data.columns.map(function lowerColumn(column) {
|
|
137
|
-
return lowerField(column.name, column.field)
|
|
143
|
+
return lowerField(column.name, column.field, classes[column.name])
|
|
138
144
|
})
|
|
139
145
|
const extension = member.data.rows.map(function lowerRow(row) {
|
|
140
146
|
return { handle: row.handle, values: row.values }
|
|
141
147
|
})
|
|
142
|
-
return { name: member.name, newtype:
|
|
148
|
+
return { name: member.name, newtype: classes.id, fields, extension }
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
/** The frozen empty class record a relation outside the schema's map lowers under (nothing classed). */
|
|
152
|
+
const noClasses: RelationClasses = Object.freeze({})
|
|
153
|
+
|
|
145
154
|
/**
|
|
146
155
|
* Lowers a whole theory to the `SchemaSpec` the bridge takes: relations in
|
|
147
156
|
* record declaration order, DECLARED statements only in written order (the
|
|
148
|
-
* engine materializes the fresh-implied and closed auto-keys itself)
|
|
157
|
+
* engine materializes the fresh-implied and closed auto-keys itself), and
|
|
158
|
+
* every field's `newtype` slot fed from the schema's law-computed class
|
|
159
|
+
* map — the ONE domain authority (fingerprint-neutral: the engine drops
|
|
160
|
+
* newtypes at descriptor lowering).
|
|
149
161
|
*/
|
|
150
162
|
function lower(theory: AnySchema): SchemaSpec {
|
|
151
|
-
const relations: RelationSpec[] = Object.
|
|
163
|
+
const relations: RelationSpec[] = Object.entries(theory.relations).map(function lowerMember([name, member]) {
|
|
164
|
+
const classes = theory.classes[name] ?? noClasses
|
|
152
165
|
if (isClosedMember(member)) {
|
|
153
|
-
return lowerClosed(member)
|
|
166
|
+
return lowerClosed(member, classes)
|
|
154
167
|
}
|
|
155
|
-
return lowerRelation(member)
|
|
168
|
+
return lowerRelation(member, classes)
|
|
156
169
|
})
|
|
157
170
|
return { relations, statements: theory.statements.map(lowerStatement) }
|
|
158
171
|
}
|
package/src/native.ts
CHANGED
|
@@ -244,13 +244,17 @@ interface Violation {
|
|
|
244
244
|
* `dbCreate`/`dbOpen`'s domain outcome. `schemaError` covers both spec
|
|
245
245
|
* resolution (unresolvable names, banned spellings — every issue in one
|
|
246
246
|
* message) and schema validation at the declaration boundary;
|
|
247
|
-
* `
|
|
247
|
+
* `newtypeMismatch` is the coherence wall's own kind — a spec whose
|
|
248
|
+
* statement pairs faces with disagreeing newtype labels (the engine twin
|
|
249
|
+
* of the schema-level class wall; unreachable through the typed builder,
|
|
250
|
+
* which computes every label from the laws, so only a raw spec can reach
|
|
251
|
+
* it); `fingerprintMismatch` is `dbOpen`'s stored-theory refusal.
|
|
248
252
|
*/
|
|
249
253
|
type DbOpenResult =
|
|
250
254
|
| { readonly ok: true; readonly db: DbHandle }
|
|
251
255
|
| {
|
|
252
256
|
readonly ok: false
|
|
253
|
-
readonly kind: "schemaError" | "fingerprintMismatch"
|
|
257
|
+
readonly kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch"
|
|
254
258
|
readonly message: string
|
|
255
259
|
}
|
|
256
260
|
|