@bjornpagen/bumbledb 1.1.0 → 1.2.1

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 (152) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +22 -59
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +66 -13
  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 +16 -12
  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 +13 -11
  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/find.d.ts +21 -6
  73. package/dist/query/find.d.ts.map +1 -1
  74. package/dist/query/find.js.map +1 -1
  75. package/dist/query/lower.d.ts +75 -10
  76. package/dist/query/lower.d.ts.map +1 -1
  77. package/dist/query/lower.js +132 -163
  78. package/dist/query/lower.js.map +1 -1
  79. package/dist/query/parse-ir.d.ts +7 -2
  80. package/dist/query/parse-ir.d.ts.map +1 -1
  81. package/dist/query/parse-ir.js +240 -53
  82. package/dist/query/parse-ir.js.map +1 -1
  83. package/dist/query/run.d.ts.map +1 -1
  84. package/dist/query/run.js +8 -10
  85. package/dist/query/run.js.map +1 -1
  86. package/dist/query/scope.d.ts +24 -16
  87. package/dist/query/scope.d.ts.map +1 -1
  88. package/dist/query/scope.js +5 -10
  89. package/dist/query/scope.js.map +1 -1
  90. package/dist/relation.d.ts +22 -48
  91. package/dist/relation.d.ts.map +1 -1
  92. package/dist/relation.js +30 -82
  93. package/dist/relation.js.map +1 -1
  94. package/dist/rows.d.ts +7 -4
  95. package/dist/rows.d.ts.map +1 -1
  96. package/dist/rows.js +44 -198
  97. package/dist/rows.js.map +1 -1
  98. package/dist/runtime-errors.d.ts +26 -2
  99. package/dist/runtime-errors.d.ts.map +1 -1
  100. package/dist/runtime-errors.js +43 -3
  101. package/dist/runtime-errors.js.map +1 -1
  102. package/dist/schema.d.ts +9 -1
  103. package/dist/schema.d.ts.map +1 -1
  104. package/dist/schema.js +85 -24
  105. package/dist/schema.js.map +1 -1
  106. package/dist/selection.d.ts +27 -0
  107. package/dist/selection.d.ts.map +1 -0
  108. package/dist/selection.js +104 -0
  109. package/dist/selection.js.map +1 -0
  110. package/dist/shape.d.ts +12 -12
  111. package/dist/shape.d.ts.map +1 -1
  112. package/dist/statements.d.ts +17 -52
  113. package/dist/statements.d.ts.map +1 -1
  114. package/dist/statements.js +92 -111
  115. package/dist/statements.js.map +1 -1
  116. package/dist/values.d.ts +25 -0
  117. package/dist/values.d.ts.map +1 -0
  118. package/dist/values.js +203 -0
  119. package/dist/values.js.map +1 -0
  120. package/pack-provenance.json +3 -3
  121. package/package.json +4 -4
  122. package/src/capacity.ts +93 -80
  123. package/src/changes.ts +183 -54
  124. package/src/closed.ts +161 -314
  125. package/src/codec.ts +116 -190
  126. package/src/compile.ts +56 -32
  127. package/src/db-native.ts +42 -0
  128. package/src/db.ts +134 -52
  129. package/src/errors.ts +16 -1
  130. package/src/face.ts +44 -74
  131. package/src/fields.ts +100 -146
  132. package/src/immutable.ts +76 -0
  133. package/src/index.ts +36 -29
  134. package/src/internal/log.ts +2 -0
  135. package/src/law.ts +21 -18
  136. package/src/lower.ts +13 -10
  137. package/src/native.ts +2 -0
  138. package/src/query/atom.ts +1 -0
  139. package/src/query/description.ts +394 -0
  140. package/src/query/find.ts +20 -7
  141. package/src/query/lower.ts +177 -187
  142. package/src/query/parse-ir.ts +247 -54
  143. package/src/query/run.ts +15 -9
  144. package/src/query/scope.ts +42 -33
  145. package/src/relation.ts +58 -142
  146. package/src/rows.ts +62 -205
  147. package/src/runtime-errors.ts +47 -3
  148. package/src/schema.ts +99 -29
  149. package/src/selection.ts +124 -0
  150. package/src/shape.ts +18 -12
  151. package/src/statements.ts +138 -189
  152. 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"
