@bjornpagen/bumbledb 0.5.0 → 0.7.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 +140 -136
- package/README.md +24 -11
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +172 -116
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +199 -203
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +39 -48
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +116 -0
- package/dist/query/find.d.ts.map +1 -0
- package/dist/query/{select.js → find.js} +22 -22
- package/dist/query/find.js.map +1 -0
- package/dist/query/lower.d.ts +131 -158
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +505 -494
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +22 -14
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +35 -42
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +8 -8
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +16 -21
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +139 -74
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +80 -33
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +293 -177
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +12 -8
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +297 -275
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +692 -724
- package/src/query/predicate.ts +37 -45
- package/src/query/run.ts +17 -22
- package/src/query/scope.ts +184 -90
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- package/src/statements.ts +30 -6
- package/dist/query/select.d.ts +0 -128
- package/dist/query/select.d.ts.map +0 -1
- package/dist/query/select.js.map +0 -1
- package/src/query/select.ts +0 -215
package/src/query/predicate.ts
CHANGED
|
@@ -5,31 +5,39 @@
|
|
|
5
5
|
*
|
|
6
6
|
* program(S, (p) => {
|
|
7
7
|
* const reach = p.rec("reach")
|
|
8
|
-
* reach.rule((r) =>
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* reach.rule((r) => {
|
|
9
|
+
* const n = v(Node)
|
|
10
|
+
* return r.match(Node, { id: n.id }).where(r.eq(n.id, r.param("root"))).find({ c: n.id })
|
|
11
|
+
* })
|
|
12
|
+
* reach.rule((r) => {
|
|
13
|
+
* const e = v(Parent)
|
|
14
|
+
* return r.match(Parent, { child: e.child, parent: e.parent }).idb(reach, { c: e.parent }).find({ c: e.child })
|
|
15
|
+
* })
|
|
16
|
+
* return p.output((r) => {
|
|
17
|
+
* const post = v(Posting)
|
|
18
|
+
* return r.match(Posting, { account: post.account, minor: post.minor })
|
|
19
|
+
* .idb(reach, { c: post.account }).find({ total: r.sum(post.minor) })
|
|
20
|
+
* })
|
|
14
21
|
* })
|
|
15
22
|
*
|
|
16
23
|
* `p.rec(name)` declares one recursive predicate (declaration order = its
|
|
17
24
|
* dense `PredId`); `rec.rule(...)` attaches one clause — its builder's
|
|
18
25
|
* `idb` accepts ONLY the rec itself (the self-recursion cut as a
|
|
19
|
-
* type-level boundary: mutual recursion is unwritable) and its head
|
|
20
|
-
* projects bound
|
|
21
|
-
*
|
|
26
|
+
* type-level boundary: mutual recursion is unwritable) and its `find` head
|
|
27
|
+
* projects bound variables only (aggregation/measure through a cycle is
|
|
28
|
+
* unrepresentable — the strata judge's roster, made unwritable);
|
|
22
29
|
* `p.output(...)` seals the recs and builds the output rules, whose `idb`
|
|
23
|
-
* folds any FINISHED stratum
|
|
24
|
-
* returns carries the params its rules
|
|
25
|
-
*
|
|
26
|
-
* rules use. Everything deeper —
|
|
27
|
-
* three oracles — is the ENGINE's
|
|
30
|
+
* folds any FINISHED stratum by NAMED record over its head keys (recipe
|
|
31
|
+
* 25's form). The rec value `.rule` returns carries the params its rules
|
|
32
|
+
* used — thread it into the output's `idb` and the program's inferred
|
|
33
|
+
* `Params` stays exactly the params the rules use. Everything deeper —
|
|
34
|
+
* strata legality, signature sealing, the three oracles — is the ENGINE's
|
|
35
|
+
* judge, surfacing typed at prepare.
|
|
28
36
|
*/
|
|
29
37
|
|
|
30
38
|
import * as errors from "@superbuilders/errors"
|
|
31
39
|
import type { SchemaClasses } from "#law.ts"
|
|
32
|
-
import type { RecData
|
|
40
|
+
import type { RecData } from "#query/atom.ts"
|
|
33
41
|
import type {
|
|
34
42
|
AnyRuleValue,
|
|
35
43
|
HeadOf,
|
|
@@ -45,7 +53,7 @@ import type {
|
|
|
45
53
|
RuleValue
|
|
46
54
|
} from "#query/lower.ts"
|
|
47
55
|
import { makeOutputRuleScope, makeQuery, makeRawScope } from "#query/lower.ts"
|
|
48
|
-
import type {
|
|
56
|
+
import type { Flatten, ParamsRecord, ShapeOf } from "#query/scope.ts"
|
|
49
57
|
import { fieldJoins, inferred, renderFieldKind } from "#query/scope.ts"
|
|
50
58
|
import type { Schema, SchemaRelations } from "#schema.ts"
|
|
51
59
|
|
|
@@ -112,20 +120,6 @@ interface RawRec<Name extends string> {
|
|
|
112
120
|
rule(build: (r: RawScope) => RuleValue<never, never>): RawRec<Name>
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
/**
|
|
116
|
-
* The classed slot one rec head column binds, through the rule's own
|
|
117
|
-
* environment: a rec head projects bound variable NAMES only (the strata
|
|
118
|
-
* roster's unwritability), so every column is a projected var and its
|
|
119
|
-
* slot is the var's first positive binding.
|
|
120
|
-
*/
|
|
121
|
-
function recHeadSlotOf(rule: RuleData, column: SelectColumn): ClassedField | undefined {
|
|
122
|
-
const entry = column.entry
|
|
123
|
-
if (entry.kind === "var") {
|
|
124
|
-
return rule.varFields[entry.over]
|
|
125
|
-
}
|
|
126
|
-
return undefined
|
|
127
|
-
}
|
|
128
|
-
|
|
129
123
|
/** Builds the runtime rec handle over shared rec data. */
|
|
130
124
|
function makeRawRec<Name extends string>(state: ProgramState, name: Name, data: RecData): RawRec<Name> {
|
|
131
125
|
const rec: RawRec<Name> = {
|
|
@@ -137,13 +131,13 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
|
|
|
137
131
|
`rec ${name}: the program's output is already declared — recursive rules attach before p.output`
|
|
138
132
|
)
|
|
139
133
|
}
|
|
140
|
-
const built = build(makeRawScope({ kind: "rec", self: data, classes: state.classes }))
|
|
134
|
+
const built = build(makeRawScope({ kind: "rec", self: data, classes: state.classes, theory: state.theory }))
|
|
141
135
|
const head = data.rules[0]
|
|
142
136
|
if (head !== undefined) {
|
|
143
|
-
const declared = head.
|
|
137
|
+
const declared = head.finds.map(function columnName(column) {
|
|
144
138
|
return column.name
|
|
145
139
|
})
|
|
146
|
-
const candidate = built.rule.
|
|
140
|
+
const candidate = built.rule.finds.map(function columnName(column) {
|
|
147
141
|
return column.name
|
|
148
142
|
})
|
|
149
143
|
if (declared.join(", ") !== candidate.join(", ")) {
|
|
@@ -153,21 +147,19 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
|
|
|
153
147
|
}
|
|
154
148
|
// The law-class wall on the sealed head: names alone do not
|
|
155
149
|
// align value spaces. Every rule must bind each head column
|
|
156
|
-
// at a slot that JOINS rule 0's (the sealing rule
|
|
157
|
-
// slot every downstream idb pairing class-checks
|
|
158
|
-
// under the same fieldJoins judgment every reuse
|
|
159
|
-
// enforces; otherwise a later rule feeds (say) bare
|
|
160
|
-
// into a column the idb joins as Node ids.
|
|
161
|
-
built.rule.
|
|
162
|
-
const lead = head.
|
|
150
|
+
// at a classed mint slot that JOINS rule 0's (the sealing rule
|
|
151
|
+
// — the one slot every downstream idb pairing class-checks
|
|
152
|
+
// against), under the same fieldJoins judgment every reuse
|
|
153
|
+
// site enforces; otherwise a later rule feeds (say) bare
|
|
154
|
+
// weights into a column the idb joins as Node ids.
|
|
155
|
+
built.rule.finds.forEach(function verifyHeadSlot(column, position) {
|
|
156
|
+
const lead = head.finds[position]
|
|
163
157
|
if (lead === undefined) {
|
|
164
158
|
return
|
|
165
159
|
}
|
|
166
|
-
|
|
167
|
-
const slot = recHeadSlotOf(built.rule, column)
|
|
168
|
-
if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
|
|
160
|
+
if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
|
|
169
161
|
throw errors.new(
|
|
170
|
-
`rec ${name}: every rule derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(
|
|
162
|
+
`rec ${name}: every rule derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in this rule (a head column joins only class-equal slots; bare pairs only with bare)`
|
|
171
163
|
)
|
|
172
164
|
}
|
|
173
165
|
})
|
|
@@ -221,7 +213,7 @@ function program<
|
|
|
221
213
|
Classes extends SchemaClasses,
|
|
222
214
|
Q extends Query<Rels, unknown, ParamsRecord, Classes>
|
|
223
215
|
>(theory: Schema<Rels, Classes>, build: (p: ProgramScope<Rels, Classes>) => Q): Q {
|
|
224
|
-
const state: ProgramState = { recs: [], classes: theory.classes, sealed: false }
|
|
216
|
+
const state: ProgramState = { recs: [], classes: theory.classes, theory, sealed: false }
|
|
225
217
|
const names = new Set<string>()
|
|
226
218
|
const made: { query: unknown } = { query: undefined }
|
|
227
219
|
const scope: ProgramScope<Rels, Classes> = {
|
package/src/query/run.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* select column IS a row (the trusted read seam), and nothing is asserted
|
|
11
11
|
* on any value. A CLOSED answer column decodes id → handle NAME through
|
|
12
12
|
* the marshal's one bijection (`handleOf` — the same read half every fact
|
|
13
|
-
* decode rides; the column's roster rides `
|
|
13
|
+
* decode rides; the column's roster rides `FindColumn.closed`), so query
|
|
14
14
|
* rows speak the vocabulary exactly as scans and gets do. Answers are
|
|
15
15
|
* SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
|
|
16
16
|
* VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import * as errors from "@superbuilders/errors"
|
|
20
20
|
import { handleOf } from "#marshal.ts"
|
|
21
21
|
import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
|
|
22
|
-
import type {
|
|
22
|
+
import type { FindColumn } from "#query/atom.ts"
|
|
23
23
|
import { ALLEN_ALL_BITS } from "#query/atom.ts"
|
|
24
24
|
import { taggedCmpLiteral } from "#query/lower.ts"
|
|
25
25
|
import type { ParamEntry } from "#query/scope.ts"
|
|
@@ -48,21 +48,16 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
|
|
|
48
48
|
* is a typed error naming the param; values tag by the anchoring use's
|
|
49
49
|
* structural type; a set param takes a readonly array (the empty set is
|
|
50
50
|
* legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
|
|
51
|
-
* entry (`
|
|
52
|
-
*
|
|
53
|
-
* roster-verification point
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* entry (`membership` present — a literal set folded into the program) is
|
|
52
|
+
* a program constant the registry already resolved through the one
|
|
53
|
+
* roster-verification point at BUILD time: it crosses as its prebuilt
|
|
54
|
+
* frozen `{ kind: "set", values }` by reference — the host's params object
|
|
55
|
+
* is never consulted for it, and no per-execute work exists.
|
|
56
56
|
*/
|
|
57
57
|
function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
|
|
58
58
|
return entries.map(function wireOne(entry): QueryParam {
|
|
59
|
-
if (entry.
|
|
60
|
-
return
|
|
61
|
-
kind: "set",
|
|
62
|
-
values: entry.members.map(function wireMember(member, index) {
|
|
63
|
-
return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
|
|
64
|
-
})
|
|
65
|
-
}
|
|
59
|
+
if (entry.membership !== undefined) {
|
|
60
|
+
return entry.membership
|
|
66
61
|
}
|
|
67
62
|
const value = supplied[entry.name]
|
|
68
63
|
if (value === undefined) {
|
|
@@ -94,10 +89,10 @@ function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<st
|
|
|
94
89
|
* re-derive).
|
|
95
90
|
*/
|
|
96
91
|
function isAnswerRow<Row>(
|
|
97
|
-
|
|
92
|
+
finds: readonly FindColumn[],
|
|
98
93
|
decoded: Readonly<Record<string, FactValue>>
|
|
99
94
|
): decoded is Readonly<Record<string, FactValue>> & Row {
|
|
100
|
-
return
|
|
95
|
+
return finds.every(function present(column) {
|
|
101
96
|
return decoded[column.name] !== undefined
|
|
102
97
|
})
|
|
103
98
|
}
|
|
@@ -109,13 +104,13 @@ function isAnswerRow<Row>(
|
|
|
109
104
|
* NAME through the marshal's bijection — an out-of-roster id is the same
|
|
110
105
|
* pointed throw a fact decode gives, never a silent fallback.
|
|
111
106
|
*/
|
|
112
|
-
function decodeAnswers<Row>(
|
|
107
|
+
function decodeAnswers<Row>(finds: readonly FindColumn[], rows: FactValue[][]): Row[] {
|
|
113
108
|
return rows.map(function decodeRow(row) {
|
|
114
|
-
if (row.length !==
|
|
115
|
-
throw errors.new(`query answer arity ${row.length} does not match the ${
|
|
109
|
+
if (row.length !== finds.length) {
|
|
110
|
+
throw errors.new(`query answer arity ${row.length} does not match the ${finds.length} find columns`)
|
|
116
111
|
}
|
|
117
112
|
const decoded: Record<string, FactValue> = {}
|
|
118
|
-
|
|
113
|
+
finds.forEach(function decodeCell(column, ordinal) {
|
|
119
114
|
const cell = row[ordinal]
|
|
120
115
|
if (cell === undefined) {
|
|
121
116
|
throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
|
|
@@ -124,8 +119,8 @@ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]
|
|
|
124
119
|
column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
|
|
125
120
|
})
|
|
126
121
|
Object.freeze(decoded)
|
|
127
|
-
if (!isAnswerRow<Row>(
|
|
128
|
-
throw errors.new("query answer row is not a complete
|
|
122
|
+
if (!isAnswerRow<Row>(finds, decoded)) {
|
|
123
|
+
throw errors.new("query answer row is not a complete find record")
|
|
129
124
|
}
|
|
130
125
|
return decoded
|
|
131
126
|
})
|
package/src/query/scope.ts
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Query scope terms,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
2
|
+
* Query scope terms, REFERENCE-IDENTITY edition: a query variable is an
|
|
3
|
+
* OBJECT, minted fresh by {@link v} over a relation's statically-known
|
|
4
|
+
* columns. `v(relation)` returns a record of fresh variables — one per
|
|
5
|
+
* column, each typed at mint by its column's descriptor AND the mint
|
|
6
|
+
* coordinate (the owner relation name and the column name), so
|
|
7
|
+
* destructuring preserves every literal and every class
|
|
8
|
+
* (`const { id, holder } = v(Account)`). Variable IDENTITY is the object
|
|
9
|
+
* reference: reusing the same var value across binding positions IS the
|
|
10
|
+
* join, and a name-collision join is unrepresentable (two `v()` calls mint
|
|
11
|
+
* two distinct batches, so two same-named vars are two variables). Params
|
|
12
|
+
* stay STRING-named — their names are the execute() params object's runtime
|
|
13
|
+
* keys, an honest load-bearing channel, not a lie.
|
|
14
|
+
*
|
|
15
|
+
* THE DESIGN THEOREM. {@link JoinOk} is an EQUALITY (kind, class, width,
|
|
16
|
+
* element, roster), so judging every binding position against the
|
|
17
|
+
* variable's MINT slot ({@link MintSlotOf}) makes all cross-binding joins
|
|
18
|
+
* mutually class-equal by transitivity — the env/sibling checks the
|
|
19
|
+
* name-keyed edition needed are subsumed, deleted rather than ported. The
|
|
20
|
+
* one check representation cannot carry is BOUNDNESS (is this var positively
|
|
21
|
+
* bound in this rule): TypeScript types cannot see object identity, so
|
|
22
|
+
* boundness moves from the type tier to construction-time walls only — an
|
|
23
|
+
* explicit essential-vs-accidental concession; every runtime twin is
|
|
24
|
+
* preserved.
|
|
25
|
+
*
|
|
26
|
+
* This module also owns the environment/typing utilities the whole surface
|
|
27
|
+
* shares: the join descriptor {@link ClassedField}, the mint-slot machinery
|
|
28
|
+
* ({@link MintSlotOf}/{@link MintClassOf}), the class-equality judgment
|
|
29
|
+
* {@link JoinOk} with its runtime twin {@link fieldJoins}, and the
|
|
30
|
+
* record-folding helpers `Params` and `Row` inference ride.
|
|
20
31
|
*/
|
|
21
32
|
|
|
33
|
+
import * as errors from "@superbuilders/errors"
|
|
34
|
+
import type { AnyClosed } from "#closed.ts"
|
|
35
|
+
import { sealedFieldsOf } from "#closed.ts"
|
|
22
36
|
import type { AnyField, Infer } from "#fields.ts"
|
|
23
37
|
import { rosterOf } from "#fields.ts"
|
|
38
|
+
import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
|
|
39
|
+
import type { QueryParam } from "#native.ts"
|
|
40
|
+
import type { AnyRelation, RelationFields } from "#relation.ts"
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
43
|
* The runtime discriminant of query term values. Host literals (bigints,
|
|
@@ -38,17 +55,47 @@ const term: unique symbol = Symbol("bumbledb.query.term")
|
|
|
38
55
|
const inferred: unique symbol = Symbol("bumbledb.query.inferred")
|
|
39
56
|
|
|
40
57
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
58
|
+
* What a query atom matches over: an ordinary relation or a CLOSED
|
|
59
|
+
* vocabulary (ψ query atoms — the engine folds a resolvable closed atom
|
|
60
|
+
* into a plan-constant member set at prepare, or joins the L1-resident
|
|
61
|
+
* virtual image when the shape does not fold; the SDK never pre-folds and
|
|
62
|
+
* never knows which — transparency is the contract).
|
|
46
63
|
*/
|
|
47
|
-
|
|
64
|
+
type MatchOwner = AnyRelation | AnyClosed
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The matchable field block of an atom owner: a relation's declared
|
|
68
|
+
* fields; a closed relation's SEALED shape — the synthetic `id` (the
|
|
69
|
+
* value's OWN roster-carrying descriptor, at its precise type) first, then
|
|
70
|
+
* the declared payload columns read through the typed `columns` carrier.
|
|
71
|
+
* The runtime twin is `sealedFieldsOf` in `#closed.ts`.
|
|
72
|
+
*/
|
|
73
|
+
type MatchFields<R extends MatchOwner> = R extends AnyClosed
|
|
74
|
+
? { readonly id: R["id"] } & R["columns"]
|
|
75
|
+
: R extends AnyRelation
|
|
76
|
+
? RelationFields<R>
|
|
77
|
+
: never
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A query variable — an OBJECT minted by {@link v}. Identity is the object
|
|
81
|
+
* reference: reuse of the same value across binding positions is the join,
|
|
82
|
+
* strictly rule-scoped (each rule numbers its own dense `VarId`s). The type
|
|
83
|
+
* carries the mint COORDINATE — `RN` the owner relation name literal, `K`
|
|
84
|
+
* the column name literal — and `F` the mint descriptor, so the mint slot
|
|
85
|
+
* (descriptor + law-computed class) is recoverable at every binding
|
|
86
|
+
* position for the join judgment.
|
|
87
|
+
*/
|
|
88
|
+
interface Var<F extends AnyField = AnyField, RN extends string = string, K extends string = string> {
|
|
48
89
|
readonly [term]: "var"
|
|
49
|
-
readonly name:
|
|
90
|
+
readonly owner: MatchOwner & { readonly name: RN }
|
|
91
|
+
readonly column: K
|
|
92
|
+
readonly field: F
|
|
93
|
+
readonly label: string
|
|
50
94
|
}
|
|
51
95
|
|
|
96
|
+
/** Any query variable, whatever its descriptor and mint coordinate. */
|
|
97
|
+
type AnyVar = Var
|
|
98
|
+
|
|
52
99
|
/**
|
|
53
100
|
* A scalar query parameter — `r.param("root")`. The name is the key of the
|
|
54
101
|
* typed params object `execute` takes; the type is the element type of the
|
|
@@ -86,14 +133,13 @@ interface MaskParam<Name extends string = string> {
|
|
|
86
133
|
/**
|
|
87
134
|
* The measure of an interval-typed variable (`ir::Term::Measure`):
|
|
88
135
|
* `|[s, e)| = e − s`, u64 — legal as one side of an order comparison, as a
|
|
89
|
-
*
|
|
90
|
-
* is unwritable, exactly as the IR rejects it typed.
|
|
91
|
-
*
|
|
92
|
-
* (`allen` against a bounded window).
|
|
136
|
+
* find entry, and as the input of `sum`/`min`/`max`; every other position
|
|
137
|
+
* is unwritable, exactly as the IR rejects it typed. Carries the interval
|
|
138
|
+
* variable it measures BY REFERENCE.
|
|
93
139
|
*/
|
|
94
|
-
interface Duration<
|
|
140
|
+
interface Duration<V extends AnyVar = AnyVar> {
|
|
95
141
|
readonly [term]: "duration"
|
|
96
|
-
readonly
|
|
142
|
+
readonly over: V
|
|
97
143
|
}
|
|
98
144
|
|
|
99
145
|
/** Any scope term value. */
|
|
@@ -104,10 +150,55 @@ function isTerm(value: unknown): value is AnyTerm {
|
|
|
104
150
|
return typeof value === "object" && value !== null && term in value
|
|
105
151
|
}
|
|
106
152
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The record of fresh variables `v(owner)` mints — one per statically-known
|
|
155
|
+
* column, each typed by its column's descriptor and mint coordinate.
|
|
156
|
+
*/
|
|
157
|
+
type VarsOf<R extends MatchOwner> = {
|
|
158
|
+
readonly [K in keyof MatchFields<R> & string]: Var<MatchFields<R>[K], R["name"], K>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The trusted admission seam of the variable-record mint (the pattern's
|
|
163
|
+
* home is `isTypedScope` in `#query/lower.ts`): the checkable fact — one own
|
|
164
|
+
* enumerable variable per sealed column — is verified before the record is
|
|
165
|
+
* admitted at its computed {@link VarsOf} type.
|
|
166
|
+
*/
|
|
167
|
+
function varsMinted<R extends MatchOwner>(owner: R, record: Readonly<Record<string, AnyVar>>): record is VarsOf<R> {
|
|
168
|
+
return sealedFieldsOf(owner).every(function columnMinted(declared) {
|
|
169
|
+
return Object.hasOwn(record, declared.name)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Mints a FRESH batch of query variables over an atom owner's
|
|
175
|
+
* statically-known columns — one variable per sealed column
|
|
176
|
+
* (`sealedFieldsOf`: a closed owner mints `id` first, then payload columns),
|
|
177
|
+
* each frozen and each defined by OWN-property definition (object-protocol
|
|
178
|
+
* column names must work, the `closed()` precedent). Every `v()` call mints
|
|
179
|
+
* new objects, so two batches are two variables; property access within one
|
|
180
|
+
* batch is stable by construction (the record is an eager frozen record,
|
|
181
|
+
* never a Proxy). Variable identity is the object reference: destructure
|
|
182
|
+
* what you need (`const { id, holder } = v(Account)`) and reuse a value
|
|
183
|
+
* across binding positions to join.
|
|
184
|
+
*/
|
|
185
|
+
function v<R extends MatchOwner>(owner: R): VarsOf<R> {
|
|
186
|
+
const record: Record<string, AnyVar> = {}
|
|
187
|
+
for (const declared of sealedFieldsOf(owner)) {
|
|
188
|
+
const variable: AnyVar = Object.freeze({
|
|
189
|
+
[term]: "var" as const,
|
|
190
|
+
owner,
|
|
191
|
+
column: declared.name,
|
|
192
|
+
field: declared.field,
|
|
193
|
+
label: `${owner.name}.${declared.name}`
|
|
194
|
+
})
|
|
195
|
+
Object.defineProperty(record, declared.name, { value: variable, enumerable: true })
|
|
196
|
+
}
|
|
197
|
+
Object.freeze(record)
|
|
198
|
+
if (!varsMinted(owner, record)) {
|
|
199
|
+
throw errors.new(`v(${owner.name}): variable-record minting incomplete`)
|
|
200
|
+
}
|
|
201
|
+
return record
|
|
111
202
|
}
|
|
112
203
|
|
|
113
204
|
/** Builds one scalar-parameter term. */
|
|
@@ -128,19 +219,17 @@ function makeMaskParam<const Name extends string>(name: Name): MaskParam<Name> {
|
|
|
128
219
|
return Object.freeze(value)
|
|
129
220
|
}
|
|
130
221
|
|
|
131
|
-
/** Builds one measure term over an interval-typed variable
|
|
132
|
-
function makeDuration<const
|
|
133
|
-
const value: Duration<
|
|
222
|
+
/** Builds one measure term over an interval-typed variable reference. */
|
|
223
|
+
function makeDuration<const V extends AnyVar>(over: V): Duration<V> {
|
|
224
|
+
const value: Duration<V> = { [term]: "duration", over }
|
|
134
225
|
return Object.freeze(value)
|
|
135
226
|
}
|
|
136
227
|
|
|
137
228
|
/**
|
|
138
229
|
* One bound field slot: the field's descriptor plus the slot's
|
|
139
230
|
* law-computed CLASS (`undefined` = bare — the slot is in no law). The one
|
|
140
|
-
* shape
|
|
141
|
-
*
|
|
142
|
-
* (the rule's `varFields` record holds exactly this shape, read off the
|
|
143
|
-
* schema value's frozen class map) — one shape, two tiers, one walk.
|
|
231
|
+
* shape every join judgment compares, at the TYPE level (a variable's mint
|
|
232
|
+
* slot, a binding position's slot) and at RUNTIME alike.
|
|
144
233
|
*/
|
|
145
234
|
interface ClassedField {
|
|
146
235
|
readonly field: AnyField
|
|
@@ -148,11 +237,25 @@ interface ClassedField {
|
|
|
148
237
|
}
|
|
149
238
|
|
|
150
239
|
/**
|
|
151
|
-
* A
|
|
152
|
-
*
|
|
153
|
-
*
|
|
240
|
+
* A variable's law-computed CLASS at the TYPE level: its column's class,
|
|
241
|
+
* read off the schema type's class map through the mint coordinate the
|
|
242
|
+
* variable carries (`RN.K`). `undefined` = bare.
|
|
154
243
|
*/
|
|
155
|
-
type
|
|
244
|
+
type MintClassOf<Classes extends SchemaClasses, V> =
|
|
245
|
+
V extends Var<AnyField, infer RN extends string, infer K extends string>
|
|
246
|
+
? ClassLookup<ClassRecordOf<Classes, RN>, K>
|
|
247
|
+
: never
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A variable's MINT slot: the descriptor it was minted at plus its
|
|
251
|
+
* law-computed class. The one slot every binding position judges against —
|
|
252
|
+
* because {@link JoinOk} is an equality, judging each position against the
|
|
253
|
+
* mint slot makes every cross-binding join transitively class-equal.
|
|
254
|
+
*/
|
|
255
|
+
type MintSlotOf<Classes extends SchemaClasses, V extends AnyVar> = {
|
|
256
|
+
readonly field: V["field"]
|
|
257
|
+
readonly class: MintClassOf<Classes, V>
|
|
258
|
+
}
|
|
156
259
|
|
|
157
260
|
/** A params object type — what `execute` takes and inference carries. */
|
|
158
261
|
type ParamsRecord = Readonly<Record<string, unknown>>
|
|
@@ -177,24 +280,28 @@ type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : u
|
|
|
177
280
|
/** Reads an interval descriptor's element kind; `undefined` on scalar kinds. */
|
|
178
281
|
type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
|
|
179
282
|
|
|
180
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Reads a closed reference's vocabulary name literal paired with its handle
|
|
285
|
+
* union; `undefined` on every non-closed kind (the roster IS descriptor
|
|
286
|
+
* structure). The name literal is the type-tier encoding of the runtime's
|
|
287
|
+
* roster VALUE-IDENTITY judgment ({@link fieldJoins}): two same-shaped
|
|
288
|
+
* vocabularies are distinct rosters, so they mismatch here exactly as they
|
|
289
|
+
* refuse at runtime.
|
|
290
|
+
*/
|
|
181
291
|
type RosterOf<F extends AnyField> = F extends {
|
|
182
|
-
readonly closed: { readonly handles: readonly (infer H extends string)[] }
|
|
292
|
+
readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
|
|
183
293
|
}
|
|
184
|
-
? H
|
|
294
|
+
? readonly [N, H]
|
|
185
295
|
: undefined
|
|
186
296
|
|
|
187
297
|
/**
|
|
188
298
|
* The join judgment: two bound slots join iff their descriptors' structure
|
|
189
299
|
* agrees (kind, width label, interval element, and the closed ROSTER — a
|
|
190
300
|
* closed reference pairs only with the same vocabulary, never with a bare
|
|
191
|
-
* u64
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* class; a bare↔classed pairing refuses). The class names come off the
|
|
196
|
-
* SCHEMA type's class map — the statements are the typing; no descriptor
|
|
197
|
-
* label beyond the roster exists to compare.
|
|
301
|
+
* u64) AND their law-computed classes agree — same class name joins, and
|
|
302
|
+
* bare (`undefined`) pairs only with bare (ruling 3). The class names come
|
|
303
|
+
* off the SCHEMA type's class map; no descriptor label beyond the roster
|
|
304
|
+
* exists to compare.
|
|
198
305
|
*/
|
|
199
306
|
type JoinOk<A extends ClassedField, B extends ClassedField> = [
|
|
200
307
|
A["field"]["kind"],
|
|
@@ -218,10 +325,9 @@ type JoinOk<A extends ClassedField, B extends ClassedField> = [
|
|
|
218
325
|
* The runtime twin of {@link JoinOk}: two bound slots join iff descriptor
|
|
219
326
|
* structure and class agree — the same comparison the type tier makes,
|
|
220
327
|
* judged on the honest runtime values (the descriptor, the roster by VALUE
|
|
221
|
-
* IDENTITY
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* not only where the compiler can see.
|
|
328
|
+
* IDENTITY, and the schema value's frozen class map). The rule builders
|
|
329
|
+
* throw through this on a class-unequal reuse, so the wall holds for untyped
|
|
330
|
+
* callers too.
|
|
225
331
|
*/
|
|
226
332
|
function fieldJoins(a: ClassedField, b: ClassedField): boolean {
|
|
227
333
|
const widthA = "width" in a.field ? a.field.width : undefined
|
|
@@ -242,9 +348,8 @@ function fieldJoins(a: ClassedField, b: ClassedField): boolean {
|
|
|
242
348
|
/**
|
|
243
349
|
* Renders one bound slot for join-mismatch diagnostics — the structural
|
|
244
350
|
* kind in the schema grammar's spelling (a closed reference names its
|
|
245
|
-
* vocabulary
|
|
246
|
-
* slot
|
|
247
|
-
* renders `bare`).
|
|
351
|
+
* vocabulary) plus the slot's law-computed class (`u64 in class Holder.id`;
|
|
352
|
+
* a lawless slot renders `bare`).
|
|
248
353
|
*/
|
|
249
354
|
function renderFieldKind(slot: ClassedField): string {
|
|
250
355
|
const field = slot.field
|
|
@@ -266,11 +371,7 @@ function renderFieldKind(slot: ClassedField): string {
|
|
|
266
371
|
* What a PARAM anchored at field `F` accepts at execution: the field's
|
|
267
372
|
* bare value type, exactly — at a CLOSED-reference field that is the
|
|
268
373
|
* handle-name union (`"DirectPass" | "Failed"`), translated name → row id
|
|
269
|
-
* at execute through the one roster-verification point
|
|
270
|
-
* (`taggedHandleId`). At an interval field the engine resolves the
|
|
271
|
-
* bivalent anchor to the INTERVAL reading (value equality) — the point
|
|
272
|
-
* reading of a param is spelled `pointIn(r.param(...), w)`, whose sibling
|
|
273
|
-
* anchors it element-typed.
|
|
374
|
+
* at execute through the one roster-verification point (`taggedHandleId`).
|
|
274
375
|
*/
|
|
275
376
|
type ParamValueAt<F extends AnyField> = Infer<F>
|
|
276
377
|
|
|
@@ -281,31 +382,34 @@ type InferredOf<T> = T extends { readonly [inferred]?: infer S } ? Exclude<S, un
|
|
|
281
382
|
* One registered parameter of a query, as the wire marshal reads it: the
|
|
282
383
|
* name, the wire shape, the field descriptor (or the measure) that anchored
|
|
283
384
|
* it, and the comparison op the anchor came from (`"binding"` for atom
|
|
284
|
-
* positions)
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* is never read from, and never demanded of, the host's params object.
|
|
385
|
+
* positions). `anchor` is `undefined` only on a query built but not yet
|
|
386
|
+
* anchored by any rule. `membership` is present exactly on a
|
|
387
|
+
* MEMBERSHIP-ARRAY entry: the FROZEN wire param, already resolved through
|
|
388
|
+
* the one roster-verification point at BUILD time (the entry stores the
|
|
389
|
+
* image, never the pre-image — no per-execute re-translation exists, and an
|
|
390
|
+
* out-of-roster handle name fails where the mistake was made).
|
|
291
391
|
*/
|
|
292
392
|
interface ParamEntry {
|
|
293
393
|
readonly name: string
|
|
294
394
|
readonly shape: "value" | "set" | "mask"
|
|
295
395
|
readonly anchor: AnyField | "measure" | undefined
|
|
296
396
|
readonly op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
|
|
297
|
-
readonly
|
|
397
|
+
readonly membership: QueryParam | undefined
|
|
298
398
|
}
|
|
299
399
|
|
|
300
400
|
export type {
|
|
301
401
|
AnyTerm,
|
|
402
|
+
AnyVar,
|
|
302
403
|
ClassedField,
|
|
303
404
|
Duration,
|
|
304
|
-
EnvShape,
|
|
305
405
|
Flatten,
|
|
306
406
|
InferredOf,
|
|
307
407
|
JoinOk,
|
|
308
408
|
MaskParam,
|
|
409
|
+
MatchFields,
|
|
410
|
+
MatchOwner,
|
|
411
|
+
MintClassOf,
|
|
412
|
+
MintSlotOf,
|
|
309
413
|
Param,
|
|
310
414
|
ParamEntry,
|
|
311
415
|
ParamsRecord,
|
|
@@ -313,17 +417,7 @@ export type {
|
|
|
313
417
|
SetParam,
|
|
314
418
|
ShapeOf,
|
|
315
419
|
UnionToIntersection,
|
|
316
|
-
Var
|
|
317
|
-
|
|
318
|
-
export {
|
|
319
|
-
fieldJoins,
|
|
320
|
-
inferred,
|
|
321
|
-
isTerm,
|
|
322
|
-
makeDuration,
|
|
323
|
-
makeMaskParam,
|
|
324
|
-
makeParam,
|
|
325
|
-
makeSetParam,
|
|
326
|
-
makeVar,
|
|
327
|
-
renderFieldKind,
|
|
328
|
-
term
|
|
420
|
+
Var,
|
|
421
|
+
VarsOf
|
|
329
422
|
}
|
|
423
|
+
export { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, renderFieldKind, term, v }
|
package/src/relation.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as errors from "@superbuilders/errors"
|
|
17
|
-
import { type AnyField, assertDeclarationOrderKey, type Infer, literalOf } from "#fields.ts"
|
|
17
|
+
import { type AnyField, assertDeclarationOrderKey, assertDeclarationRecord, type Infer, literalOf } from "#fields.ts"
|
|
18
18
|
import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
|
|
19
19
|
|
|
20
20
|
/** Flattens an intersection into one displayed object type (hover legibility). */
|
|
@@ -198,6 +198,8 @@ function relation<const Name extends string, Fields extends FieldsShape>(
|
|
|
198
198
|
name: Name,
|
|
199
199
|
fields: Fields
|
|
200
200
|
): Relation<Name, Fields> {
|
|
201
|
+
assertDeclarationOrderKey("relation", name)
|
|
202
|
+
assertDeclarationRecord(`relation ${name} fields`, fields)
|
|
201
203
|
const ordered: RelationField[] = []
|
|
202
204
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
203
205
|
assertDeclarationOrderKey(`relation ${name} field`, fieldName)
|