@bjornpagen/bumbledb 0.2.0 → 0.4.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/COOKBOOK.md +503 -427
- package/README.md +84 -36
- package/dist/closed.d.ts +111 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +94 -99
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +16 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +46 -8
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +114 -69
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +38 -21
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +72 -87
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +35 -67
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +224 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +224 -0
- package/dist/law.js.map +1 -0
- package/dist/lower.d.ts +17 -10
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +34 -23
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +67 -6
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +6 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +139 -56
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +7 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +71 -56
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +341 -69
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +10 -9
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +2 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -6
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +100 -43
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +89 -30
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +5 -5
- package/dist/query/select.d.ts.map +1 -1
- package/dist/relation.d.ts +29 -15
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +45 -17
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +41 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +16 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +10 -8
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +67 -31
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +97 -20
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +214 -146
- package/src/db.ts +65 -10
- package/src/face.ts +169 -102
- package/src/fields.ts +97 -164
- package/src/index.ts +43 -18
- package/src/law.ts +519 -0
- package/src/lower.ts +36 -23
- package/src/marshal.ts +74 -7
- package/src/native.ts +6 -2
- package/src/query/atom.ts +155 -65
- package/src/query/lower.ts +572 -167
- package/src/query/predicate.ts +43 -33
- package/src/query/run.ts +26 -6
- package/src/query/scope.ts +161 -51
- package/src/query/select.ts +5 -5
- package/src/relation.ts +60 -26
- package/src/schema.ts +48 -7
- package/src/spec.ts +10 -8
- package/src/statements.ts +165 -46
package/src/query/lower.ts
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import * as errors from "@superbuilders/errors"
|
|
29
|
-
import type { AnyField } from "#fields.ts"
|
|
29
|
+
import type { AnyField, ClosedRoster } from "#fields.ts"
|
|
30
30
|
import { assertDeclarationOrderKey } from "#fields.ts"
|
|
31
|
+
import type { ClassRecordOf, SchemaClasses } from "#law.ts"
|
|
31
32
|
import type {
|
|
32
33
|
AtomIr,
|
|
33
34
|
ComparisonIr,
|
|
@@ -56,6 +57,8 @@ import type {
|
|
|
56
57
|
CondData,
|
|
57
58
|
CondParamsShape,
|
|
58
59
|
MaskData,
|
|
60
|
+
MatchFields,
|
|
61
|
+
MatchOwner,
|
|
59
62
|
MatchShape,
|
|
60
63
|
ParamUse,
|
|
61
64
|
RecData,
|
|
@@ -66,7 +69,16 @@ import type {
|
|
|
66
69
|
TreeData
|
|
67
70
|
} from "#query/atom.ts"
|
|
68
71
|
import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
|
|
69
|
-
import type {
|
|
72
|
+
import type {
|
|
73
|
+
ClassedField,
|
|
74
|
+
EnvShape,
|
|
75
|
+
Flatten,
|
|
76
|
+
InferredOf,
|
|
77
|
+
JoinOk,
|
|
78
|
+
ParamEntry,
|
|
79
|
+
ParamsRecord,
|
|
80
|
+
Var
|
|
81
|
+
} from "#query/scope.ts"
|
|
70
82
|
import {
|
|
71
83
|
fieldJoins,
|
|
72
84
|
inferred,
|
|
@@ -76,20 +88,27 @@ import {
|
|
|
76
88
|
makeParam,
|
|
77
89
|
makeSetParam,
|
|
78
90
|
makeVar,
|
|
91
|
+
makeVars,
|
|
79
92
|
renderFieldKind,
|
|
80
93
|
term
|
|
81
94
|
} from "#query/scope.ts"
|
|
82
95
|
import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
|
|
83
96
|
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
|
|
84
|
-
import type {
|
|
97
|
+
import type { FieldsShape, RelationField } from "#relation.ts"
|
|
85
98
|
import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
|
|
86
99
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The matchable members of a schema's record — ordinary relations AND
|
|
102
|
+
* closed vocabularies (ψ query atoms: a closed atom is an ordinary EDB
|
|
103
|
+
* atom over the sealed extension; the ENGINE decides whether it folds to a
|
|
104
|
+
* plan-constant member set or joins the L1-resident virtual image — the
|
|
105
|
+
* SDK lowers pass-through and never knows which).
|
|
106
|
+
*/
|
|
107
|
+
type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
|
|
89
108
|
|
|
90
|
-
/** The environment after one bindings record: the incoming env plus every var the record binds. */
|
|
91
|
-
type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, B> =
|
|
92
|
-
Flatten<Env & BindEnv<F, B>> extends infer E extends EnvShape ? E : never
|
|
109
|
+
/** The environment after one bindings record: the incoming env plus every var the record binds (as classed slots). */
|
|
110
|
+
type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, CR, B> =
|
|
111
|
+
Flatten<Env & BindEnv<F, CR, B>> extends infer E extends EnvShape ? E : never
|
|
93
112
|
|
|
94
113
|
/** Reads an inferred-params carrier off a rec reference or rule value. */
|
|
95
114
|
type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
|
|
@@ -98,12 +117,13 @@ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends Para
|
|
|
98
117
|
type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
|
|
99
118
|
|
|
100
119
|
/**
|
|
101
|
-
* A recursive predicate's HEAD signature as
|
|
102
|
-
* for position — carried on the rec
|
|
103
|
-
*
|
|
104
|
-
* query rule, or an unthreaded rec
|
|
120
|
+
* A recursive predicate's HEAD signature as classed slots (descriptor +
|
|
121
|
+
* law-computed class), position for position — carried on the rec
|
|
122
|
+
* reference so an `idb` join can be judged against it; `undefined` on
|
|
123
|
+
* values that carry no head (a plain query rule, or an unthreaded rec
|
|
124
|
+
* handle before its first rule).
|
|
105
125
|
*/
|
|
106
|
-
type HeadShape = readonly
|
|
126
|
+
type HeadShape = readonly ClassedField[] | undefined
|
|
107
127
|
|
|
108
128
|
/**
|
|
109
129
|
* One finished rule as a plain value: the runtime data plus the inferred
|
|
@@ -125,7 +145,7 @@ type HeadFieldsOf<Env extends EnvShape, S extends readonly string[]> = {
|
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
/** Reads an inferred-head carrier off a rule value or rec reference. */
|
|
128
|
-
type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly
|
|
148
|
+
type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly ClassedField[] } ? H : undefined
|
|
129
149
|
|
|
130
150
|
/**
|
|
131
151
|
* A recursive predicate REFERENCE — the shape `idb()` targets carry: the
|
|
@@ -142,11 +162,11 @@ interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadS
|
|
|
142
162
|
readonly [inferred]?: { readonly params: P; readonly head: Head }
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
/** One `idb` position's judgment: the var must be bound by a relation atom,
|
|
165
|
+
/** One `idb` position's judgment: the var must be bound by a relation atom, class-equal to the head slot when the head is carried. */
|
|
146
166
|
type IdbVarOk<Env extends EnvShape, T, F> =
|
|
147
167
|
T extends Var<infer N extends string>
|
|
148
168
|
? N extends keyof Env
|
|
149
|
-
? F extends
|
|
169
|
+
? F extends ClassedField
|
|
150
170
|
? JoinOk<Env[N], F>
|
|
151
171
|
: true
|
|
152
172
|
: false
|
|
@@ -156,12 +176,12 @@ type IdbVarOk<Env extends EnvShape, T, F> =
|
|
|
156
176
|
* The validated `idb` variable tuple: every var must already be bound by a
|
|
157
177
|
* relation atom of the rule; and when the target carries its head
|
|
158
178
|
* signature (the threaded rec handle), the tuple must match the head's
|
|
159
|
-
* arity and every position must be
|
|
179
|
+
* arity and every position must be class-equal to its head slot — the
|
|
160
180
|
* same wall `JoinOk` holds for EDB atoms. An unthreaded handle carries no
|
|
161
|
-
* head; its joins stay boundness-checked here and arity/
|
|
181
|
+
* head; its joins stay boundness-checked here and arity/class-judged at
|
|
162
182
|
* prepare (the engine's law stands behind both tiers).
|
|
163
183
|
*/
|
|
164
|
-
type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly
|
|
184
|
+
type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly ClassedField[]
|
|
165
185
|
? V extends readonly unknown[]
|
|
166
186
|
? V["length"] extends Head["length"]
|
|
167
187
|
? { readonly [I in keyof V]: IdbVarOk<Env, V[I], Head[I & keyof Head]> extends true ? V[I] : never }
|
|
@@ -178,6 +198,8 @@ type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> =
|
|
|
178
198
|
interface TermOps {
|
|
179
199
|
/** Declares/names one variable: typed by the field it first binds; reuse joins. */
|
|
180
200
|
readonly var: typeof makeVar
|
|
201
|
+
/** Mints several variables at once — `const { service, w } = r.vars("service", "w")`: each name typed exactly; duplicates refuse. */
|
|
202
|
+
readonly vars: typeof makeVars
|
|
181
203
|
/** Names one scalar parameter: typed by its use; the key of the execute params object. */
|
|
182
204
|
readonly param: typeof makeParam
|
|
183
205
|
/** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
|
|
@@ -208,51 +230,77 @@ interface TermOps {
|
|
|
208
230
|
readonly pack: typeof pack
|
|
209
231
|
}
|
|
210
232
|
|
|
211
|
-
/** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom. */
|
|
212
|
-
interface QueryRuleScope<Rels extends SchemaRelations> extends TermOps {
|
|
213
|
-
/** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join
|
|
214
|
-
match<R extends QueryRelation<Rels>, const B extends MatchShape<
|
|
233
|
+
/** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom (`Classes` — the schema type's class map, the join judge's authority). */
|
|
234
|
+
interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
235
|
+
/** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join class-equal). */
|
|
236
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
215
237
|
relation: R,
|
|
216
|
-
bindings: B & CheckBindings<Record<never, never>,
|
|
217
|
-
): QueryRuleChain<
|
|
238
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
239
|
+
): QueryRuleChain<
|
|
240
|
+
Rels,
|
|
241
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
242
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
243
|
+
Classes
|
|
244
|
+
>
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
/** The chain of a plain query rule: more atoms, residual predicates, then the head. */
|
|
221
|
-
interface QueryRuleChain<
|
|
222
|
-
|
|
223
|
-
|
|
248
|
+
interface QueryRuleChain<
|
|
249
|
+
Rels extends SchemaRelations,
|
|
250
|
+
Env extends EnvShape,
|
|
251
|
+
P extends ParamsRecord,
|
|
252
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
253
|
+
> {
|
|
254
|
+
/** One more positive EDB atom — var reuse joins, class-equal by the environment check. */
|
|
255
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
224
256
|
relation: R,
|
|
225
|
-
bindings: B & CheckBindings<Env,
|
|
226
|
-
): QueryRuleChain<
|
|
257
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
258
|
+
): QueryRuleChain<
|
|
259
|
+
Rels,
|
|
260
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
261
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
262
|
+
Classes
|
|
263
|
+
>
|
|
227
264
|
/** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
|
|
228
265
|
where<const C extends AnyCond>(
|
|
229
|
-
cond: CheckCond<Env, C> & C
|
|
230
|
-
): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C
|
|
266
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
267
|
+
): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
231
268
|
/** The head projection: var names, the measure, and aggregates; written order = answer column order. */
|
|
232
269
|
select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
/** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
|
|
236
|
-
interface OutputRuleScope<Rels extends SchemaRelations> extends TermOps {
|
|
237
|
-
match<R extends QueryRelation<Rels>, const B extends MatchShape<
|
|
273
|
+
interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
274
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
238
275
|
relation: R,
|
|
239
|
-
bindings: B & CheckBindings<Record<never, never>,
|
|
276
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
240
277
|
): OutputRuleChain<
|
|
241
278
|
Rels,
|
|
242
|
-
EnvOfMatch<Record<never, never>,
|
|
243
|
-
BindParamsShape<
|
|
279
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
280
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
281
|
+
Classes
|
|
244
282
|
>
|
|
245
283
|
}
|
|
246
284
|
|
|
247
285
|
/** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
|
|
248
|
-
interface OutputRuleChain<
|
|
249
|
-
|
|
286
|
+
interface OutputRuleChain<
|
|
287
|
+
Rels extends SchemaRelations,
|
|
288
|
+
Env extends EnvShape,
|
|
289
|
+
P extends ParamsRecord,
|
|
290
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
291
|
+
> {
|
|
292
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
250
293
|
relation: R,
|
|
251
|
-
bindings: B & CheckBindings<Env,
|
|
252
|
-
): OutputRuleChain<
|
|
294
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
295
|
+
): OutputRuleChain<
|
|
296
|
+
Rels,
|
|
297
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
298
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
299
|
+
Classes
|
|
300
|
+
>
|
|
253
301
|
where<const C extends AnyCond>(
|
|
254
|
-
cond: CheckCond<Env, C> & C
|
|
255
|
-
): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C
|
|
302
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
303
|
+
): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
256
304
|
/**
|
|
257
305
|
* One `idb` atom over a FINISHED stratum (any rec of this program): a
|
|
258
306
|
* positional join against the rec's head. An idb atom is a join
|
|
@@ -260,26 +308,28 @@ interface OutputRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P
|
|
|
260
308
|
* the rule (the theory's own domain relation; the rec's answers are
|
|
261
309
|
* theory values, so the join is identity). Threading the rec value the
|
|
262
310
|
* last `.rule(...)` returned carries its rules' params into `Params`
|
|
263
|
-
* AND its head signature, so the join is arity- and
|
|
311
|
+
* AND its head signature, so the join is arity- and class-checked
|
|
264
312
|
* against the head at compile time.
|
|
265
313
|
*/
|
|
266
314
|
idb<Target extends RecRef<string, ParamsRecord>, const V extends readonly Var<string>[]>(
|
|
267
315
|
target: Target,
|
|
268
316
|
...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
|
|
269
|
-
): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target
|
|
317
|
+
): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>, Classes>
|
|
270
318
|
select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
|
|
271
319
|
}
|
|
272
320
|
|
|
273
321
|
/** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
|
|
274
|
-
interface RecRuleScope<Rels extends SchemaRelations, Self extends string
|
|
275
|
-
|
|
322
|
+
interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classes extends SchemaClasses = SchemaClasses>
|
|
323
|
+
extends TermOps {
|
|
324
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
276
325
|
relation: R,
|
|
277
|
-
bindings: B & CheckBindings<Record<never, never>,
|
|
326
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
278
327
|
): RecRuleChain<
|
|
279
328
|
Rels,
|
|
280
329
|
Self,
|
|
281
|
-
EnvOfMatch<Record<never, never>,
|
|
282
|
-
BindParamsShape<
|
|
330
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
331
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
332
|
+
Classes
|
|
283
333
|
>
|
|
284
334
|
}
|
|
285
335
|
|
|
@@ -295,21 +345,28 @@ interface RecRuleChain<
|
|
|
295
345
|
Rels extends SchemaRelations,
|
|
296
346
|
Self extends string,
|
|
297
347
|
Env extends EnvShape,
|
|
298
|
-
P extends ParamsRecord
|
|
348
|
+
P extends ParamsRecord,
|
|
349
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
299
350
|
> {
|
|
300
|
-
match<R extends QueryRelation<Rels>, const B extends MatchShape<
|
|
351
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
301
352
|
relation: R,
|
|
302
|
-
bindings: B & CheckBindings<Env,
|
|
303
|
-
): RecRuleChain<
|
|
353
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
354
|
+
): RecRuleChain<
|
|
355
|
+
Rels,
|
|
356
|
+
Self,
|
|
357
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
358
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
359
|
+
Classes
|
|
360
|
+
>
|
|
304
361
|
where<const C extends AnyCond>(
|
|
305
|
-
cond: CheckCond<Env, C> & C
|
|
306
|
-
): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C
|
|
307
|
-
/** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and
|
|
362
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
363
|
+
): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
364
|
+
/** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and class-checks the join). */
|
|
308
365
|
idb<Target extends RecRef<Self, ParamsRecord>, const V extends readonly Var<string>[]>(
|
|
309
366
|
target: Target,
|
|
310
367
|
...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
|
|
311
|
-
): RecRuleChain<Rels, Self, Env, P>
|
|
312
|
-
/** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's
|
|
368
|
+
): RecRuleChain<Rels, Self, Env, P, Classes>
|
|
369
|
+
/** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's classed slots for `idb` pairing. */
|
|
313
370
|
select<const S extends readonly string[]>(
|
|
314
371
|
...names: CheckNameSelect<Env, S> & S
|
|
315
372
|
): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
|
|
@@ -333,13 +390,18 @@ interface QueryData {
|
|
|
333
390
|
* the rules use. Prepare with `db.prepare(q)`; nothing here touches an
|
|
334
391
|
* engine.
|
|
335
392
|
*/
|
|
336
|
-
interface Query<
|
|
337
|
-
|
|
393
|
+
interface Query<
|
|
394
|
+
Rels extends SchemaRelations,
|
|
395
|
+
Row,
|
|
396
|
+
Params extends ParamsRecord,
|
|
397
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
398
|
+
> {
|
|
399
|
+
readonly schema: Schema<Rels, Classes>
|
|
338
400
|
readonly data: QueryData
|
|
339
401
|
/** One more rule — the query's answers are the SET UNION of its rules' answers; every rule derives the same head. */
|
|
340
402
|
rule<RV extends AnyRuleValue>(
|
|
341
|
-
build: (r: QueryRuleScope<Rels>) => RV
|
|
342
|
-
): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV
|
|
403
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
404
|
+
): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
|
|
343
405
|
readonly [inferred]?: { readonly row: Row; readonly params: Params }
|
|
344
406
|
}
|
|
345
407
|
|
|
@@ -360,13 +422,16 @@ type QueryRow<Q extends AnyQuery> = RowOf<Q>
|
|
|
360
422
|
type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
|
|
361
423
|
|
|
362
424
|
/** The entry value of `query(S)`: the first `.rule` mints the query. */
|
|
363
|
-
interface QueryStart<Rels extends SchemaRelations> {
|
|
364
|
-
rule<RV extends AnyRuleValue>(
|
|
425
|
+
interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
|
|
426
|
+
rule<RV extends AnyRuleValue>(
|
|
427
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
428
|
+
): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes>
|
|
365
429
|
}
|
|
366
430
|
|
|
367
431
|
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
368
432
|
const termOps: TermOps = Object.freeze({
|
|
369
433
|
var: makeVar,
|
|
434
|
+
vars: makeVars,
|
|
370
435
|
param: makeParam,
|
|
371
436
|
inSet: makeSetParam,
|
|
372
437
|
maskParam: makeMaskParam,
|
|
@@ -396,7 +461,7 @@ const termOps: TermOps = Object.freeze({
|
|
|
396
461
|
/** One rule under construction: immutable — every chain step is a fresh state. */
|
|
397
462
|
interface RuleBuildState {
|
|
398
463
|
readonly items: readonly RuleItem[]
|
|
399
|
-
readonly varFields: Readonly<Record<string,
|
|
464
|
+
readonly varFields: Readonly<Record<string, ClassedField>>
|
|
400
465
|
readonly paramUses: readonly ParamUse[]
|
|
401
466
|
}
|
|
402
467
|
|
|
@@ -407,56 +472,143 @@ const EMPTY_RULE: RuleBuildState = Object.freeze({
|
|
|
407
472
|
paramUses: Object.freeze([])
|
|
408
473
|
})
|
|
409
474
|
|
|
410
|
-
/** One resolved bindings record: the atom entries, the vars it binds, and the params it uses. */
|
|
475
|
+
/** One resolved bindings record: the atom entries, the vars it binds (as classed slots), and the params it uses. */
|
|
411
476
|
interface ResolvedBindings {
|
|
412
477
|
readonly atom: AtomData
|
|
413
|
-
readonly vars: ReadonlyArray<{ readonly name: string; readonly
|
|
478
|
+
readonly vars: ReadonlyArray<{ readonly name: string; readonly slot: ClassedField }>
|
|
414
479
|
readonly uses: readonly ParamUse[]
|
|
415
480
|
}
|
|
416
481
|
|
|
417
482
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
483
|
+
* The ordered matchable fields of an atom owner — the runtime twin of the
|
|
484
|
+
* type tier's `MatchFields`: a relation's declared fields; a closed
|
|
485
|
+
* relation's SEALED shape, the synthetic `id` (the value's own
|
|
486
|
+
* roster-carrying descriptor, by identity) at ordinal 0 and the declared
|
|
487
|
+
* payload columns at declared index + 1 (the sealed shift, mirroring
|
|
488
|
+
* `spec.rs`'s resolver — a `ClosedColumn` is structurally a
|
|
489
|
+
* {@link RelationField}). The lowering golden pins this mapping.
|
|
490
|
+
*/
|
|
491
|
+
function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
|
|
492
|
+
if ("axioms" in owner) {
|
|
493
|
+
return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns]
|
|
494
|
+
}
|
|
495
|
+
return owner.data.fields
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Judges one membership ARRAY at a binding position — legal exactly at a
|
|
500
|
+
* CLOSED-reference field (the owner ruling: ordinary u64/str membership is
|
|
501
|
+
* spelled through `r.inSet` params; literal arrays are the closed
|
|
502
|
+
* vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
|
|
503
|
+
* degenerate sets are refusals: empty selects nothing, one element is the
|
|
504
|
+
* bare literal respelled, and a duplicate member is the same respelling in
|
|
505
|
+
* disguise — write each member once). The returned name is
|
|
506
|
+
* CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
|
|
507
|
+
* so two spellings of one set, reordered or not, share one dense
|
|
508
|
+
* `ParamId`); the members are shape-checked strings here and
|
|
509
|
+
* roster-verified at the one verification point (`taggedHandleId`) when
|
|
510
|
+
* the SDK supplies the set at execute — the same moment a bound `r.inSet`
|
|
511
|
+
* param's members are judged.
|
|
512
|
+
*/
|
|
513
|
+
function membershipSet(
|
|
514
|
+
context: string,
|
|
515
|
+
field: AnyField,
|
|
516
|
+
value: readonly unknown[]
|
|
517
|
+
): { readonly name: string; readonly members: readonly string[] } {
|
|
518
|
+
if (!("closed" in field)) {
|
|
519
|
+
throw errors.new(
|
|
520
|
+
`${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
if (value.length === 0) {
|
|
524
|
+
throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`)
|
|
525
|
+
}
|
|
526
|
+
if (value.length === 1) {
|
|
527
|
+
throw errors.new(
|
|
528
|
+
`${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
const seen = new Set<string>()
|
|
532
|
+
const members = value.map(function memberName(member) {
|
|
533
|
+
if (typeof member !== "string") {
|
|
534
|
+
throw literalShapeError(context, `a ${field.closed.name} handle name (string)`, member)
|
|
535
|
+
}
|
|
536
|
+
if (seen.has(member)) {
|
|
537
|
+
throw errors.new(
|
|
538
|
+
`${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
seen.add(member)
|
|
542
|
+
return member
|
|
543
|
+
})
|
|
544
|
+
const key = [...members].sort()
|
|
545
|
+
return { name: `∈ ${field.closed.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Resolves a bindings record against an atom owner's matchable fields (a
|
|
550
|
+
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
551
|
+
* the record's written order: terms classify by their runtime tag,
|
|
552
|
+
* everything else is a bare literal (typed by the FIELD at lowering — the
|
|
553
|
+
* membership typing rule included). Every bound field carries its
|
|
554
|
+
* law-computed class, read off the schema value's frozen class map — the
|
|
555
|
+
* runtime twin of the type tier's `SlotAt` lookups.
|
|
422
556
|
*/
|
|
423
557
|
function resolveBindings(
|
|
424
558
|
context: string,
|
|
425
|
-
relation:
|
|
426
|
-
bindings: Readonly<Record<string, unknown
|
|
559
|
+
relation: MatchOwner,
|
|
560
|
+
bindings: Readonly<Record<string, unknown>>,
|
|
561
|
+
classes: SchemaClasses
|
|
427
562
|
): ResolvedBindings {
|
|
428
563
|
const entries: BindingEntry[] = []
|
|
429
|
-
const vars: Array<{ readonly name: string; readonly
|
|
564
|
+
const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
|
|
430
565
|
const uses: ParamUse[] = []
|
|
566
|
+
const relationClasses = classes[relation.name]
|
|
567
|
+
const ordered = matchFieldsOf(relation)
|
|
431
568
|
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
432
569
|
if (value === undefined) {
|
|
433
570
|
continue
|
|
434
571
|
}
|
|
435
|
-
const declared =
|
|
572
|
+
const declared = ordered.find(function byName(candidate) {
|
|
436
573
|
return candidate.name === fieldName
|
|
437
574
|
})
|
|
438
575
|
if (declared === undefined) {
|
|
439
576
|
throw errors.new(`${context} has no field ${fieldName}`)
|
|
440
577
|
}
|
|
578
|
+
const fieldClass = relationClasses?.[fieldName]
|
|
441
579
|
let bound: BindingEntry["term"]
|
|
442
580
|
if (isTerm(value)) {
|
|
443
581
|
switch (value[term]) {
|
|
444
582
|
case "var": {
|
|
445
583
|
bound = Object.freeze({ kind: "var" as const, name: value.name })
|
|
446
|
-
vars.push(
|
|
584
|
+
vars.push(
|
|
585
|
+
Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) })
|
|
586
|
+
)
|
|
447
587
|
break
|
|
448
588
|
}
|
|
449
589
|
case "param": {
|
|
450
590
|
bound = Object.freeze({ kind: "param" as const, name: value.name })
|
|
451
591
|
uses.push(
|
|
452
|
-
Object.freeze({
|
|
592
|
+
Object.freeze({
|
|
593
|
+
name: value.name,
|
|
594
|
+
shape: "value" as const,
|
|
595
|
+
anchor: declared.field,
|
|
596
|
+
op: "binding" as const,
|
|
597
|
+
members: undefined
|
|
598
|
+
})
|
|
453
599
|
)
|
|
454
600
|
break
|
|
455
601
|
}
|
|
456
602
|
case "setParam": {
|
|
457
603
|
bound = Object.freeze({ kind: "setParam" as const, name: value.name })
|
|
458
604
|
uses.push(
|
|
459
|
-
Object.freeze({
|
|
605
|
+
Object.freeze({
|
|
606
|
+
name: value.name,
|
|
607
|
+
shape: "set" as const,
|
|
608
|
+
anchor: declared.field,
|
|
609
|
+
op: "binding" as const,
|
|
610
|
+
members: undefined
|
|
611
|
+
})
|
|
460
612
|
)
|
|
461
613
|
break
|
|
462
614
|
}
|
|
@@ -469,10 +621,22 @@ function resolveBindings(
|
|
|
469
621
|
`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
|
|
470
622
|
)
|
|
471
623
|
}
|
|
624
|
+
} else if (Array.isArray(value)) {
|
|
625
|
+
const set = membershipSet(`${context}.${fieldName}`, declared.field, value)
|
|
626
|
+
bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
|
|
627
|
+
uses.push(
|
|
628
|
+
Object.freeze({
|
|
629
|
+
name: set.name,
|
|
630
|
+
shape: "set" as const,
|
|
631
|
+
anchor: declared.field,
|
|
632
|
+
op: "binding" as const,
|
|
633
|
+
members: set.members
|
|
634
|
+
})
|
|
635
|
+
)
|
|
472
636
|
} else {
|
|
473
637
|
bound = Object.freeze({ kind: "literal" as const, value })
|
|
474
638
|
}
|
|
475
|
-
entries.push(Object.freeze({ field: fieldName, data: declared.field, term: bound }))
|
|
639
|
+
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
|
|
476
640
|
}
|
|
477
641
|
return {
|
|
478
642
|
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
@@ -484,24 +648,25 @@ function resolveBindings(
|
|
|
484
648
|
/**
|
|
485
649
|
* Extends a rule state with one positive atom. Vars bind on first
|
|
486
650
|
* occurrence; every LATER occurrence (a later atom's field or a same-record
|
|
487
|
-
* sibling) is a join and must be
|
|
488
|
-
* of the type tier's `JoinOk
|
|
489
|
-
* callers too.
|
|
651
|
+
* sibling) is a join and must be class-equal — the construction-time twin
|
|
652
|
+
* of the type tier's `JoinOk` (bare pairs only with bare), so the domain
|
|
653
|
+
* wall holds for untyped callers too.
|
|
490
654
|
*/
|
|
491
655
|
function advanceMatch(
|
|
492
656
|
state: RuleBuildState,
|
|
493
|
-
relation:
|
|
494
|
-
bindings: Readonly<Record<string, unknown
|
|
657
|
+
relation: MatchOwner,
|
|
658
|
+
bindings: Readonly<Record<string, unknown>>,
|
|
659
|
+
classes: SchemaClasses
|
|
495
660
|
): RuleBuildState {
|
|
496
|
-
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings)
|
|
497
|
-
const varFields: Record<string,
|
|
661
|
+
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes)
|
|
662
|
+
const varFields: Record<string, ClassedField> = { ...state.varFields }
|
|
498
663
|
for (const bound of resolved.vars) {
|
|
499
664
|
const existing = varFields[bound.name]
|
|
500
665
|
if (existing === undefined) {
|
|
501
|
-
varFields[bound.name] = bound.
|
|
502
|
-
} else if (!fieldJoins(existing, bound.
|
|
666
|
+
varFields[bound.name] = bound.slot
|
|
667
|
+
} else if (!fieldJoins(existing, bound.slot)) {
|
|
503
668
|
throw errors.new(
|
|
504
|
-
`relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.
|
|
669
|
+
`relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.slot)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
505
670
|
)
|
|
506
671
|
}
|
|
507
672
|
}
|
|
@@ -541,7 +706,7 @@ function sideUses(
|
|
|
541
706
|
op: CmpKind,
|
|
542
707
|
side: CmpTermData,
|
|
543
708
|
sibling: CmpTermData,
|
|
544
|
-
varFields: Readonly<Record<string,
|
|
709
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
545
710
|
uses: ParamUse[]
|
|
546
711
|
): void {
|
|
547
712
|
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
@@ -549,7 +714,7 @@ function sideUses(
|
|
|
549
714
|
}
|
|
550
715
|
let anchor: AnyField | "measure" | undefined
|
|
551
716
|
if (sibling.kind === "var") {
|
|
552
|
-
anchor = varFields[sibling.name]
|
|
717
|
+
anchor = varFields[sibling.name]?.field
|
|
553
718
|
} else if (sibling.kind === "measure") {
|
|
554
719
|
anchor = "measure"
|
|
555
720
|
} else {
|
|
@@ -560,13 +725,14 @@ function sideUses(
|
|
|
560
725
|
name: side.name,
|
|
561
726
|
shape: side.kind === "param" ? ("value" as const) : ("set" as const),
|
|
562
727
|
anchor,
|
|
563
|
-
op
|
|
728
|
+
op,
|
|
729
|
+
members: undefined
|
|
564
730
|
})
|
|
565
731
|
)
|
|
566
732
|
}
|
|
567
733
|
|
|
568
734
|
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
569
|
-
function condDataOf(cond: AnyCond, varFields: Readonly<Record<string,
|
|
735
|
+
function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedField>>, uses: ParamUse[]): CondData {
|
|
570
736
|
if (cond.cond === "cmp") {
|
|
571
737
|
const lhs = cmpTermDataOf(cond.op, cond.lhs)
|
|
572
738
|
const rhs = cmpTermDataOf(cond.op, cond.rhs)
|
|
@@ -580,7 +746,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>
|
|
|
580
746
|
} else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
|
|
581
747
|
mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
|
|
582
748
|
uses.push(
|
|
583
|
-
Object.freeze({
|
|
749
|
+
Object.freeze({
|
|
750
|
+
name: maskValue.name,
|
|
751
|
+
shape: "mask" as const,
|
|
752
|
+
anchor: undefined,
|
|
753
|
+
op: "allen" as const,
|
|
754
|
+
members: undefined
|
|
755
|
+
})
|
|
584
756
|
)
|
|
585
757
|
} else {
|
|
586
758
|
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
|
|
@@ -602,18 +774,18 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>
|
|
|
602
774
|
}
|
|
603
775
|
|
|
604
776
|
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
605
|
-
function advanceWhere(state: RuleBuildState, cond: AnyCond): RuleBuildState {
|
|
777
|
+
function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClasses): RuleBuildState {
|
|
606
778
|
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
607
779
|
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
|
|
608
780
|
}
|
|
609
781
|
if (cond.cond === "not") {
|
|
610
|
-
const relation:
|
|
782
|
+
const relation: MatchOwner = cond.relation
|
|
611
783
|
const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
|
|
612
784
|
Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
613
785
|
return value !== undefined
|
|
614
786
|
})
|
|
615
787
|
)
|
|
616
|
-
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings)
|
|
788
|
+
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes)
|
|
617
789
|
return {
|
|
618
790
|
items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
|
|
619
791
|
varFields: state.varFields,
|
|
@@ -651,14 +823,27 @@ function isAggregateEntry(
|
|
|
651
823
|
return typeof value === "object" && value !== null && "agg" in value
|
|
652
824
|
}
|
|
653
825
|
|
|
654
|
-
/**
|
|
826
|
+
/**
|
|
827
|
+
* Classifies one select entry into its named answer column. The `closed`
|
|
828
|
+
* slice is resolved LATER, at rule completion (`completeRule`), where the
|
|
829
|
+
* rule's `varFields` are in hand — until then every column is provisionally
|
|
830
|
+
* bare.
|
|
831
|
+
*/
|
|
655
832
|
function selectColumnOf(entry: unknown): SelectColumn {
|
|
656
833
|
if (typeof entry === "string") {
|
|
657
|
-
return Object.freeze({
|
|
834
|
+
return Object.freeze({
|
|
835
|
+
name: entry,
|
|
836
|
+
entry: Object.freeze({ kind: "var" as const, over: entry }),
|
|
837
|
+
closed: undefined
|
|
838
|
+
})
|
|
658
839
|
}
|
|
659
840
|
if (isTerm(entry)) {
|
|
660
841
|
if (entry[term] === "duration") {
|
|
661
|
-
return Object.freeze({
|
|
842
|
+
return Object.freeze({
|
|
843
|
+
name: entry.name,
|
|
844
|
+
entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
|
|
845
|
+
closed: undefined
|
|
846
|
+
})
|
|
662
847
|
}
|
|
663
848
|
throw errors.new(
|
|
664
849
|
`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
|
|
@@ -677,7 +862,11 @@ function aggregateColumnOf(entry: {
|
|
|
677
862
|
readonly key: unknown
|
|
678
863
|
}): SelectColumn {
|
|
679
864
|
function column(name: string, agg: AggData): SelectColumn {
|
|
680
|
-
return Object.freeze({
|
|
865
|
+
return Object.freeze({
|
|
866
|
+
name,
|
|
867
|
+
entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
|
|
868
|
+
closed: undefined
|
|
869
|
+
})
|
|
681
870
|
}
|
|
682
871
|
const over = entry.over
|
|
683
872
|
switch (entry.agg) {
|
|
@@ -718,36 +907,76 @@ function aggregateColumnOf(entry: {
|
|
|
718
907
|
}
|
|
719
908
|
}
|
|
720
909
|
|
|
910
|
+
/**
|
|
911
|
+
* The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
|
|
912
|
+
* § orderability): a closed reference is equality-and-membership only —
|
|
913
|
+
* its declaration-id order is an encoding accident, so every
|
|
914
|
+
* order-comparison and fold position refuses it. The construction-time
|
|
915
|
+
* twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
|
|
916
|
+
* untyped callers too (the engine cannot backstop this one: the wire IR
|
|
917
|
+
* carries plain u64s, no rosters).
|
|
918
|
+
*/
|
|
919
|
+
function closedOrderError(context: string, position: string, vocabulary: string): Error {
|
|
920
|
+
return errors.new(
|
|
921
|
+
`${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** The comparison ops the orderable ban covers (order roster + point membership — every order-comparison position). */
|
|
926
|
+
function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
|
|
927
|
+
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
|
|
928
|
+
}
|
|
929
|
+
|
|
721
930
|
/** Requires a var name to be bound by a relation atom of the rule. */
|
|
722
|
-
function assertBound(context: string, varFields: Readonly<Record<string,
|
|
723
|
-
const
|
|
724
|
-
if (
|
|
931
|
+
function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
|
|
932
|
+
const slot = varFields[name]
|
|
933
|
+
if (slot === undefined) {
|
|
725
934
|
throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
|
|
726
935
|
}
|
|
727
|
-
return
|
|
936
|
+
return slot
|
|
728
937
|
}
|
|
729
938
|
|
|
730
939
|
/** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
|
|
731
|
-
function assertIntervalBound(context: string, varFields: Readonly<Record<string,
|
|
732
|
-
const
|
|
733
|
-
if (field.kind !== "interval") {
|
|
940
|
+
function assertIntervalBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): void {
|
|
941
|
+
const slot = assertBound(context, varFields, name)
|
|
942
|
+
if (slot.field.kind !== "interval") {
|
|
734
943
|
throw errors.new(
|
|
735
944
|
`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
|
|
736
945
|
)
|
|
737
946
|
}
|
|
738
947
|
}
|
|
739
948
|
|
|
740
|
-
/**
|
|
741
|
-
|
|
949
|
+
/**
|
|
950
|
+
* Validates one condition's variable references against the rule's bound
|
|
951
|
+
* names — and, for `eq`/`ne` over two variables, holds the class wall: the
|
|
952
|
+
* unification IS a join, so the two slots must be class-equal exactly as a
|
|
953
|
+
* match-reuse join must be (the construction-time twin of the type tier's
|
|
954
|
+
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
955
|
+
* this one — the query IR carries no domains — so the wall lives here for
|
|
956
|
+
* untyped callers too.
|
|
957
|
+
*/
|
|
958
|
+
function validateCond(context: string, varFields: Readonly<Record<string, ClassedField>>, cond: CondData): void {
|
|
742
959
|
if (cond.kind === "cmp") {
|
|
743
960
|
for (const side of [cond.lhs, cond.rhs]) {
|
|
744
961
|
if (side.kind === "var") {
|
|
745
|
-
assertBound(context, varFields, side.name)
|
|
962
|
+
const slot = assertBound(context, varFields, side.name)
|
|
963
|
+
if (isOrderOp(cond.op) && "closed" in slot.field) {
|
|
964
|
+
throw closedOrderError(context, `the ${cond.op} side ${side.name}`, slot.field.closed.name)
|
|
965
|
+
}
|
|
746
966
|
}
|
|
747
967
|
if (side.kind === "measure") {
|
|
748
968
|
assertIntervalBound(context, varFields, side.name)
|
|
749
969
|
}
|
|
750
970
|
}
|
|
971
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
972
|
+
const lhs = assertBound(context, varFields, cond.lhs.name)
|
|
973
|
+
const rhs = assertBound(context, varFields, cond.rhs.name)
|
|
974
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
975
|
+
throw errors.new(
|
|
976
|
+
`${context}: ${cond.op}(${cond.lhs.name}, ${cond.rhs.name}) unifies domain-unequal fields — ${cond.lhs.name} bound at ${renderFieldKind(lhs)}, ${cond.rhs.name} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
}
|
|
751
980
|
return
|
|
752
981
|
}
|
|
753
982
|
for (const child of cond.children) {
|
|
@@ -756,7 +985,11 @@ function validateCond(context: string, varFields: Readonly<Record<string, AnyFie
|
|
|
756
985
|
}
|
|
757
986
|
|
|
758
987
|
/** Validates one select column's variable references. */
|
|
759
|
-
function validateColumn(
|
|
988
|
+
function validateColumn(
|
|
989
|
+
context: string,
|
|
990
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
991
|
+
column: SelectColumn
|
|
992
|
+
): void {
|
|
760
993
|
const entry = column.entry
|
|
761
994
|
if (entry.kind === "var") {
|
|
762
995
|
assertBound(`${context} select ${column.name}`, varFields, entry.over)
|
|
@@ -775,22 +1008,69 @@ function validateColumn(context: string, varFields: Readonly<Record<string, AnyF
|
|
|
775
1008
|
return
|
|
776
1009
|
case "fold": {
|
|
777
1010
|
if (typeof agg.over === "string") {
|
|
778
|
-
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
1011
|
+
const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
1012
|
+
if ("closed" in slot.field) {
|
|
1013
|
+
throw closedOrderError(
|
|
1014
|
+
`${context} select ${column.name}`,
|
|
1015
|
+
`the ${agg.fold} input ${agg.over}`,
|
|
1016
|
+
slot.field.closed.name
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
779
1019
|
return
|
|
780
1020
|
}
|
|
781
1021
|
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
|
|
782
1022
|
return
|
|
783
1023
|
}
|
|
784
|
-
case "arg":
|
|
1024
|
+
case "arg": {
|
|
785
1025
|
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
786
|
-
assertBound(`${context} select ${column.name}`, varFields, agg.key)
|
|
1026
|
+
const key = assertBound(`${context} select ${column.name}`, varFields, agg.key)
|
|
1027
|
+
if ("closed" in key.field) {
|
|
1028
|
+
throw closedOrderError(
|
|
1029
|
+
`${context} select ${column.name}`,
|
|
1030
|
+
`the ${agg.direction} key ${agg.key}`,
|
|
1031
|
+
key.field.closed.name
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
787
1034
|
return
|
|
1035
|
+
}
|
|
788
1036
|
case "pack":
|
|
789
1037
|
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
790
1038
|
return
|
|
791
1039
|
}
|
|
792
1040
|
}
|
|
793
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Resolves the roster one select column decodes through: a projected var,
|
|
1044
|
+
* or an Arg-carried payload, bound at a closed-referencing field carries
|
|
1045
|
+
* that field's roster (read off `varFields` — the same slot the domain
|
|
1046
|
+
* machinery reads), and `decodeAnswers` lifts the column's row ids back to
|
|
1047
|
+
* handle NAMES through it — the runtime twin of the row type's `Infer`
|
|
1048
|
+
* claim. Every other entry decodes bare: counts are counts, the measure
|
|
1049
|
+
* and `pack` are never closed, and a closed FOLD is banned outright
|
|
1050
|
+
* ({@link closedOrderError}) before this resolution runs.
|
|
1051
|
+
*/
|
|
1052
|
+
function selectClosedOf(
|
|
1053
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
1054
|
+
entry: SelectEntryData
|
|
1055
|
+
): ClosedRoster | undefined {
|
|
1056
|
+
let over: string | undefined
|
|
1057
|
+
if (entry.kind === "var") {
|
|
1058
|
+
over = entry.over
|
|
1059
|
+
} else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
1060
|
+
over = entry.agg.over
|
|
1061
|
+
} else {
|
|
1062
|
+
over = undefined
|
|
1063
|
+
}
|
|
1064
|
+
if (over === undefined) {
|
|
1065
|
+
return undefined
|
|
1066
|
+
}
|
|
1067
|
+
const field = varFields[over]?.field
|
|
1068
|
+
if (field !== undefined && "closed" in field) {
|
|
1069
|
+
return field.closed
|
|
1070
|
+
}
|
|
1071
|
+
return undefined
|
|
1072
|
+
}
|
|
1073
|
+
|
|
794
1074
|
/**
|
|
795
1075
|
* Completes one rule: classifies the select record (written order = answer
|
|
796
1076
|
* column order, names must be declaration-order-safe keys), and validates
|
|
@@ -821,9 +1101,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
|
|
|
821
1101
|
`${context}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.name} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
|
|
822
1102
|
)
|
|
823
1103
|
}
|
|
824
|
-
|
|
1104
|
+
const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
|
|
1105
|
+
if (!fieldJoins(bound, negatedSlot)) {
|
|
825
1106
|
throw errors.new(
|
|
826
|
-
`${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(
|
|
1107
|
+
`${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(negatedSlot)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only class-equal slots; bare pairs only with bare`
|
|
827
1108
|
)
|
|
828
1109
|
}
|
|
829
1110
|
}
|
|
@@ -842,10 +1123,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
|
|
|
842
1123
|
if (column === undefined || column.entry.kind !== "var") {
|
|
843
1124
|
return
|
|
844
1125
|
}
|
|
845
|
-
const
|
|
846
|
-
if (
|
|
1126
|
+
const headSlot = head?.varFields[column.entry.over]
|
|
1127
|
+
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
847
1128
|
throw errors.new(
|
|
848
|
-
`${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(
|
|
1129
|
+
`${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headSlot)}) — a var joins only class-equal slots; bare pairs only with bare`
|
|
849
1130
|
)
|
|
850
1131
|
}
|
|
851
1132
|
})
|
|
@@ -856,7 +1137,15 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
|
|
|
856
1137
|
}
|
|
857
1138
|
return Object.freeze({
|
|
858
1139
|
items: state.items,
|
|
859
|
-
select: Object.freeze(
|
|
1140
|
+
select: Object.freeze(
|
|
1141
|
+
columns.map(function enrichColumn(column): SelectColumn {
|
|
1142
|
+
return Object.freeze({
|
|
1143
|
+
name: column.name,
|
|
1144
|
+
entry: column.entry,
|
|
1145
|
+
closed: selectClosedOf(state.varFields, column.entry)
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
),
|
|
860
1149
|
varFields: state.varFields,
|
|
861
1150
|
paramUses: state.paramUses
|
|
862
1151
|
})
|
|
@@ -877,7 +1166,7 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
|
|
|
877
1166
|
* (bound variable names only — the creation quarantine).
|
|
878
1167
|
*/
|
|
879
1168
|
interface RawChain {
|
|
880
|
-
match(relation:
|
|
1169
|
+
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
881
1170
|
where(cond: AnyCond): RawChain
|
|
882
1171
|
idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
|
|
883
1172
|
select(...entries: readonly SelectEntry[]): RuleValue<never, never>
|
|
@@ -885,14 +1174,15 @@ interface RawChain {
|
|
|
885
1174
|
|
|
886
1175
|
/** The runtime rule-builder shape beneath every typed scope. */
|
|
887
1176
|
interface RawScope extends TermOps {
|
|
888
|
-
match(relation:
|
|
1177
|
+
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
889
1178
|
}
|
|
890
1179
|
|
|
891
|
-
/** Which rule family a chain builds — gates `idb` and the recursive select. */
|
|
892
|
-
type ChainContext =
|
|
1180
|
+
/** Which rule family a chain builds — gates `idb` and the recursive select — plus the schema's runtime class map (the join judge's authority). */
|
|
1181
|
+
type ChainContext = { readonly classes: SchemaClasses } & (
|
|
893
1182
|
| { readonly kind: "query" }
|
|
894
1183
|
| { readonly kind: "rec"; readonly self: RecData }
|
|
895
1184
|
| { readonly kind: "output"; readonly program: ProgramState }
|
|
1185
|
+
)
|
|
896
1186
|
|
|
897
1187
|
/** The diagnostic label of a chain context. */
|
|
898
1188
|
function contextLabel(context: ChainContext): string {
|
|
@@ -948,10 +1238,10 @@ function selectColumns(context: ChainContext, entries: readonly SelectEntry[]):
|
|
|
948
1238
|
function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
|
|
949
1239
|
const chain: RawChain = {
|
|
950
1240
|
match(relation, bindings) {
|
|
951
|
-
return makeRawChain(context, advanceMatch(state, relation, bindings))
|
|
1241
|
+
return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes))
|
|
952
1242
|
},
|
|
953
1243
|
where(cond) {
|
|
954
|
-
return makeRawChain(context, advanceWhere(state, cond))
|
|
1244
|
+
return makeRawChain(context, advanceWhere(state, cond, context.classes))
|
|
955
1245
|
},
|
|
956
1246
|
idb(target, ...vars) {
|
|
957
1247
|
return makeRawChain(context, idbAdvance(context, state, target, vars))
|
|
@@ -969,7 +1259,7 @@ function makeRawScope(context: ChainContext): RawScope {
|
|
|
969
1259
|
const scope: RawScope = {
|
|
970
1260
|
...termOps,
|
|
971
1261
|
match(relation, bindings) {
|
|
972
|
-
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings))
|
|
1262
|
+
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes))
|
|
973
1263
|
}
|
|
974
1264
|
}
|
|
975
1265
|
Object.freeze(scope)
|
|
@@ -990,29 +1280,39 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
|
|
|
990
1280
|
}
|
|
991
1281
|
|
|
992
1282
|
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
993
|
-
function makeQueryRuleScope<Rels extends SchemaRelations>(
|
|
994
|
-
|
|
995
|
-
|
|
1283
|
+
function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1284
|
+
classes: SchemaClasses
|
|
1285
|
+
): QueryRuleScope<Rels, Classes> {
|
|
1286
|
+
const raw = makeRawScope({ kind: "query", classes })
|
|
1287
|
+
if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
|
|
996
1288
|
throw errors.new("query rule builder construction incomplete")
|
|
997
1289
|
}
|
|
998
1290
|
return raw
|
|
999
1291
|
}
|
|
1000
1292
|
|
|
1001
1293
|
/** Builds one output-rule builder over a program's recs. */
|
|
1002
|
-
function makeOutputRuleScope<Rels extends SchemaRelations
|
|
1003
|
-
|
|
1004
|
-
|
|
1294
|
+
function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1295
|
+
program: ProgramState
|
|
1296
|
+
): OutputRuleScope<Rels, Classes> {
|
|
1297
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes })
|
|
1298
|
+
if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
|
|
1005
1299
|
throw errors.new("program output rule builder construction incomplete")
|
|
1006
1300
|
}
|
|
1007
1301
|
return raw
|
|
1008
1302
|
}
|
|
1009
1303
|
|
|
1010
|
-
/** One program's build-time registry: its recs in declaration order
|
|
1304
|
+
/** One program's build-time registry: its recs in declaration order (sealed when the output is declared) and the theory's class map. */
|
|
1011
1305
|
interface ProgramState {
|
|
1012
1306
|
readonly recs: RecData[]
|
|
1307
|
+
readonly classes: SchemaClasses
|
|
1013
1308
|
sealed: boolean
|
|
1014
1309
|
}
|
|
1015
1310
|
|
|
1311
|
+
/** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
|
|
1312
|
+
function renderClosedSlice(closed: ClosedRoster | undefined): string {
|
|
1313
|
+
return closed === undefined ? "a bare value" : `a ${closed.name} reference`
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1016
1316
|
/** Renders one head column's signature for the rule-alignment check. */
|
|
1017
1317
|
function headSignature(column: SelectColumn): string {
|
|
1018
1318
|
const entry = column.entry
|
|
@@ -1029,32 +1329,91 @@ function headSignature(column: SelectColumn): string {
|
|
|
1029
1329
|
return `${column.name}:${agg.op}`
|
|
1030
1330
|
}
|
|
1031
1331
|
|
|
1332
|
+
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
1333
|
+
function anchorRosterOf(anchor: AnyField | "measure" | undefined): ClosedRoster | undefined {
|
|
1334
|
+
if (anchor === undefined || anchor === "measure") {
|
|
1335
|
+
return undefined
|
|
1336
|
+
}
|
|
1337
|
+
if ("closed" in anchor) {
|
|
1338
|
+
return anchor.closed
|
|
1339
|
+
}
|
|
1340
|
+
return undefined
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Renders one param anchor's closedness for the registry's coherence diagnostics. */
|
|
1344
|
+
function renderParamAnchor(roster: ClosedRoster | undefined): string {
|
|
1345
|
+
return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1032
1348
|
/**
|
|
1033
1349
|
* Folds every rule's param uses (recs in declaration order first, output
|
|
1034
1350
|
* rules last — exactly the lowering walk) into the query's registry:
|
|
1035
1351
|
* first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
|
|
1036
|
-
* the wire, and one name must keep one shape
|
|
1352
|
+
* the wire, and one name must keep one shape AND one closedness — every
|
|
1353
|
+
* anchored use of one name must agree on the roster (value identity), so a
|
|
1354
|
+
* param anchored at a closed reference is GUARANTEED to ride the one
|
|
1355
|
+
* roster-verification point (`taggedHandleId`) at execute; a name anchored
|
|
1356
|
+
* both at a closed reference and at a non-closed position (or at two
|
|
1357
|
+
* vocabularies) is refused here, because the wire would translate only the
|
|
1358
|
+
* first anchor's reading (the type tier intersects the uses to `never`;
|
|
1359
|
+
* this is its runtime twin for untyped callers). A param whose anchor is a
|
|
1360
|
+
* CLOSED reference must never sit in an order-comparison position — the
|
|
1361
|
+
* anchor types its value a handle name and the engine would order the
|
|
1362
|
+
* translated row ids, so the pairing is refused here too (the registry is
|
|
1363
|
+
* the one place a name's every use and its anchoring field meet).
|
|
1037
1364
|
*/
|
|
1038
1365
|
function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
|
|
1039
1366
|
const order: string[] = []
|
|
1040
|
-
const byName = new Map<
|
|
1367
|
+
const byName = new Map<
|
|
1368
|
+
string,
|
|
1369
|
+
{
|
|
1370
|
+
shape: ParamEntry["shape"]
|
|
1371
|
+
anchor: ParamEntry["anchor"]
|
|
1372
|
+
op: ParamEntry["op"]
|
|
1373
|
+
members: ParamEntry["members"]
|
|
1374
|
+
orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
|
|
1375
|
+
}
|
|
1376
|
+
>()
|
|
1041
1377
|
function fold(uses: readonly ParamUse[]): void {
|
|
1042
1378
|
for (const use of uses) {
|
|
1043
1379
|
const existing = byName.get(use.name)
|
|
1044
1380
|
if (existing === undefined) {
|
|
1045
1381
|
order.push(use.name)
|
|
1046
|
-
byName.set(use.name, {
|
|
1382
|
+
byName.set(use.name, {
|
|
1383
|
+
shape: use.shape,
|
|
1384
|
+
anchor: use.anchor,
|
|
1385
|
+
op: use.op,
|
|
1386
|
+
members: use.members,
|
|
1387
|
+
orderOp: isOrderOp(use.op) ? use.op : undefined
|
|
1388
|
+
})
|
|
1047
1389
|
continue
|
|
1048
1390
|
}
|
|
1391
|
+
if ((existing.members === undefined) !== (use.members === undefined)) {
|
|
1392
|
+
throw errors.new(
|
|
1393
|
+
`query param ${use.name} collides with a membership array's registry entry — name the param differently`
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1049
1396
|
if (existing.shape !== use.shape) {
|
|
1050
1397
|
throw errors.new(
|
|
1051
1398
|
`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
|
|
1052
1399
|
)
|
|
1053
1400
|
}
|
|
1401
|
+
if (existing.anchor !== undefined && use.anchor !== undefined) {
|
|
1402
|
+
const registered = anchorRosterOf(existing.anchor)
|
|
1403
|
+
const anchored = anchorRosterOf(use.anchor)
|
|
1404
|
+
if (registered !== anchored) {
|
|
1405
|
+
throw errors.new(
|
|
1406
|
+
`query param ${use.name} is anchored at ${renderParamAnchor(registered)} and at ${renderParamAnchor(anchored)} — a closed-anchored param translates handle names through ONE roster (one name, one domain); name the params differently`
|
|
1407
|
+
)
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1054
1410
|
if (existing.anchor === undefined && use.anchor !== undefined) {
|
|
1055
1411
|
existing.anchor = use.anchor
|
|
1056
1412
|
existing.op = use.op
|
|
1057
1413
|
}
|
|
1414
|
+
if (existing.orderOp === undefined && isOrderOp(use.op)) {
|
|
1415
|
+
existing.orderOp = use.op
|
|
1416
|
+
}
|
|
1058
1417
|
}
|
|
1059
1418
|
}
|
|
1060
1419
|
for (const rec of recs) {
|
|
@@ -1071,7 +1430,15 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
|
|
|
1071
1430
|
if (entry === undefined) {
|
|
1072
1431
|
throw errors.new(`query param ${name} lost its registry entry`)
|
|
1073
1432
|
}
|
|
1074
|
-
|
|
1433
|
+
if (
|
|
1434
|
+
entry.orderOp !== undefined &&
|
|
1435
|
+
entry.anchor !== undefined &&
|
|
1436
|
+
entry.anchor !== "measure" &&
|
|
1437
|
+
"closed" in entry.anchor
|
|
1438
|
+
) {
|
|
1439
|
+
throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, entry.anchor.closed.name)
|
|
1440
|
+
}
|
|
1441
|
+
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
|
|
1075
1442
|
})
|
|
1076
1443
|
)
|
|
1077
1444
|
}
|
|
@@ -1102,6 +1469,19 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
|
|
|
1102
1469
|
`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
|
|
1103
1470
|
)
|
|
1104
1471
|
}
|
|
1472
|
+
// The closed slice is part of the head too: one answer column decodes
|
|
1473
|
+
// through one roster, so a union whose rules bind a column at
|
|
1474
|
+
// different vocabularies (or one closed, one bare — the ids would
|
|
1475
|
+
// mistranslate silently) is refused pointed. Vocabulary identity is
|
|
1476
|
+
// value identity, the SDK's membership rule everywhere.
|
|
1477
|
+
rule.select.forEach(function verifyClosedSlice(column, position) {
|
|
1478
|
+
const lead = first.select[position]
|
|
1479
|
+
if (lead !== undefined && column.closed !== lead.closed) {
|
|
1480
|
+
throw errors.new(
|
|
1481
|
+
`every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
|
|
1482
|
+
)
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1105
1485
|
})
|
|
1106
1486
|
const data: QueryData = Object.freeze({
|
|
1107
1487
|
recs: Object.freeze([...recs]),
|
|
@@ -1113,7 +1493,7 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
|
|
|
1113
1493
|
schema: theory,
|
|
1114
1494
|
data,
|
|
1115
1495
|
rule(build) {
|
|
1116
|
-
const built = build(makeRawScope({ kind: "query" }))
|
|
1496
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
|
|
1117
1497
|
return makeRawQuery(theory, recs, [...rules, built.rule])
|
|
1118
1498
|
}
|
|
1119
1499
|
}
|
|
@@ -1126,21 +1506,21 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
|
|
|
1126
1506
|
* the checkable fact — the value was assembled over the identical theory —
|
|
1127
1507
|
* is verified before the raw value is admitted at its typed face.
|
|
1128
1508
|
*/
|
|
1129
|
-
function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
|
|
1130
|
-
theory: Schema<Rels>,
|
|
1509
|
+
function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
|
|
1510
|
+
theory: Schema<Rels, Classes>,
|
|
1131
1511
|
value: RawQuery
|
|
1132
|
-
): value is RawQuery & Query<Rels, Row, P> {
|
|
1512
|
+
): value is RawQuery & Query<Rels, Row, P, Classes> {
|
|
1133
1513
|
return value.schema === theory
|
|
1134
1514
|
}
|
|
1135
1515
|
|
|
1136
1516
|
/** Assembles one typed query value (rules already completed). */
|
|
1137
|
-
function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
|
|
1138
|
-
theory: Schema<Rels>,
|
|
1517
|
+
function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
|
|
1518
|
+
theory: Schema<Rels, Classes>,
|
|
1139
1519
|
recs: readonly RecData[],
|
|
1140
1520
|
rules: readonly RuleData[]
|
|
1141
|
-
): Query<Rels, Row, P> {
|
|
1521
|
+
): Query<Rels, Row, P, Classes> {
|
|
1142
1522
|
const raw = makeRawQuery(theory, recs, rules)
|
|
1143
|
-
if (!isQueryValue<Rels, Row, P>(theory, raw)) {
|
|
1523
|
+
if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
|
|
1144
1524
|
throw errors.new("query value construction incomplete")
|
|
1145
1525
|
}
|
|
1146
1526
|
return raw
|
|
@@ -1149,13 +1529,19 @@ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
|
|
|
1149
1529
|
/**
|
|
1150
1530
|
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
1151
1531
|
* adds one conjunctive rule; multiple rules are the set union (answers are
|
|
1152
|
-
* SETS — no order or limit exists anywhere; hosts sort).
|
|
1532
|
+
* SETS — no order or limit exists anywhere; hosts sort). The schema's
|
|
1533
|
+
* law-computed class map rides into every rule builder — the join walls
|
|
1534
|
+
* compare class names off it, at the type level and at construction alike.
|
|
1153
1535
|
*/
|
|
1154
|
-
function query<Rels extends SchemaRelations
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1536
|
+
function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1537
|
+
theory: Schema<Rels, Classes>
|
|
1538
|
+
): QueryStart<Rels, Classes> {
|
|
1539
|
+
const start: QueryStart<Rels, Classes> = {
|
|
1540
|
+
rule<RV extends AnyRuleValue>(
|
|
1541
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
1542
|
+
): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
|
|
1543
|
+
const built = build(makeQueryRuleScope<Rels, Classes>(theory.classes))
|
|
1544
|
+
return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
|
|
1159
1545
|
}
|
|
1160
1546
|
}
|
|
1161
1547
|
Object.freeze(start)
|
|
@@ -1180,25 +1566,30 @@ function isIntervalShaped(value: unknown): value is { readonly start: bigint; re
|
|
|
1180
1566
|
}
|
|
1181
1567
|
|
|
1182
1568
|
/**
|
|
1183
|
-
* Tags one closed-reference literal: the
|
|
1184
|
-
*
|
|
1185
|
-
* make any
|
|
1186
|
-
* never handle
|
|
1569
|
+
* Tags one closed-reference literal: the handle NAME, verified against the
|
|
1570
|
+
* roster (the belt the wide fallback type cannot provide — structural
|
|
1571
|
+
* values make any string spellable here) and translated to its
|
|
1572
|
+
* declaration-order row id, tagged u64 — queries cross ids, never handle
|
|
1573
|
+
* names; the wire is untouched. THE single roster-verification point of
|
|
1574
|
+
* the query surface: atom-binding literals, comparison literals,
|
|
1575
|
+
* execute-time params, and membership-array members all reach it (never
|
|
1576
|
+
* duplicate the check per call site).
|
|
1187
1577
|
*/
|
|
1188
1578
|
function taggedHandleId(
|
|
1189
1579
|
context: string,
|
|
1190
1580
|
closed: { readonly name: string; readonly handles: readonly string[] },
|
|
1191
1581
|
value: unknown
|
|
1192
1582
|
): TaggedValue {
|
|
1193
|
-
if (typeof value !== "
|
|
1194
|
-
throw literalShapeError(context, `a ${closed.name} handle
|
|
1583
|
+
if (typeof value !== "string") {
|
|
1584
|
+
throw literalShapeError(context, `a ${closed.name} handle name (string)`, value)
|
|
1195
1585
|
}
|
|
1196
|
-
|
|
1586
|
+
const id = closed.handles.indexOf(value)
|
|
1587
|
+
if (id < 0) {
|
|
1197
1588
|
throw errors.new(
|
|
1198
|
-
`${context}:
|
|
1589
|
+
`${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
|
|
1199
1590
|
)
|
|
1200
1591
|
}
|
|
1201
|
-
return { kind: "u64", value }
|
|
1592
|
+
return { kind: "u64", value: BigInt(id) }
|
|
1202
1593
|
}
|
|
1203
1594
|
|
|
1204
1595
|
/**
|
|
@@ -1348,7 +1739,13 @@ function paramIdOf(ctx: LowerContext, name: string): number {
|
|
|
1348
1739
|
return id
|
|
1349
1740
|
}
|
|
1350
1741
|
|
|
1351
|
-
/**
|
|
1742
|
+
/**
|
|
1743
|
+
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
1744
|
+
* same edb source — its ordinal is its record-declaration slot exactly like
|
|
1745
|
+
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
1746
|
+
* at 0, each payload column at its declared index + 1 (`matchFieldsOf`
|
|
1747
|
+
* carries the shift; the lowering golden pins it).
|
|
1748
|
+
*/
|
|
1352
1749
|
function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
1353
1750
|
const member = ctx.theory.relations[atom.relation.name]
|
|
1354
1751
|
if (member !== atom.relation) {
|
|
@@ -1360,8 +1757,9 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
|
1360
1757
|
if (relationId === undefined) {
|
|
1361
1758
|
throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
|
|
1362
1759
|
}
|
|
1760
|
+
const ordered = matchFieldsOf(atom.relation)
|
|
1363
1761
|
const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
|
|
1364
|
-
const ordinal =
|
|
1762
|
+
const ordinal = ordered.findIndex(function byName(candidate) {
|
|
1365
1763
|
return candidate.name === binding.field
|
|
1366
1764
|
})
|
|
1367
1765
|
if (ordinal < 0) {
|
|
@@ -1372,7 +1770,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
|
1372
1770
|
return { source: { kind: "edb", relation: relationId }, bindings }
|
|
1373
1771
|
}
|
|
1374
1772
|
|
|
1375
|
-
/**
|
|
1773
|
+
/**
|
|
1774
|
+
* Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
|
|
1775
|
+
* existing param-set term over its content-addressed registry entry — the
|
|
1776
|
+
* program IR is byte-identical to the same set spelled `r.inSet`; the SDK
|
|
1777
|
+
* supplies the translated member set itself at execute (`wireParams`).
|
|
1778
|
+
*/
|
|
1376
1779
|
function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
|
|
1377
1780
|
const bound = binding.term
|
|
1378
1781
|
switch (bound.kind) {
|
|
@@ -1382,6 +1785,8 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
|
|
|
1382
1785
|
return { kind: "param", param: paramIdOf(ctx, bound.name) }
|
|
1383
1786
|
case "setParam":
|
|
1384
1787
|
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
|
|
1788
|
+
case "literalSet":
|
|
1789
|
+
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
|
|
1385
1790
|
case "literal":
|
|
1386
1791
|
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
|
|
1387
1792
|
}
|
|
@@ -1436,7 +1841,7 @@ function lowerCmpTerm(
|
|
|
1436
1841
|
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1437
1842
|
function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
|
|
1438
1843
|
if (sibling.kind === "var") {
|
|
1439
|
-
return rule.varFields[sibling.name]
|
|
1844
|
+
return rule.varFields[sibling.name]?.field
|
|
1440
1845
|
}
|
|
1441
1846
|
if (sibling.kind === "measure") {
|
|
1442
1847
|
return "measure"
|