@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.
Files changed (148) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +26 -3
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +89 -44
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +4 -9
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +108 -183
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +16 -16
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +36 -15
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +39 -0
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +28 -8
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +91 -38
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +14 -10
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +2 -0
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +2 -0
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +10 -10
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/native.d.ts +1 -1
  63. package/dist/native.d.ts.map +1 -1
  64. package/dist/native.js.map +1 -1
  65. package/dist/query/atom.d.ts +1 -1
  66. package/dist/query/atom.d.ts.map +1 -1
  67. package/dist/query/atom.js.map +1 -1
  68. package/dist/query/description.d.ts +33 -0
  69. package/dist/query/description.d.ts.map +1 -0
  70. package/dist/query/description.js +302 -0
  71. package/dist/query/description.js.map +1 -0
  72. package/dist/query/lower.d.ts +60 -4
  73. package/dist/query/lower.d.ts.map +1 -1
  74. package/dist/query/lower.js +126 -161
  75. package/dist/query/lower.js.map +1 -1
  76. package/dist/query/parse-ir.d.ts +7 -2
  77. package/dist/query/parse-ir.d.ts.map +1 -1
  78. package/dist/query/parse-ir.js +240 -53
  79. package/dist/query/parse-ir.js.map +1 -1
  80. package/dist/query/run.d.ts.map +1 -1
  81. package/dist/query/run.js +8 -10
  82. package/dist/query/run.js.map +1 -1
  83. package/dist/query/scope.d.ts +3 -4
  84. package/dist/query/scope.d.ts.map +1 -1
  85. package/dist/query/scope.js +5 -10
  86. package/dist/query/scope.js.map +1 -1
  87. package/dist/relation.d.ts +22 -48
  88. package/dist/relation.d.ts.map +1 -1
  89. package/dist/relation.js +30 -82
  90. package/dist/relation.js.map +1 -1
  91. package/dist/rows.d.ts +7 -4
  92. package/dist/rows.d.ts.map +1 -1
  93. package/dist/rows.js +44 -198
  94. package/dist/rows.js.map +1 -1
  95. package/dist/runtime-errors.d.ts +26 -2
  96. package/dist/runtime-errors.d.ts.map +1 -1
  97. package/dist/runtime-errors.js +43 -3
  98. package/dist/runtime-errors.js.map +1 -1
  99. package/dist/schema.d.ts +9 -1
  100. package/dist/schema.d.ts.map +1 -1
  101. package/dist/schema.js +85 -24
  102. package/dist/schema.js.map +1 -1
  103. package/dist/selection.d.ts +27 -0
  104. package/dist/selection.d.ts.map +1 -0
  105. package/dist/selection.js +104 -0
  106. package/dist/selection.js.map +1 -0
  107. package/dist/shape.d.ts +6 -9
  108. package/dist/shape.d.ts.map +1 -1
  109. package/dist/statements.d.ts +17 -52
  110. package/dist/statements.d.ts.map +1 -1
  111. package/dist/statements.js +92 -111
  112. package/dist/statements.js.map +1 -1
  113. package/dist/values.d.ts +25 -0
  114. package/dist/values.d.ts.map +1 -0
  115. package/dist/values.js +203 -0
  116. package/dist/values.js.map +1 -0
  117. package/pack-provenance.json +3 -3
  118. package/package.json +4 -4
  119. package/src/capacity.ts +86 -76
  120. package/src/changes.ts +183 -54
  121. package/src/closed.ts +161 -314
  122. package/src/codec.ts +116 -190
  123. package/src/compile.ts +56 -32
  124. package/src/db-native.ts +42 -0
  125. package/src/db.ts +134 -52
  126. package/src/errors.ts +16 -1
  127. package/src/face.ts +44 -74
  128. package/src/fields.ts +100 -146
  129. package/src/immutable.ts +76 -0
  130. package/src/index.ts +34 -28
  131. package/src/internal/log.ts +2 -0
  132. package/src/law.ts +13 -13
  133. package/src/lower.ts +13 -10
  134. package/src/native.ts +2 -0
  135. package/src/query/atom.ts +1 -0
  136. package/src/query/description.ts +394 -0
  137. package/src/query/lower.ts +139 -173
  138. package/src/query/parse-ir.ts +247 -54
  139. package/src/query/run.ts +15 -9
  140. package/src/query/scope.ts +10 -18
  141. package/src/relation.ts +58 -142
  142. package/src/rows.ts +62 -205
  143. package/src/runtime-errors.ts +47 -3
  144. package/src/schema.ts +99 -29
  145. package/src/selection.ts +124 -0
  146. package/src/shape.ts +9 -9
  147. package/src/statements.ts +138 -189
  148. package/src/values.ts +223 -0
@@ -1,75 +1,268 @@
1
1
  import { AuthoringError } from "#errors.ts"
