@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/spec.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lowered wire shapes — a 1:1 TypeScript mirror of bumbledb's
|
|
3
|
+
* `SchemaSpec` bindings contract (PRD-01;
|
|
4
|
+
* `bumbledb/crates/bumbledb/src/schema/spec.rs`): a schema as named plain
|
|
5
|
+
* data, relations and dependency statements each in declaration order (the
|
|
6
|
+
* declaration-order law that mints every id). The SDK's `lower()` emits
|
|
7
|
+
* these values; the napi bridge (PRD-04) marshals them into the Rust
|
|
8
|
+
* `SchemaSpec` verbatim, and the engine's own judge (`SchemaSpec::descriptor`
|
|
9
|
+
* name resolution + `SchemaDescriptor::validate` at `Db.create`/`Db.open`)
|
|
10
|
+
* stays the single semantic authority — the SDK lowers, it never re-judges.
|
|
11
|
+
*
|
|
12
|
+
* Every u64 crosses as `bigint`, never `number` (PRD-04's marshaling law: no
|
|
13
|
+
* 53-bit hazards, no branch). Object keys are always written in one fixed
|
|
14
|
+
* literal order per shape, so serialization of a lowered schema is
|
|
15
|
+
* deterministic (byte-stable) by construction.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A structural value type — the one type vocabulary of the `schema!` field
|
|
20
|
+
* grammar (`ValueType` in Rust): `bool`, `u64`, `i64`, `str` (`string`
|
|
21
|
+
* here), `bytes<N>` (`fixedBytes`), and the interval family (`width:
|
|
22
|
+
* undefined` is the general 16-byte encoding with rays representable;
|
|
23
|
+
* `width: w` is the fixed-width `interval<E, w>` whose encoding stores only
|
|
24
|
+
* the start).
|
|
25
|
+
*/
|
|
26
|
+
type ValueTypeSpec =
|
|
27
|
+
| { readonly kind: "bool" }
|
|
28
|
+
| { readonly kind: "u64" }
|
|
29
|
+
| { readonly kind: "i64" }
|
|
30
|
+
| { readonly kind: "string" }
|
|
31
|
+
| { readonly kind: "fixedBytes"; readonly len: number }
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: "interval"
|
|
34
|
+
readonly element: "u64" | "i64"
|
|
35
|
+
readonly width: bigint | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One plain engine value as carried by a lowered literal — the mirror of
|
|
40
|
+
* `bumbledb::Value` restricted to the schema-literal vocabulary (Allen masks
|
|
41
|
+
* are query-side values, never schema literals). Intervals are half-open
|
|
42
|
+
* `[start, end)`.
|
|
43
|
+
*/
|
|
44
|
+
type ValueSpec =
|
|
45
|
+
| { readonly kind: "bool"; readonly value: boolean }
|
|
46
|
+
| { readonly kind: "u64"; readonly value: bigint }
|
|
47
|
+
| { readonly kind: "i64"; readonly value: bigint }
|
|
48
|
+
| { readonly kind: "string"; readonly value: string }
|
|
49
|
+
| { readonly kind: "fixedBytes"; readonly value: Uint8Array }
|
|
50
|
+
| { readonly kind: "intervalU64"; readonly start: bigint; readonly end: bigint }
|
|
51
|
+
| { readonly kind: "intervalI64"; readonly start: bigint; readonly end: bigint }
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One literal as spelled: a plain value, or a closed relation's handle by
|
|
55
|
+
* name (the `| status == Frozen` spelling) — resolved by the engine through
|
|
56
|
+
* the selected field's newtype to the handle's declaration-order row id,
|
|
57
|
+
* exactly as the macro resolves it at expansion.
|
|
58
|
+
*/
|
|
59
|
+
type LiteralSpec =
|
|
60
|
+
| { readonly kind: "value"; readonly value: ValueSpec }
|
|
61
|
+
| { readonly kind: "handle"; readonly handle: string }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One σ binding's right side: a single literal or a literal set (read
|
|
65
|
+
* disjunctively). The SDK's selection constructors make the degenerate sets
|
|
66
|
+
* unwritable (`oneOf` demands two leading literals), so a lowered `many`
|
|
67
|
+
* always carries ≥ 2 literals.
|
|
68
|
+
*/
|
|
69
|
+
type LiteralSetSpec =
|
|
70
|
+
| { readonly kind: "one"; readonly literal: LiteralSpec }
|
|
71
|
+
| { readonly kind: "many"; readonly literals: readonly LiteralSpec[] }
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One side of a containment or window: `R(fields… | field == literal…)`,
|
|
75
|
+
* all names. `projection` is π in the statement's written order (positional
|
|
76
|
+
* pairing with the other side); `selection` is σ as (field, literal-or-set)
|
|
77
|
+
* pairs, read conjunctively.
|
|
78
|
+
*/
|
|
79
|
+
interface SideSpec {
|
|
80
|
+
readonly relation: string
|
|
81
|
+
readonly projection: readonly string[]
|
|
82
|
+
readonly selection: ReadonlyArray<readonly [string, LiteralSetSpec]>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A cardinality window's bounds — the canonical-utterance law's surviving
|
|
87
|
+
* spellings only, since the SDK's `Count` constructors make every banned
|
|
88
|
+
* spelling unwritable or a construction error: `exact` is `{n}` (`{0}` the
|
|
89
|
+
* exclusion), `range` is `{lo..hi}` with lo < hi, `floor` is `{lo..*}` with
|
|
90
|
+
* lo ≥ 2.
|
|
91
|
+
*/
|
|
92
|
+
type WindowSpec =
|
|
93
|
+
| { readonly kind: "exact"; readonly n: bigint }
|
|
94
|
+
| { readonly kind: "range"; readonly lo: bigint; readonly hi: bigint }
|
|
95
|
+
| { readonly kind: "floor"; readonly lo: bigint }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One field: name, structural type, host newtype name (carried for handle
|
|
99
|
+
* resolution only — dropped by the engine at descriptor lowering), and the
|
|
100
|
+
* `fresh` mint mark.
|
|
101
|
+
*/
|
|
102
|
+
interface FieldSpec {
|
|
103
|
+
readonly name: string
|
|
104
|
+
readonly valueType: ValueTypeSpec
|
|
105
|
+
readonly newtype: string | undefined
|
|
106
|
+
readonly fresh: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One ground axiom of a closed relation: the handle plus one literal per
|
|
111
|
+
* declared intrinsic column, in field-declaration order (row id = index).
|
|
112
|
+
*/
|
|
113
|
+
interface RowSpec {
|
|
114
|
+
readonly handle: string
|
|
115
|
+
readonly values: readonly LiteralSpec[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One relation. `extension: rows` declares it closed (the option is the
|
|
120
|
+
* kind); a closed relation's `fields` are its declared intrinsic columns
|
|
121
|
+
* only — the synthetic (`id`, u64) handle field is materialized by the
|
|
122
|
+
* engine's schema validation. `newtype` is the handle newtype of a closed
|
|
123
|
+
* relation (the SDK uses the closed relation's own name), undefined on an
|
|
124
|
+
* ordinary one.
|
|
125
|
+
*/
|
|
126
|
+
interface RelationSpec {
|
|
127
|
+
readonly name: string
|
|
128
|
+
readonly newtype: string | undefined
|
|
129
|
+
readonly fields: readonly FieldSpec[]
|
|
130
|
+
readonly extension: readonly RowSpec[] | undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One dependency statement, tagged by form. `==` is not a variant: a
|
|
135
|
+
* bidirectional containment is `containment` with `bidirectional: true`,
|
|
136
|
+
* lowered by the engine to the two adjacent containments (`source <=
|
|
137
|
+
* target` first). `cardinality` is B-family, target-left: the target is the
|
|
138
|
+
* per-group parent, the source is counted.
|
|
139
|
+
*/
|
|
140
|
+
type StatementSpec =
|
|
141
|
+
| { readonly kind: "fd"; readonly relation: string; readonly projection: readonly string[] }
|
|
142
|
+
| {
|
|
143
|
+
readonly kind: "containment"
|
|
144
|
+
readonly source: SideSpec
|
|
145
|
+
readonly target: SideSpec
|
|
146
|
+
readonly bidirectional: boolean
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
readonly kind: "cardinality"
|
|
150
|
+
readonly target: SideSpec
|
|
151
|
+
readonly window: WindowSpec
|
|
152
|
+
readonly source: SideSpec
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The whole theory as named plain data — what `lower()` produces and what
|
|
157
|
+
* the bridge's `dbCreate`/`dbOpen` take. Both lists are in declaration
|
|
158
|
+
* order. Only DECLARED statements appear: the engine materializes the
|
|
159
|
+
* fresh-implied and closed auto-keys itself
|
|
160
|
+
* (`SchemaDescriptor::materialized_statements` — fresh keys first, closed
|
|
161
|
+
* auto-keys second, declared statements last), so re-stating them here
|
|
162
|
+
* would double them and change the fingerprint.
|
|
163
|
+
*/
|
|
164
|
+
interface SchemaSpec {
|
|
165
|
+
readonly relations: readonly RelationSpec[]
|
|
166
|
+
readonly statements: readonly StatementSpec[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The characters Rust's `char::escape_debug` (the engine renderer's string
|
|
171
|
+
* formatter, `schema/render.rs` `literal`) escapes as `\u{…}`: everything
|
|
172
|
+
* non-printable per rustc's generated tables — the C categories (Cc, Cf,
|
|
173
|
+
* Cs, Co, Cn) and the Z separators (Zs, Zl, Zp) except U+0020 itself
|
|
174
|
+
* (`library/core/src/unicode/printable.py`).
|
|
175
|
+
*/
|
|
176
|
+
const NON_PRINTABLE = /[\p{C}\p{Z}]/u
|
|
177
|
+
|
|
178
|
+
/** Grapheme-extending characters, which `char::escape_debug` always escapes even when printable. */
|
|
179
|
+
const GRAPHEME_EXTEND = /\p{Grapheme_Extend}/u
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One char exactly as Rust's `char::escape_debug` spells it (the engine
|
|
183
|
+
* renders strings char by char through it): `\0`, `\t`, `\r`, `\n`,
|
|
184
|
+
* backslash-escaped `\\`/`\'`/`\"`, `\u{hex}` (lowercase, unpadded) for
|
|
185
|
+
* grapheme-extending and non-printable chars, the char itself otherwise.
|
|
186
|
+
*/
|
|
187
|
+
function escapeDebugChar(ch: string): string {
|
|
188
|
+
if (ch === "\0") {
|
|
189
|
+
return "\\0"
|
|
190
|
+
}
|
|
191
|
+
if (ch === "\t") {
|
|
192
|
+
return "\\t"
|
|
193
|
+
}
|
|
194
|
+
if (ch === "\r") {
|
|
195
|
+
return "\\r"
|
|
196
|
+
}
|
|
197
|
+
if (ch === "\n") {
|
|
198
|
+
return "\\n"
|
|
199
|
+
}
|
|
200
|
+
if (ch === "\\" || ch === "'" || ch === '"') {
|
|
201
|
+
return `\\${ch}`
|
|
202
|
+
}
|
|
203
|
+
if (GRAPHEME_EXTEND.test(ch) || (ch !== " " && NON_PRINTABLE.test(ch))) {
|
|
204
|
+
const codePoint = ch.codePointAt(0)
|
|
205
|
+
if (codePoint === undefined) {
|
|
206
|
+
return ch
|
|
207
|
+
}
|
|
208
|
+
return `\\u{${codePoint.toString(16)}}`
|
|
209
|
+
}
|
|
210
|
+
return ch
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One byte exactly as Rust's `u8::escape_ascii` spells it (the engine
|
|
215
|
+
* renders `bytes<N>` literals byte by byte through it): `\t`, `\r`, `\n`,
|
|
216
|
+
* `\\`, `\'`, `\"` as two-char escapes, printable ASCII (0x20–0x7e)
|
|
217
|
+
* verbatim, everything else `\xNN` lowercase.
|
|
218
|
+
*/
|
|
219
|
+
function escapeAsciiByte(byte: number): string {
|
|
220
|
+
if (byte === 0x09) {
|
|
221
|
+
return "\\t"
|
|
222
|
+
}
|
|
223
|
+
if (byte === 0x0d) {
|
|
224
|
+
return "\\r"
|
|
225
|
+
}
|
|
226
|
+
if (byte === 0x0a) {
|
|
227
|
+
return "\\n"
|
|
228
|
+
}
|
|
229
|
+
if (byte === 0x5c) {
|
|
230
|
+
return "\\\\"
|
|
231
|
+
}
|
|
232
|
+
if (byte === 0x27) {
|
|
233
|
+
return "\\'"
|
|
234
|
+
}
|
|
235
|
+
if (byte === 0x22) {
|
|
236
|
+
return '\\"'
|
|
237
|
+
}
|
|
238
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
239
|
+
return String.fromCharCode(byte)
|
|
240
|
+
}
|
|
241
|
+
return `\\x${byte.toString(16).padStart(2, "0")}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Renders one lowered literal in the exact macro spelling the engine's own
|
|
246
|
+
* renderer uses (`schema/render.rs` `literal`): handles bare by name,
|
|
247
|
+
* integers as digits, `true`/`false`, intervals as `start..end`, strings
|
|
248
|
+
* char-escaped through the `char::escape_debug` mirror, bytes as `b"…"`
|
|
249
|
+
* byte-escaped through the `u8::escape_ascii` mirror — byte-for-byte the
|
|
250
|
+
* engine's violation canonicals, so TS-side construction errors and
|
|
251
|
+
* engine-side violations read identically and `renderStatement` equals the
|
|
252
|
+
* violation's `canonical`.
|
|
253
|
+
*/
|
|
254
|
+
function renderLiteral(literal: LiteralSpec): string {
|
|
255
|
+
if (literal.kind === "handle") {
|
|
256
|
+
return literal.handle
|
|
257
|
+
}
|
|
258
|
+
const value = literal.value
|
|
259
|
+
switch (value.kind) {
|
|
260
|
+
case "bool":
|
|
261
|
+
return value.value ? "true" : "false"
|
|
262
|
+
case "u64":
|
|
263
|
+
case "i64":
|
|
264
|
+
return value.value.toString()
|
|
265
|
+
case "string": {
|
|
266
|
+
let out = '"'
|
|
267
|
+
for (const ch of value.value) {
|
|
268
|
+
out += escapeDebugChar(ch)
|
|
269
|
+
}
|
|
270
|
+
return `${out}"`
|
|
271
|
+
}
|
|
272
|
+
case "fixedBytes": {
|
|
273
|
+
let out = 'b"'
|
|
274
|
+
for (const byte of value.value) {
|
|
275
|
+
out += escapeAsciiByte(byte)
|
|
276
|
+
}
|
|
277
|
+
return `${out}"`
|
|
278
|
+
}
|
|
279
|
+
case "intervalU64":
|
|
280
|
+
case "intervalI64":
|
|
281
|
+
return `${value.start}..${value.end}`
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Renders one σ binding's right side: a bare literal, or a disjunctive
|
|
287
|
+
* literal set in braces (`{A, B}`).
|
|
288
|
+
*/
|
|
289
|
+
function renderLiteralSet(set: LiteralSetSpec): string {
|
|
290
|
+
if (set.kind === "one") {
|
|
291
|
+
return renderLiteral(set.literal)
|
|
292
|
+
}
|
|
293
|
+
return `{${set.literals.map(renderLiteral).join(", ")}}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Renders window bounds in their one canonical spelling: `{n}` exact
|
|
298
|
+
* (`{0}` the exclusion), `{lo..hi}`, `{lo..*}` — the spelling set the
|
|
299
|
+
* engine's renderer emits for sealed statements.
|
|
300
|
+
*/
|
|
301
|
+
function renderWindow(window: WindowSpec): string {
|
|
302
|
+
switch (window.kind) {
|
|
303
|
+
case "exact":
|
|
304
|
+
return `{${window.n}}`
|
|
305
|
+
case "range":
|
|
306
|
+
return `{${window.lo}..${window.hi}}`
|
|
307
|
+
case "floor":
|
|
308
|
+
return `{${window.lo}..*}`
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export type {
|
|
313
|
+
FieldSpec,
|
|
314
|
+
LiteralSetSpec,
|
|
315
|
+
LiteralSpec,
|
|
316
|
+
RelationSpec,
|
|
317
|
+
RowSpec,
|
|
318
|
+
SchemaSpec,
|
|
319
|
+
SideSpec,
|
|
320
|
+
StatementSpec,
|
|
321
|
+
ValueSpec,
|
|
322
|
+
ValueTypeSpec,
|
|
323
|
+
WindowSpec
|
|
324
|
+
}
|
|
325
|
+
export { renderLiteral, renderLiteralSet, renderWindow }
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency statements as typed values (`docs/architecture/30-dependencies.md`
|
|
3
|
+
* owns the semantics; `docs/architecture/70-api.md` the surface): the FD key
|
|
4
|
+
* form, conditional containment, the bidirectional `==` abbreviation, and
|
|
5
|
+
* the cardinality window. A statement value is opaque and inert — no
|
|
6
|
+
* methods, no fluent continuation: a fact about the theory, not a builder.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as errors from "@superbuilders/errors"
|
|
10
|
+
import { phantom } from "#brand.ts"
|
|
11
|
+
import type { Count } from "#count.ts"
|
|
12
|
+
import { type AnyFace, type FaceData, renderFace, type SameArity } from "#face.ts"
|
|
13
|
+
import type { AnyRelation, RelationFields } from "#relation.ts"
|
|
14
|
+
import { renderWindow, type WindowSpec } from "#spec.ts"
|
|
15
|
+
|
|
16
|
+
/** One statement's runtime description, tagged by form. */
|
|
17
|
+
type StatementData =
|
|
18
|
+
| { readonly kind: "key"; readonly owner: AnyRelation; readonly projection: readonly string[] }
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: "containment"
|
|
21
|
+
readonly source: FaceData
|
|
22
|
+
readonly target: FaceData
|
|
23
|
+
readonly bidirectional: boolean
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
readonly kind: "window"
|
|
27
|
+
readonly target: FaceData
|
|
28
|
+
readonly window: WindowSpec
|
|
29
|
+
readonly source: FaceData
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An opaque statement value — what `schema()` assembles into a theory. */
|
|
33
|
+
interface Statement {
|
|
34
|
+
readonly data: StatementData
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A `key()` statement as a TYPED value: the statement plus a phantom
|
|
39
|
+
* carrying its owner and projection tuple — what the key-statement-selected
|
|
40
|
+
* `get(relation, keyStatement, key)` overload types its key object by
|
|
41
|
+
* (`docs/architecture/70-api.md` § the freeze, the multi-key typed get).
|
|
42
|
+
* Structurally still a plain {@link Statement}; the phantom is never present
|
|
43
|
+
* at runtime.
|
|
44
|
+
*/
|
|
45
|
+
interface KeyStatement<R extends AnyRelation, Projection extends readonly string[]> extends Statement {
|
|
46
|
+
readonly [phantom]?: { readonly owner: R; readonly projection: Projection }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `R(X) -> R` — the FD key form, composite keys as tuples. No selection
|
|
51
|
+
* parameter exists (the FD-with-selection shape is unrepresentable, as in
|
|
52
|
+
* the grammar), and only ordinary relations are accepted: a closed
|
|
53
|
+
* relation's key `R(id) -> R` is materialized by the engine, so an
|
|
54
|
+
* explicit one would only ever be a duplicate. The projection tuple is
|
|
55
|
+
* carried in the returned value's type ({@link KeyStatement}), so keyed
|
|
56
|
+
* point reads through THIS statement are typed field-for-field.
|
|
57
|
+
*/
|
|
58
|
+
function key<
|
|
59
|
+
R extends AnyRelation,
|
|
60
|
+
const Projection extends readonly [keyof RelationFields<R> & string, ...(keyof RelationFields<R> & string)[]]
|
|
61
|
+
>(relation: R, fields: Projection): KeyStatement<R, Projection> {
|
|
62
|
+
if (!("fields" in relation.data)) {
|
|
63
|
+
throw errors.new(
|
|
64
|
+
`key(${relation.name}, ...): closedness already materializes ${relation.name}(id) -> ${relation.name} — an explicit key on a closed relation is rejected as a duplicate`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
const data: StatementData = Object.freeze({
|
|
68
|
+
kind: "key",
|
|
69
|
+
owner: relation,
|
|
70
|
+
projection: Object.freeze([...fields])
|
|
71
|
+
})
|
|
72
|
+
return Object.freeze({ data })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `A(X|φ) <= B(Y|ψ)` — conditional inclusion, source left. The target
|
|
77
|
+
* side must resolve a declared key of B — DELIBERATELY judged by the
|
|
78
|
+
* engine at `Db.create`/`Db.open` (`SchemaError`), never re-checked here.
|
|
79
|
+
* Arity mismatch between the two faces is a type error ({@link SameArity}).
|
|
80
|
+
*/
|
|
81
|
+
function contained<A extends AnyFace, B extends AnyFace>(source: A, target: B & SameArity<A, B>): Statement {
|
|
82
|
+
const data: StatementData = Object.freeze({
|
|
83
|
+
kind: "containment",
|
|
84
|
+
source: source.data,
|
|
85
|
+
target: target.data,
|
|
86
|
+
bidirectional: false
|
|
87
|
+
})
|
|
88
|
+
return Object.freeze({ data })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `A(X|φ) == B(Y|ψ)` — the bidirectional abbreviation, one utterance. It
|
|
93
|
+
* lowers to the two adjacent containments in the `A <= B` first order
|
|
94
|
+
* (macro parity) and renders as `==` once, in the written orientation.
|
|
95
|
+
*/
|
|
96
|
+
function mirrors<A extends AnyFace, B extends AnyFace>(source: A, target: B & SameArity<A, B>): Statement {
|
|
97
|
+
const data: StatementData = Object.freeze({
|
|
98
|
+
kind: "containment",
|
|
99
|
+
source: source.data,
|
|
100
|
+
target: target.data,
|
|
101
|
+
bidirectional: true
|
|
102
|
+
})
|
|
103
|
+
return Object.freeze({ data })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `B(Y|ψ) <={window} A(X|φ)` — the cardinality window. READ CAREFULLY: the
|
|
108
|
+
* LEFT face is the window's TARGET, the per-group parent (B-family,
|
|
109
|
+
* target-left — macro parity), and the RIGHT face is the counted source.
|
|
110
|
+
* `window(on(Holder, "id"), atMost(3n), on(Account, "holder"))` says: each
|
|
111
|
+
* Holder id groups at most three Account rows by holder.
|
|
112
|
+
*/
|
|
113
|
+
function window<B extends AnyFace, A extends AnyFace>(target: B, count: Count, source: A & SameArity<B, A>): Statement {
|
|
114
|
+
const data: StatementData = Object.freeze({
|
|
115
|
+
kind: "window",
|
|
116
|
+
target: target.data,
|
|
117
|
+
window: count.window,
|
|
118
|
+
source: source.data
|
|
119
|
+
})
|
|
120
|
+
return Object.freeze({ data })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Renders one statement in the CANONICAL macro spelling
|
|
125
|
+
* (`docs/architecture/70-api.md`; the engine's `schema/render.rs` emits the
|
|
126
|
+
* same shapes for violations) — `Account(id) -> Account`,
|
|
127
|
+
* `Account(holder) <= Holder(id)`,
|
|
128
|
+
* `Account(id | kind == Savings) == SavingsTerms(account)`,
|
|
129
|
+
* `Holder(id) <={0..3} Account(holder)` — so TS-side errors and
|
|
130
|
+
* engine-side diagnostics read identically. A renderer, never a parser:
|
|
131
|
+
* strings are output-only.
|
|
132
|
+
*/
|
|
133
|
+
function renderStatement(statement: Statement): string {
|
|
134
|
+
const data = statement.data
|
|
135
|
+
switch (data.kind) {
|
|
136
|
+
case "key":
|
|
137
|
+
return `${data.owner.name}(${data.projection.join(", ")}) -> ${data.owner.name}`
|
|
138
|
+
case "containment": {
|
|
139
|
+
const operator = data.bidirectional ? "==" : "<="
|
|
140
|
+
return `${renderFace(data.source)} ${operator} ${renderFace(data.target)}`
|
|
141
|
+
}
|
|
142
|
+
case "window":
|
|
143
|
+
return `${renderFace(data.target)} <=${renderWindow(data.window)} ${renderFace(data.source)}`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type { KeyStatement, Statement, StatementData }
|
|
148
|
+
export { contained, key, mirrors, renderStatement, window }
|