@bjornpagen/bumbledb 1.1.0 → 1.2.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 +114 -11
- package/README.md +52 -9
- package/dist/capacity.d.ts +15 -55
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +63 -12
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +26 -3
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +89 -44
- package/dist/changes.js.map +1 -1
- package/dist/closed.d.ts +23 -51
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +110 -168
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +4 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +108 -183
- package/dist/codec.js.map +1 -1
- package/dist/compile.d.ts +16 -16
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +36 -15
- package/dist/compile.js.map +1 -1
- package/dist/db-native.d.ts +39 -0
- package/dist/db-native.d.ts.map +1 -1
- package/dist/db-native.js +8 -0
- package/dist/db-native.js.map +1 -1
- package/dist/db.d.ts +28 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +91 -38
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +11 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -1
- package/dist/errors.js.map +1 -1
- package/dist/face.d.ts +19 -25
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +30 -24
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +13 -26
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +89 -116
- package/dist/fields.js.map +1 -1
- package/dist/immutable.d.ts +10 -0
- package/dist/immutable.d.ts.map +1 -0
- package/dist/immutable.js +75 -0
- package/dist/immutable.js.map +1 -0
- package/dist/index.d.ts +14 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/log.d.ts +2 -0
- package/dist/internal/log.d.ts.map +1 -1
- package/dist/internal/log.js +2 -0
- package/dist/internal/log.js.map +1 -1
- package/dist/law.d.ts +10 -10
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +1 -1
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +11 -6
- package/dist/lower.js.map +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +1 -1
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/description.d.ts +33 -0
- package/dist/query/description.d.ts.map +1 -0
- package/dist/query/description.js +302 -0
- package/dist/query/description.js.map +1 -0
- package/dist/query/lower.d.ts +60 -4
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +126 -161
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +7 -2
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +240 -53
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +8 -10
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +3 -4
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +5 -10
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +22 -48
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +30 -82
- package/dist/relation.js.map +1 -1
- package/dist/rows.d.ts +7 -4
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +44 -198
- package/dist/rows.js.map +1 -1
- package/dist/runtime-errors.d.ts +26 -2
- package/dist/runtime-errors.d.ts.map +1 -1
- package/dist/runtime-errors.js +43 -3
- package/dist/runtime-errors.js.map +1 -1
- package/dist/schema.d.ts +9 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +85 -24
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +27 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +104 -0
- package/dist/selection.js.map +1 -0
- package/dist/shape.d.ts +6 -9
- package/dist/shape.d.ts.map +1 -1
- package/dist/statements.d.ts +17 -52
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +92 -111
- package/dist/statements.js.map +1 -1
- package/dist/values.d.ts +25 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +203 -0
- package/dist/values.js.map +1 -0
- package/pack-provenance.json +3 -3
- package/package.json +4 -4
- package/src/capacity.ts +86 -76
- package/src/changes.ts +183 -54
- package/src/closed.ts +161 -314
- package/src/codec.ts +116 -190
- package/src/compile.ts +56 -32
- package/src/db-native.ts +42 -0
- package/src/db.ts +134 -52
- package/src/errors.ts +16 -1
- package/src/face.ts +44 -74
- package/src/fields.ts +100 -146
- package/src/immutable.ts +76 -0
- package/src/index.ts +34 -28
- package/src/internal/log.ts +2 -0
- package/src/law.ts +13 -13
- package/src/lower.ts +13 -10
- package/src/native.ts +2 -0
- package/src/query/atom.ts +1 -0
- package/src/query/description.ts +394 -0
- package/src/query/lower.ts +139 -173
- package/src/query/parse-ir.ts +247 -54
- package/src/query/run.ts +15 -9
- package/src/query/scope.ts +10 -18
- package/src/relation.ts +58 -142
- package/src/rows.ts +62 -205
- package/src/runtime-errors.ts +47 -3
- package/src/schema.ts +99 -29
- package/src/selection.ts +124 -0
- package/src/shape.ts +9 -9
- package/src/statements.ts +138 -189
- package/src/values.ts +223 -0
package/src/query/parse-ir.ts
CHANGED
|
@@ -1,75 +1,268 @@
|
|
|
1
1
|
import { AuthoringError } from "#errors.ts"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AtomIr,
|
|
4
|
+
CmpOpIr,
|
|
5
|
+
ConditionTreeIr,
|
|
6
|
+
FindTermIr,
|
|
7
|
+
HeadTermIr,
|
|
8
|
+
ParsedQuery,
|
|
9
|
+
QueryIr,
|
|
10
|
+
RuleIr,
|
|
11
|
+
ScalarExprIr,
|
|
12
|
+
TermIr
|
|
13
|
+
} from "#native.ts"
|
|
14
|
+
import { arrayValue as array, recordValue, valueDescriptor } from "#values.ts"
|
|
3
15
|
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
align("query", ir.head, ir.rules)
|
|
9
|
-
ir.interiors.forEach(function checkInterior(interior, index) {
|
|
10
|
-
align(`interior ${index}`, interior.head, interior.rules)
|
|
16
|
+
function fail(context: string, expected: string): never {
|
|
17
|
+
throw new AuthoringError({
|
|
18
|
+
message: `${context}: ${expected}`,
|
|
19
|
+
diagnostic: { code: "InvalidQuery", context, expected }
|
|
11
20
|
})
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tagged(context: string, input: unknown): Readonly<Record<string, unknown>> {
|
|
24
|
+
if (typeof input !== "object" || input === null) return fail(context, "expected a descriptor record")
|
|
25
|
+
return recordValue(context, input, Object.keys(input))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ordinal(context: string, input: unknown, maximum = 0xffff): number {
|
|
29
|
+
if (typeof input !== "number" || !Number.isInteger(input) || input < 0 || input > maximum)
|
|
30
|
+
return fail(context, `expected an ordinal from 0 through ${maximum}`)
|
|
31
|
+
return input
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function term(context: string, input: unknown): TermIr {
|
|
35
|
+
const raw = tagged(context, input)
|
|
36
|
+
switch (raw.kind) {
|
|
37
|
+
case "var":
|
|
38
|
+
recordValue(context, raw, ["kind", "var"])
|
|
39
|
+
return Object.freeze({ kind: raw.kind, var: ordinal(context, raw.var) })
|
|
40
|
+
case "param":
|
|
41
|
+
case "paramSet":
|
|
42
|
+
recordValue(context, raw, ["kind", "param"])
|
|
43
|
+
return Object.freeze({ kind: raw.kind, param: ordinal(context, raw.param) })
|
|
44
|
+
case "literal":
|
|
45
|
+
recordValue(context, raw, ["kind", "value"])
|
|
46
|
+
return Object.freeze({ kind: raw.kind, value: valueDescriptor(`${context}.value`, raw.value) })
|
|
47
|
+
default:
|
|
48
|
+
return fail(context, "unknown term kind")
|
|
21
49
|
}
|
|
22
|
-
return ir as ParsedQuery
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
function scalar(context: string, input: unknown, depth = 1): ScalarExprIr {
|
|
53
|
+
if (depth > 128) return fail(context, "scalar expression deeper than 128")
|
|
54
|
+
const raw = tagged(context, input)
|
|
55
|
+
switch (raw.kind) {
|
|
56
|
+
case "var":
|
|
57
|
+
recordValue(context, raw, ["kind", "var"])
|
|
58
|
+
return Object.freeze({ kind: raw.kind, var: ordinal(context, raw.var) })
|
|
59
|
+
case "literal":
|
|
60
|
+
recordValue(context, raw, ["kind", "value"])
|
|
61
|
+
return Object.freeze({ kind: raw.kind, value: valueDescriptor(`${context}.value`, raw.value) })
|
|
62
|
+
case "negate":
|
|
63
|
+
case "isNaN":
|
|
64
|
+
case "isFinite":
|
|
65
|
+
recordValue(context, raw, ["kind", "expr"])
|
|
66
|
+
return Object.freeze({ kind: raw.kind, expr: scalar(`${context}.expr`, raw.expr, depth + 1) })
|
|
67
|
+
case "add":
|
|
68
|
+
case "subtract":
|
|
69
|
+
case "multiply":
|
|
70
|
+
case "divide":
|
|
71
|
+
recordValue(context, raw, ["kind", "left", "right"])
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
kind: raw.kind,
|
|
74
|
+
left: scalar(`${context}.left`, raw.left, depth + 1),
|
|
75
|
+
right: scalar(`${context}.right`, raw.right, depth + 1)
|
|
30
76
|
})
|
|
77
|
+
case "cast": {
|
|
78
|
+
recordValue(context, raw, ["kind", "cast", "expr"])
|
|
79
|
+
const cast = raw.cast
|
|
80
|
+
if (cast !== "toF64" && cast !== "toF64Exact" && cast !== "toI64Exact" && cast !== "toU64Exact")
|
|
81
|
+
return fail(context, "unknown numeric cast")
|
|
82
|
+
return Object.freeze({ kind: raw.kind, cast, expr: scalar(`${context}.expr`, raw.expr, depth + 1) })
|
|
31
83
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const family = findFamily(find)
|
|
35
|
-
if (family !== head[position]?.kind) {
|
|
36
|
-
throw new AuthoringError({
|
|
37
|
-
message: `${context}: rule ${ruleIndex} find ${position} is ${family}, not head ${head[position]?.kind}`
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
})
|
|
84
|
+
default:
|
|
85
|
+
return fail(context, "unknown scalar expression kind")
|
|
41
86
|
}
|
|
42
87
|
}
|
|
43
88
|
|
|
44
|
-
function
|
|
45
|
-
const raw =
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (find.kind === "pack" || find.kind === "aggregate") {
|
|
53
|
-
if (!("over" in raw)) {
|
|
54
|
-
throw new AuthoringError({ message: `${context}: ${find.kind} requires over` })
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (find.kind === "compute" && !("expr" in raw)) {
|
|
58
|
-
throw new AuthoringError({ message: `${context}: compute requires expr` })
|
|
89
|
+
function headTerm(context: string, input: unknown): HeadTermIr {
|
|
90
|
+
const raw = tagged(context, input)
|
|
91
|
+
if (raw.kind === "var" || raw.kind === "compute") {
|
|
92
|
+
recordValue(context, raw, ["kind"])
|
|
93
|
+
return Object.freeze({ kind: raw.kind })
|
|
59
94
|
}
|
|
95
|
+
recordValue(context, raw, ["kind", "op"])
|
|
96
|
+
if (raw.kind !== "aggregate") return fail(context, "unknown head kind")
|
|
97
|
+
const op = raw.op
|
|
98
|
+
if (op !== "sum" && op !== "mean" && op !== "min" && op !== "max" && op !== "count" && op !== "pack")
|
|
99
|
+
return fail(context, "unknown head aggregate")
|
|
100
|
+
return Object.freeze({ kind: raw.kind, op })
|
|
60
101
|
}
|
|
61
102
|
|
|
62
|
-
function
|
|
63
|
-
|
|
103
|
+
function find(context: string, input: unknown): FindTermIr {
|
|
104
|
+
const raw = tagged(context, input)
|
|
105
|
+
switch (raw.kind) {
|
|
64
106
|
case "var":
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return "compute"
|
|
107
|
+
recordValue(context, raw, ["kind", "var"])
|
|
108
|
+
return Object.freeze({ kind: raw.kind, var: ordinal(context, raw.var) })
|
|
68
109
|
case "count":
|
|
110
|
+
if (Object.hasOwn(raw, "over")) return fail(context, "Count carries no over")
|
|
111
|
+
recordValue(context, raw, ["kind"])
|
|
112
|
+
return Object.freeze({ kind: raw.kind })
|
|
113
|
+
case "compute":
|
|
114
|
+
if (!Object.hasOwn(raw, "expr")) return fail(context, "compute requires expr")
|
|
115
|
+
recordValue(context, raw, ["kind", "expr"])
|
|
116
|
+
return Object.freeze({ kind: raw.kind, expr: scalar(`${context}.expr`, raw.expr) })
|
|
69
117
|
case "pack":
|
|
70
|
-
|
|
71
|
-
return
|
|
118
|
+
recordValue(context, raw, ["kind", "over"])
|
|
119
|
+
return Object.freeze({ kind: raw.kind, over: ordinal(context, raw.over) })
|
|
120
|
+
case "aggregate": {
|
|
121
|
+
recordValue(context, raw, ["kind", "op", "over"])
|
|
122
|
+
const op = recordValue(`${context}.op`, raw.op, ["kind"]).kind
|
|
123
|
+
if (op !== "sum" && op !== "mean" && op !== "min" && op !== "max") return fail(context, "unknown fold operator")
|
|
124
|
+
return Object.freeze({ kind: raw.kind, op: Object.freeze({ kind: op }), over: ordinal(context, raw.over) })
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return fail(context, "unknown find kind")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function atom(context: string, input: unknown): AtomIr {
|
|
132
|
+
const raw = recordValue(context, input, ["source", "bindings"])
|
|
133
|
+
const source = tagged(`${context}.source`, raw.source)
|
|
134
|
+
let owned: AtomIr["source"]
|
|
135
|
+
if (source.kind === "edb") {
|
|
136
|
+
recordValue(context, source, ["kind", "relation"])
|
|
137
|
+
owned = { kind: source.kind, relation: ordinal(`${context}.source.relation`, source.relation, 0xffffffff) }
|
|
138
|
+
} else if (source.kind === "interior") {
|
|
139
|
+
recordValue(context, source, ["kind", "interior"])
|
|
140
|
+
owned = { kind: source.kind, interior: ordinal(`${context}.source.interior`, source.interior, 0xffffffff) }
|
|
141
|
+
} else return fail(context, "unknown atom source")
|
|
142
|
+
const seen = new Set<number>()
|
|
143
|
+
const bindings = array(`${context}.bindings`, raw.bindings, (path, pair): readonly [number, TermIr] => {
|
|
144
|
+
const cells = array(path, pair, (_, value) => value)
|
|
145
|
+
if (cells.length !== 2) return fail(path, "a binding has exactly two elements")
|
|
146
|
+
const field = ordinal(`${path}[0]`, cells[0])
|
|
147
|
+
if (seen.has(field)) return fail(path, "duplicate field binding")
|
|
148
|
+
seen.add(field)
|
|
149
|
+
return Object.freeze([field, term(`${path}[1]`, cells[1])])
|
|
150
|
+
})
|
|
151
|
+
return Object.freeze({ source: Object.freeze(owned), bindings })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function comparisonOp(context: string, input: unknown): CmpOpIr {
|
|
155
|
+
const raw = tagged(context, input)
|
|
156
|
+
if (raw.kind === "allen") {
|
|
157
|
+
recordValue(context, raw, ["kind", "mask"])
|
|
158
|
+
return Object.freeze({ kind: raw.kind, mask: ordinal(`${context}.mask`, raw.mask, 0x1fff) })
|
|
159
|
+
}
|
|
160
|
+
recordValue(context, raw, ["kind"])
|
|
161
|
+
const kind = raw.kind
|
|
162
|
+
if (
|
|
163
|
+
kind !== "eq" &&
|
|
164
|
+
kind !== "ne" &&
|
|
165
|
+
kind !== "lt" &&
|
|
166
|
+
kind !== "le" &&
|
|
167
|
+
kind !== "gt" &&
|
|
168
|
+
kind !== "ge" &&
|
|
169
|
+
kind !== "pointIn"
|
|
170
|
+
)
|
|
171
|
+
return fail(context, "unknown comparison operator")
|
|
172
|
+
return Object.freeze({ kind })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function condition(context: string, input: unknown, depth = 1): ConditionTreeIr {
|
|
176
|
+
if (depth > 64) return fail(context, "condition tree deeper than 64")
|
|
177
|
+
const raw = tagged(context, input)
|
|
178
|
+
if (raw.kind === "leaf") {
|
|
179
|
+
recordValue(context, raw, ["kind", "cmp"])
|
|
180
|
+
const cmp = recordValue(`${context}.cmp`, raw.cmp, ["op", "lhs", "rhs"])
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
kind: raw.kind,
|
|
183
|
+
cmp: Object.freeze({
|
|
184
|
+
op: comparisonOp(`${context}.cmp.op`, cmp.op),
|
|
185
|
+
lhs: term(`${context}.cmp.lhs`, cmp.lhs),
|
|
186
|
+
rhs: term(`${context}.cmp.rhs`, cmp.rhs)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
if (raw.kind !== "and" && raw.kind !== "or") return fail(context, "unknown condition kind")
|
|
191
|
+
recordValue(context, raw, ["kind", "children"])
|
|
192
|
+
return Object.freeze({
|
|
193
|
+
kind: raw.kind,
|
|
194
|
+
children: array(`${context}.children`, raw.children, (path, value) => condition(path, value, depth + 1))
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function rule(context: string, input: unknown): RuleIr {
|
|
199
|
+
const raw = recordValue(context, input, ["finds", "atoms", "negated", "conditions"])
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
finds: array(`${context}.finds`, raw.finds, find),
|
|
202
|
+
atoms: array(`${context}.atoms`, raw.atoms, atom),
|
|
203
|
+
negated: array(`${context}.negated`, raw.negated, atom),
|
|
204
|
+
conditions: array(`${context}.conditions`, raw.conditions, condition)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function align(context: string, head: readonly HeadTermIr[], rules: readonly RuleIr[]): void {
|
|
209
|
+
for (const [index, rule] of rules.entries()) {
|
|
210
|
+
if (rule.finds.length !== head.length) fail(`${context}.rules[${index}]`, "finds width does not match head width")
|
|
211
|
+
for (const [position, find] of rule.finds.entries()) {
|
|
212
|
+
const term = head[position]
|
|
213
|
+
const family = find.kind === "var" || find.kind === "compute" ? find.kind : "aggregate"
|
|
214
|
+
if (term?.kind !== family)
|
|
215
|
+
fail(`${context}.rules[${index}].finds[${position}]`, "find family does not match head")
|
|
216
|
+
if (term.kind === "aggregate") {
|
|
217
|
+
const op = find.kind === "aggregate" ? find.op.kind : find.kind
|
|
218
|
+
if (op !== term.op)
|
|
219
|
+
fail(`${context}.rules[${index}].finds[${position}]`, "aggregate operator does not match head")
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Own and validate the complete wire grammar. The native compiler then
|
|
227
|
+
* checks schema membership, binding types, safety and recursion semantics;
|
|
228
|
+
* parsing alone does not assert that a query is admitted by a schema.
|
|
229
|
+
*/
|
|
230
|
+
function parseQueryIr(input: unknown): ParsedQuery {
|
|
231
|
+
const raw = tagged("query", input)
|
|
232
|
+
if (raw.kind !== "cq" && raw.kind !== "reach") return fail("query", "unknown query kind")
|
|
233
|
+
recordValue(
|
|
234
|
+
"query",
|
|
235
|
+
raw,
|
|
236
|
+
raw.kind === "cq" ? ["kind", "interiors", "head", "rules"] : ["kind", "interiors", "rec", "head", "rules"]
|
|
237
|
+
)
|
|
238
|
+
const rules = array("query.rules", raw.rules, rule)
|
|
239
|
+
if (rules.length === 0) return fail("parseQueryIr", "main rules are empty")
|
|
240
|
+
const head = array("query.head", raw.head, headTerm)
|
|
241
|
+
align("query", head, rules)
|
|
242
|
+
const interiors = array("query.interiors", raw.interiors, (path, input) => {
|
|
243
|
+
const raw = recordValue(path, input, ["head", "rules"])
|
|
244
|
+
const head = array(`${path}.head`, raw.head, headTerm)
|
|
245
|
+
const rules = array(`${path}.rules`, raw.rules, rule)
|
|
246
|
+
if (rules.length === 0) return fail(path, "interior rules are empty")
|
|
247
|
+
align(path, head, rules)
|
|
248
|
+
return Object.freeze({ head, rules })
|
|
249
|
+
})
|
|
250
|
+
let owned: QueryIr
|
|
251
|
+
if (raw.kind === "cq") owned = { kind: raw.kind, interiors, head, rules }
|
|
252
|
+
else {
|
|
253
|
+
const rawRec = recordValue("query.rec", raw.rec, ["head", "base", "rec"])
|
|
254
|
+
const headRec = array("query.rec.head", rawRec.head, headTerm)
|
|
255
|
+
const base = array("query.rec.base", rawRec.base, rule)
|
|
256
|
+
if (base.length === 0) return fail("parseQueryIr", "rec base is empty")
|
|
257
|
+
const rec = array("query.rec.rec", rawRec.rec, rule)
|
|
258
|
+
if (rec.length === 0) return fail("parseQueryIr", "rec step is empty")
|
|
259
|
+
align("rec base", headRec, base)
|
|
260
|
+
align("rec step", headRec, rec)
|
|
261
|
+
if (headRec.some((term) => term.kind !== "var") || [...base, ...rec].some((rule) => rule.negated.length !== 0))
|
|
262
|
+
return fail("query.rec", "recursion requires projection-only heads and no negation")
|
|
263
|
+
owned = { kind: raw.kind, interiors, head, rules, rec: Object.freeze({ head: headRec, base, rec }) }
|
|
72
264
|
}
|
|
265
|
+
return Object.freeze(owned) as ParsedQuery
|
|
73
266
|
}
|
|
74
267
|
|
|
75
268
|
export type { ParsedQuery }
|
package/src/query/run.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { FindColumn } from "#query/atom.ts"
|
|
|
4
4
|
import { taggedCmpLiteral } from "#query/lower.ts"
|
|
5
5
|
import type { ParamEntry } from "#query/scope.ts"
|
|
6
6
|
import type { CellValue } from "#rows.ts"
|
|
7
|
-
import { decodeCell, handleOf } from "#rows.ts"
|
|
7
|
+
import { decodeCell, handleOf, setOwnField } from "#rows.ts"
|
|
8
|
+
import { arrayValue, recordValue } from "#values.ts"
|
|
8
9
|
|
|
9
10
|
function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedValue {
|
|
10
11
|
if (entry.anchor === undefined) {
|
|
@@ -16,22 +17,24 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
|
|
20
|
+
const input = recordValue(
|
|
21
|
+
"query parameters",
|
|
22
|
+
supplied,
|
|
23
|
+
entries.filter((entry) => entry.membership === undefined).map((entry) => entry.name)
|
|
24
|
+
)
|
|
19
25
|
return entries.map(function wireOne(entry): QueryParam {
|
|
20
26
|
if (entry.membership !== undefined) {
|
|
21
27
|
return entry.membership
|
|
22
28
|
}
|
|
23
|
-
const value =
|
|
29
|
+
const value = input[entry.name]
|
|
24
30
|
if (value === undefined) {
|
|
25
31
|
throw new AuthoringError({ message: `execute params object is missing param ${entry.name}` })
|
|
26
32
|
}
|
|
27
33
|
if (entry.shape === "set") {
|
|
28
|
-
if (!Array.isArray(value)) {
|
|
29
|
-
throw new AuthoringError({ message: `param ${entry.name}: a set param binds a readonly array of values` })
|
|
30
|
-
}
|
|
31
34
|
return {
|
|
32
35
|
kind: "set",
|
|
33
|
-
values: value
|
|
34
|
-
return wireValue(entry,
|
|
36
|
+
values: arrayValue(`param ${entry.name}`, value, function wireElement(context, element) {
|
|
37
|
+
return wireValue(entry, context, element)
|
|
35
38
|
})
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -71,11 +74,14 @@ function decodeAnswers<Row>(finds: readonly FindColumn[], rows: readonly (readon
|
|
|
71
74
|
throw new SdkInvariantError({ message: `query answer cell ${ordinal} (${column.name}) is absent` })
|
|
72
75
|
}
|
|
73
76
|
if (column.slot !== undefined) {
|
|
74
|
-
decoded
|
|
77
|
+
setOwnField(decoded, column.name, decodeCell(`query answer column ${column.name}`, column.slot.field, cell))
|
|
75
78
|
return
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
setOwnField(
|
|
81
|
+
decoded,
|
|
82
|
+
column.name,
|
|
78
83
|
column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
|
|
84
|
+
)
|
|
79
85
|
})
|
|
80
86
|
Object.freeze(decoded)
|
|
81
87
|
if (!isAnswerRow<Row>(finds, decoded)) {
|
package/src/query/scope.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyClosed } from "#closed.ts"
|
|
2
|
-
import { sealedFieldsOf } from "#closed.ts"
|
|
2
|
+
import { memberDescriptor, sealedFieldsOf } from "#closed.ts"
|
|
3
3
|
import { AuthoringError, SdkInvariantError } from "#errors.ts"
|
|
4
4
|
import type { AnyField, Infer, SignatureOf } from "#fields.ts"
|
|
5
5
|
import { rosterOf, signaturesAgree } from "#fields.ts"
|
|
@@ -7,7 +7,8 @@ import type { Same, SameLen } from "#judgment.ts"
|
|
|
7
7
|
import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
|
|
8
8
|
import type { QueryParam } from "#native.ts"
|
|
9
9
|
import type { FindColumn } from "#query/atom.ts"
|
|
10
|
-
import type { AnyRelation
|
|
10
|
+
import type { AnyRelation } from "#relation.ts"
|
|
11
|
+
import type { FieldsOf } from "#selection.ts"
|
|
11
12
|
|
|
12
13
|
const term: unique symbol = Symbol("bumbledb.query.term")
|
|
13
14
|
|
|
@@ -15,11 +16,7 @@ const inferred: unique symbol = Symbol("bumbledb.query.inferred")
|
|
|
15
16
|
|
|
16
17
|
type MatchOwner = AnyRelation | AnyClosed
|
|
17
18
|
|
|
18
|
-
type MatchFields<R extends MatchOwner> = R
|
|
19
|
-
? { readonly id: R["id"] } & R["columns"]
|
|
20
|
-
: R extends AnyRelation
|
|
21
|
-
? RelationFields<R>
|
|
22
|
-
: never
|
|
19
|
+
type MatchFields<R extends MatchOwner> = FieldsOf<R>
|
|
23
20
|
|
|
24
21
|
interface Var<F extends AnyField, RN extends string, K extends string> {
|
|
25
22
|
readonly [term]: "var"
|
|
@@ -157,7 +154,7 @@ function importFacadeOf(source: ImportedSource): ImportFacade {
|
|
|
157
154
|
importOrdinal += 1
|
|
158
155
|
const name = importLabels.get(source) ?? `\u0000import:${importOrdinal}`
|
|
159
156
|
const columns = new Map<string, FindColumn>()
|
|
160
|
-
const fields: Array<
|
|
157
|
+
const fields: Array<readonly [string, AnyField]> = []
|
|
161
158
|
for (const column of source.data.finds) {
|
|
162
159
|
if (column.slot === undefined) {
|
|
163
160
|
throw new AuthoringError({
|
|
@@ -165,18 +162,13 @@ function importFacadeOf(source: ImportedSource): ImportFacade {
|
|
|
165
162
|
})
|
|
166
163
|
}
|
|
167
164
|
columns.set(column.name, column)
|
|
168
|
-
fields.push(
|
|
165
|
+
fields.push([column.name, column.slot.field])
|
|
169
166
|
}
|
|
170
|
-
const data = Object.freeze({ name, fields: Object.freeze(fields) })
|
|
171
167
|
const owner = Object.freeze({
|
|
168
|
+
kind: "relation" as const,
|
|
172
169
|
name,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
throw new AuthoringError({
|
|
176
|
-
message: `imported query ${name}: an imported relation expression takes no selection — filter through where() in the consuming rule`
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
}) as unknown as MatchOwner & { readonly name: string }
|
|
170
|
+
fields: Object.freeze(Object.fromEntries(fields))
|
|
171
|
+
})
|
|
180
172
|
const facade: ImportFacade = Object.freeze({ owner, source, columns })
|
|
181
173
|
facadeBySource.set(source, facade)
|
|
182
174
|
facadeByOwner.set(owner, facade)
|
|
@@ -210,7 +202,7 @@ function v(owner: MatchOwner | ImportedSource): Readonly<Record<string, AnyVar>>
|
|
|
210
202
|
}
|
|
211
203
|
return Object.freeze(record)
|
|
212
204
|
}
|
|
213
|
-
const member = owner as MatchOwner
|
|
205
|
+
const member = memberDescriptor(owner as MatchOwner)
|
|
214
206
|
const record: Record<string, AnyVar> = {}
|
|
215
207
|
for (const declared of sealedFieldsOf(member)) {
|
|
216
208
|
const variable: AnyVar = Object.freeze({
|
package/src/relation.ts
CHANGED
|
@@ -1,160 +1,76 @@
|
|
|
1
1
|
import { AuthoringError } from "#errors.ts"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
|
|
18
|
-
|
|
19
|
-
function resolveEntry(context: string, field: AnyField, entry: unknown): LiteralSetSpec {
|
|
20
|
-
if (Array.isArray(entry)) {
|
|
21
|
-
if (entry.length < 2) {
|
|
22
|
-
throw new AuthoringError({
|
|
23
|
-
message:
|
|
24
|
-
entry.length === 0
|
|
25
|
-
? `${context}: an empty literal set selects nothing — write the selection you mean`
|
|
26
|
-
: `${context}: a one-element literal set is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
const seen = new Set<string>()
|
|
30
|
-
const literals: LiteralSpec[] = entry.map(function lowerSetLiteral(literal: unknown) {
|
|
31
|
-
const lowered = Object.freeze(literalOf(field, literal))
|
|
32
|
-
const rendered = renderLiteral(lowered)
|
|
33
|
-
if (seen.has(rendered)) {
|
|
34
|
-
throw new AuthoringError({
|
|
35
|
-
message: `${context}: the literal set spells ${rendered} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
seen.add(rendered)
|
|
39
|
-
return lowered
|
|
40
|
-
})
|
|
41
|
-
return Object.freeze({ kind: "many", literals: Object.freeze(literals) })
|
|
42
|
-
}
|
|
43
|
-
return Object.freeze({ kind: "one", literal: Object.freeze(literalOf(field, entry)) })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function resolveSelection(
|
|
47
|
-
name: string,
|
|
48
|
-
ordered: readonly RelationField[],
|
|
49
|
-
entries: ReadonlyArray<readonly [string, unknown]>
|
|
50
|
-
): readonly SelectionBinding[] {
|
|
51
|
-
const bindings: SelectionBinding[] = []
|
|
52
|
-
for (const [fieldName, entry] of entries) {
|
|
53
|
-
if (entry === undefined) {
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
const declared = ordered.find(function byName(candidate) {
|
|
57
|
-
return candidate.name === fieldName
|
|
58
|
-
})
|
|
59
|
-
if (declared === undefined) {
|
|
60
|
-
throw new AuthoringError({ message: `relation ${name} has no field ${fieldName}` })
|
|
61
|
-
}
|
|
62
|
-
bindings.push(
|
|
63
|
-
Object.freeze({ field: fieldName, set: resolveEntry(`relation ${name}.${fieldName}`, declared.field, entry) })
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
if (bindings.length === 0) {
|
|
67
|
-
throw new AuthoringError({
|
|
68
|
-
message: `relation ${name}: an empty selection is the bare relation respelled — pass the relation itself (the canonical-utterance law: one meaning, one spelling)`
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
return Object.freeze(bindings)
|
|
2
|
+
import {
|
|
3
|
+
type AnyField,
|
|
4
|
+
assertDeclarationOrderKey,
|
|
5
|
+
assertDeclarationRecord,
|
|
6
|
+
fieldDescriptor,
|
|
7
|
+
type Infer
|
|
8
|
+
} from "#fields.ts"
|
|
9
|
+
import { descriptorCache } from "#immutable.ts"
|
|
10
|
+
import { recordValue } from "#values.ts"
|
|
11
|
+
|
|
12
|
+
/** Ordinary relations are structural declarations. Field order defines native ordinals. */
|
|
13
|
+
interface Relation<Name extends string = string, Fields extends FieldsShape = FieldsShape> {
|
|
14
|
+
readonly kind: "relation"
|
|
15
|
+
readonly name: Name
|
|
16
|
+
readonly fields: Fields
|
|
72
17
|
}
|
|
73
|
-
|
|
74
|
-
type
|
|
18
|
+
type FieldsShape = Readonly<Record<string, AnyField>>
|
|
19
|
+
type AnyRelation = Relation
|
|
20
|
+
type RelationFields<R extends AnyRelation> = R["fields"]
|
|
21
|
+
type Fact<R extends AnyRelation> = { -readonly [K in keyof R["fields"]]: Infer<R["fields"][K]> }
|
|
75
22
|
|
|
76
23
|
interface RelationField {
|
|
77
24
|
readonly name: string
|
|
78
25
|
readonly field: AnyField
|
|
79
26
|
}
|
|
80
27
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
readonly relation: Relation<Name, Fields>
|
|
97
|
-
readonly selection: readonly SelectionBinding[]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface Relation<Name extends string, Fields extends FieldsShape> {
|
|
101
|
-
readonly name: Name
|
|
102
|
-
readonly data: RelationData
|
|
103
|
-
where(selection: SelectionInput<Fields>): Selected<Name, Fields>
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type AnyRelation = Relation<string, FieldsShape>
|
|
107
|
-
|
|
108
|
-
interface AnySelected {
|
|
109
|
-
readonly relation: AnyRelation
|
|
110
|
-
readonly selection: readonly SelectionBinding[]
|
|
28
|
+
/** Validate and own every field, without freezing or retaining caller records. */
|
|
29
|
+
function ownFields<F extends FieldsShape>(context: string, input: F): F
|
|
30
|
+
function ownFields(context: string, input: unknown): FieldsShape
|
|
31
|
+
function ownFields(context: string, input: unknown): FieldsShape {
|
|
32
|
+
if (typeof input !== "object" || input === null)
|
|
33
|
+
throw new AuthoringError({ message: `${context}: expected a field record` })
|
|
34
|
+
assertDeclarationRecord(context, input)
|
|
35
|
+
return Object.freeze(
|
|
36
|
+
Object.fromEntries(
|
|
37
|
+
Object.entries(input).map(([name, field]) => {
|
|
38
|
+
assertDeclarationOrderKey(context, name)
|
|
39
|
+
return [name, fieldDescriptor(`${context}.${name}`, field)]
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
)
|
|
111
43
|
}
|
|
112
44
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
45
|
+
function relationDescriptor<R extends AnyRelation>(input: R): R
|
|
46
|
+
function relationDescriptor(input: unknown): AnyRelation
|
|
47
|
+
function relationDescriptor(input: unknown): AnyRelation {
|
|
48
|
+
return checkedRelation(input)
|
|
117
49
|
}
|
|
118
50
|
|
|
119
|
-
|
|
51
|
+
const checkedRelation = descriptorCache((input): AnyRelation => {
|
|
52
|
+
const raw = recordValue("relation", input, ["kind", "name", "fields"])
|
|
53
|
+
if (raw.kind !== "relation" || typeof raw.name !== "string")
|
|
54
|
+
throw new AuthoringError({ message: "relation: expected a relation declaration" })
|
|
55
|
+
assertDeclarationOrderKey("relation", raw.name)
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: "relation",
|
|
58
|
+
name: raw.name,
|
|
59
|
+
fields: ownFields(`relation ${raw.name} fields`, raw.fields)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function relation<const Name extends string, const Fields extends FieldsShape>(
|
|
120
64
|
name: Name,
|
|
121
65
|
fields: Fields
|
|
122
66
|
): Relation<Name, Fields> {
|
|
123
|
-
|
|
124
|
-
assertDeclarationRecord(`relation ${name} fields`, fields)
|
|
125
|
-
const ordered: RelationField[] = []
|
|
126
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
127
|
-
assertDeclarationOrderKey(`relation ${name} field`, fieldName)
|
|
128
|
-
ordered.push(Object.freeze({ name: fieldName, field }))
|
|
129
|
-
}
|
|
130
|
-
const data: RelationData = Object.freeze({ name, fields: Object.freeze(ordered) })
|
|
131
|
-
const holder: { value: Relation<Name, Fields> | undefined } = { value: undefined }
|
|
132
|
-
function where(selection: SelectionInput<Fields>): Selected<Name, Fields> {
|
|
133
|
-
const owner = holder.value
|
|
134
|
-
if (owner === undefined) {
|
|
135
|
-
throw new AuthoringError({ message: `relation ${name}: self-reference read before construction completed` })
|
|
136
|
-
}
|
|
137
|
-
return Object.freeze({
|
|
138
|
-
relation: owner,
|
|
139
|
-
selection: resolveSelection(name, ordered, Object.entries(selection))
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
const value: Relation<Name, Fields> = Object.freeze({ name, data, where })
|
|
143
|
-
holder.value = value
|
|
144
|
-
return value
|
|
67
|
+
return relationDescriptor({ kind: "relation", name, fields })
|
|
145
68
|
}
|
|
146
69
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Fact,
|
|
151
|
-
FieldsShape,
|
|
152
|
-
Relation,
|
|
153
|
-
RelationData,
|
|
154
|
-
RelationField,
|
|
155
|
-
RelationFields,
|
|
156
|
-
Selected,
|
|
157
|
-
SelectionBinding,
|
|
158
|
-
SelectionInput
|
|
70
|
+
/** Ordered field view derived from the one declaration record. */
|
|
71
|
+
function relationFields(relation: AnyRelation): readonly RelationField[] {
|
|
72
|
+
return Object.entries(relation.fields).map(([name, field]) => ({ name, field }))
|
|
159
73
|
}
|
|
160
|
-
|
|
74
|
+
|
|
75
|
+
export type { AnyRelation, Fact, FieldsShape, Relation, RelationField, RelationFields }
|
|
76
|
+
export { ownFields, relation, relationDescriptor, relationFields }
|