@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,855 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `query()` and the IR lowering (PRD-08). A query is built inside a scope
|
|
3
|
+
* callback (variable identity is lexical and typed) and is an INERT value:
|
|
4
|
+
* `Query<Rels, Row, Params>` with `Row` inferred from `select` and
|
|
5
|
+
* `Params` from every parameter the returned rules (and the predicates
|
|
6
|
+
* they reach) use. Lowering is a pure function of the query value down to
|
|
7
|
+
* the bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`, the
|
|
8
|
+
* bijection target): relations and predicates by declaration ordinal (the
|
|
9
|
+
* declaration-order-is-ids law the engine's manifest pins), variables by
|
|
10
|
+
* dense per-rule first-occurrence ids (rule-scoped, exactly as the IR
|
|
11
|
+
* scopes them), params by scope declaration order. Lowering is STABLE —
|
|
12
|
+
* the same query value lowers to deeply-equal IR every time, and two
|
|
13
|
+
* identically-written queries lower identically (prepared-query caching
|
|
14
|
+
* upstream keys on this). Construction validates negation safety (typed,
|
|
15
|
+
* naming the variable — earlier and warmer than the engine's refusal);
|
|
16
|
+
* everything else (strata, types, aggregates rosters, rule caps) is the
|
|
17
|
+
* ENGINE's judge, surfacing its typed errors at prepare. No invented
|
|
18
|
+
* limits: rule counts and predicate counts are never pre-checked here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as errors from "@superbuilders/errors"
|
|
22
|
+
import { phantom } from "#brand.ts"
|
|
23
|
+
import { assertDeclarationOrderKey, type FieldData } from "#fields.ts"
|
|
24
|
+
import type {
|
|
25
|
+
AtomIr,
|
|
26
|
+
ComparisonIr,
|
|
27
|
+
ConditionTreeIr,
|
|
28
|
+
FindTermIr,
|
|
29
|
+
HeadOpIr,
|
|
30
|
+
HeadTermIr,
|
|
31
|
+
PredicateDefIr,
|
|
32
|
+
ProgramIr,
|
|
33
|
+
RuleIr,
|
|
34
|
+
TaggedValue,
|
|
35
|
+
TermIr
|
|
36
|
+
} from "#native.ts"
|
|
37
|
+
|
|
38
|
+
import type {
|
|
39
|
+
AnyBodyItem,
|
|
40
|
+
AnyCondition,
|
|
41
|
+
AtomSourceData,
|
|
42
|
+
BindingEntry,
|
|
43
|
+
CmpOpData,
|
|
44
|
+
CmpTerm,
|
|
45
|
+
ComparisonItem,
|
|
46
|
+
MatchAtom
|
|
47
|
+
} from "#query/atom.ts"
|
|
48
|
+
|
|
49
|
+
import type {
|
|
50
|
+
ColumnValues,
|
|
51
|
+
Predicate,
|
|
52
|
+
PredicateColumnsInput,
|
|
53
|
+
PredicateData,
|
|
54
|
+
PredicateParams,
|
|
55
|
+
PredicateRuleInput,
|
|
56
|
+
PredicateSelf
|
|
57
|
+
} from "#query/predicate.ts"
|
|
58
|
+
import { makePredicate } from "#query/predicate.ts"
|
|
59
|
+
import type {
|
|
60
|
+
AnyTerm,
|
|
61
|
+
AnyVar,
|
|
62
|
+
ItemParams,
|
|
63
|
+
MaskParam,
|
|
64
|
+
Param,
|
|
65
|
+
ParamSet,
|
|
66
|
+
ParamsRecord,
|
|
67
|
+
ParamsShape,
|
|
68
|
+
QueryRegistry,
|
|
69
|
+
Var
|
|
70
|
+
} from "#query/scope.ts"
|
|
71
|
+
|
|
72
|
+
import { createRegistry, isTerm, scopeAllenParam, scopeParam, scopeParamSet, scopeVar, term } from "#query/scope.ts"
|
|
73
|
+
import type { AggregateData, RowOf, SelectShape } from "#query/select.ts"
|
|
74
|
+
import type { FieldRef } from "#relation.ts"
|
|
75
|
+
import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The scope value the `query()` build callback receives: variable and
|
|
79
|
+
* parameter declaration plus predicate declaration (engine recursion).
|
|
80
|
+
*/
|
|
81
|
+
interface Scope<Rels extends SchemaRelations> {
|
|
82
|
+
/**
|
|
83
|
+
* Declares one query variable, typed by the field it is declared from
|
|
84
|
+
* — usable in ANY atom position whose field carries the same brand
|
|
85
|
+
* (the nominal join discipline). Two calls are two variables.
|
|
86
|
+
*/
|
|
87
|
+
var<V>(field: FieldRef<keyof Rels & string, string, V>): Var<V>
|
|
88
|
+
/**
|
|
89
|
+
* Declares one scalar parameter under a mandatory name literal — the
|
|
90
|
+
* key of the typed params object `execute` takes.
|
|
91
|
+
*/
|
|
92
|
+
param<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): Param<Name, V>
|
|
93
|
+
/**
|
|
94
|
+
* Declares one set parameter (the IR's `ParamSet`): bound at execution
|
|
95
|
+
* to a readonly array; a binding position matches on set membership.
|
|
96
|
+
*/
|
|
97
|
+
paramSet<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): ParamSet<Name, V>
|
|
98
|
+
/**
|
|
99
|
+
* Declares one Allen-mask parameter (`MaskTerm::Param`): the temporal
|
|
100
|
+
* relation as a bind-time 13-bit mask argument.
|
|
101
|
+
*/
|
|
102
|
+
allenParam<const Name extends string>(name: Name): MaskParam<Name>
|
|
103
|
+
/**
|
|
104
|
+
* Declares one IDB predicate with a named, field-typed head; its rules
|
|
105
|
+
* are given in the declaration through a callback receiving the
|
|
106
|
+
* predicate's own reference (self-recursion is `self.match({...})`).
|
|
107
|
+
* `.match` on the returned value uses it as a body atom elsewhere.
|
|
108
|
+
*/
|
|
109
|
+
predicate<
|
|
110
|
+
const Cols extends PredicateColumnsInput,
|
|
111
|
+
const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
|
|
112
|
+
>(
|
|
113
|
+
name: string,
|
|
114
|
+
columns: Cols,
|
|
115
|
+
rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
|
|
116
|
+
): Predicate<ColumnValues<Cols>, PredicateParams<Rules>>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What the build callback returns: `rules` is an array of conjunctions
|
|
121
|
+
* (multiple rules = set union — answers are SETS, no order/limit exists;
|
|
122
|
+
* the host sorts); `select` is the head record — non-aggregate entries are
|
|
123
|
+
* the implicit group key.
|
|
124
|
+
*/
|
|
125
|
+
interface QueryBuild {
|
|
126
|
+
readonly rules: readonly (readonly AnyBodyItem[])[]
|
|
127
|
+
readonly select: SelectShape
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One classified select column as runtime data. */
|
|
131
|
+
type SelectEntryData =
|
|
132
|
+
| { readonly kind: "var"; readonly over: AnyVar }
|
|
133
|
+
| { readonly kind: "measure"; readonly over: AnyVar }
|
|
134
|
+
| { readonly kind: "aggregate"; readonly aggregate: AggregateData }
|
|
135
|
+
|
|
136
|
+
/** One answer column: its name (the row object key) and its entry. */
|
|
137
|
+
interface SelectColumn {
|
|
138
|
+
readonly name: string
|
|
139
|
+
readonly entry: SelectEntryData
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A query's runtime description — everything lowering and execution read. */
|
|
143
|
+
interface QueryData {
|
|
144
|
+
readonly registry: QueryRegistry
|
|
145
|
+
readonly rules: readonly (readonly AnyBodyItem[])[]
|
|
146
|
+
readonly select: readonly SelectColumn[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* An inert query value. `Row` is the inferred answer-row object type;
|
|
151
|
+
* `Params` the inferred execute-params object type. Prepare with
|
|
152
|
+
* `db.prepare(q)`; nothing here touches an engine.
|
|
153
|
+
*/
|
|
154
|
+
interface Query<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
|
|
155
|
+
readonly schema: Schema<Rels>
|
|
156
|
+
readonly data: QueryData
|
|
157
|
+
readonly [phantom]?: { readonly row: Row; readonly params: Params }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Any query value, whatever its schema and inferred types. */
|
|
161
|
+
type AnyQuery = Query<SchemaRelations, unknown, ParamsRecord>
|
|
162
|
+
|
|
163
|
+
/** Extracts a query value's inferred answer-row type. */
|
|
164
|
+
type QueryRow<Q extends AnyQuery> = Exclude<Q[typeof phantom], undefined>["row"]
|
|
165
|
+
|
|
166
|
+
/** Extracts a query value's inferred execute-params type. */
|
|
167
|
+
type QueryParams<Q extends AnyQuery> = Exclude<Q[typeof phantom], undefined>["params"]
|
|
168
|
+
|
|
169
|
+
/** The params object type a build result implies (union over every rule item). */
|
|
170
|
+
type BuildParams<R extends QueryBuild> = ParamsShape<ItemParams<R["rules"][number][number]>>
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Narrows an aggregate select value — a trusted seam over the surface's
|
|
174
|
+
* own constructors (only `#query/select.ts` produces `aggregate`-carrying
|
|
175
|
+
* values), the same direction as every other constructor-owned shape here.
|
|
176
|
+
*/
|
|
177
|
+
function isAggregateEntry(value: object): value is { readonly aggregate: AggregateData } {
|
|
178
|
+
return "aggregate" in value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Narrows a `duration()` select value (only `duration` produces `measure`). */
|
|
182
|
+
function isMeasureEntry(value: object): value is { readonly measure: AnyVar } {
|
|
183
|
+
return "measure" in value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Classifies one select record value into its runtime entry. */
|
|
187
|
+
function selectEntryOf(name: string, value: unknown): SelectEntryData {
|
|
188
|
+
if (isTerm(value)) {
|
|
189
|
+
if (value[term] !== "var") {
|
|
190
|
+
throw errors.new(
|
|
191
|
+
`query select column ${name}: a ${value[term]} is not projectable — select takes variables, duration(v), or aggregates`
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
return Object.freeze({ kind: "var" as const, over: value })
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === "object" && value !== null) {
|
|
197
|
+
if (isAggregateEntry(value)) {
|
|
198
|
+
return Object.freeze({ kind: "aggregate" as const, aggregate: value.aggregate })
|
|
199
|
+
}
|
|
200
|
+
if (isMeasureEntry(value)) {
|
|
201
|
+
return Object.freeze({ kind: "measure" as const, over: value.measure })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw errors.new(
|
|
205
|
+
`query select column ${name}: not a select entry — select takes variables, duration(v), or aggregates`
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Renders an atom source's name for construction diagnostics. */
|
|
210
|
+
function sourceName(source: AtomSourceData): string {
|
|
211
|
+
if (source.kind === "relation") {
|
|
212
|
+
return source.relation.name
|
|
213
|
+
}
|
|
214
|
+
return `predicate ${source.pred.name}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Collects every variable a rule's positive atoms bind. */
|
|
218
|
+
function positiveVarsOf(body: readonly AnyBodyItem[]): Set<AnyVar> {
|
|
219
|
+
const positive = new Set<AnyVar>()
|
|
220
|
+
for (const item of body) {
|
|
221
|
+
if (item.item !== "atom" || item.negated) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
for (const binding of item.bindings) {
|
|
225
|
+
if (binding.term.kind === "term" && binding.term.value[term] === "var") {
|
|
226
|
+
positive.add(binding.term.value)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return positive
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Judges one negated atom against the rule's positively-bound variables. */
|
|
234
|
+
function assertNegatedAtomSafe(context: string, positive: ReadonlySet<AnyVar>, atom: MatchAtom<ParamsRecord>): void {
|
|
235
|
+
for (const binding of atom.bindings) {
|
|
236
|
+
if (binding.term.kind === "oneOf") {
|
|
237
|
+
throw errors.new(
|
|
238
|
+
`${context}: negated ${sourceName(atom.source)} atom binds ${binding.field} with oneOf(...) — its lowering mints a variable no positive atom binds (the safety rule); write one negated atom per literal, or bind a paramSet`
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
if (binding.term.kind !== "term" || binding.term.value[term] !== "var") {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
const variable = binding.term.value
|
|
245
|
+
if (!positive.has(variable)) {
|
|
246
|
+
throw errors.new(
|
|
247
|
+
`${context}: negated ${sourceName(atom.source)} atom binds the variable declared from ${variable.relation}.${variable.field} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The negation safety rule, judged at construction (earlier and warmer
|
|
255
|
+
* than the engine's refusal, which also stands): every variable a negated
|
|
256
|
+
* atom uses must be bound by a positive atom of the same rule — a negated
|
|
257
|
+
* atom binds nothing, only rejects.
|
|
258
|
+
*/
|
|
259
|
+
function assertNegationSafety(context: string, body: readonly AnyBodyItem[]): void {
|
|
260
|
+
const positive = positiveVarsOf(body)
|
|
261
|
+
for (const item of body) {
|
|
262
|
+
if (item.item === "atom" && item.negated) {
|
|
263
|
+
assertNegatedAtomSafe(context, positive, item)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Builds a query as a typed value: runs `build` inside a fresh scope,
|
|
270
|
+
* classifies the select record (written order = answer column order),
|
|
271
|
+
* validates negation safety across every rule (the predicates' rules
|
|
272
|
+
* included), and freezes. Rule counts, strata legality, and every deeper
|
|
273
|
+
* roster stay the engine's judge at prepare.
|
|
274
|
+
*/
|
|
275
|
+
function query<Rels extends SchemaRelations, const R extends QueryBuild>(
|
|
276
|
+
theory: Schema<Rels>,
|
|
277
|
+
build: ($: Scope<Rels>) => R
|
|
278
|
+
): Query<Rels, RowOf<R["select"]>, BuildParams<R>> {
|
|
279
|
+
const registry = createRegistry(theory)
|
|
280
|
+
const scope: Scope<Rels> = Object.freeze({
|
|
281
|
+
var<V>(field: FieldRef<keyof Rels & string, string, V>): Var<V> {
|
|
282
|
+
return scopeVar(registry, field)
|
|
283
|
+
},
|
|
284
|
+
param<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): Param<Name, V> {
|
|
285
|
+
return scopeParam(registry, name, field)
|
|
286
|
+
},
|
|
287
|
+
paramSet<const Name extends string, V>(
|
|
288
|
+
name: Name,
|
|
289
|
+
field: FieldRef<keyof Rels & string, string, V>
|
|
290
|
+
): ParamSet<Name, V> {
|
|
291
|
+
return scopeParamSet(registry, name, field)
|
|
292
|
+
},
|
|
293
|
+
allenParam<const Name extends string>(name: Name): MaskParam<Name> {
|
|
294
|
+
return scopeAllenParam(registry, name)
|
|
295
|
+
},
|
|
296
|
+
predicate<
|
|
297
|
+
const Cols extends PredicateColumnsInput,
|
|
298
|
+
const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
|
|
299
|
+
>(
|
|
300
|
+
name: string,
|
|
301
|
+
columns: Cols,
|
|
302
|
+
rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
|
|
303
|
+
): Predicate<ColumnValues<Cols>, PredicateParams<Rules>> {
|
|
304
|
+
return makePredicate(registry, name, columns, rules)
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
const built = build(scope)
|
|
308
|
+
const select: SelectColumn[] = []
|
|
309
|
+
for (const [name, value] of Object.entries(built.select)) {
|
|
310
|
+
assertDeclarationOrderKey("query select column", name)
|
|
311
|
+
select.push(Object.freeze({ name, entry: selectEntryOf(name, value) }))
|
|
312
|
+
}
|
|
313
|
+
for (const pred of registry.predicates) {
|
|
314
|
+
pred.rules.forEach(function validateRule(rule, index) {
|
|
315
|
+
assertNegationSafety(`query construction (predicate ${pred.name}, rule ${index})`, rule.body)
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
const rules = built.rules.map(function freezeRule(rule, index) {
|
|
319
|
+
assertNegationSafety(`query construction (rule ${index})`, rule)
|
|
320
|
+
return Object.freeze([...rule])
|
|
321
|
+
})
|
|
322
|
+
Object.freeze(registry.params)
|
|
323
|
+
Object.freeze(registry.predicates)
|
|
324
|
+
Object.freeze(registry)
|
|
325
|
+
return Object.freeze({
|
|
326
|
+
schema: theory,
|
|
327
|
+
data: Object.freeze({ registry, rules: Object.freeze(rules), select: Object.freeze(select) })
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The typed shape refusal of the literal tagger — a genuine failure, never data. */
|
|
332
|
+
function literalShapeError(context: string, expected: string, value: unknown): Error {
|
|
333
|
+
return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Narrows an interval-shaped literal (a plain `{ start, end }` bigint pair). */
|
|
337
|
+
function isIntervalShaped(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
|
|
338
|
+
return (
|
|
339
|
+
typeof value === "object" &&
|
|
340
|
+
value !== null &&
|
|
341
|
+
"start" in value &&
|
|
342
|
+
"end" in value &&
|
|
343
|
+
typeof value.start === "bigint" &&
|
|
344
|
+
typeof value.end === "bigint"
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
350
|
+
* structural type directs the tag, never a guess. At an interval field a
|
|
351
|
+
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
352
|
+
* rule (point membership), an interval-shaped literal as the interval
|
|
353
|
+
* (value equality). A closed-reference literal is its branded id, tagged
|
|
354
|
+
* u64 after a roster re-verification (queries cross ids, never handle
|
|
355
|
+
* names).
|
|
356
|
+
*/
|
|
357
|
+
function taggedLiteral(context: string, field: FieldData, value: unknown): TaggedValue {
|
|
358
|
+
if (field.closed !== undefined) {
|
|
359
|
+
return taggedHandleId(context, field.closed, value)
|
|
360
|
+
}
|
|
361
|
+
switch (field.type.kind) {
|
|
362
|
+
case "bool": {
|
|
363
|
+
if (typeof value !== "boolean") {
|
|
364
|
+
throw literalShapeError(context, "boolean", value)
|
|
365
|
+
}
|
|
366
|
+
return { kind: "bool", value }
|
|
367
|
+
}
|
|
368
|
+
case "u64": {
|
|
369
|
+
if (typeof value !== "bigint") {
|
|
370
|
+
throw literalShapeError(context, "bigint", value)
|
|
371
|
+
}
|
|
372
|
+
return { kind: "u64", value }
|
|
373
|
+
}
|
|
374
|
+
case "i64": {
|
|
375
|
+
if (typeof value !== "bigint") {
|
|
376
|
+
throw literalShapeError(context, "bigint", value)
|
|
377
|
+
}
|
|
378
|
+
return { kind: "i64", value }
|
|
379
|
+
}
|
|
380
|
+
case "string": {
|
|
381
|
+
if (typeof value !== "string") {
|
|
382
|
+
throw literalShapeError(context, "string", value)
|
|
383
|
+
}
|
|
384
|
+
return { kind: "string", value }
|
|
385
|
+
}
|
|
386
|
+
case "fixedBytes": {
|
|
387
|
+
if (!(value instanceof Uint8Array)) {
|
|
388
|
+
throw literalShapeError(context, "Uint8Array", value)
|
|
389
|
+
}
|
|
390
|
+
return { kind: "fixedBytes", value }
|
|
391
|
+
}
|
|
392
|
+
case "interval":
|
|
393
|
+
return taggedAtElementDomain(context, field.type.element, value)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Tags one closed-reference literal: the branded id, re-verified against
|
|
399
|
+
* the roster (the belt the type level cannot provide against forged
|
|
400
|
+
* brands) and tagged u64 — queries cross ids, never handle names.
|
|
401
|
+
*/
|
|
402
|
+
function taggedHandleId(context: string, closed: NonNullable<FieldData["closed"]>, value: unknown): TaggedValue {
|
|
403
|
+
if (typeof value !== "bigint") {
|
|
404
|
+
throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
|
|
405
|
+
}
|
|
406
|
+
if (closed.handles[Number(value)] === undefined) {
|
|
407
|
+
throw errors.new(
|
|
408
|
+
`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
return { kind: "u64", value }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Tags one literal in an interval element domain: a bigint tags as the
|
|
416
|
+
* element (the membership typing rule's point side), an interval-shaped
|
|
417
|
+
* value as the interval (value equality).
|
|
418
|
+
*/
|
|
419
|
+
function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
|
|
420
|
+
if (typeof value === "bigint") {
|
|
421
|
+
if (element === "u64") {
|
|
422
|
+
return { kind: "u64", value }
|
|
423
|
+
}
|
|
424
|
+
return { kind: "i64", value }
|
|
425
|
+
}
|
|
426
|
+
if (isIntervalShaped(value)) {
|
|
427
|
+
if (element === "u64") {
|
|
428
|
+
return { kind: "intervalU64", start: value.start, end: value.end }
|
|
429
|
+
}
|
|
430
|
+
return { kind: "intervalI64", start: value.start, end: value.end }
|
|
431
|
+
}
|
|
432
|
+
throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Tags one host literal at a COMPARISON position, where no field position
|
|
437
|
+
* directs the type: the sibling term's element domain does — a measure
|
|
438
|
+
* sibling is u64, an interval-field sibling contributes its element type
|
|
439
|
+
* (so both a point literal in `covers` and a `span` literal in `allen`
|
|
440
|
+
* tag correctly), a scalar sibling its own type. At `pointIn` the operand
|
|
441
|
+
* order is interval-left, point-right (`ir::CmpOp::PointIn`), so an
|
|
442
|
+
* interval-shaped literal beside a scalar element-typed sibling is the
|
|
443
|
+
* LEGAL lhs of `covers(span(...), t)` and tags as the interval of the
|
|
444
|
+
* sibling's element domain; under every other operator an interval shape
|
|
445
|
+
* against a scalar sibling stays refused (the engine's IllegalComparison
|
|
446
|
+
* — eq/lt interval-vs-scalar is not a comparison).
|
|
447
|
+
*/
|
|
448
|
+
function taggedCmpLiteral(
|
|
449
|
+
context: string,
|
|
450
|
+
sibling: FieldData | "measure",
|
|
451
|
+
value: unknown,
|
|
452
|
+
op: CmpOpData["kind"]
|
|
453
|
+
): TaggedValue {
|
|
454
|
+
if (sibling === "measure") {
|
|
455
|
+
if (typeof value !== "bigint") {
|
|
456
|
+
throw literalShapeError(context, "bigint (the measure is u64)", value)
|
|
457
|
+
}
|
|
458
|
+
return { kind: "u64", value }
|
|
459
|
+
}
|
|
460
|
+
if (sibling.type.kind === "interval") {
|
|
461
|
+
return taggedAtElementDomain(context, sibling.type.element, value)
|
|
462
|
+
}
|
|
463
|
+
if (op === "pointIn" && (sibling.type.kind === "u64" || sibling.type.kind === "i64") && isIntervalShaped(value)) {
|
|
464
|
+
return taggedAtElementDomain(context, sibling.type.kind, value)
|
|
465
|
+
}
|
|
466
|
+
return taggedLiteral(context, sibling, value)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The shared lowering context of one `lowerQuery` run. */
|
|
470
|
+
interface LowerContext {
|
|
471
|
+
readonly theory: AnySchema
|
|
472
|
+
readonly relationIds: ReadonlyMap<string, number>
|
|
473
|
+
readonly predicateIds: ReadonlyMap<PredicateData, number>
|
|
474
|
+
readonly registry: QueryRegistry
|
|
475
|
+
/** Every dense param id some lowered term referenced — the usage census the dead-declaration refusal reads. */
|
|
476
|
+
readonly usedParams: Set<number>
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** One rule's dense variable numbering: first occurrence in written order. */
|
|
480
|
+
interface VarIds {
|
|
481
|
+
of(variable: AnyVar): number
|
|
482
|
+
synthetic(): number
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Creates one rule-scoped variable numberer. */
|
|
486
|
+
function makeVarIds(registry: QueryRegistry): VarIds {
|
|
487
|
+
const assigned = new Map<AnyVar, number>()
|
|
488
|
+
const state = { next: 0 }
|
|
489
|
+
return {
|
|
490
|
+
of(variable) {
|
|
491
|
+
const existing = assigned.get(variable)
|
|
492
|
+
if (existing !== undefined) {
|
|
493
|
+
return existing
|
|
494
|
+
}
|
|
495
|
+
if (!registry.vars.has(variable)) {
|
|
496
|
+
throw errors.new(
|
|
497
|
+
`query lowering: the variable declared from ${variable.relation}.${variable.field} belongs to a different query scope`
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
const id = state.next
|
|
501
|
+
state.next += 1
|
|
502
|
+
assigned.set(variable, id)
|
|
503
|
+
return id
|
|
504
|
+
},
|
|
505
|
+
synthetic() {
|
|
506
|
+
const id = state.next
|
|
507
|
+
state.next += 1
|
|
508
|
+
return id
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Resolves a param term to its dense positional id, recording the use. */
|
|
514
|
+
function paramIdOf(ctx: LowerContext, value: AnyTerm, name: string): number {
|
|
515
|
+
const index = ctx.registry.paramIndex.get(value)
|
|
516
|
+
if (index === undefined) {
|
|
517
|
+
throw errors.new(`query lowering: param ${name} belongs to a different query scope`)
|
|
518
|
+
}
|
|
519
|
+
ctx.usedParams.add(index)
|
|
520
|
+
return index
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Lowers one atom (either polarity) and appends any `oneOf` disjunctions. */
|
|
524
|
+
function lowerAtom(
|
|
525
|
+
ctx: LowerContext,
|
|
526
|
+
atom: MatchAtom<ParamsRecord>,
|
|
527
|
+
ids: VarIds,
|
|
528
|
+
extraConditions: ConditionTreeIr[]
|
|
529
|
+
): AtomIr {
|
|
530
|
+
const source = lowerSource(ctx, atom.source)
|
|
531
|
+
const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
|
|
532
|
+
return [
|
|
533
|
+
fieldOrdinal(atom.source, binding),
|
|
534
|
+
lowerBindingTerm(ctx, atom.source, binding, ids, extraConditions)
|
|
535
|
+
] as const
|
|
536
|
+
})
|
|
537
|
+
return { source, bindings }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Lowers an atom source to its numeric id, verifying scope membership. */
|
|
541
|
+
function lowerSource(ctx: LowerContext, source: AtomSourceData): AtomIr["source"] {
|
|
542
|
+
if (source.kind === "relation") {
|
|
543
|
+
const member = ctx.theory.relations[source.relation.name]
|
|
544
|
+
if (member !== source.relation) {
|
|
545
|
+
throw errors.new(
|
|
546
|
+
`query lowering: relation ${source.relation.name} is not the relation value schema ${ctx.theory.name} declares`
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
const id = ctx.relationIds.get(source.relation.name)
|
|
550
|
+
if (id === undefined) {
|
|
551
|
+
throw errors.new(`query lowering: relation ${source.relation.name} has no ordinal`)
|
|
552
|
+
}
|
|
553
|
+
return { kind: "edb", relation: id }
|
|
554
|
+
}
|
|
555
|
+
const pred = ctx.predicateIds.get(source.pred)
|
|
556
|
+
if (pred === undefined) {
|
|
557
|
+
throw errors.new(`query lowering: predicate ${source.pred.name} was declared in a different query scope`)
|
|
558
|
+
}
|
|
559
|
+
return { kind: "idb", pred }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** A binding's field ordinal: declaration index (relations) or head position (predicates). */
|
|
563
|
+
function fieldOrdinal(source: AtomSourceData, binding: BindingEntry): number {
|
|
564
|
+
if (source.kind === "relation") {
|
|
565
|
+
const ordinal = source.relation.data.fields.findIndex(function byName(candidate) {
|
|
566
|
+
return candidate.name === binding.field
|
|
567
|
+
})
|
|
568
|
+
if (ordinal < 0) {
|
|
569
|
+
throw errors.new(`query lowering: relation ${source.relation.name} has no field ${binding.field}`)
|
|
570
|
+
}
|
|
571
|
+
return ordinal
|
|
572
|
+
}
|
|
573
|
+
const ordinal = source.pred.columns.findIndex(function byName(candidate) {
|
|
574
|
+
return candidate.name === binding.field
|
|
575
|
+
})
|
|
576
|
+
if (ordinal < 0) {
|
|
577
|
+
throw errors.new(`query lowering: predicate ${source.pred.name} has no column ${binding.field}`)
|
|
578
|
+
}
|
|
579
|
+
return ordinal
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Lowers one binding term; an `oneOf` mints a fresh variable + disjunction. */
|
|
583
|
+
function lowerBindingTerm(
|
|
584
|
+
ctx: LowerContext,
|
|
585
|
+
source: AtomSourceData,
|
|
586
|
+
binding: BindingEntry,
|
|
587
|
+
ids: VarIds,
|
|
588
|
+
extraConditions: ConditionTreeIr[]
|
|
589
|
+
): TermIr {
|
|
590
|
+
const context = `${sourceName(source)}.${binding.field}`
|
|
591
|
+
const bound = binding.term
|
|
592
|
+
if (bound.kind === "term") {
|
|
593
|
+
const value = bound.value
|
|
594
|
+
switch (value[term]) {
|
|
595
|
+
case "var":
|
|
596
|
+
return { kind: "var", var: ids.of(value) }
|
|
597
|
+
case "param":
|
|
598
|
+
return { kind: "param", param: paramIdOf(ctx, value, value.name) }
|
|
599
|
+
case "paramSet":
|
|
600
|
+
return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) }
|
|
601
|
+
case "maskParam":
|
|
602
|
+
throw errors.new(`${context}: an Allen-mask param is not a field-typed binding`)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (bound.kind === "oneOf") {
|
|
606
|
+
const minted = ids.synthetic()
|
|
607
|
+
const leaves: ConditionTreeIr[] = bound.values.map(function equalityLeaf(candidate) {
|
|
608
|
+
return {
|
|
609
|
+
kind: "leaf",
|
|
610
|
+
cmp: {
|
|
611
|
+
op: { kind: "eq" },
|
|
612
|
+
lhs: { kind: "var", var: minted },
|
|
613
|
+
rhs: { kind: "literal", value: taggedLiteral(context, binding.data, candidate) }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
extraConditions.push({ kind: "or", children: leaves })
|
|
618
|
+
return { kind: "var", var: minted }
|
|
619
|
+
}
|
|
620
|
+
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Lowers one comparison side; literals tag by the sibling's element domain (op-aware at `pointIn`). */
|
|
624
|
+
function lowerCmpTerm(ctx: LowerContext, side: CmpTerm, sibling: CmpTerm, ids: VarIds, op: CmpOpData["kind"]): TermIr {
|
|
625
|
+
if (side.kind === "term") {
|
|
626
|
+
const value = side.value
|
|
627
|
+
switch (value[term]) {
|
|
628
|
+
case "var":
|
|
629
|
+
return { kind: "var", var: ids.of(value) }
|
|
630
|
+
case "param":
|
|
631
|
+
return { kind: "param", param: paramIdOf(ctx, value, value.name) }
|
|
632
|
+
case "paramSet":
|
|
633
|
+
return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) }
|
|
634
|
+
case "maskParam":
|
|
635
|
+
throw errors.new(
|
|
636
|
+
"query lowering: an Allen-mask param is not a comparison term — masks live in allen()'s mask position"
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (side.kind === "measure") {
|
|
641
|
+
return { kind: "measure", var: ids.of(side.over) }
|
|
642
|
+
}
|
|
643
|
+
if (sibling.kind === "term") {
|
|
644
|
+
const value = sibling.value
|
|
645
|
+
if (value[term] === "maskParam") {
|
|
646
|
+
throw errors.new("query lowering: an Allen-mask param cannot type a literal side")
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
kind: "literal",
|
|
650
|
+
value: taggedCmpLiteral("comparison literal", value.data, side.value, op)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (sibling.kind === "measure") {
|
|
654
|
+
return {
|
|
655
|
+
kind: "literal",
|
|
656
|
+
value: taggedCmpLiteral("comparison literal", "measure", side.value, op)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
throw errors.new("query lowering: a comparison without a variable or parameter side is constant-valued")
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Lowers one comparison. */
|
|
663
|
+
function lowerComparison(ctx: LowerContext, cmp: ComparisonItem<ParamsRecord>, ids: VarIds): ComparisonIr {
|
|
664
|
+
const op = cmp.op
|
|
665
|
+
if (op.kind === "allen") {
|
|
666
|
+
const mask =
|
|
667
|
+
op.mask.kind === "literal"
|
|
668
|
+
? ({ kind: "literal", mask: op.mask.mask } as const)
|
|
669
|
+
: ({ kind: "param", param: paramIdOf(ctx, op.mask.param, op.mask.param.name) } as const)
|
|
670
|
+
return {
|
|
671
|
+
op: { kind: "allen", mask },
|
|
672
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
673
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
op: { kind: op.kind },
|
|
678
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, op.kind),
|
|
679
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, op.kind)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
684
|
+
function lowerCondition(ctx: LowerContext, condition: AnyCondition, ids: VarIds): ConditionTreeIr {
|
|
685
|
+
if (condition.item === "cmp") {
|
|
686
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, condition, ids) }
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
kind: condition.op,
|
|
690
|
+
children: condition.children.map(function lowerChild(child) {
|
|
691
|
+
return lowerCondition(ctx, child, ids)
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Lowers one select entry to its per-rule find term. */
|
|
697
|
+
function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
|
|
698
|
+
if (entry.kind === "var") {
|
|
699
|
+
return { kind: "var", var: ids.of(entry.over) }
|
|
700
|
+
}
|
|
701
|
+
if (entry.kind === "measure") {
|
|
702
|
+
return { kind: "measure", var: ids.of(entry.over) }
|
|
703
|
+
}
|
|
704
|
+
const aggregate = entry.aggregate
|
|
705
|
+
switch (aggregate.op) {
|
|
706
|
+
case "count":
|
|
707
|
+
return { kind: "aggregate", op: { kind: "count" } }
|
|
708
|
+
case "countDistinct":
|
|
709
|
+
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(aggregate.over) }
|
|
710
|
+
case "fold": {
|
|
711
|
+
const over = aggregate.over
|
|
712
|
+
if (isTerm(over)) {
|
|
713
|
+
return { kind: "aggregate", op: { kind: aggregate.fold }, over: ids.of(over) }
|
|
714
|
+
}
|
|
715
|
+
return { kind: "aggregateMeasure", op: { kind: aggregate.fold }, over: ids.of(over.measure) }
|
|
716
|
+
}
|
|
717
|
+
case "arg":
|
|
718
|
+
return {
|
|
719
|
+
kind: "aggregate",
|
|
720
|
+
op: { kind: aggregate.direction, key: ids.of(aggregate.key) },
|
|
721
|
+
over: ids.of(aggregate.over)
|
|
722
|
+
}
|
|
723
|
+
case "pack":
|
|
724
|
+
return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(aggregate.over) }
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** One aggregate's var-free head-op kind (`AggOp::head_op`). */
|
|
729
|
+
function headOpOf(aggregate: AggregateData): HeadOpIr {
|
|
730
|
+
switch (aggregate.op) {
|
|
731
|
+
case "count":
|
|
732
|
+
return "count"
|
|
733
|
+
case "countDistinct":
|
|
734
|
+
return "countDistinct"
|
|
735
|
+
case "fold":
|
|
736
|
+
return aggregate.fold
|
|
737
|
+
case "arg":
|
|
738
|
+
return aggregate.direction
|
|
739
|
+
case "pack":
|
|
740
|
+
return "pack"
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** One select entry's var-free head shape. */
|
|
745
|
+
function headTermOf(column: SelectColumn): HeadTermIr {
|
|
746
|
+
const entry = column.entry
|
|
747
|
+
if (entry.kind === "var" || entry.kind === "measure") {
|
|
748
|
+
return { kind: "var" }
|
|
749
|
+
}
|
|
750
|
+
return { kind: "aggregate", op: headOpOf(entry.aggregate) }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Lowers one rule: body walked in written order, finds supplied per shape. */
|
|
754
|
+
function lowerRule(ctx: LowerContext, body: readonly AnyBodyItem[], finds: (ids: VarIds) => FindTermIr[]): RuleIr {
|
|
755
|
+
const ids = makeVarIds(ctx.registry)
|
|
756
|
+
const atoms: AtomIr[] = []
|
|
757
|
+
const negated: AtomIr[] = []
|
|
758
|
+
const conditions: ConditionTreeIr[] = []
|
|
759
|
+
const extraConditions: ConditionTreeIr[] = []
|
|
760
|
+
for (const item of body) {
|
|
761
|
+
if (item.item === "atom") {
|
|
762
|
+
const lowered = lowerAtom(ctx, item, ids, extraConditions)
|
|
763
|
+
if (item.negated) {
|
|
764
|
+
negated.push(lowered)
|
|
765
|
+
} else {
|
|
766
|
+
atoms.push(lowered)
|
|
767
|
+
}
|
|
768
|
+
continue
|
|
769
|
+
}
|
|
770
|
+
conditions.push(lowerCondition(ctx, item, ids))
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
finds: finds(ids),
|
|
774
|
+
atoms,
|
|
775
|
+
negated,
|
|
776
|
+
conditions: [...conditions, ...extraConditions]
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
782
|
+
* declared predicates in declaration order (`PredId` = index), the output
|
|
783
|
+
* predicate (built from `rules` + `select`) appended last. Relations lower
|
|
784
|
+
* by declaration ordinal, the law the engine's own manifest pins;
|
|
785
|
+
* `db.prepare` re-verifies the alignment against the live manifest before
|
|
786
|
+
* sending.
|
|
787
|
+
*/
|
|
788
|
+
function lowerQuery(q: AnyQuery): ProgramIr {
|
|
789
|
+
const theory = q.schema
|
|
790
|
+
const relationIds = new Map<string, number>()
|
|
791
|
+
Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
|
|
792
|
+
relationIds.set(name, index)
|
|
793
|
+
})
|
|
794
|
+
const registry = q.data.registry
|
|
795
|
+
const predicateIds = new Map<PredicateData, number>()
|
|
796
|
+
registry.predicates.forEach(function assignPredId(pred, index) {
|
|
797
|
+
predicateIds.set(pred, index)
|
|
798
|
+
})
|
|
799
|
+
const ctx: LowerContext = { theory, relationIds, predicateIds, registry, usedParams: new Set() }
|
|
800
|
+
const predicates: PredicateDefIr[] = registry.predicates.map(function lowerPredicate(pred) {
|
|
801
|
+
return {
|
|
802
|
+
head: pred.columns.map(function boundHead(): HeadTermIr {
|
|
803
|
+
return { kind: "var" }
|
|
804
|
+
}),
|
|
805
|
+
rules: pred.rules.map(function lowerClause(rule) {
|
|
806
|
+
return lowerRule(ctx, rule.body, function clauseFinds(ids) {
|
|
807
|
+
return rule.finds.map(function findVar(variable): FindTermIr {
|
|
808
|
+
return { kind: "var", var: ids.of(variable) }
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
predicates.push({
|
|
815
|
+
head: q.data.select.map(headTermOf),
|
|
816
|
+
rules: q.data.rules.map(function lowerOutputRule(body) {
|
|
817
|
+
return lowerRule(ctx, body, function outputFinds(ids) {
|
|
818
|
+
return q.data.select.map(function findOf(column) {
|
|
819
|
+
return lowerFind(column.entry, ids)
|
|
820
|
+
})
|
|
821
|
+
})
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
/**
|
|
825
|
+
* The dead-declaration refusal: a declared param no rule reached is
|
|
826
|
+
* unexecutable BOTH ways — its inferred `Params` contribution is nothing
|
|
827
|
+
* (params ride item phantoms), yet the wire marshal is the full registry
|
|
828
|
+
* in declaration order, while the ENGINE's arity is usage-derived. The
|
|
829
|
+
* contradiction is refused here, the earliest seam that knows usage, so
|
|
830
|
+
* every query that lowers has registry == used set (and the dense-id
|
|
831
|
+
* hole a skipped middle declaration would open never exists).
|
|
832
|
+
*/
|
|
833
|
+
registry.params.forEach(function assertUsed(entry, index) {
|
|
834
|
+
if (!ctx.usedParams.has(index)) {
|
|
835
|
+
throw errors.new(
|
|
836
|
+
`query declares param ${entry.name} but no rule uses it — remove the declaration or reference it`
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
return { predicates, output: registry.predicates.length }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export type {
|
|
844
|
+
AnyQuery,
|
|
845
|
+
BuildParams,
|
|
846
|
+
Query,
|
|
847
|
+
QueryBuild,
|
|
848
|
+
QueryData,
|
|
849
|
+
QueryParams,
|
|
850
|
+
QueryRow,
|
|
851
|
+
Scope,
|
|
852
|
+
SelectColumn,
|
|
853
|
+
SelectEntryData
|
|
854
|
+
}
|
|
855
|
+
export { lowerQuery, query, taggedLiteral }
|