2
- import type { FindTermIr, HeadTermIr, ParsedQuery, QueryIr, RuleIr } from "#native.ts"
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 parseQueryIr(ir: QueryIr): ParsedQuery {
5
- if (ir.rules.length === 0) {
6
- throw new AuthoringError({ message: "parseQueryIr: main rules are empty" })
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
- if (ir.kind === "reach") {
13
- if (ir.rec.base.length === 0) {
14
- throw new AuthoringError({ message: "parseQueryIr: rec base is empty" })
15
- }
16
- if (ir.rec.rec.length === 0) {
17
- throw new AuthoringError({ message: "parseQueryIr: rec step is empty" })
18
- }
19
- align("rec base", ir.rec.head, ir.rec.base)
20
- align("rec step", ir.rec.head, ir.rec.rec)
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 align(context: string, head: readonly HeadTermIr[], rules: readonly RuleIr[]): void {
26
- for (const [ruleIndex, rule] of rules.entries()) {
27
- if (rule.finds.length !== head.length) {
28
- throw new AuthoringError({
29
- message: `${context}: rule ${ruleIndex} finds width ${rule.finds.length} does not match head width ${head.length}`
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
- rule.finds.forEach(function checkFind(find, position) {
33
- parseFind(`${context} rule ${ruleIndex} find ${position}`, find)
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 parseFind(context: string, find: FindTermIr): void {
45
- const raw = find as Record<string, unknown>
46
- if (find.kind === "count") {
47
- if ("over" in raw) {
48
- throw new AuthoringError({ message: `${context}: Count carries no over` })
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 findFamily(find: FindTermIr): "var" | "aggregate" | "compute" {
63
- switch (find.kind) {
103
+ function find(context: string, input: unknown): FindTermIr {
104
+ const raw = tagged(context, input)
105
+ switch (raw.kind) {
64
106
  case "var":
65
- return "var"
66
- case "compute":
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
- case "aggregate":
71
- return "aggregate"
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 = supplied[entry.name]
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.map(function wireElement(element, index) {
34
- return wireValue(entry, `param ${entry.name}[${index}]`, element)
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[column.name] = decodeCell(`query answer column ${column.name}`, column.slot.field, cell)
77
+ setOwnField(decoded, column.name, decodeCell(`query answer column ${column.name}`, column.slot.field, cell))
75
78
  return
76
79
  }
77
- decoded[column.name] =
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)) {
@@ -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, RelationFields } from "#relation.ts"
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 extends AnyClosed
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<{ readonly name: string; readonly field: AnyField }> = []
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(Object.freeze({ name: column.name, field: column.slot.field }))
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
- data,
174
- where(): never {
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
- * `relation` — the ordinary-relation half of the theory's signature. A
4
- * relation value is a frozen plain object carrying its name, its ordered
5
- * field descriptors (declaration order = ordinal ids, the macro's law),
6
- * and — since selections are the relation's own vocabulary — `where`,
7
- * which resolves a selection into lowered bindings eagerly (handles
8
- * verified against their roster at construction). Fields are addressed by
9
- * NAME everywhere statements (`on(R, "holder")`), selections, and match
10
- * records all spell the field's own name, checked by type
11
- * (`FaceFields`/`MatchShape`). `Fact<>` is the inferred row object type
12
- * at BARE structural value types: every field is present. Identities are
13
- * ordinary application-owned values (`Uuid`); the database mints none.
14
- */
15
-
16
- import { type AnyField, assertDeclarationOrderKey, assertDeclarationRecord, type Infer, literalOf } from "#fields.ts"
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 FieldsShape = Record<string, AnyField>
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
- interface RelationData {
82
- readonly name: string
83
- readonly fields: readonly RelationField[]
84
- }
85
-
86
- interface SelectionBinding {
87
- readonly field: string
88
- readonly set: LiteralSetSpec
89
- }
90
-
91
- type SelectionInput<Fields extends FieldsShape> = {
92
- readonly [K in keyof Fields]?: Infer<Fields[K]> | readonly Infer<Fields[K]>[]
93
- }
94
-
95
- interface Selected<Name extends string, Fields extends FieldsShape> {
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
- type RelationFields<R extends AnyRelation> = R extends Relation<string, infer F extends FieldsShape> ? F : never
114
-
115
- type Fact<R extends AnyRelation> = {
116
- [K in keyof RelationFields<R>]: Infer<RelationFields<R>[K]>
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
- function relation<const Name extends string, Fields extends FieldsShape>(
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
- assertDeclarationOrderKey("relation", name)
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
- export type {
148
- AnyRelation,
149
- AnySelected,
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
- export { relation, resolveSelection }
74
+
75
+ export type { AnyRelation, Fact, FieldsShape, Relation, RelationField, RelationFields }
76
+ export { ownFields, relation, relationDescriptor, relationFields }