@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.
Files changed (108) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +84 -0
  3. package/dist/brand.d.ts +59 -0
  4. package/dist/brand.d.ts.map +1 -0
  5. package/dist/brand.js +47 -0
  6. package/dist/brand.js.map +1 -0
  7. package/dist/closed.d.ts +97 -0
  8. package/dist/closed.d.ts.map +1 -0
  9. package/dist/closed.js +107 -0
  10. package/dist/closed.js.map +1 -0
  11. package/dist/count.d.ts +55 -0
  12. package/dist/count.d.ts.map +1 -0
  13. package/dist/count.js +92 -0
  14. package/dist/count.js.map +1 -0
  15. package/dist/db.d.ts +341 -0
  16. package/dist/db.d.ts.map +1 -0
  17. package/dist/db.js +1016 -0
  18. package/dist/db.js.map +1 -0
  19. package/dist/exhume.d.ts +130 -0
  20. package/dist/exhume.d.ts.map +1 -0
  21. package/dist/exhume.js +145 -0
  22. package/dist/exhume.js.map +1 -0
  23. package/dist/face.d.ts +91 -0
  24. package/dist/face.d.ts.map +1 -0
  25. package/dist/face.js +69 -0
  26. package/dist/face.js.map +1 -0
  27. package/dist/fields.d.ts +187 -0
  28. package/dist/fields.d.ts.map +1 -0
  29. package/dist/fields.js +204 -0
  30. package/dist/fields.js.map +1 -0
  31. package/dist/index.d.ts +49 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lower.d.ts +34 -0
  36. package/dist/lower.d.ts.map +1 -0
  37. package/dist/lower.js +105 -0
  38. package/dist/lower.js.map +1 -0
  39. package/dist/marshal.d.ts +79 -0
  40. package/dist/marshal.d.ts.map +1 -0
  41. package/dist/marshal.js +167 -0
  42. package/dist/marshal.js.map +1 -0
  43. package/dist/native.d.ts +532 -0
  44. package/dist/native.d.ts.map +1 -0
  45. package/dist/native.js +64 -0
  46. package/dist/native.js.map +1 -0
  47. package/dist/query/atom.d.ts +285 -0
  48. package/dist/query/atom.d.ts.map +1 -0
  49. package/dist/query/atom.js +281 -0
  50. package/dist/query/atom.js.map +1 -0
  51. package/dist/query/lower.d.ts +145 -0
  52. package/dist/query/lower.d.ts.map +1 -0
  53. package/dist/query/lower.js +604 -0
  54. package/dist/query/lower.js.map +1 -0
  55. package/dist/query/predicate.d.ts +101 -0
  56. package/dist/query/predicate.d.ts.map +1 -0
  57. package/dist/query/predicate.js +85 -0
  58. package/dist/query/predicate.js.map +1 -0
  59. package/dist/query/run.d.ts +30 -0
  60. package/dist/query/run.d.ts.map +1 -0
  61. package/dist/query/run.js +94 -0
  62. package/dist/query/run.js.map +1 -0
  63. package/dist/query/scope.d.ts +168 -0
  64. package/dist/query/scope.d.ts.map +1 -0
  65. package/dist/query/scope.js +134 -0
  66. package/dist/query/scope.js.map +1 -0
  67. package/dist/query/select.d.ts +106 -0
  68. package/dist/query/select.d.ts.map +1 -0
  69. package/dist/query/select.js +69 -0
  70. package/dist/query/select.js.map +1 -0
  71. package/dist/relation.d.ts +120 -0
  72. package/dist/relation.d.ts.map +1 -0
  73. package/dist/relation.js +108 -0
  74. package/dist/relation.js.map +1 -0
  75. package/dist/schema.d.ts +50 -0
  76. package/dist/schema.d.ts.map +1 -0
  77. package/dist/schema.js +235 -0
  78. package/dist/schema.js.map +1 -0
  79. package/dist/spec.d.ts +218 -0
  80. package/dist/spec.d.ts.map +1 -0
  81. package/dist/spec.js +154 -0
  82. package/dist/spec.js.map +1 -0
  83. package/dist/statements.d.ts +91 -0
  84. package/dist/statements.d.ts.map +1 -0
  85. package/dist/statements.js +101 -0
  86. package/dist/statements.js.map +1 -0
  87. package/package.json +66 -0
  88. package/src/brand.ts +82 -0
  89. package/src/closed.ts +228 -0
  90. package/src/count.ts +117 -0
  91. package/src/db.ts +1519 -0
  92. package/src/exhume.ts +243 -0
  93. package/src/face.ts +161 -0
  94. package/src/fields.ts +385 -0
  95. package/src/index.ts +185 -0
  96. package/src/lower.ts +118 -0
  97. package/src/marshal.ts +220 -0
  98. package/src/native.ts +576 -0
  99. package/src/query/atom.ts +556 -0
  100. package/src/query/lower.ts +855 -0
  101. package/src/query/predicate.ts +195 -0
  102. package/src/query/run.ts +106 -0
  103. package/src/query/scope.ts +301 -0
  104. package/src/query/select.ts +140 -0
  105. package/src/relation.ts +252 -0
  106. package/src/schema.ts +297 -0
  107. package/src/spec.ts +325 -0
  108. 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 }
@@ -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 }