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