@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
package/src/lower.ts CHANGED
@@ -15,10 +15,10 @@ import { AuthoringError } from "#errors.ts"
15
15
 
16
16
  import type { AnyClosed } from "#closed.ts"
17
17
  import { isClosedMember } from "#closed.ts"
18
- import type { FaceData } from "#face.ts"
19
- import type { AnyField } from "#fields.ts"
18
+ import type { AnyFace } from "#face.ts"
19
+ import { type AnyField, literalOf } from "#fields.ts"
20
20
  import type { RelationClasses } from "#law.ts"
21
- import type { AnyRelation } from "#relation.ts"
21
+ import { type AnyRelation, relationFields } from "#relation.ts"
22
22
  import type { AnySchema } from "#schema.ts"
23
23
  import type {
24
24
  FieldSpec,
@@ -60,7 +60,7 @@ function lowerField(name: string, field: AnyField, newtype: string | undefined):
60
60
  }
61
61
  }
62
62
 
63
- function lowerFace(face: FaceData): SideSpec {
63
+ function lowerFace(face: AnyFace): SideSpec {
64
64
  return {
65
65
  relation: face.owner.name,
66
66
  projection: [...face.projection],
@@ -71,7 +71,7 @@ function lowerFace(face: FaceData): SideSpec {
71
71
  }
72
72
 
73
73
  function lowerStatement(statement: Statement): StatementSpec {
74
- const data = statement.data
74
+ const data = statement
75
75
  switch (data.kind) {
76
76
  case "key":
77
77
  return { kind: "fd", relation: data.owner.name, projection: [...data.projection] }
@@ -101,18 +101,21 @@ function lowerStatement(statement: Statement): StatementSpec {
101
101
  }
102
102
 
103
103
  function lowerRelation(relation: AnyRelation, classes: RelationClasses): RelationSpec {
104
- const fields: FieldSpec[] = relation.data.fields.map(function lowerDeclared(declared) {
104
+ const fields: FieldSpec[] = relationFields(relation).map(function lowerDeclared(declared) {
105
105
  return lowerField(declared.name, declared.field, classes[declared.name])
106
106
  })
107
107
  return { name: relation.name, fields, closed: undefined }
108
108
  }
109
109
 
110
110
  function lowerClosed(member: AnyClosed, classes: RelationClasses): RelationSpec {
111
- const fields: FieldSpec[] = member.data.columns.map(function lowerColumn(column) {
112
- return lowerField(column.name, column.field, classes[column.name])
111
+ const fields: FieldSpec[] = Object.entries(member.columns).map(function lowerColumn([name, field]) {
112
+ return lowerField(name, field, classes[name])
113
113
  })
114
- const rows = member.data.rows.map(function lowerRow(row) {
115
- return { handle: row.handle, values: row.values }
114
+ const rows = member.handles.map(function lowerRow(handle) {
115
+ return {
116
+ handle,
117
+ values: Object.entries(member.columns).map(([name, field]) => literalOf(field, member.axioms[handle]?.[name]))
118
+ }
116
119
  })
117
120
  const newtype = classes.id
118
121
  if (newtype === undefined) {
package/src/native.ts CHANGED
@@ -663,6 +663,7 @@ export type {
663
663
  FindTermIr,
664
664
  HeadOpIr,
665
665
  HeadTermIr,
666
+ InteriorIr,
666
667
  IntervalValue,
667
668
  LogAccess,
668
669
  LogActivation,
@@ -692,6 +693,7 @@ export type {
692
693
  PrepareKind,
693
694
  QueryIr,
694
695
  QueryParam,
696
+ RecIr,
695
697
  RuleIr,
696
698
  ScalarExprIr,
697
699
  SealedDescriptor,
package/src/query/atom.ts CHANGED
@@ -582,6 +582,7 @@ type CondParamsShape<C> = ShapeOf<CondParams<C>>
582
582
  export type {
583
583
  AggData,
584
584
  AnyCond,
585
+ AnyTreeChild,
585
586
  AtomData,
586
587
  BindingEntry,
587
588
  BindingInput,
@@ -0,0 +1,394 @@
1
+ import { isDeepStrictEqual } from "node:util"
2
+ import { sealedFieldsOf } from "#closed.ts"
3
+ import { AuthoringError } from "#errors.ts"
4
+ import { type AnyField, fieldDescriptor, type Infer, rosterOf, signaturesAgree } from "#fields.ts"
5
+ import type { SchemaClasses } from "#law.ts"
6
+ import type { AtomIr, ConditionTreeIr, QueryIr, RuleIr, ScalarExprIr, TaggedValue, TermIr } from "#native.ts"
7
+ import type { AnyTreeChild, DerivedTable, FindColumn, InteriorData, RecData, RuleData } from "#query/atom.ts"
8
+ import type { QueryNode } from "#query/compute.ts"
9
+ import type { AnyQuery, ChainContext, Query } from "#query/lower.ts"
10
+ import { alignedHeadOf, EMPTY_RULE, lowerQuery, makeRawChain, makeRawQuery, taggedCmpLiteral } from "#query/lower.ts"
11
+ import { parseQueryIr } from "#query/parse-ir.ts"
12
+ import { type AnyVar, type MatchOwner, makeParam, makeSetParam, type ParamsRecord, v } from "#query/scope.ts"
13
+ import type { FieldsShape } from "#relation.ts"
14
+ import { handleOf } from "#rows.ts"
15
+ import { queryVarLeaf, scalarBinary, scalarCast, scalarFloatPredicate, scalarLiteral, scalarNegate } from "#scalar.ts"
16
+ import type { Schema, SchemaRelations } from "#schema.ts"
17
+ import { schemaDescriptor } from "#schema.ts"
18
+ import { arrayValue, recordValue, valueDescriptor } from "#values.ts"
19
+
20
+ /** Logical ordinals are scoped to this description and the schema's ordered declarations. */
21
+ interface QueryDescription {
22
+ readonly ir: QueryIr
23
+ readonly columns: readonly string[]
24
+ readonly interiors: readonly DescriptionTable[]
25
+ readonly recursion: DescriptionTable | undefined
26
+ readonly parameters: readonly DescriptionParameter[]
27
+ }
28
+
29
+ interface DescriptionTable {
30
+ readonly name: string
31
+ readonly columns: readonly string[]
32
+ }
33
+
34
+ interface DescriptionParameter {
35
+ readonly name: string
36
+ /** Closed-membership literals generated by the typed builder; absent for supplied parameters. */
37
+ readonly members: readonly TaggedValue[] | undefined
38
+ }
39
+
40
+ type DescriptionRow<F extends FieldsShape> = { readonly [K in keyof F]: Infer<F[K]> }
41
+
42
+ function refused(context: string, expected: string): never {
43
+ throw new AuthoringError({
44
+ message: `${context}: ${expected}`,
45
+ diagnostic: { code: "InvalidQuery", context, expected }
46
+ })
47
+ }
48
+
49
+ function name(context: string, input: unknown): string {
50
+ if (typeof input !== "string" || !input.isWellFormed()) return refused(context, "expected a well-formed string name")
51
+ return input
52
+ }
53
+
54
+ function names(context: string, input: unknown): readonly string[] {
55
+ const values = arrayValue(context, input, name)
56
+ if (new Set(values).size !== values.length) refused(context, "names must be unique")
57
+ return values
58
+ }
59
+
60
+ function table(context: string, input: unknown): DescriptionTable {
61
+ const raw = recordValue(context, input, ["name", "columns"])
62
+ return Object.freeze({ name: name(`${context}.name`, raw.name), columns: names(`${context}.columns`, raw.columns) })
63
+ }
64
+
65
+ function checkedDescription(input: unknown): QueryDescription {
66
+ const raw = recordValue("query description", input, ["ir", "columns", "interiors", "recursion", "parameters"])
67
+ const ir = parseQueryIr(raw.ir)
68
+ const columns = names("query columns", raw.columns)
69
+ if (columns.length !== ir.head.length) refused("query columns", "column names must match the query head width")
70
+ const interiors = arrayValue("query interiors", raw.interiors, table)
71
+ if (interiors.length !== ir.interiors.length) refused("query interiors", "names must match the IR interiors")
72
+ for (const [ordinal, interior] of interiors.entries()) {
73
+ if (interior.columns.length !== ir.interiors[ordinal]?.head.length)
74
+ refused(`query interiors[${ordinal}]`, "column names must match the interior head width")
75
+ }
76
+ const recursion = raw.recursion === undefined ? undefined : table("query recursion", raw.recursion)
77
+ if (
78
+ ir.kind === "cq"
79
+ ? recursion !== undefined
80
+ : recursion === undefined || recursion.columns.length !== ir.rec.head.length
81
+ )
82
+ refused("query recursion", "recursion names must match the IR recursion head")
83
+ const tableNames = [...interiors.map((entry) => entry.name), ...(recursion === undefined ? [] : [recursion.name])]
84
+ if (new Set(tableNames).size !== tableNames.length) refused("query tables", "names must be unique")
85
+ const parameters = arrayValue("query parameters", raw.parameters, (context, value): DescriptionParameter => {
86
+ const record = recordValue(context, value, ["name", "members"])
87
+ return Object.freeze({
88
+ name: name(`${context}.name`, record.name),
89
+ members:
90
+ record.members === undefined ? undefined : arrayValue(`${context}.members`, record.members, valueDescriptor)
91
+ })
92
+ })
93
+ if (new Set(parameters.map((entry) => entry.name)).size !== parameters.length)
94
+ refused("query parameters", "names must be unique")
95
+ return Object.freeze({ ir, columns, interiors, recursion, parameters })
96
+ }
97
+
98
+ /** Return an owned logical description; no builder variables or native resource handles escape. */
99
+ function describeQuery(query: AnyQuery): QueryDescription {
100
+ return checkedDescription({
101
+ ir: lowerQuery(query),
102
+ columns: query.data.finds.map((column) => column.name),
103
+ interiors: query.data.interiors.map((entry, ordinal) => ({
104
+ name: `interior${ordinal}`,
105
+ columns: entry.finds.map((column) => column.name)
106
+ })),
107
+ recursion:
108
+ query.data.kind === "cq"
109
+ ? undefined
110
+ : { name: "recursion", columns: query.data.rec.finds.map((column) => column.name) },
111
+ parameters: query.data.params.map((entry) => ({
112
+ name: entry.name,
113
+ members: entry.membership?.kind === "set" ? entry.membership.values : undefined
114
+ }))
115
+ })
116
+ }
117
+
118
+ function hostLiteral(
119
+ field: AnyField,
120
+ value: TaggedValue,
121
+ op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
122
+ ): unknown {
123
+ const roster = rosterOf(field)
124
+ let host: unknown = "value" in value ? value.value : { start: value.start, end: value.end }
125
+ if (roster !== undefined) host = handleOf("query literal", roster, host)
126
+ if (!isDeepStrictEqual(taggedCmpLiteral("query literal", field, host, op), value))
127
+ refused("query literal", "literal tag must match the field domain")
128
+ return host
129
+ }
130
+
131
+ /** Rehydrate into the existing checked rule builder, then use its ordinary lowering and execution. */
132
+ function replayRule(
133
+ context: ChainContext,
134
+ rule: RuleIr,
135
+ columns: readonly string[],
136
+ parameters: readonly DescriptionParameter[],
137
+ usedParameters: Set<number>
138
+ ): RuleData {
139
+ const relations = Object.values(context.theory.relations)
140
+ const derived: readonly DerivedTable[] = [
141
+ ...context.interiors,
142
+ ...("rec" in context && "finds" in context.rec ? [context.rec] : [])
143
+ ]
144
+ const variables = new Map<number, AnyVar>()
145
+ const relationAt = (ordinal: number): MatchOwner =>
146
+ relations[ordinal] ?? refused("query atom", `unknown relation ordinal ${ordinal}`)
147
+ const derivedAt = (ordinal: number): DerivedTable =>
148
+ derived[ordinal] ?? refused("query atom", `unavailable interior ordinal ${ordinal}`)
149
+ const variableAt = (ordinal: number): AnyVar =>
150
+ variables.get(ordinal) ?? refused("query variable", `variable ${ordinal} has no positive binding`)
151
+ const fieldsAt = (atom: AtomIr): readonly { readonly name: string; readonly field: AnyField }[] => {
152
+ if (atom.source.kind === "edb") return sealedFieldsOf(relationAt(atom.source.relation))
153
+ return derivedAt(atom.source.interior).finds.map((column) => ({
154
+ name: column.name,
155
+ field: column.slot?.field ?? refused("query interior", "column has no field")
156
+ }))
157
+ }
158
+ // Establish each variable's source domain from a positive occurrence. The
159
+ // shared builder verifies every other occurrence and all provenance joins.
160
+ for (const atom of rule.atoms) {
161
+ const fields = fieldsAt(atom)
162
+ const minted: Readonly<Record<string, AnyVar>> =
163
+ atom.source.kind === "edb"
164
+ ? v(relationAt(atom.source.relation))
165
+ : v({ schema: context.theory, data: { kind: "cq", finds: derivedAt(atom.source.interior).finds } })
166
+ for (const [ordinal, term] of atom.bindings) {
167
+ const field = fields[ordinal] ?? refused("query atom", `unknown field ordinal ${ordinal}`)
168
+ if (term.kind === "var" && !variables.has(term.var)) {
169
+ const variable = minted[field.name] ?? refused("query atom", "variable mint missing")
170
+ variables.set(term.var, variable)
171
+ }
172
+ }
173
+ }
174
+ let chain = makeRawChain(context, EMPTY_RULE)
175
+ const termValue = (term: TermIr, field: AnyField, op: Parameters<typeof hostLiteral>[2]): unknown => {
176
+ if (term.kind === "var") return variableAt(term.var)
177
+ if (term.kind === "literal") return hostLiteral(field, term.value, op)
178
+ const parameter = parameters[term.param] ?? refused("query parameter", `unknown parameter ordinal ${term.param}`)
179
+ usedParameters.add(term.param)
180
+ if (parameter.members !== undefined) {
181
+ if (term.kind !== "paramSet" || op !== "binding")
182
+ refused("query parameter", "fixed membership belongs in a set binding")
183
+ return parameter.members.map((value) => hostLiteral(field, value, "binding"))
184
+ }
185
+ return term.kind === "param" ? makeParam(parameter.name) : makeSetParam(parameter.name)
186
+ }
187
+ const bindingsOf = (atom: AtomIr): Readonly<Record<string, unknown>> => {
188
+ const fields = fieldsAt(atom)
189
+ return Object.fromEntries(
190
+ atom.bindings.map(([ordinal, term]) => {
191
+ const field = fields[ordinal] ?? refused("query atom", `unknown field ordinal ${ordinal}`)
192
+ if (atom.source.kind === "interior" && term.kind !== "var")
193
+ refused("query interior", "interior bindings require variables")
194
+ return [field.name, termValue(term, field.field, "binding")]
195
+ })
196
+ )
197
+ }
198
+ for (const atom of rule.atoms) {
199
+ chain =
200
+ atom.source.kind === "edb"
201
+ ? chain.match(relationAt(atom.source.relation), bindingsOf(atom))
202
+ : chain.interior(derivedAt(atom.source.interior).name, bindingsOf(atom))
203
+ }
204
+ for (const atom of rule.negated) {
205
+ chain = chain.where(
206
+ atom.source.kind === "edb"
207
+ ? { cond: "not", relation: relationAt(atom.source.relation), bindings: bindingsOf(atom) }
208
+ : { cond: "notInterior", name: derivedAt(atom.source.interior).name, bindings: bindingsOf(atom) }
209
+ )
210
+ }
211
+ const condition = (input: ConditionTreeIr): AnyTreeChild => {
212
+ if (input.kind !== "leaf") {
213
+ const children = input.children.map(condition)
214
+ // This grammar contains comparisons and trees only, never negated atoms.
215
+ return {
216
+ cond: "tree",
217
+ op: input.kind,
218
+ children
219
+ }
220
+ }
221
+ const { lhs, rhs, op } = input.cmp
222
+ let anchor: AnyField
223
+ if (lhs.kind === "var") anchor = variableAt(lhs.var).field
224
+ else if (rhs.kind === "var") anchor = variableAt(rhs.var).field
225
+ else return refused("query comparison", "requires a variable operand")
226
+ return {
227
+ cond: "cmp",
228
+ op: op.kind,
229
+ lhs: termValue(lhs, anchor, op.kind),
230
+ rhs: termValue(rhs, anchor, op.kind),
231
+ mask: op.kind === "allen" ? op.mask : undefined
232
+ }
233
+ }
234
+ for (const input of rule.conditions) chain = chain.where(condition(input))
235
+ const scalar = (input: ScalarExprIr): QueryNode => {
236
+ switch (input.kind) {
237
+ case "var": {
238
+ const variable = variableAt(input.var)
239
+ const kind = variable.field.kind
240
+ if (
241
+ rosterOf(variable.field) !== undefined ||
242
+ (kind !== "bool" && kind !== "u64" && kind !== "i64" && kind !== "f64")
243
+ )
244
+ refused("query compute", "requires a scalar variable")
245
+ return queryVarLeaf(variable, kind)
246
+ }
247
+ case "literal": {
248
+ const value = input.value
249
+ switch (value.kind) {
250
+ case "u64":
251
+ return scalarLiteral("query-var", { u64: value.value })
252
+ case "i64":
253
+ return scalarLiteral("query-var", { i64: value.value })
254
+ case "f64":
255
+ return scalarLiteral("query-var", { f64: value.value })
256
+ case "bool":
257
+ return scalarLiteral("query-var", { bool: value.value })
258
+ default:
259
+ return refused("query compute", "requires a scalar literal")
260
+ }
261
+ }
262
+ case "negate":
263
+ return scalarNegate("query compute", scalar(input.expr))
264
+ case "isNaN":
265
+ case "isFinite":
266
+ return scalarFloatPredicate("query compute", input.kind, scalar(input.expr))
267
+ case "cast":
268
+ return scalarCast(
269
+ "query compute",
270
+ input.cast,
271
+ ({ toI64Exact: "i64", toU64Exact: "u64", toF64: "f64", toF64Exact: "f64" } as const)[input.cast],
272
+ scalar(input.expr)
273
+ )
274
+ default:
275
+ return scalarBinary("query compute", input.kind, scalar(input.left), scalar(input.right))
276
+ }
277
+ }
278
+ const finds = Object.fromEntries(
279
+ rule.finds.map((find, ordinal) => {
280
+ const column = columns[ordinal] ?? refused("query find", "column name missing")
281
+ let value: unknown
282
+ if (find.kind === "var") value = variableAt(find.var)
283
+ else if (find.kind === "compute") value = scalar(find.expr)
284
+ else if (find.kind === "count") value = { agg: "count" }
285
+ else value = { agg: find.kind === "pack" ? "pack" : find.op.kind, over: variableAt(find.over) }
286
+ return [column, value]
287
+ })
288
+ )
289
+ return chain.find(finds).rule
290
+ }
291
+
292
+ function queryFromDescription<Rels extends SchemaRelations, Classes extends SchemaClasses, const F extends FieldsShape>(
293
+ schema: Schema<Rels, Classes>,
294
+ input: unknown,
295
+ result: F
296
+ ): Query<Rels, DescriptionRow<F>, ParamsRecord, Classes> {
297
+ const theory = schemaDescriptor(schema)
298
+ const description = checkedDescription(input)
299
+ const resultRecord = recordValue(
300
+ "query result",
301
+ result,
302
+ typeof result === "object" && result !== null ? Object.keys(result) : []
303
+ )
304
+ const fields = Object.fromEntries(
305
+ Object.entries(resultRecord).map(([name, field]) => [name, fieldDescriptor(`query result.${name}`, field)])
306
+ )
307
+ const interiors: InteriorData[] = []
308
+ const usedParameters = new Set<number>()
309
+ for (const [ordinal, item] of description.ir.interiors.entries()) {
310
+ const names = description.interiors[ordinal] ?? refused("query interior", "names missing")
311
+ const rules = item.rules.map((rule) =>
312
+ replayRule(
313
+ { theory, classes: theory.classes, kind: "interior", self: names.name, interiors },
314
+ rule,
315
+ names.columns,
316
+ description.parameters,
317
+ usedParameters
318
+ )
319
+ )
320
+ interiors.push(
321
+ Object.freeze({
322
+ name: names.name,
323
+ finds: alignedHeadOf(`interior ${names.name}`, rules),
324
+ rules: Object.freeze(rules)
325
+ })
326
+ )
327
+ }
328
+ let rec: RecData | undefined
329
+ if (description.ir.kind === "reach") {
330
+ const names = description.recursion ?? refused("query recursion", "names missing")
331
+ const self = { name: names.name }
332
+ const base = description.ir.rec.base.map((rule) =>
333
+ replayRule(
334
+ { theory, classes: theory.classes, kind: "rec-base", self, rec: self, interiors },
335
+ rule,
336
+ names.columns,
337
+ description.parameters,
338
+ usedParameters
339
+ )
340
+ )
341
+ const baseHead = alignedHeadOf("recursion base", base)
342
+ const first = baseHead[0] ?? refused("query recursion", "requires a nonempty head")
343
+ const head: { readonly name: string; readonly finds: readonly [FindColumn, ...FindColumn[]] } = {
344
+ name: names.name,
345
+ finds: [first, ...baseHead.slice(1)]
346
+ }
347
+ const steps = description.ir.rec.rec.map((rule) =>
348
+ replayRule(
349
+ { theory, classes: theory.classes, kind: "rec-arm", self: head, rec: head, interiors },
350
+ rule,
351
+ names.columns,
352
+ description.parameters,
353
+ usedParameters
354
+ )
355
+ )
356
+ const finds = alignedHeadOf("recursion", [...base, ...steps])
357
+ const firstFind = finds[0] ?? refused("query recursion", "requires a nonempty head")
358
+ const firstBase = base[0] ?? refused("query recursion", "requires a base rule")
359
+ const firstStep = steps[0] ?? refused("query recursion", "requires a recursive rule")
360
+ rec = Object.freeze({
361
+ name: names.name,
362
+ finds: Object.freeze([firstFind, ...finds.slice(1)] as const),
363
+ base: Object.freeze([firstBase, ...base.slice(1)] as const),
364
+ rec: Object.freeze([firstStep, ...steps.slice(1)] as const)
365
+ })
366
+ }
367
+ const context: ChainContext = {
368
+ theory,
369
+ classes: theory.classes,
370
+ kind: "query",
371
+ interiors,
372
+ ...(rec === undefined ? {} : { rec })
373
+ }
374
+ const rules = description.ir.rules.map((rule) =>
375
+ replayRule(context, rule, description.columns, description.parameters, usedParameters)
376
+ )
377
+ const value = makeRawQuery(theory, interiors, rec, rules)
378
+ const expected = Object.entries(fields)
379
+ if (
380
+ expected.length !== value.data.finds.length ||
381
+ expected.some(([name, field], ordinal) => {
382
+ const column = value.data.finds[ordinal]
383
+ return column?.name !== name || column.slot === undefined || !signaturesAgree(column.slot.field, field)
384
+ })
385
+ )
386
+ refused("query result", "result names and field domains must match the derived query head in order")
387
+ if (usedParameters.size !== description.parameters.length) refused("query parameters", "declared parameter is unused")
388
+ // Complete shared lowering also checks tagged literals and ordinal bounds.
389
+ lowerQuery(value)
390
+ return value as unknown as Query<Rels, DescriptionRow<F>, ParamsRecord, Classes>
391
+ }
392
+
393
+ export type { DescriptionParameter, DescriptionRow, DescriptionTable, QueryDescription }
394
+ export { describeQuery, queryFromDescription }
package/src/query/find.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AnyField, Infer } from "#fields.ts"
1
+ import type { BoolField, F64Field, I64Field, Infer, U64Field } from "#fields.ts"
2
2
  import type { SchemaClasses } from "#law.ts"
3
3
  import type { IntervalVarOk, NumericVarOk } from "#query/atom.ts"
4
4
  import type { AnyComputeExpr, ComputeExpr, ComputeValue } from "#query/compute.ts"
@@ -95,17 +95,30 @@ type FindValue<E> = E extends AnyVar
95
95
 
96
96
  type RowOfFind<F extends FindShape> = { readonly [K in keyof F]: FindValue<F[K]> }
97
97
 
98
+ type ComputeFields = {
99
+ readonly u64: U64Field
100
+ readonly i64: I64Field
101
+ readonly f64: F64Field
102
+ readonly bool: BoolField
103
+ }
104
+
98
105
  /**
99
- * The head signature of a recursive rule's find record as classed mint
100
- * slots (descriptor + law-computed class), keyed by column name — the
101
- * signature an interior join pairs against (`F` is variable-only there).
106
+ * The head signature as classed slots, shared by derived-stage and imported
107
+ * query variables. Projection preserves its carrier; aggregates/computations
108
+ * produce bare values, matching findColumnSlotOf at runtime.
102
109
  */
103
110
  type HeadRecordOf<Classes extends SchemaClasses, F extends FindShape> = {
104
111
  readonly [K in keyof F]: F[K] extends AnyVar
105
112
  ? MintSlotOf<Classes, F[K]>
106
- : F[K] extends AnyComputeExpr
107
- ? { readonly field: AnyField; readonly class: undefined }
108
- : never
113
+ : F[K] extends ComputeExpr<infer Kind extends ScalarKind>
114
+ ? { readonly field: ComputeFields[Kind]; readonly class: undefined }
115
+ : F[K] extends CountAgg
116
+ ? { readonly field: U64Field; readonly class: undefined }
117
+ : F[K] extends Agg<"mean", AnyVar>
118
+ ? { readonly field: F64Field; readonly class: undefined }
119
+ : F[K] extends Agg<FoldOpName, infer Over extends AnyVar>
120
+ ? { readonly field: Over["field"]; readonly class: undefined }
121
+ : never
109
122
  }
110
123
 
111
124
  export type { Agg, CheckFind, CheckRecFind, FindEntry, FindShape, HeadRecordOf, RowOfFind }