@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select entries and aggregates (PRD-08) — the head vocabulary, mirroring
|
|
3
|
+
* the IR's aggregate roster exactly (`bumbledb/crates/bumbledb/src/ir.rs`
|
|
4
|
+
* `AggOp`/`FindTerm`; `docs/architecture/20-query-ir.md` § aggregation):
|
|
5
|
+
* `count` (nullary), `countDistinct`, `sum`/`min`/`max` (over a u64/i64
|
|
6
|
+
* variable or the measure), `argmax`/`argmin` (arg-restriction: carried
|
|
7
|
+
* value + orderable key; a tie yields every attaining row), and `pack`
|
|
8
|
+
* (the coalescing fold — RELATION-SHAPED: one answer row per (group,
|
|
9
|
+
* maximal segment), the result position interval-typed). Grouping is
|
|
10
|
+
* implicit: the non-aggregate select entries are the group key; over empty
|
|
11
|
+
* input an all-aggregate select yields the EMPTY SET, never a zero row.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IntervalValue } from "#brand.ts"
|
|
15
|
+
import { phantom } from "#brand.ts"
|
|
16
|
+
import type { Duration } from "#query/atom.ts"
|
|
17
|
+
import type { AnyVar, Flatten, Var } from "#query/scope.ts"
|
|
18
|
+
import { term } from "#query/scope.ts"
|
|
19
|
+
|
|
20
|
+
/** The three folds the measure admits (and the plain-variable folds). */
|
|
21
|
+
type FoldOp = "sum" | "min" | "max"
|
|
22
|
+
|
|
23
|
+
/** One aggregate's runtime description. */
|
|
24
|
+
type AggregateData =
|
|
25
|
+
| { readonly op: "count" }
|
|
26
|
+
| { readonly op: "countDistinct"; readonly over: AnyVar }
|
|
27
|
+
| { readonly op: "fold"; readonly fold: FoldOp; readonly over: AnyVar | Duration }
|
|
28
|
+
| {
|
|
29
|
+
readonly op: "arg"
|
|
30
|
+
readonly direction: "argMax" | "argMin"
|
|
31
|
+
readonly key: AnyVar
|
|
32
|
+
readonly over: AnyVar
|
|
33
|
+
}
|
|
34
|
+
| { readonly op: "pack"; readonly over: AnyVar }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One aggregate select value; the phantom carries the answer column's
|
|
38
|
+
* host type (`count`/`countDistinct` → `bigint` whatever they counted;
|
|
39
|
+
* folds carry their input's type; the Arg forms the carried payload's
|
|
40
|
+
* type; `pack` its interval type).
|
|
41
|
+
*/
|
|
42
|
+
interface Aggregate<R> {
|
|
43
|
+
readonly aggregate: AggregateData
|
|
44
|
+
readonly [phantom]?: R
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What a select record's values may be: a projection, a measure, or an aggregate. */
|
|
48
|
+
type SelectEntryInput = AnyVar | Duration | Aggregate<unknown>
|
|
49
|
+
|
|
50
|
+
/** The select record: answer column name to entry, written order = column order. */
|
|
51
|
+
type SelectShape = Readonly<Record<string, SelectEntryInput>>
|
|
52
|
+
|
|
53
|
+
/** One select entry's answer type. */
|
|
54
|
+
type SelectValue<T> = T extends { readonly aggregate: AggregateData; readonly [phantom]?: infer R }
|
|
55
|
+
? Exclude<R, undefined>
|
|
56
|
+
: T extends { readonly measure: AnyVar }
|
|
57
|
+
? bigint
|
|
58
|
+
: T extends { readonly [term]: "var"; readonly [phantom]?: infer V }
|
|
59
|
+
? Exclude<V, undefined>
|
|
60
|
+
: never
|
|
61
|
+
|
|
62
|
+
/** The inferred answer-row object type of a select record. */
|
|
63
|
+
type RowOf<Sel extends SelectShape> = Flatten<{ [K in keyof Sel]: SelectValue<Sel[K]> }>
|
|
64
|
+
|
|
65
|
+
/** Nullary count: |the group's set of distinct full bindings|, `bigint`. */
|
|
66
|
+
function count(): Aggregate<bigint> {
|
|
67
|
+
return Object.freeze({ aggregate: Object.freeze({ op: "count" as const }) })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** |the distinct values of `over` across the group|, `bigint`; legal over every type. */
|
|
71
|
+
function countDistinct<V>(over: Var<V>): Aggregate<bigint> {
|
|
72
|
+
return Object.freeze({ aggregate: Object.freeze({ op: "countDistinct" as const, over }) })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Exact checked sum over a u64/i64 variable (wide accumulator, one
|
|
77
|
+
* finalize range check — overflow is the engine's typed runtime error,
|
|
78
|
+
* never a wrap), or over the measure (`sum(duration(v))`).
|
|
79
|
+
*/
|
|
80
|
+
function sum<V extends bigint>(over: Var<V>): Aggregate<V>
|
|
81
|
+
function sum(over: Duration): Aggregate<bigint>
|
|
82
|
+
function sum(over: Var<bigint> | Duration): Aggregate<bigint> {
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
aggregate: Object.freeze({ op: "fold" as const, fold: "sum" as const, over })
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Minimum over a u64/i64 variable or the measure (orderable types only). */
|
|
89
|
+
function min<V extends bigint>(over: Var<V>): Aggregate<V>
|
|
90
|
+
function min(over: Duration): Aggregate<bigint>
|
|
91
|
+
function min(over: Var<bigint> | Duration): Aggregate<bigint> {
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
aggregate: Object.freeze({ op: "fold" as const, fold: "min" as const, over })
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Maximum over a u64/i64 variable or the measure (orderable types only). */
|
|
98
|
+
function max<V extends bigint>(over: Var<V>): Aggregate<V>
|
|
99
|
+
function max(over: Duration): Aggregate<bigint>
|
|
100
|
+
function max(over: Var<bigint> | Duration): Aggregate<bigint> {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
aggregate: Object.freeze({ op: "fold" as const, fold: "max" as const, over })
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Arg-restriction toward the maximum of `key` (`ir::AggOp::ArgMax`): the
|
|
108
|
+
* group's binding set is restricted to the bindings attaining the extreme
|
|
109
|
+
* of the orderable key, and `value` is the carried payload — a tie yields
|
|
110
|
+
* every attaining row. All Arg entries of one query share one key and one
|
|
111
|
+
* direction; Arg and fold aggregates never mix (both the engine's typed
|
|
112
|
+
* rules).
|
|
113
|
+
*/
|
|
114
|
+
function argmax<K extends bigint, V>(key: Var<K>, value: Var<V>): Aggregate<V> {
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
aggregate: Object.freeze({ op: "arg" as const, direction: "argMax" as const, key, over: value })
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Arg-restriction toward the minimum of `key`; rules as {@link argmax}. */
|
|
121
|
+
function argmin<K extends bigint, V>(key: Var<K>, value: Var<V>): Aggregate<V> {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
aggregate: Object.freeze({ op: "arg" as const, direction: "argMin" as const, key, over: value })
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The coalescing fold (Snodgrass coalesce, `ir::AggOp::Pack`): per group,
|
|
129
|
+
* the maximal disjoint half-open segments of the union of the group's
|
|
130
|
+
* interval point sets — RELATION-SHAPED, one answer row per (group,
|
|
131
|
+
* maximal segment), the result position carrying one interval of the
|
|
132
|
+
* input's element type. At most one `pack` per select, never beside a
|
|
133
|
+
* fold or an Arg entry (the engine's typed rules).
|
|
134
|
+
*/
|
|
135
|
+
function pack<IV extends IntervalValue>(over: Var<IV>): Aggregate<IV> {
|
|
136
|
+
return Object.freeze({ aggregate: Object.freeze({ op: "pack" as const, over }) })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type { Aggregate, AggregateData, FoldOp, RowOf, SelectEntryInput, SelectShape, SelectValue }
|
|
140
|
+
export { argmax, argmin, count, countDistinct, max, min, pack, sum }
|
package/src/relation.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `relation()` — the ordinary-relation half of the theory's signature. A
|
|
3
|
+
* relation value is a frozen plain object carrying its name, its ordered
|
|
4
|
+
* field metadata (declaration order = ordinal ids, the macro's law), typed
|
|
5
|
+
* field references (`R.fields.holder`), and — since selections are the
|
|
6
|
+
* relation's own vocabulary — `where()`, which resolves a selection into
|
|
7
|
+
* lowered bindings eagerly (handles re-verified against their roster at
|
|
8
|
+
* construction). `Fact<>`/`InsertFact<>` are the inferred row object
|
|
9
|
+
* types: fresh fields are optional on insert input (omit-to-mint) and
|
|
10
|
+
* present on read (resupply-to-preserve-identity), typed exactly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as errors from "@superbuilders/errors"
|
|
14
|
+
import { phantom } from "#brand.ts"
|
|
15
|
+
import type { OneOf } from "#face.ts"
|
|
16
|
+
import { type AnyField, assertDeclarationOrderKey, type FieldData, type FieldValue, literalOf } from "#fields.ts"
|
|
17
|
+
import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
|
|
18
|
+
|
|
19
|
+
/** Flattens an intersection into one displayed object type (hover legibility). */
|
|
20
|
+
type Flatten<T> = { [K in keyof T]: T[K] }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The one nominal step of `relation()`: the reference record is built by
|
|
24
|
+
* iterating the declared fields, and this guard verifies the checkable
|
|
25
|
+
* facts — one reference per declared field, each carrying its own name —
|
|
26
|
+
* before the record is admitted as the typed {@link FieldRefs}. The
|
|
27
|
+
* phantom halves (brands) are carried by construction; this is the
|
|
28
|
+
* module's single trusted seam, the macro-emission analog.
|
|
29
|
+
*/
|
|
30
|
+
function refsComplete<RName extends string, Fields extends FieldsShape>(
|
|
31
|
+
refs: Record<string, unknown>,
|
|
32
|
+
fields: Fields
|
|
33
|
+
): refs is FieldRefs<RName, Fields> {
|
|
34
|
+
return Object.keys(fields).every(function hasRef(fieldName) {
|
|
35
|
+
const ref = refs[fieldName]
|
|
36
|
+
return typeof ref === "object" && ref !== null && "field" in ref && ref.field === fieldName
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves one selection entry to its lowered literal set: an `oneOf`
|
|
42
|
+
* value (detected by its `literals` tuple — no field value is ever an
|
|
43
|
+
* object carrying `literals`) becomes a disjunctive set (≥ 2 by the
|
|
44
|
+
* `oneOf` signature — the one-element set is unwritable); anything else is
|
|
45
|
+
* the bare literal.
|
|
46
|
+
*/
|
|
47
|
+
function resolveEntry(field: FieldData, entry: unknown): LiteralSetSpec {
|
|
48
|
+
if (typeof entry === "object" && entry !== null && "literals" in entry && Array.isArray(entry.literals)) {
|
|
49
|
+
const literals: LiteralSpec[] = entry.literals.map(function lowerSetLiteral(literal: unknown) {
|
|
50
|
+
return Object.freeze(literalOf(field, literal))
|
|
51
|
+
})
|
|
52
|
+
return Object.freeze({ kind: "many", literals: Object.freeze(literals) })
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({ kind: "one", literal: Object.freeze(literalOf(field, entry)) })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves a whole `where()` selection against the declared fields, in the
|
|
59
|
+
* selection's written order (macro parity: σ is spelled, not sorted). An
|
|
60
|
+
* empty selection is the bare relation respelled and rejected (the
|
|
61
|
+
* canonical-utterance law).
|
|
62
|
+
*/
|
|
63
|
+
function resolveSelection(
|
|
64
|
+
name: string,
|
|
65
|
+
ordered: readonly RelationField[],
|
|
66
|
+
entries: ReadonlyArray<readonly [string, unknown]>
|
|
67
|
+
): readonly SelectionBinding[] {
|
|
68
|
+
const bindings: SelectionBinding[] = []
|
|
69
|
+
for (const [fieldName, entry] of entries) {
|
|
70
|
+
if (entry === undefined) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
const declared = ordered.find(function byName(candidate) {
|
|
74
|
+
return candidate.name === fieldName
|
|
75
|
+
})
|
|
76
|
+
if (declared === undefined) {
|
|
77
|
+
throw errors.new(`relation ${name} has no field ${fieldName}`)
|
|
78
|
+
}
|
|
79
|
+
bindings.push(Object.freeze({ field: fieldName, set: resolveEntry(declared.field, entry) }))
|
|
80
|
+
}
|
|
81
|
+
if (bindings.length === 0) {
|
|
82
|
+
throw errors.new(
|
|
83
|
+
`relation ${name}: an empty selection is the bare relation respelled — pass the relation itself (the canonical-utterance law: one meaning, one spelling)`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze(bindings)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The field block of a relation: field name to field constructor value. */
|
|
90
|
+
type FieldsShape = Record<string, AnyField>
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A typed field reference (`Account.fields.holder`) — the value statements,
|
|
94
|
+
* selections, and queries address a field through; its hover shows the
|
|
95
|
+
* field's brand in the phantom position.
|
|
96
|
+
*/
|
|
97
|
+
interface FieldRef<Rel extends string, Name extends string, V> {
|
|
98
|
+
readonly relation: Rel
|
|
99
|
+
readonly field: Name
|
|
100
|
+
readonly [phantom]?: V
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The typed field-reference record of a relation. */
|
|
104
|
+
type FieldRefs<RName extends string, Fields extends FieldsShape> = {
|
|
105
|
+
readonly [K in keyof Fields & string]: FieldRef<RName, K, FieldValue<Fields[K]>>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One declared field: name plus its runtime description, in declaration order. */
|
|
109
|
+
interface RelationField {
|
|
110
|
+
readonly name: string
|
|
111
|
+
readonly field: FieldData
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A relation's runtime description. */
|
|
115
|
+
interface RelationData {
|
|
116
|
+
readonly name: string
|
|
117
|
+
readonly fields: readonly RelationField[]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One resolved σ binding: the field name and its lowered literal set —
|
|
122
|
+
* handles already resolved to names, values already tagged by structural
|
|
123
|
+
* type.
|
|
124
|
+
*/
|
|
125
|
+
interface SelectionBinding {
|
|
126
|
+
readonly field: string
|
|
127
|
+
readonly set: LiteralSetSpec
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The `where()` argument: per field, a branded literal of that field's
|
|
132
|
+
* type (a closed handle constant IS such a literal — it carries the closed
|
|
133
|
+
* relation's brand, so it is legal exactly where the field is that closed
|
|
134
|
+
* relation's id type), an `oneOf(a, b, ...)` literal set, or a `span(start,
|
|
135
|
+
* end)` interval literal. Equality-only by construction: no operator
|
|
136
|
+
* parameter exists anywhere.
|
|
137
|
+
*/
|
|
138
|
+
type SelectionInput<Fields extends FieldsShape> = {
|
|
139
|
+
readonly [K in keyof Fields]?: FieldValue<Fields[K]> | OneOf<FieldValue<Fields[K]>>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A relation with a selection applied — what `on()` consumes as a σ-carrying source. */
|
|
143
|
+
interface Selected<Name extends string, Fields extends FieldsShape> {
|
|
144
|
+
readonly relation: Relation<Name, Fields>
|
|
145
|
+
readonly selection: readonly SelectionBinding[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A relation value. */
|
|
149
|
+
interface Relation<Name extends string, Fields extends FieldsShape> {
|
|
150
|
+
readonly name: Name
|
|
151
|
+
readonly data: RelationData
|
|
152
|
+
readonly fields: FieldRefs<Name, Fields>
|
|
153
|
+
where(selection: SelectionInput<Fields>): Selected<Name, Fields>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Any relation value, whatever its name and field block. */
|
|
157
|
+
type AnyRelation = Relation<string, FieldsShape>
|
|
158
|
+
|
|
159
|
+
/** Any selected relation value. */
|
|
160
|
+
interface AnySelected {
|
|
161
|
+
readonly relation: AnyRelation
|
|
162
|
+
readonly selection: readonly SelectionBinding[]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Extracts a relation's field block type. */
|
|
166
|
+
type RelationFields<R extends AnyRelation> = R extends Relation<string, infer F> ? F : never
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The inferred row object type of a relation as READ: every field present,
|
|
170
|
+
* branded. Closed relations have no `Fact` — they are unwritable, and the
|
|
171
|
+
* type constraint refuses them because a closed value lacks the relation
|
|
172
|
+
* shape.
|
|
173
|
+
*/
|
|
174
|
+
type Fact<R extends AnyRelation> = {
|
|
175
|
+
[K in keyof RelationFields<R>]: FieldValue<RelationFields<R>[K]>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The field names of `R` that carry the fresh mint mark. */
|
|
179
|
+
type FreshKeys<R extends AnyRelation> = {
|
|
180
|
+
[K in keyof RelationFields<R>]: RelationFields<R>[K] extends {
|
|
181
|
+
readonly data: { readonly minted: true }
|
|
182
|
+
}
|
|
183
|
+
? K
|
|
184
|
+
: never
|
|
185
|
+
}[keyof RelationFields<R>]
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The inferred row object type of a relation as INSERTED: fresh fields
|
|
189
|
+
* optional — omitted, the engine mints; supplied, identity is preserved
|
|
190
|
+
* (the ETL resupply idiom).
|
|
191
|
+
*/
|
|
192
|
+
type InsertFact<R extends AnyRelation> = Flatten<Omit<Fact<R>, FreshKeys<R>> & Partial<Pick<Fact<R>, FreshKeys<R>>>>
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Declares one relation: `relation("Account", { id: AccountId.fresh,
|
|
196
|
+
* holder: HolderId, ... })` — every field references a declared newtype
|
|
197
|
+
* (`const AccountId = u64.newtype("AccountId")`) or a bare constructor.
|
|
198
|
+
* Field declaration order is ordinal-id order (macro parity); the returned
|
|
199
|
+
* value is frozen and side-effect free.
|
|
200
|
+
*/
|
|
201
|
+
function relation<const Name extends string, Fields extends FieldsShape>(
|
|
202
|
+
name: Name,
|
|
203
|
+
fields: Fields
|
|
204
|
+
): Relation<Name, Fields> {
|
|
205
|
+
const ordered: RelationField[] = []
|
|
206
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
207
|
+
assertDeclarationOrderKey(`relation ${name} field`, fieldName)
|
|
208
|
+
ordered.push(Object.freeze({ name: fieldName, field: field.data }))
|
|
209
|
+
}
|
|
210
|
+
const data: RelationData = Object.freeze({ name, fields: Object.freeze(ordered) })
|
|
211
|
+
const refs: Record<string, unknown> = {}
|
|
212
|
+
for (const declared of ordered) {
|
|
213
|
+
refs[declared.name] = Object.freeze({ relation: name, field: declared.name })
|
|
214
|
+
}
|
|
215
|
+
Object.freeze(refs)
|
|
216
|
+
if (!refsComplete<Name, Fields>(refs, fields)) {
|
|
217
|
+
throw errors.new(`relation ${name}: field-reference construction incomplete`)
|
|
218
|
+
}
|
|
219
|
+
const holder: { value: Relation<Name, Fields> | undefined } = { value: undefined }
|
|
220
|
+
function where(selection: SelectionInput<Fields>): Selected<Name, Fields> {
|
|
221
|
+
const owner = holder.value
|
|
222
|
+
if (owner === undefined) {
|
|
223
|
+
throw errors.new(`relation ${name}: self-reference read before construction completed`)
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
relation: owner,
|
|
227
|
+
selection: resolveSelection(name, ordered, Object.entries(selection))
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
const value: Relation<Name, Fields> = Object.freeze({ name, data, fields: refs, where })
|
|
231
|
+
holder.value = value
|
|
232
|
+
return value
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type {
|
|
236
|
+
AnyRelation,
|
|
237
|
+
AnySelected,
|
|
238
|
+
Fact,
|
|
239
|
+
FieldRef,
|
|
240
|
+
FieldRefs,
|
|
241
|
+
FieldsShape,
|
|
242
|
+
FreshKeys,
|
|
243
|
+
InsertFact,
|
|
244
|
+
Relation,
|
|
245
|
+
RelationData,
|
|
246
|
+
RelationField,
|
|
247
|
+
RelationFields,
|
|
248
|
+
Selected,
|
|
249
|
+
SelectionBinding,
|
|
250
|
+
SelectionInput
|
|
251
|
+
}
|
|
252
|
+
export { relation }
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schema()` — assembles relations and statements into a theory value (the
|
|
3
|
+
* `Theory` analog; what `Db.create`/`Db.open` take). Construction-time
|
|
4
|
+
* validation is the macro-EXPANSION-boundary analog and nothing more:
|
|
5
|
+
* membership, implied-key duplicates, duplicate statements, and a
|
|
6
|
+
* belt-and-braces handle re-verification. Everything semantically deeper
|
|
7
|
+
* (containment targets resolving declared keys, fresh-on-u64, …) is
|
|
8
|
+
* DELIBERATELY left to the engine's `SchemaError` at `Db.create` — the
|
|
9
|
+
* same judge, the same two-boundary split as Rust.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as errors from "@superbuilders/errors"
|
|
13
|
+
import type { AnyClosed } from "#closed.ts"
|
|
14
|
+
import type { FaceData } from "#face.ts"
|
|
15
|
+
import { assertDeclarationOrderKey, type FieldData } from "#fields.ts"
|
|
16
|
+
import type { AnyRelation } from "#relation.ts"
|
|
17
|
+
import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
|
|
18
|
+
import { renderStatement, type Statement } from "#statements.ts"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates the relation record and collects the implied keys: the
|
|
22
|
+
* fresh-implied `R(field) -> R` per minted field and the closed auto-key
|
|
23
|
+
* `R(id) -> R` per closed relation, each rendered canonically so an
|
|
24
|
+
* explicit duplicate is caught by string identity with the renderer as the
|
|
25
|
+
* single spelling authority.
|
|
26
|
+
*/
|
|
27
|
+
function collectImplied(name: string, relations: SchemaRelations): Set<string> {
|
|
28
|
+
const implied = new Set<string>()
|
|
29
|
+
for (const [recordKey, member] of Object.entries(relations)) {
|
|
30
|
+
assertDeclarationOrderKey(`schema ${name} relation`, recordKey)
|
|
31
|
+
if (member.name !== recordKey) {
|
|
32
|
+
throw errors.new(
|
|
33
|
+
`schema ${name}: record key ${recordKey} holds relation ${member.name} — the key must equal the relation's declared name`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
if ("handles" in member.data) {
|
|
37
|
+
implied.add(`${member.name}(id) -> ${member.name}`)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
for (const declared of member.data.fields) {
|
|
41
|
+
if (declared.field.minted) {
|
|
42
|
+
implied.add(`${member.name}(${declared.name}) -> ${member.name}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return implied
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The relation values a statement addresses, for membership checking. */
|
|
50
|
+
function statementOwners(statement: Statement): readonly SchemaRelation[] {
|
|
51
|
+
const data = statement.data
|
|
52
|
+
if (data.kind === "key") {
|
|
53
|
+
return [data.owner]
|
|
54
|
+
}
|
|
55
|
+
return [data.source.owner, data.target.owner]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Requires every relation a statement addresses to be the IDENTICAL value
|
|
60
|
+
* the schema record declares — same-name-different-value is a forgery, not
|
|
61
|
+
* a membership.
|
|
62
|
+
*/
|
|
63
|
+
function verifyMembership(name: string, relations: SchemaRelations, statement: Statement, rendered: string): void {
|
|
64
|
+
for (const owner of statementOwners(statement)) {
|
|
65
|
+
const member = relations[owner.name]
|
|
66
|
+
if (member === undefined) {
|
|
67
|
+
throw errors.new(`schema ${name}: relation ${owner.name} is not declared in this schema — ${rendered}`)
|
|
68
|
+
}
|
|
69
|
+
if (member !== owner) {
|
|
70
|
+
throw errors.new(
|
|
71
|
+
`schema ${name}: statement references a different relation value named ${owner.name} than the one this schema declares — ${rendered}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Finds a face's field description by name, across both relation kinds. */
|
|
78
|
+
function faceField(face: FaceData, fieldName: string): FieldData | undefined {
|
|
79
|
+
const data = face.owner.data
|
|
80
|
+
if ("handles" in data) {
|
|
81
|
+
const column = data.columns.find(function byName(candidate) {
|
|
82
|
+
return candidate.name === fieldName
|
|
83
|
+
})
|
|
84
|
+
return column?.field
|
|
85
|
+
}
|
|
86
|
+
const declared = data.fields.find(function byName(candidate) {
|
|
87
|
+
return candidate.name === fieldName
|
|
88
|
+
})
|
|
89
|
+
return declared?.field
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Flattens one binding's literal set into its literals. */
|
|
93
|
+
function bindingLiterals(set: LiteralSetSpec): readonly LiteralSpec[] {
|
|
94
|
+
if (set.kind === "one") {
|
|
95
|
+
return [set.literal]
|
|
96
|
+
}
|
|
97
|
+
return set.literals
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Re-verifies one binding's handle literals against the field's roster —
|
|
102
|
+
* belt-and-braces over what `where()` already resolved and the type level
|
|
103
|
+
* already blocked, so a forged binding fails here rather than at the
|
|
104
|
+
* engine boundary with a colder message.
|
|
105
|
+
*/
|
|
106
|
+
function verifyBindingHandles(
|
|
107
|
+
name: string,
|
|
108
|
+
face: FaceData,
|
|
109
|
+
binding: { readonly field: string; readonly set: LiteralSetSpec },
|
|
110
|
+
rendered: string
|
|
111
|
+
): void {
|
|
112
|
+
const field = faceField(face, binding.field)
|
|
113
|
+
for (const literal of bindingLiterals(binding.set)) {
|
|
114
|
+
if (literal.kind !== "handle") {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (field?.closed === undefined) {
|
|
118
|
+
throw errors.new(
|
|
119
|
+
`schema ${name}: ${face.owner.name}.${binding.field} is not a closed-relation reference — the handle literal ${literal.handle} is legal only on a field whose newtype is a closed relation's handle newtype — ${rendered}`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (!field.closed.handles.includes(literal.handle)) {
|
|
123
|
+
throw errors.new(
|
|
124
|
+
`schema ${name}: closed relation ${field.closed.name} has no handle ${literal.handle} — ${rendered}`
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Walks every face of a statement through the handle re-verification. */
|
|
131
|
+
function verifyHandles(name: string, statement: Statement, rendered: string): void {
|
|
132
|
+
const data = statement.data
|
|
133
|
+
if (data.kind === "key") {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
for (const face of [data.source, data.target]) {
|
|
137
|
+
for (const binding of face.selection) {
|
|
138
|
+
verifyBindingHandles(name, face, binding, rendered)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves the closed relation a `(relation, field)` pair references
|
|
145
|
+
* through the DECLARED containments — the identical walk the engine's
|
|
146
|
+
* canonical renderer performs (`schema/render.rs` `closed_target_of`): one
|
|
147
|
+
* hop, source projecting exactly `[field]`, target projecting exactly the
|
|
148
|
+
* closed relation's `[id]`, first declared match wins; a `mirrors`
|
|
149
|
+
* contributes both of its materialized orientations. `undefined` = the
|
|
150
|
+
* engine would render the field's selection literals as raw row ids.
|
|
151
|
+
*/
|
|
152
|
+
function closedTargetOf(statements: readonly Statement[], owner: string, field: string): string | undefined {
|
|
153
|
+
for (const statement of statements) {
|
|
154
|
+
const data = statement.data
|
|
155
|
+
if (data.kind !== "containment") {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
const pairs: Array<readonly [FaceData, FaceData]> = [[data.source, data.target]]
|
|
159
|
+
if (data.bidirectional) {
|
|
160
|
+
pairs.push([data.target, data.source])
|
|
161
|
+
}
|
|
162
|
+
for (const [source, target] of pairs) {
|
|
163
|
+
if (
|
|
164
|
+
source.owner.name === owner &&
|
|
165
|
+
source.projection.length === 1 &&
|
|
166
|
+
source.projection[0] === field &&
|
|
167
|
+
target.projection.length === 1 &&
|
|
168
|
+
target.projection[0] === "id" &&
|
|
169
|
+
"handles" in target.owner.data
|
|
170
|
+
) {
|
|
171
|
+
return target.owner.name
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Admits a handle spelling only when the schema also declares the
|
|
180
|
+
* containment the ENGINE's canonical renderer resolves it through
|
|
181
|
+
* (`docs/architecture/10-data-model.md` § closed relations: a closed
|
|
182
|
+
* reference is the plain u64 column PLUS a declared containment). Without
|
|
183
|
+
* it the two renderers drift — `renderStatement` prints the handle name,
|
|
184
|
+
* the engine's violation `canonical` prints the raw row id — and the
|
|
185
|
+
* paste-back law (`violation.canonical === renderStatement(statement)`)
|
|
186
|
+
* breaks. Runs over the COMPLETE statement list, so declaration order
|
|
187
|
+
* never matters. The closed relation's own `id` field resolves directly
|
|
188
|
+
* (the walk's field-0 case).
|
|
189
|
+
*/
|
|
190
|
+
function verifyClosedReferences(name: string, statements: readonly Statement[]): void {
|
|
191
|
+
for (const statement of statements) {
|
|
192
|
+
const data = statement.data
|
|
193
|
+
if (data.kind === "key") {
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
const rendered = renderStatement(statement)
|
|
197
|
+
for (const face of [data.source, data.target]) {
|
|
198
|
+
for (const binding of face.selection) {
|
|
199
|
+
verifyClosedReferenceBinding(name, statements, face, binding, rendered)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** One binding's closed-reference resolution check (the {@link verifyClosedReferences} leaf). */
|
|
206
|
+
function verifyClosedReferenceBinding(
|
|
207
|
+
name: string,
|
|
208
|
+
statements: readonly Statement[],
|
|
209
|
+
face: FaceData,
|
|
210
|
+
binding: { readonly field: string; readonly set: LiteralSetSpec },
|
|
211
|
+
rendered: string
|
|
212
|
+
): void {
|
|
213
|
+
const spellsHandle = bindingLiterals(binding.set).some(function isHandle(literal) {
|
|
214
|
+
return literal.kind === "handle"
|
|
215
|
+
})
|
|
216
|
+
if (!spellsHandle) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const roster = faceField(face, binding.field)?.closed
|
|
220
|
+
if (roster === undefined) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if ("handles" in face.owner.data && binding.field === "id") {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const resolved = closedTargetOf(statements, face.owner.name, binding.field)
|
|
227
|
+
if (resolved !== roster.name) {
|
|
228
|
+
throw errors.new(
|
|
229
|
+
`schema ${name}: ${face.owner.name}.${binding.field} spells a ${roster.name} handle, but no declared containment resolves the closed reference — a closed reference is the plain u64 column plus its declared containment; declare contained(on(${face.owner.name}, "${binding.field}"), on(${roster.name}, "id")) — ${rendered}`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** One member of a schema's relation record. */
|
|
235
|
+
type SchemaRelation = AnyRelation | AnyClosed
|
|
236
|
+
|
|
237
|
+
/** The relation record a schema is generic over — what `Db` and queries key on. */
|
|
238
|
+
type SchemaRelations = Record<string, SchemaRelation>
|
|
239
|
+
|
|
240
|
+
/** A theory value: named relations plus the DECLARED dependency statements. */
|
|
241
|
+
interface Schema<Rels extends SchemaRelations> {
|
|
242
|
+
readonly name: string
|
|
243
|
+
readonly relations: Rels
|
|
244
|
+
readonly statements: readonly Statement[]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Any schema value, whatever its relation record. */
|
|
248
|
+
type AnySchema = Schema<SchemaRelations>
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Assembles a theory:
|
|
252
|
+
* `schema("Ledger", { Kind, Account, Holder }, [ ...statements ])`.
|
|
253
|
+
*
|
|
254
|
+
* Rejected here, each with the offending statement rendered canonically:
|
|
255
|
+
* a record key differing from its relation's declared name; a statement
|
|
256
|
+
* whose relation is not (identically) a member of the record; an explicit
|
|
257
|
+
* duplicate of a fresh-implied or closedness-implied key (macro parity:
|
|
258
|
+
* "redundant here — and rejected as a duplicate"); a duplicate statement
|
|
259
|
+
* (two statements rendering to one canonical utterance ARE one judgment);
|
|
260
|
+
* a handle selection that its roster does not hold (belt-and-braces —
|
|
261
|
+
* the type level already blocks it); and a handle selection whose closed
|
|
262
|
+
* reference no declared containment resolves (the engine's canonical
|
|
263
|
+
* renderer would print the raw row id where `renderStatement` prints the
|
|
264
|
+
* handle — the paste-back law demands the two spellings agree).
|
|
265
|
+
*
|
|
266
|
+
* The fresh-implied and closed auto-keys are NOT added to the statement
|
|
267
|
+
* list: the engine materializes them itself, in its own pinned order
|
|
268
|
+
* (`SchemaDescriptor::materialized_statements`), and restating them would
|
|
269
|
+
* double them.
|
|
270
|
+
*/
|
|
271
|
+
function schema<const Rels extends SchemaRelations>(
|
|
272
|
+
name: string,
|
|
273
|
+
relations: Rels,
|
|
274
|
+
statements: readonly Statement[]
|
|
275
|
+
): Schema<Rels> {
|
|
276
|
+
const implied = collectImplied(name, relations)
|
|
277
|
+
const seen = new Set<string>()
|
|
278
|
+
for (const statement of statements) {
|
|
279
|
+
const rendered = renderStatement(statement)
|
|
280
|
+
verifyMembership(name, relations, statement, rendered)
|
|
281
|
+
if (implied.has(rendered)) {
|
|
282
|
+
throw errors.new(
|
|
283
|
+
`schema ${name}: ${rendered} is redundant here (the fresh mark or closedness already implies it) — and rejected as a duplicate`
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
if (seen.has(rendered)) {
|
|
287
|
+
throw errors.new(`schema ${name}: duplicate statement — ${rendered}`)
|
|
288
|
+
}
|
|
289
|
+
seen.add(rendered)
|
|
290
|
+
verifyHandles(name, statement, rendered)
|
|
291
|
+
}
|
|
292
|
+
verifyClosedReferences(name, statements)
|
|
293
|
+
return Object.freeze({ name, relations, statements: Object.freeze([...statements]) })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export type { AnySchema, Schema, SchemaRelation, SchemaRelations }
|
|
297
|
+
export { schema }
|