@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,195 @@
1
+ /**
2
+ * IDB predicates (PRD-08) — engine recursion as values, mirroring the IR's
3
+ * cut exactly (`ir::Program`/`PredicateDef`/`AtomSource::Idb`;
4
+ * `docs/architecture/20-query-ir.md` § engine recursion): `$.predicate`
5
+ * declares a predicate with a named, field-typed head; its rules are given
6
+ * IN the declaration (a callback receiving the predicate's own reference,
7
+ * so self-recursion is writable and the rules are data the type system can
8
+ * see — the params they use flow into the query's inferred `Params`); and
9
+ * `.match({...})` uses it as a body atom, bindings addressing head
10
+ * positions by the declared column names (lowered positionally —
11
+ * `FieldId(i)` is head position i, exactly as the IR reads it). Strata
12
+ * legality (no negation/aggregation through a cycle) is the ENGINE's
13
+ * judge; its typed error surfaces at prepare.
14
+ */
15
+
16
+ import * as errors from "@superbuilders/errors"
17
+ import { phantom } from "#brand.ts"
18
+ import { assertDeclarationOrderKey, type FieldData } from "#fields.ts"
19
+ import type { AnyBodyItem, BindingsParams, MatchAtom, TermInput } from "#query/atom.ts"
20
+ import { resolveBindings } from "#query/atom.ts"
21
+ import type { AnyVar, Flatten, ItemParams, ParamsRecord, ParamsShape, QueryRegistry, Var } from "#query/scope.ts"
22
+ import { isTerm, resolveFieldData, term } from "#query/scope.ts"
23
+ import type { FieldRef } from "#relation.ts"
24
+
25
+ /** One declared head column: name plus the field description that types it. */
26
+ interface PredicateColumn {
27
+ readonly name: string
28
+ readonly data: FieldData
29
+ }
30
+
31
+ /** One clause of a predicate: head projections (column order) and the body. */
32
+ interface PredicateRuleData {
33
+ readonly finds: readonly AnyVar[]
34
+ readonly body: readonly AnyBodyItem[]
35
+ }
36
+
37
+ /** A predicate's runtime description; identity keys the dense `PredId` at lowering. */
38
+ interface PredicateData {
39
+ readonly name: string
40
+ readonly columns: readonly PredicateColumn[]
41
+ readonly rules: readonly PredicateRuleData[]
42
+ }
43
+
44
+ /** Any field reference, whatever its brand — the column-declaration position. */
45
+ type AnyFieldRef = FieldRef<string, string, unknown>
46
+
47
+ /** The columns record `$.predicate` takes: column name to typing field reference. */
48
+ type PredicateColumnsInput = Readonly<Record<string, AnyFieldRef>>
49
+
50
+ /** Extracts a field reference's host value type. */
51
+ type RefValue<T> = T extends { readonly [phantom]?: infer V } ? Exclude<V, undefined> : never
52
+
53
+ /** The head's typed column record, derived from the declaration. */
54
+ type ColumnValues<Cols> = { [K in keyof Cols]: RefValue<Cols[K]> }
55
+
56
+ /**
57
+ * The `.match` bindings of a predicate atom: per head column, a term of
58
+ * the column's brand; unmentioned columns are wildcards, exactly as
59
+ * relation atoms.
60
+ */
61
+ type PredicateBindings<ColsV> = { readonly [K in keyof ColsV]?: TermInput<ColsV[K]> }
62
+
63
+ /**
64
+ * One rule of a predicate as the declaration callback returns it: `finds`
65
+ * names the projected variable per head column (interior heads project
66
+ * bound variables only — the creation quarantine; the engine's strata
67
+ * judge enforces it), `body` is the clause's conjunction.
68
+ */
69
+ interface PredicateRuleInput<ColsV> {
70
+ readonly finds: { readonly [K in keyof ColsV]: Var<ColsV[K]> }
71
+ readonly body: readonly AnyBodyItem[]
72
+ }
73
+
74
+ /**
75
+ * The predicate's own reference, passed INTO its rules callback — the
76
+ * fixpoint spelling: `self.match({...})` inside a rule of the same
77
+ * predicate is the recursive atom.
78
+ */
79
+ interface PredicateSelf<ColsV> {
80
+ readonly data: PredicateData
81
+ match<const B extends PredicateBindings<ColsV>>(bindings: B): MatchAtom<BindingsParams<B>>
82
+ }
83
+
84
+ /**
85
+ * A declared predicate. `.match({...})` uses it as a body atom — in
86
+ * another predicate's rules or in the output rules; the phantom `P`
87
+ * carries the params its own rules contributed, so a query's `Params`
88
+ * type sees through predicates it only reaches transitively.
89
+ */
90
+ interface Predicate<ColsV, P extends ParamsRecord> {
91
+ readonly data: PredicateData
92
+ match<const B extends PredicateBindings<ColsV>>(bindings: B): MatchAtom<Flatten<P & BindingsParams<B>>>
93
+ }
94
+
95
+ /** The params contributed by a predicate's declared rules. */
96
+ type PredicateParams<Rules extends readonly { readonly body: readonly AnyBodyItem[] }[]> = ParamsShape<
97
+ ItemParams<Rules[number]["body"][number]>
98
+ >
99
+
100
+ /** Builds one predicate atom (shared by `self.match` and `.match`). */
101
+ function predicateAtom(data: PredicateData, bindings: Readonly<Record<string, unknown>>): MatchAtom<never> {
102
+ return Object.freeze({
103
+ item: "atom" as const,
104
+ negated: false,
105
+ source: Object.freeze({ kind: "predicate" as const, pred: data }),
106
+ bindings: resolveBindings(
107
+ `predicate ${data.name}`,
108
+ data.columns.map(function asField(column) {
109
+ return { name: column.name, field: column.data }
110
+ }),
111
+ bindings
112
+ )
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Declares one predicate in the scope (the `$.predicate` implementation):
118
+ * resolves the head columns, runs the rules callback against the
119
+ * predicate's own reference, verifies every rule projects a declared
120
+ * scope variable per column, and registers the predicate in declaration
121
+ * order (= its dense `PredId`).
122
+ */
123
+ function makePredicate<
124
+ const Cols extends PredicateColumnsInput,
125
+ const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
126
+ >(
127
+ registry: QueryRegistry,
128
+ name: string,
129
+ columns: Cols,
130
+ rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
131
+ ): Predicate<ColumnValues<Cols>, PredicateParams<Rules>> {
132
+ const ordered: PredicateColumn[] = []
133
+ for (const [columnName, ref] of Object.entries(columns)) {
134
+ assertDeclarationOrderKey(`predicate ${name} column`, columnName)
135
+ ordered.push(
136
+ Object.freeze({
137
+ name: columnName,
138
+ data: resolveFieldData(registry.theory, ref.relation, ref.field)
139
+ })
140
+ )
141
+ }
142
+ if (ordered.length === 0) {
143
+ throw errors.new(`predicate ${name}: a predicate head needs at least one column`)
144
+ }
145
+ const ruleSlots: PredicateRuleData[] = []
146
+ const data: PredicateData = Object.freeze({
147
+ name,
148
+ columns: Object.freeze(ordered),
149
+ rules: ruleSlots
150
+ })
151
+ const self: PredicateSelf<ColumnValues<Cols>> = Object.freeze({
152
+ data,
153
+ match(bindings: Readonly<Record<string, unknown>>) {
154
+ return predicateAtom(data, bindings)
155
+ }
156
+ })
157
+ const declared = rules(self)
158
+ for (const rule of declared) {
159
+ const finds: AnyVar[] = []
160
+ const record: Readonly<Record<string, unknown>> = Object.fromEntries(Object.entries(rule.finds))
161
+ for (const column of ordered) {
162
+ const found = record[column.name]
163
+ if (!isTerm(found) || found[term] !== "var") {
164
+ throw errors.new(
165
+ `predicate ${name}: rule finds must project a scope variable for column ${column.name} (interior heads project bound variables only)`
166
+ )
167
+ }
168
+ finds.push(found)
169
+ }
170
+ ruleSlots.push(Object.freeze({ finds: Object.freeze(finds), body: Object.freeze([...rule.body]) }))
171
+ }
172
+ Object.freeze(ruleSlots)
173
+ registry.predicates.push(data)
174
+ return Object.freeze({
175
+ data,
176
+ match(bindings: Readonly<Record<string, unknown>>) {
177
+ return predicateAtom(data, bindings)
178
+ }
179
+ })
180
+ }
181
+
182
+ export type {
183
+ AnyFieldRef,
184
+ ColumnValues,
185
+ Predicate,
186
+ PredicateBindings,
187
+ PredicateColumn,
188
+ PredicateColumnsInput,
189
+ PredicateData,
190
+ PredicateParams,
191
+ PredicateRuleData,
192
+ PredicateRuleInput,
193
+ PredicateSelf
194
+ }
195
+ export { makePredicate }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Prepared-query marshaling seams (PRD-08), the two rides every execution
3
+ * takes — the typed params object down to the bridge's positional
4
+ * `QueryParam[]` (declaration order = dense `ParamId`s, values tagged by
5
+ * each param's declaring field), and answer rows (positional, head order)
6
+ * back up to plain objects with branded values (the store is the proof
7
+ * carrier: the engine computed the answer, so readback asserts the brand,
8
+ * the same trust direction as fact readback in `#marshal.ts`). Answers are
9
+ * SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
10
+ * VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
11
+ */
12
+
13
+ import * as errors from "@superbuilders/errors"
14
+ import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
15
+ import type { SelectColumn } from "#query/lower.ts"
16
+ import { taggedLiteral } from "#query/lower.ts"
17
+ import type { ParamEntry } from "#query/scope.ts"
18
+
19
+ /** The 13-bit Allen mask ceiling (`bumbledb/crates/bumbledb/src/allen.rs`: bits above the low 13 are unrepresentable). */
20
+ const ALLEN_ALL_BITS = (1 << 13) - 1
21
+
22
+ /** Tags one supplied mask-param value. */
23
+ function wireMask(name: string, value: unknown): TaggedValue {
24
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > ALLEN_ALL_BITS) {
25
+ throw errors.new(`param ${name}: an Allen-mask param binds a 13-bit mask number built from the ALLEN constants`)
26
+ }
27
+ return { kind: "allenMask", mask: value }
28
+ }
29
+
30
+ /**
31
+ * Marshals the typed params object to the bridge's positional arguments,
32
+ * in param declaration order (= the lowering's dense `ParamId`s). A
33
+ * missing entry is a typed error naming the param; values tag by the
34
+ * declaring field's structural type; a set param takes a readonly array
35
+ * (the empty set is legal and matches nothing — the engine's rule).
36
+ */
37
+ function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
38
+ return entries.map(function wireOne(entry): QueryParam {
39
+ const value = supplied[entry.name]
40
+ if (value === undefined) {
41
+ throw errors.new(`execute params object is missing param ${entry.name}`)
42
+ }
43
+ if (entry.shape === "mask") {
44
+ return wireMask(entry.name, value)
45
+ }
46
+ if (entry.data === undefined) {
47
+ throw errors.new(`param ${entry.name}: registry entry carries no declaring field`)
48
+ }
49
+ if (entry.shape === "set") {
50
+ if (!Array.isArray(value)) {
51
+ throw errors.new(`param ${entry.name}: a set param binds a readonly array of values`)
52
+ }
53
+ const data = entry.data
54
+ return {
55
+ kind: "set",
56
+ values: value.map(function wireElement(element, index) {
57
+ return taggedLiteral(`param ${entry.name}[${index}]`, data, element)
58
+ })
59
+ }
60
+ }
61
+ return taggedLiteral(`param ${entry.name}`, entry.data, value)
62
+ })
63
+ }
64
+
65
+ /**
66
+ * The read-side trusted seam of answers: a decoded row carrying every
67
+ * select column IS a `Row` — the engine computed it under the prepared
68
+ * head, so the brands are asserted, not re-derived (the `#marshal.ts`
69
+ * trust direction).
70
+ */
71
+ function isAnswerRow<Row>(
72
+ select: readonly SelectColumn[],
73
+ decoded: Readonly<Record<string, FactValue>>
74
+ ): decoded is Readonly<Record<string, FactValue>> & Row {
75
+ return select.every(function present(column) {
76
+ return decoded[column.name] !== undefined
77
+ })
78
+ }
79
+
80
+ /**
81
+ * Decodes positional answer rows (column order = the program's head order
82
+ * = the select record's written order) to named, branded, frozen row
83
+ * objects.
84
+ */
85
+ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]): Row[] {
86
+ return rows.map(function decodeRow(row) {
87
+ if (row.length !== select.length) {
88
+ throw errors.new(`query answer arity ${row.length} does not match the ${select.length} select columns`)
89
+ }
90
+ const decoded: Record<string, FactValue> = {}
91
+ select.forEach(function decodeCell(column, ordinal) {
92
+ const cell = row[ordinal]
93
+ if (cell === undefined) {
94
+ throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
95
+ }
96
+ decoded[column.name] = cell
97
+ })
98
+ Object.freeze(decoded)
99
+ if (!isAnswerRow<Row>(select, decoded)) {
100
+ throw errors.new("query answer row is not a complete select record")
101
+ }
102
+ return decoded
103
+ })
104
+ }
105
+
106
+ export { decodeAnswers, wireParams }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Query scope terms (PRD-08): typed variables and parameters, declared
3
+ * inside a `query()` scope callback so identity is lexical. A `Var` is
4
+ * typed by the field it is declared from and joins are brand-equal by
5
+ * construction (the nominal join discipline); var identity is OBJECT
6
+ * identity — two `$.var` calls are two variables even from the same field,
7
+ * and no name-collision machinery exists. Params are query-global, carry a
8
+ * mandatory literal name (the key of `execute`'s params object), and lower
9
+ * to dense positional `ParamId`s in declaration order. This module also
10
+ * owns the type-level params extraction: every atom and condition value
11
+ * carries its contributed params object in a phantom, and the query's
12
+ * `Params` type is the flattened intersection over the returned rules —
13
+ * inference rides the return value, never a mutable type accumulator.
14
+ */
15
+
16
+ import * as errors from "@superbuilders/errors"
17
+ import { phantom } from "#brand.ts"
18
+ import type { AnyClosed } from "#closed.ts"
19
+ import type { FieldData } from "#fields.ts"
20
+ import type { PredicateData } from "#query/predicate.ts"
21
+ import type { AnySchema, SchemaRelation } from "#schema.ts"
22
+
23
+ /**
24
+ * The runtime discriminant of query term values. Host literals (bigints,
25
+ * strings, interval objects, `oneOf` sets) never carry it, so "is this
26
+ * binding a term or a literal" is one property probe, never a guess.
27
+ */
28
+ const term: unique symbol = Symbol("bumbledb.query.term")
29
+
30
+ /**
31
+ * A query variable, typed by the field it was declared from
32
+ * (`$.var(Holder.fields.id)` → `Var<Brand<bigint, "HolderId">>`). Usable in
33
+ * any atom position whose field carries the same brand; a brand-mismatched
34
+ * placement is a TYPE error. Identity is object identity.
35
+ */
36
+ interface Var<V> {
37
+ readonly [term]: "var"
38
+ readonly relation: string
39
+ readonly field: string
40
+ readonly data: FieldData
41
+ readonly [phantom]?: V
42
+ }
43
+
44
+ /**
45
+ * A scalar query parameter — `$.param("root", Holder.fields.id)`. The name
46
+ * literal is the key of the typed params object `execute` takes; the value
47
+ * is marshaled by the declaring field's structural type at bind.
48
+ */
49
+ interface Param<Name extends string, V> {
50
+ readonly [term]: "param"
51
+ readonly name: Name
52
+ readonly relation: string
53
+ readonly field: string
54
+ readonly data: FieldData
55
+ readonly [phantom]?: V
56
+ }
57
+
58
+ /**
59
+ * A set-valued query parameter (the IR's `ParamSet` term): bound at
60
+ * execution to an ARRAY of values of the declaring field's type; a binding
61
+ * position matches iff the field value is in the set. Legal in atom
62
+ * bindings (positive and negated) and as the right side of `is` — nowhere
63
+ * else, exactly as the IR rules it.
64
+ */
65
+ interface ParamSet<Name extends string, V> {
66
+ readonly [term]: "paramSet"
67
+ readonly name: Name
68
+ readonly relation: string
69
+ readonly field: string
70
+ readonly data: FieldData
71
+ readonly [phantom]?: V
72
+ }
73
+
74
+ /**
75
+ * An Allen-mask parameter (the IR's `MaskTerm::Param`): the temporal
76
+ * relation as a bind-time argument — one prepared query answers any of the
77
+ * mask questions per execution. Bound to a 13-bit mask number built from
78
+ * the `ALLEN` constants.
79
+ */
80
+ interface MaskParam<Name extends string> {
81
+ readonly [term]: "maskParam"
82
+ readonly name: Name
83
+ }
84
+
85
+ /** Any query variable, whatever its brand. */
86
+ type AnyVar = Var<unknown>
87
+
88
+ /** Any parameter term of the scope, whatever its name and brand. */
89
+ type AnyParamTerm = Param<string, unknown> | ParamSet<string, unknown> | MaskParam<string>
90
+
91
+ /** Any scope term value. */
92
+ type AnyTerm = AnyVar | AnyParamTerm
93
+
94
+ /** Narrows an unknown binding value to a scope term (vs a host literal). */
95
+ function isTerm(value: unknown): value is AnyTerm {
96
+ return typeof value === "object" && value !== null && term in value
97
+ }
98
+
99
+ /** A params object type — what `execute` takes and the phantoms carry. */
100
+ type ParamsRecord = Readonly<Record<string, unknown>>
101
+
102
+ /** Flattens an intersection into one displayed object type (hover legibility). */
103
+ type Flatten<T> = { [K in keyof T]: T[K] }
104
+
105
+ /** The standard union-to-intersection fold (distributes over `U`). */
106
+ type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never) extends (member: infer I) => void
107
+ ? I
108
+ : never
109
+
110
+ /**
111
+ * One term's contribution to the query's params object type: a `Param`
112
+ * contributes its value type under its name, a `ParamSet` the readonly
113
+ * array of it, a `MaskParam` a mask number; everything else contributes
114
+ * nothing.
115
+ */
116
+ type TermContribution<T> = T extends {
117
+ readonly [term]: "param"
118
+ readonly name: infer N extends string
119
+ readonly [phantom]?: infer V
120
+ }
121
+ ? { readonly [K in N]: Exclude<V, undefined> }
122
+ : T extends {
123
+ readonly [term]: "paramSet"
124
+ readonly name: infer N extends string
125
+ readonly [phantom]?: infer V
126
+ }
127
+ ? { readonly [K in N]: readonly Exclude<V, undefined>[] }
128
+ : T extends { readonly [term]: "maskParam"; readonly name: infer N extends string }
129
+ ? { readonly [K in N]: number }
130
+ : Record<never, never>
131
+
132
+ /**
133
+ * Folds a union of per-term/per-item params objects into the one flattened
134
+ * params record (the query's `Params` type).
135
+ */
136
+ type ParamsShape<U> = [U] extends [never] ? Record<never, never> : Flatten<UnionToIntersection<U>>
137
+
138
+ /** Reads an atom's/condition's contributed params object off its phantom. */
139
+ type ItemParams<T> = T extends { readonly [phantom]?: infer P } ? Exclude<P, undefined> : Record<never, never>
140
+
141
+ /** One registered parameter: name, wire shape, and the declaring field. */
142
+ interface ParamEntry {
143
+ readonly name: string
144
+ readonly shape: "value" | "set" | "mask"
145
+ readonly data: FieldData | undefined
146
+ }
147
+
148
+ /**
149
+ * The mutable build-time registry one `query()` scope owns: declared vars
150
+ * (membership polices cross-scope smuggling), params in declaration order
151
+ * (= dense `ParamId`s), and declared predicates in declaration order
152
+ * (= dense `PredId`s; the output predicate is appended by lowering).
153
+ */
154
+ interface QueryRegistry {
155
+ readonly theory: AnySchema
156
+ readonly vars: Set<AnyVar>
157
+ readonly params: ParamEntry[]
158
+ readonly paramIndex: Map<AnyTerm, number>
159
+ readonly predicates: PredicateData[]
160
+ }
161
+
162
+ /** Creates one empty scope registry over the query's theory. */
163
+ function createRegistry(theory: AnySchema): QueryRegistry {
164
+ return {
165
+ theory,
166
+ vars: new Set(),
167
+ params: [],
168
+ paramIndex: new Map(),
169
+ predicates: []
170
+ }
171
+ }
172
+
173
+ /**
174
+ * The relation-kind discriminant: a closed relation's runtime description
175
+ * carries its handle roster, an ordinary relation's never does.
176
+ */
177
+ function isClosedMember(member: SchemaRelation): member is AnyClosed {
178
+ return "handles" in member.data
179
+ }
180
+
181
+ /**
182
+ * Resolves a field reference's runtime description through the schema —
183
+ * the seam that types `$.var`/`$.param` declarations at runtime (the type
184
+ * level already carries the brand; this recovers the structural type the
185
+ * lowering and the param marshaler direct by).
186
+ */
187
+ function resolveFieldData(theory: AnySchema, relationName: string, fieldName: string): FieldData {
188
+ const member: SchemaRelation | undefined = theory.relations[relationName]
189
+ if (member === undefined) {
190
+ throw errors.new(`schema ${theory.name} has no relation ${relationName}`)
191
+ }
192
+ if (isClosedMember(member)) {
193
+ if (fieldName === "id") {
194
+ return member.id.data
195
+ }
196
+ const column = member.data.columns.find(function byName(candidate) {
197
+ return candidate.name === fieldName
198
+ })
199
+ if (column === undefined) {
200
+ throw errors.new(`closed relation ${relationName} has no column ${fieldName}`)
201
+ }
202
+ return column.field
203
+ }
204
+ const declared = member.data.fields.find(function byName(candidate) {
205
+ return candidate.name === fieldName
206
+ })
207
+ if (declared === undefined) {
208
+ throw errors.new(`relation ${relationName} has no field ${fieldName}`)
209
+ }
210
+ return declared.field
211
+ }
212
+
213
+ /** A field reference's runtime half, as the scope factories consume it. */
214
+ interface RefNames {
215
+ readonly relation: string
216
+ readonly field: string
217
+ }
218
+
219
+ /** Declares one variable in the scope (the `$.var` implementation). */
220
+ function scopeVar<V>(registry: QueryRegistry, ref: RefNames): Var<V> {
221
+ const value: Var<V> = Object.freeze({
222
+ [term]: "var" as const,
223
+ relation: ref.relation,
224
+ field: ref.field,
225
+ data: resolveFieldData(registry.theory, ref.relation, ref.field)
226
+ })
227
+ registry.vars.add(value)
228
+ return value
229
+ }
230
+
231
+ /** Rejects a second parameter under an already-taken name. */
232
+ function assertFreshParamName(registry: QueryRegistry, name: string): void {
233
+ const taken = registry.params.some(function byName(entry) {
234
+ return entry.name === name
235
+ })
236
+ if (taken) {
237
+ throw errors.new(
238
+ `query scope already declares a param named ${name} — param names key the execute params object, one declaration each`
239
+ )
240
+ }
241
+ }
242
+
243
+ /** Declares one scalar parameter (the `$.param` implementation). */
244
+ function scopeParam<Name extends string, V>(registry: QueryRegistry, name: Name, ref: RefNames): Param<Name, V> {
245
+ assertFreshParamName(registry, name)
246
+ const data = resolveFieldData(registry.theory, ref.relation, ref.field)
247
+ const value: Param<Name, V> = Object.freeze({
248
+ [term]: "param" as const,
249
+ name,
250
+ relation: ref.relation,
251
+ field: ref.field,
252
+ data
253
+ })
254
+ registry.paramIndex.set(value, registry.params.length)
255
+ registry.params.push(Object.freeze({ name, shape: "value" as const, data }))
256
+ return value
257
+ }
258
+
259
+ /** Declares one set parameter (the `$.paramSet` implementation). */
260
+ function scopeParamSet<Name extends string, V>(registry: QueryRegistry, name: Name, ref: RefNames): ParamSet<Name, V> {
261
+ assertFreshParamName(registry, name)
262
+ const data = resolveFieldData(registry.theory, ref.relation, ref.field)
263
+ const value: ParamSet<Name, V> = Object.freeze({
264
+ [term]: "paramSet" as const,
265
+ name,
266
+ relation: ref.relation,
267
+ field: ref.field,
268
+ data
269
+ })
270
+ registry.paramIndex.set(value, registry.params.length)
271
+ registry.params.push(Object.freeze({ name, shape: "set" as const, data }))
272
+ return value
273
+ }
274
+
275
+ /** Declares one Allen-mask parameter (the `$.allenParam` implementation). */
276
+ function scopeAllenParam<Name extends string>(registry: QueryRegistry, name: Name): MaskParam<Name> {
277
+ assertFreshParamName(registry, name)
278
+ const value: MaskParam<Name> = Object.freeze({ [term]: "maskParam" as const, name })
279
+ registry.paramIndex.set(value, registry.params.length)
280
+ registry.params.push(Object.freeze({ name, shape: "mask" as const, data: undefined }))
281
+ return value
282
+ }
283
+
284
+ export type {
285
+ AnyParamTerm,
286
+ AnyTerm,
287
+ AnyVar,
288
+ Flatten,
289
+ ItemParams,
290
+ MaskParam,
291
+ Param,
292
+ ParamEntry,
293
+ ParamSet,
294
+ ParamsRecord,
295
+ ParamsShape,
296
+ QueryRegistry,
297
+ TermContribution,
298
+ UnionToIntersection,
299
+ Var
300
+ }
301
+ export { createRegistry, isTerm, resolveFieldData, scopeAllenParam, scopeParam, scopeParamSet, scopeVar, term }