@@ -125,9 +122,23 @@ function isImportedSource(value: unknown): value is ImportedSource {
125
122
 
126
123
  type RowOfImport<Q> = InferredOf<Q> extends { readonly row: infer R } ? R : never
127
124
 
125
+ type HeadOfImport<Q> = InferredOf<Q> extends { readonly head: infer H } ? H : never
126
+
127
+ /** Named imported slot so downstream declarations need not spell its private marker. */
128
+ interface ImportedFieldVar<S extends ClassedField, K extends string> extends Var<S["field"], string, K> {
129
+ readonly [inferred]?: { readonly slot: S }
130
+ }
131
+
132
+ // Retain the same descriptor and carrier class as the runtime import facade.
133
+ // A host bigint alone cannot distinguish i64 from u64 or a closed identifier.
134
+ type ImportedVar<Q, K extends string> =
135
+ HeadOfImport<Q> extends Readonly<Record<K, infer S extends ClassedField>>
136
+ ? ImportedFieldVar<S, K>
137
+ : Var<AnyField, string, K>
138
+
128
139
  type ImportVars<Q> = [RowOfImport<Q>] extends [never]
129
140
  ? Readonly<Record<string, Var<AnyField, string, string>>>
130
- : { readonly [K in keyof RowOfImport<Q> & string]: Var<AnyField, string, K> }
141
+ : { readonly [K in keyof RowOfImport<Q> & string]: ImportedVar<Q, K> }
131
142
 
132
143
  interface ImportFacade {
133
144
  /** The pseudo-owner every import var carries (structurally an owner). */
@@ -157,7 +168,7 @@ function importFacadeOf(source: ImportedSource): ImportFacade {
157
168
  importOrdinal += 1
158
169
  const name = importLabels.get(source) ?? `\u0000import:${importOrdinal}`
159
170
  const columns = new Map<string, FindColumn>()
160
- const fields: Array<{ readonly name: string; readonly field: AnyField }> = []
171
+ const fields: Array<readonly [string, AnyField]> = []
161
172
  for (const column of source.data.finds) {
162
173
  if (column.slot === undefined) {
163
174
  throw new AuthoringError({
@@ -165,18 +176,13 @@ function importFacadeOf(source: ImportedSource): ImportFacade {
165
176
  })
166
177
  }
167
178
  columns.set(column.name, column)
168
- fields.push(Object.freeze({ name: column.name, field: column.slot.field }))
179
+ fields.push([column.name, column.slot.field])
169
180
  }
170
- const data = Object.freeze({ name, fields: Object.freeze(fields) })
171
181
  const owner = Object.freeze({
182
+ kind: "relation" as const,
172
183
  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 }
184
+ fields: Object.freeze(Object.fromEntries(fields))
185
+ })
180
186
  const facade: ImportFacade = Object.freeze({ owner, source, columns })
181
187
  facadeBySource.set(source, facade)
182
188
  facadeByOwner.set(owner, facade)
@@ -210,7 +216,7 @@ function v(owner: MatchOwner | ImportedSource): Readonly<Record<string, AnyVar>>
210
216
  }
211
217
  return Object.freeze(record)
212
218
  }
213
- const member = owner as MatchOwner
219
+ const member = memberDescriptor(owner as MatchOwner)
214
220
  const record: Record<string, AnyVar> = {}
215
221
  for (const declared of sealedFieldsOf(member)) {
216
222
  const variable: AnyVar = Object.freeze({
@@ -249,10 +255,11 @@ type MintClassOf<Classes extends SchemaClasses, V> =
249
255
  ? ClassLookup<ClassRecordOf<Classes, RN>, K>
250
256
  : never
251
257
 
252
- type MintSlotOf<Classes extends SchemaClasses, V extends AnyVar> = {
253
- readonly field: V["field"]
254
- readonly class: MintClassOf<Classes, V>
255
- }
258
+ type MintSlotOf<Classes extends SchemaClasses, V extends AnyVar> = [InferredOf<V>] extends [never]
259
+ ? { readonly field: V["field"]; readonly class: MintClassOf<Classes, V> }
260
+ : InferredOf<V> extends { readonly slot: infer S extends ClassedField }
261
+ ? S
262
+ : { readonly field: V["field"]; readonly class: MintClassOf<Classes, V> }
256
263
 
257
264
  type ParamsRecord = Readonly<Record<string, unknown>>
258
265
 
@@ -265,15 +272,8 @@ type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never)
265
272
  type ShapeOf<U> = [U] extends [never] ? Record<never, never> : Flatten<UnionToIntersection<U>>
266
273
 
267
274
  /**
268
- * A slot's identity for the positive-join judgment: the field's structural
269
- * signature ({@link SignatureOf} — the ONE interpreter, shared with the
270
- * face pairing wall) plus the slot's law class.
271
- */
272
- type SlotSignature<S extends ClassedField> = readonly [SignatureOf<S["field"]>, S["class"]]
273
-
274
- /**
275
- * A widened slot — a variable minted from an imported query template (its
276
- * facade columns carry `AnyField`) or fully generic code — cannot be judged
275
+ * A widened slot — an explicitly erased query template or fully generic
276
+ * code with no retained descriptor — cannot be judged
277
277
  * at the type tier. The degradation law applies: best effort degrades to
278
278
  * SILENT, never to a wrong refusal; the runtime join judgment (real
279
279
  * descriptors off the imported head's slots) stays authoritative.
@@ -281,7 +281,15 @@ type SlotSignature<S extends ClassedField> = readonly [SignatureOf<S["field"]>,
281
281
  type WidenedSlot<S extends ClassedField> = [AnyField] extends [S["field"]] ? true : false
282
282
 
283
283
  type JoinOk<A extends ClassedField, B extends ClassedField> =
284
- WidenedSlot<A> extends true ? true : WidenedSlot<B> extends true ? true : Same<SlotSignature<A>, SlotSignature<B>>
284
+ WidenedSlot<A> extends true
285
+ ? true
286
+ : WidenedSlot<B> extends true
287
+ ? true
288
+ : Same<SignatureOf<A["field"]>, SignatureOf<B["field"]>> extends true
289
+ ? string extends A["class"] | B["class"]
290
+ ? true
291
+ : Same<A["class"], B["class"]>
292
+ : false
285
293
 
286
294
  type U64Wire<F extends AnyField> = F extends { readonly kind: "u64" }
287
295
  ? F extends { readonly closed: unknown }
@@ -398,6 +406,7 @@ export type {
398
406
  ClassedField,
399
407
  ExactVars,
400
408
  Flatten,
409
+ ImportedFieldVar,
401
410
  ImportedSource,
402
411
  ImportVars,
403
412
  InferredOf,