@bjornpagen/bumbledb 0.3.0 → 0.5.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 (98) hide show
  1. package/COOKBOOK.md +246 -92
  2. package/README.md +31 -16
  3. package/dist/closed.d.ts +80 -75
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +102 -127
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +37 -7
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +95 -55
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +40 -40
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +9 -17
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +46 -15
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +58 -29
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +13 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts +33 -6
  34. package/dist/marshal.d.ts.map +1 -1
  35. package/dist/marshal.js +75 -26
  36. package/dist/marshal.js.map +1 -1
  37. package/dist/native.d.ts +21 -2
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/native.js +20 -3
  40. package/dist/native.js.map +1 -1
  41. package/dist/order.d.ts +36 -0
  42. package/dist/order.d.ts.map +1 -0
  43. package/dist/order.js +135 -0
  44. package/dist/order.js.map +1 -0
  45. package/dist/query/atom.d.ts +76 -28
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -16
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +5 -8
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +308 -72
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/predicate.d.ts.map +1 -1
  54. package/dist/query/predicate.js +34 -2
  55. package/dist/query/predicate.js.map +1 -1
  56. package/dist/query/run.d.ts +15 -5
  57. package/dist/query/run.d.ts.map +1 -1
  58. package/dist/query/run.js +27 -8
  59. package/dist/query/run.js.map +1 -1
  60. package/dist/query/scope.d.ts +37 -29
  61. package/dist/query/scope.d.ts.map +1 -1
  62. package/dist/query/scope.js +19 -47
  63. package/dist/query/scope.js.map +1 -1
  64. package/dist/relation.d.ts +17 -29
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +41 -38
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +7 -31
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +3 -2
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +13 -4
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +75 -8
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +2 -5
  79. package/src/closed.ts +144 -206
  80. package/src/db.ts +143 -68
  81. package/src/exhume.ts +1 -15
  82. package/src/face.ts +38 -48
  83. package/src/fields.ts +103 -49
  84. package/src/index.ts +11 -10
  85. package/src/law.ts +15 -14
  86. package/src/lower.ts +2 -9
  87. package/src/marshal.ts +82 -31
  88. package/src/native.ts +22 -4
  89. package/src/order.ts +156 -0
  90. package/src/query/atom.ts +70 -35
  91. package/src/query/lower.ts +354 -82
  92. package/src/query/predicate.ts +39 -4
  93. package/src/query/run.ts +27 -9
  94. package/src/query/scope.ts +52 -70
  95. package/src/relation.ts +54 -68
  96. package/src/schema.ts +7 -33
  97. package/src/spec.ts +3 -2
  98. package/src/statements.ts +82 -8
@@ -29,7 +29,7 @@
29
29
 
30
30
  import * as errors from "@superbuilders/errors"
31
31
  import type { SchemaClasses } from "#law.ts"
32
- import type { RecData } from "#query/atom.ts"
32
+ import type { RecData, RuleData, SelectColumn } from "#query/atom.ts"
33
33
  import type {
34
34
  AnyRuleValue,
35
35
  HeadOf,
@@ -45,8 +45,8 @@ import type {
45
45
  RuleValue
46
46
  } from "#query/lower.ts"
47
47
  import { makeOutputRuleScope, makeQuery, makeRawScope } from "#query/lower.ts"
48
- import type { Flatten, ParamsRecord, ShapeOf } from "#query/scope.ts"
49
- import { inferred } from "#query/scope.ts"
48
+ import type { ClassedField, Flatten, ParamsRecord, ShapeOf } from "#query/scope.ts"
49
+ import { fieldJoins, inferred, renderFieldKind } from "#query/scope.ts"
50
50
  import type { Schema, SchemaRelations } from "#schema.ts"
51
51
 
52
52
  /**
@@ -112,6 +112,20 @@ interface RawRec<Name extends string> {
112
112
  rule(build: (r: RawScope) => RuleValue<never, never>): RawRec<Name>
113
113
  }
114
114
 
115
+ /**
116
+ * The classed slot one rec head column binds, through the rule's own
117
+ * environment: a rec head projects bound variable NAMES only (the strata
118
+ * roster's unwritability), so every column is a projected var and its
119
+ * slot is the var's first positive binding.
120
+ */
121
+ function recHeadSlotOf(rule: RuleData, column: SelectColumn): ClassedField | undefined {
122
+ const entry = column.entry
123
+ if (entry.kind === "var") {
124
+ return rule.varFields[entry.over]
125
+ }
126
+ return undefined
127
+ }
128
+
115
129
  /** Builds the runtime rec handle over shared rec data. */
116
130
  function makeRawRec<Name extends string>(state: ProgramState, name: Name, data: RecData): RawRec<Name> {
117
131
  const rec: RawRec<Name> = {
@@ -137,6 +151,26 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
137
151
  `rec ${name}: every rule derives the same head — rule 0 projects (${declared.join(", ")}), this rule projects (${candidate.join(", ")})`
138
152
  )
139
153
  }
154
+ // The law-class wall on the sealed head: names alone do not
155
+ // align value spaces. Every rule must bind each head column
156
+ // at a slot that JOINS rule 0's (the sealing rule — the one
157
+ // slot every downstream idb pairing class-checks against),
158
+ // under the same fieldJoins judgment every reuse site
159
+ // enforces; otherwise a later rule feeds (say) bare weights
160
+ // into a column the idb joins as Node ids.
161
+ built.rule.select.forEach(function verifyHeadSlot(column, position) {
162
+ const lead = head.select[position]
163
+ if (lead === undefined) {
164
+ return
165
+ }
166
+ const leadSlot = recHeadSlotOf(head, lead)
167
+ const slot = recHeadSlotOf(built.rule, column)
168
+ if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
169
+ throw errors.new(
170
+ `rec ${name}: every rule derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(leadSlot)} in rule 0 but at ${renderFieldKind(slot)} in this rule (a head column joins only class-equal slots; bare pairs only with bare)`
171
+ )
172
+ }
173
+ })
140
174
  }
141
175
  data.rules.push(built.rule)
142
176
  return makeRawRec<Name>(state, name, data)
@@ -147,7 +181,8 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
147
181
  }
148
182
 
149
183
  /**
150
- * The rec handles' trusted admission seam (the `refsComplete` pattern):
184
+ * The rec handles' trusted admission seam (the pattern's home is
185
+ * `isTypedScope` in query/lower.ts):
151
186
  * the checkable fact — the handle owns exactly the rec data it names — is
152
187
  * verified before the raw handle is admitted at its typed face.
153
188
  */
package/src/query/run.ts CHANGED
@@ -8,20 +8,22 @@
8
8
  * values — the marshal boundary is pure both ways: the engine computed
9
9
  * the answer under the prepared head, so a decoded row that carries every
10
10
  * select column IS a row (the trusted read seam), and nothing is asserted
11
- * on any value. Answers are SETS no order or limit exists anywhere;
12
- * hosts sort. The `Prepared` VALUE itself (no lifecycle, GC-reclaimed
13
- * plan) lives in `#db.ts`.
11
+ * on any value. A CLOSED answer column decodes id handle NAME through
12
+ * the marshal's one bijection (`handleOf` the same read half every fact
13
+ * decode rides; the column's roster rides `SelectColumn.closed`), so query
14
+ * rows speak the vocabulary exactly as scans and gets do. Answers are
15
+ * SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
16
+ * VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
14
17
  */
15
18
 
16
19
  import * as errors from "@superbuilders/errors"
20
+ import { handleOf } from "#marshal.ts"
17
21
  import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
18
22
  import type { SelectColumn } from "#query/atom.ts"
23
+ import { ALLEN_ALL_BITS } from "#query/atom.ts"
19
24
  import { taggedCmpLiteral } from "#query/lower.ts"
20
25
  import type { ParamEntry } from "#query/scope.ts"
21
26
 
22
- /** The 13-bit Allen mask ceiling (`bumbledb/crates/bumbledb/src/allen.rs`: bits above the low 13 are unrepresentable). */
23
- const ALLEN_ALL_BITS = (1 << 13) - 1
24
-
25
27
  /** Tags one supplied mask-param value. */
26
28
  function wireMask(name: string, value: unknown): TaggedValue {
27
29
  if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > ALLEN_ALL_BITS) {
@@ -45,10 +47,23 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
45
47
  * in registry order (= the lowering's dense `ParamId`s). A missing entry
46
48
  * is a typed error naming the param; values tag by the anchoring use's
47
49
  * structural type; a set param takes a readonly array (the empty set is
48
- * legal and matches nothing — the engine's rule).
50
+ * legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
51
+ * entry (`members` present — a literal set folded into the program) is
52
+ * supplied by the SDK itself: each handle name rides the one
53
+ * roster-verification point (`taggedHandleId`, through `wireValue`) and
54
+ * crosses as the same `{ kind: "set", values }` a bound `r.inSet` param
55
+ * crosses as — the host's params object is never consulted for it.
49
56
  */
50
57
  function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
51
58
  return entries.map(function wireOne(entry): QueryParam {
59
+ if (entry.members !== undefined) {
60
+ return {
61
+ kind: "set",
62
+ values: entry.members.map(function wireMember(member, index) {
63
+ return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
64
+ })
65
+ }
66
+ }
52
67
  const value = supplied[entry.name]
53
68
  if (value === undefined) {
54
69
  throw errors.new(`execute params object is missing param ${entry.name}`)
@@ -90,7 +105,9 @@ function isAnswerRow<Row>(
90
105
  /**
91
106
  * Decodes positional answer rows (column order = the program's head order
92
107
  * = the select's written order) to named, frozen row objects of bare
93
- * structural values.
108
+ * structural values. A closed column lifts its row id back to the handle
109
+ * NAME through the marshal's bijection — an out-of-roster id is the same
110
+ * pointed throw a fact decode gives, never a silent fallback.
94
111
  */
95
112
  function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]): Row[] {
96
113
  return rows.map(function decodeRow(row) {
@@ -103,7 +120,8 @@ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]
103
120
  if (cell === undefined) {
104
121
  throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
105
122
  }
106
- decoded[column.name] = cell
123
+ decoded[column.name] =
124
+ column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
107
125
  })
108
126
  Object.freeze(decoded)
109
127
  if (!isAnswerRow<Row>(select, decoded)) {
@@ -19,8 +19,8 @@
19
19
  * inference ride.
20
20
  */
21
21
 
22
- import * as errors from "@superbuilders/errors"
23
22
  import type { AnyField, Infer } from "#fields.ts"
23
+ import { rosterOf } from "#fields.ts"
24
24
 
25
25
  /**
26
26
  * The runtime discriminant of query term values. Host literals (bigints,
@@ -110,57 +110,6 @@ function makeVar<const Name extends string>(name: Name): Var<Name> {
110
110
  return Object.freeze(value)
111
111
  }
112
112
 
113
- /** The record `makeVars` mints: one own frozen `Var<Name>` per requested name, each typed exactly. */
114
- type VarsRecord<Names extends string> = { readonly [N in Names]: Var<N> }
115
-
116
- /**
117
- * The trusted seam of the vars mint: every requested name reads back as an
118
- * own var term of exactly that name — verified before the record is
119
- * admitted at the {@link VarsRecord} type (a name riding the
120
- * object-protocol accessor instead of an own definition would fail exactly
121
- * this check).
122
- */
123
- function varsMinted<Names extends string>(
124
- record: Readonly<Record<string, unknown>>,
125
- names: readonly Names[]
126
- ): record is Readonly<Record<string, unknown>> & VarsRecord<Names> {
127
- return names.every(function varMinted(name) {
128
- if (!Object.hasOwn(record, name)) {
129
- return false
130
- }
131
- const value = record[name]
132
- return isTerm(value) && value[term] === "var" && value.name === name
133
- })
134
- }
135
-
136
- /**
137
- * Mints several variables at once — `const { service, w } = r.vars("service",
138
- * "w")`: tuple-to-object, each name typed exactly (`Var<"service">`),
139
- * inference identical to the one-at-a-time `r.var` spelling (one lowering,
140
- * two entry flavors). Each key is defined as an OWN property (a name like
141
- * `"__proto__"` is a record key like any other, never a prototype write),
142
- * and a duplicate name in one call is a construction error: each name mints
143
- * one variable — write it once and reuse the binding.
144
- */
145
- function makeVars<const Names extends readonly string[]>(...names: Names): VarsRecord<Names[number]> {
146
- const out: Record<string, unknown> = {}
147
- const seen = new Set<string>()
148
- for (const name of names) {
149
- if (seen.has(name)) {
150
- throw errors.new(
151
- `vars: duplicate name ${name} — each name mints one variable; write it once and reuse the binding`
152
- )
153
- }
154
- seen.add(name)
155
- Object.defineProperty(out, name, { value: makeVar(name), enumerable: true })
156
- }
157
- Object.freeze(out)
158
- if (!varsMinted<Names[number]>(out, names)) {
159
- throw errors.new("vars: variable minting incomplete")
160
- }
161
- return out
162
- }
163
-
164
113
  /** Builds one scalar-parameter term. */
165
114
  function makeParam<const Name extends string>(name: Name): Param<Name> {
166
115
  const value: Param<Name> = { [term]: "param", name }
@@ -228,25 +177,38 @@ type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : u
228
177
  /** Reads an interval descriptor's element kind; `undefined` on scalar kinds. */
229
178
  type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
230
179
 
180
+ /** Reads a closed reference's handle union; `undefined` on every non-closed kind (the roster IS descriptor structure). */
181
+ type RosterOf<F extends AnyField> = F extends {
182
+ readonly closed: { readonly handles: readonly (infer H extends string)[] }
183
+ }
184
+ ? H
185
+ : undefined
186
+
231
187
  /**
232
188
  * The join judgment: two bound slots join iff their descriptors' structure
233
- * agrees (kind, width label, interval element) AND their law-computed
234
- * classes agree same class name joins, and bare (`undefined`) pairs only
235
- * with bare (ruling 3: a field in no law has no class; a bare↔classed
236
- * pairing refuses). The class names come off the SCHEMA type's class map —
237
- * the statements are the typing; no descriptor label exists to compare.
189
+ * agrees (kind, width label, interval element, and the closed ROSTER — a
190
+ * closed reference pairs only with the same vocabulary, never with a bare
191
+ * u64: the roster keys every closed judgment downstream, so a join across
192
+ * it would decode/order/translate incoherently by binding order) AND their
193
+ * law-computed classes agree same class name joins, and bare
194
+ * (`undefined`) pairs only with bare (ruling 3: a field in no law has no
195
+ * class; a bare↔classed pairing refuses). The class names come off the
196
+ * SCHEMA type's class map — the statements are the typing; no descriptor
197
+ * label beyond the roster exists to compare.
238
198
  */
239
199
  type JoinOk<A extends ClassedField, B extends ClassedField> = [
240
200
  A["field"]["kind"],
241
201
  A["class"],
242
202
  WidthOf<A["field"]>,
243
- ElementOf<A["field"]>
244
- ] extends [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>]
245
- ? [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>] extends [
203
+ ElementOf<A["field"]>,
204
+ RosterOf<A["field"]>
205
+ ] extends [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>]
206
+ ? [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>] extends [
246
207
  A["field"]["kind"],
247
208
  A["class"],
248
209
  WidthOf<A["field"]>,
249
- ElementOf<A["field"]>
210
+ ElementOf<A["field"]>,
211
+ RosterOf<A["field"]>
250
212
  ]
251
213
  ? true
252
214
  : false
@@ -255,7 +217,8 @@ type JoinOk<A extends ClassedField, B extends ClassedField> = [
255
217
  /**
256
218
  * The runtime twin of {@link JoinOk}: two bound slots join iff descriptor
257
219
  * structure and class agree — the same comparison the type tier makes,
258
- * judged on the honest runtime values (the descriptor and the schema
220
+ * judged on the honest runtime values (the descriptor, the roster by VALUE
221
+ * IDENTITY — vocabulary identity is value identity — and the schema
259
222
  * value's frozen class map). The rule builders throw through this on a
260
223
  * class-unequal variable reuse, so the wall holds for untyped callers too,
261
224
  * not only where the compiler can see.
@@ -265,17 +228,31 @@ function fieldJoins(a: ClassedField, b: ClassedField): boolean {
265
228
  const widthB = "width" in b.field ? b.field.width : undefined
266
229
  const elementA = "element" in a.field ? a.field.element : undefined
267
230
  const elementB = "element" in b.field ? b.field.element : undefined
268
- return a.field.kind === b.field.kind && a.class === b.class && widthA === widthB && elementA === elementB
231
+ const rosterA = rosterOf(a.field)
232
+ const rosterB = rosterOf(b.field)
233
+ return (
234
+ a.field.kind === b.field.kind &&
235
+ a.class === b.class &&
236
+ widthA === widthB &&
237
+ elementA === elementB &&
238
+ rosterA === rosterB
239
+ )
269
240
  }
270
241
 
271
242
  /**
272
243
  * Renders one bound slot for join-mismatch diagnostics — the structural
273
- * kind in the schema grammar's spelling plus the slot's law-computed class
274
- * (`u64 in class Holder.id`; a lawless slot renders `bare`).
244
+ * kind in the schema grammar's spelling (a closed reference names its
245
+ * vocabulary: the roster is part of the structure being compared) plus the
246
+ * slot's law-computed class (`u64 in class Holder.id`; a lawless slot
247
+ * renders `bare`).
275
248
  */
276
249
  function renderFieldKind(slot: ClassedField): string {
277
250
  const field = slot.field
278
251
  let base: string = field.kind
252
+ const roster = rosterOf(field)
253
+ if (roster !== undefined) {
254
+ base = `u64 referencing ${roster.name}`
255
+ }
279
256
  if (field.kind === "bytes") {
280
257
  base = `bytes<${field.width}>`
281
258
  }
@@ -287,7 +264,10 @@ function renderFieldKind(slot: ClassedField): string {
287
264
 
288
265
  /**
289
266
  * What a PARAM anchored at field `F` accepts at execution: the field's
290
- * bare value type, exactly. At an interval field the engine resolves the
267
+ * bare value type, exactly at a CLOSED-reference field that is the
268
+ * handle-name union (`"DirectPass" | "Failed"`), translated name → row id
269
+ * at execute through the one roster-verification point
270
+ * (`taggedHandleId`). At an interval field the engine resolves the
291
271
  * bivalent anchor to the INTERVAL reading (value equality) — the point
292
272
  * reading of a param is spelled `pointIn(r.param(...), w)`, whose sibling
293
273
  * anchors it element-typed.
@@ -304,13 +284,17 @@ type InferredOf<T> = T extends { readonly [inferred]?: infer S } ? Exclude<S, un
304
284
  * positions) — the op keeps literal tagging op-aware at `pointIn`
305
285
  * (the bug-hunt fix, preserved). `anchor` is `undefined` only on a query
306
286
  * built but not yet anchored by any rule; lowering and the wire both refuse
307
- * that state typed.
287
+ * that state typed. `members` is present exactly on a MEMBERSHIP-ARRAY
288
+ * entry (a literal set at a closed field, folded into the program): the
289
+ * SDK itself translates and supplies the set at every execute — the entry
290
+ * is never read from, and never demanded of, the host's params object.
308
291
  */
309
292
  interface ParamEntry {
310
293
  readonly name: string
311
294
  readonly shape: "value" | "set" | "mask"
312
295
  readonly anchor: AnyField | "measure" | undefined
313
296
  readonly op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
297
+ readonly members: readonly string[] | undefined
314
298
  }
315
299
 
316
300
  export type {
@@ -329,8 +313,7 @@ export type {
329
313
  SetParam,
330
314
  ShapeOf,
331
315
  UnionToIntersection,
332
- Var,
333
- VarsRecord
316
+ Var
334
317
  }
335
318
  export {
336
319
  fieldJoins,
@@ -341,7 +324,6 @@ export {
341
324
  makeParam,
342
325
  makeSetParam,
343
326
  makeVar,
344
- makeVars,
345
327
  renderFieldKind,
346
328
  term
347
329
  }
package/src/relation.ts CHANGED
@@ -2,51 +2,60 @@
2
2
  * `relation()` — the ordinary-relation half of the theory's signature. A
3
3
  * relation value is a frozen plain object carrying its name, its ordered
4
4
  * field descriptors (declaration order = ordinal ids, the macro's law),
5
- * typed field references (`R.fields.holder`), and — since selections are
6
- * the relation's own vocabulary `where()`, which resolves a selection
7
- * into lowered bindings eagerly (handles verified against their roster at
8
- * construction). `Fact<>`/`InsertFact<>` are the inferred row object
9
- * types at BARE structural value types (no brands): fresh fields are
10
- * optional on insert input (omit-to-mint) and present on read
5
+ * and — since selections are the relation's own vocabulary — `where()`,
6
+ * which resolves a selection into lowered bindings eagerly (handles
7
+ * verified against their roster at construction). Fields are addressed by
8
+ * NAME everywhere — statements (`on(R, "holder")`), selections, and match
9
+ * records all spell the field's own name, checked by type
10
+ * (`FaceFields`/`MatchShape`). `Fact<>`/`InsertFact<>` are the inferred
11
+ * row object types at BARE structural value types (no brands): fresh
12
+ * fields are optional on insert input (omit-to-mint) and present on read
11
13
  * (resupply-to-preserve-identity), typed exactly.
12
14
  */
13
15
 
14
16
  import * as errors from "@superbuilders/errors"
15
- import type { OneOf } from "#face.ts"
16
17
  import { type AnyField, assertDeclarationOrderKey, type Infer, literalOf } from "#fields.ts"
17
- import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
18
+ import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
18
19
 
19
20
  /** Flattens an intersection into one displayed object type (hover legibility). */
20
21
  type Flatten<T> = { [K in keyof T]: T[K] }
21
22
 
22
23
  /**
23
- * The one trusted seam of `relation()`: the reference record is built by
24
- * iterating the declared fields, and this guard verifies the checkable
25
- * facts one reference per declared field, each carrying its own name —
26
- * before the record is admitted as the typed {@link FieldRefs} (the
27
- * macro-emission analog).
24
+ * Resolves one selection entry to its lowered literal set: a plain ARRAY
25
+ * (detected by `Array.isArray` no field's value type is an array;
26
+ * `Uint8Array` is not one) becomes a disjunctive set, anything else the
27
+ * bare literal. The degenerate sets are construction errors, each
28
+ * self-locating (`context` names the relation and field) — the empty set
29
+ * selects nothing, the one-element set is the bare literal respelled, and
30
+ * a DUPLICATE literal (judged on the canonical rendering — the engine's
31
+ * own duplicate test, reached here first so its index-speak twin at
32
+ * `Db.create` stays unreachable from this surface) is the same respelling
33
+ * in disguise (the canonical-utterance law; the old set combinator's
34
+ * signature made the length degenerates unwritable, and the refusals here
35
+ * are that law's runtime seat). The lowered set — `{ kind: "many",
36
+ * literals }` — is byte-identical to what the combinator produced, so no
37
+ * fingerprint moves.
28
38
  */
29
- function refsComplete<RName extends string, Fields extends FieldsShape>(
30
- refs: Record<string, unknown>,
31
- fields: Fields
32
- ): refs is FieldRefs<RName, Fields> {
33
- return Object.keys(fields).every(function hasRef(fieldName) {
34
- const ref = refs[fieldName]
35
- return typeof ref === "object" && ref !== null && "field" in ref && ref.field === fieldName
36
- })
37
- }
38
-
39
- /**
40
- * Resolves one selection entry to its lowered literal set: an `oneOf`
41
- * value (detected by its `literals` tuple — no field value is ever an
42
- * object carrying `literals`) becomes a disjunctive set (≥ 2 by the
43
- * `oneOf` signature — the one-element set is unwritable); anything else is
44
- * the bare literal.
45
- */
46
- function resolveEntry(field: AnyField, entry: unknown): LiteralSetSpec {
47
- if (typeof entry === "object" && entry !== null && "literals" in entry && Array.isArray(entry.literals)) {
48
- const literals: LiteralSpec[] = entry.literals.map(function lowerSetLiteral(literal: unknown) {
49
- return Object.freeze(literalOf(field, literal))
39
+ function resolveEntry(context: string, field: AnyField, entry: unknown): LiteralSetSpec {
40
+ if (Array.isArray(entry)) {
41
+ if (entry.length < 2) {
42
+ throw errors.new(
43
+ entry.length === 0
44
+ ? `${context}: an empty literal set selects nothing — write the selection you mean`
45
+ : `${context}: a one-element literal set is the bare literal respelled write the literal (the canonical-utterance law: one meaning, one spelling)`
46
+ )
47
+ }
48
+ const seen = new Set<string>()
49
+ const literals: LiteralSpec[] = entry.map(function lowerSetLiteral(literal: unknown) {
50
+ const lowered = Object.freeze(literalOf(field, literal))
51
+ const rendered = renderLiteral(lowered)
52
+ if (seen.has(rendered)) {
53
+ throw errors.new(
54
+ `${context}: the literal set spells ${rendered} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
55
+ )
56
+ }
57
+ seen.add(rendered)
58
+ return lowered
50
59
  })
51
60
  return Object.freeze({ kind: "many", literals: Object.freeze(literals) })
52
61
  }
@@ -78,7 +87,9 @@ function resolveSelection(
78
87
  if (declared === undefined) {
79
88
  throw errors.new(`relation ${name} has no field ${fieldName}`)
80
89
  }
81
- bindings.push(Object.freeze({ field: fieldName, set: resolveEntry(declared.field, entry) }))
90
+ bindings.push(
91
+ Object.freeze({ field: fieldName, set: resolveEntry(`relation ${name}.${fieldName}`, declared.field, entry) })
92
+ )
82
93
  }
83
94
  if (bindings.length === 0) {
84
95
  throw errors.new(
@@ -91,22 +102,6 @@ function resolveSelection(
91
102
  /** The field block of a relation: field name to field descriptor. */
92
103
  type FieldsShape = Record<string, AnyField>
93
104
 
94
- /**
95
- * A typed field reference (`Account.fields.holder`) — the value statements,
96
- * selections, and queries address a field through. Purely positional
97
- * (relation name + field name); the field's descriptor is read off the
98
- * relation's schema type structurally.
99
- */
100
- interface FieldRef<Rel extends string, Name extends string> {
101
- readonly relation: Rel
102
- readonly field: Name
103
- }
104
-
105
- /** The typed field-reference record of a relation. */
106
- type FieldRefs<RName extends string, Fields extends FieldsShape> = {
107
- readonly [K in keyof Fields & string]: FieldRef<RName, K>
108
- }
109
-
110
105
  /** One declared field: name plus its descriptor, in declaration order. */
111
106
  interface RelationField {
112
107
  readonly name: string
@@ -131,13 +126,15 @@ interface SelectionBinding {
131
126
 
132
127
  /**
133
128
  * The `where()` argument: per field, a bare structural literal of that
134
- * field's value type (a closed handle constant IS such a literal a
135
- * bigint verified against the roster at construction), an `oneOf(a, b,
136
- * ...)` literal set, or a `span(start, end)` interval literal.
137
- * Equality-only by construction: no operator parameter exists anywhere.
129
+ * field's value type (a closed reference's literal IS its handle name
130
+ * `"Savings"`, verified against the roster at construction), a plain
131
+ * ARRAY of such literals read disjunctively `kind: ["Checking",
132
+ * "Savings"]` or a `span(start, end)` interval literal. Membership is
133
+ * an array, never an operator (the drizzle law); equality-only by
134
+ * construction: no operator parameter exists anywhere.
138
135
  */
139
136
  type SelectionInput<Fields extends FieldsShape> = {
140
- readonly [K in keyof Fields]?: Infer<Fields[K]> | OneOf<Infer<Fields[K]>>
137
+ readonly [K in keyof Fields]?: Infer<Fields[K]> | readonly Infer<Fields[K]>[]
141
138
  }
142
139
 
143
140
  /** A relation with a selection applied — what `on()` consumes as a σ-carrying source. */
@@ -150,7 +147,6 @@ interface Selected<Name extends string, Fields extends FieldsShape> {
150
147
  interface Relation<Name extends string, Fields extends FieldsShape> {
151
148
  readonly name: Name
152
149
  readonly data: RelationData
153
- readonly fields: FieldRefs<Name, Fields>
154
150
  where(selection: SelectionInput<Fields>): Selected<Name, Fields>
155
151
  }
156
152
 
@@ -208,14 +204,6 @@ function relation<const Name extends string, Fields extends FieldsShape>(
208
204
  ordered.push(Object.freeze({ name: fieldName, field }))
209
205
  }
210
206
  const data: RelationData = Object.freeze({ name, fields: Object.freeze(ordered) })
211
- const refs: Record<string, unknown> = {}
212
- for (const declared of ordered) {
213
- refs[declared.name] = Object.freeze({ relation: name, field: declared.name })
214
- }
215
- Object.freeze(refs)
216
- if (!refsComplete<Name, Fields>(refs, fields)) {
217
- throw errors.new(`relation ${name}: field-reference construction incomplete`)
218
- }
219
207
  const holder: { value: Relation<Name, Fields> | undefined } = { value: undefined }
220
208
  function where(selection: SelectionInput<Fields>): Selected<Name, Fields> {
221
209
  const owner = holder.value
@@ -227,7 +215,7 @@ function relation<const Name extends string, Fields extends FieldsShape>(
227
215
  selection: resolveSelection(name, ordered, Object.entries(selection))
228
216
  })
229
217
  }
230
- const value: Relation<Name, Fields> = Object.freeze({ name, data, fields: refs, where })
218
+ const value: Relation<Name, Fields> = Object.freeze({ name, data, where })
231
219
  holder.value = value
232
220
  return value
233
221
  }
@@ -236,8 +224,6 @@ export type {
236
224
  AnyRelation,
237
225
  AnySelected,
238
226
  Fact,
239
- FieldRef,
240
- FieldRefs,
241
227
  FieldsShape,
242
228
  FreshKeys,
243
229
  InsertFact,
package/src/schema.ts CHANGED
@@ -11,8 +11,9 @@
11
11
 
12
12
  import * as errors from "@superbuilders/errors"
13
13
  import type { AnyClosed } from "#closed.ts"
14
+ import { isClosedMember, sealedFieldOf } from "#closed.ts"
14
15
  import type { FaceData } from "#face.ts"
15
- import { type AnyField, assertDeclarationOrderKey, type ClosedRoster } from "#fields.ts"
16
+ import { assertDeclarationOrderKey, rosterOf } from "#fields.ts"
16
17
  import { type ClassesOf, classesComplete, computeClasses, type LawfulStatements, type SchemaClasses } from "#law.ts"
17
18
  import type { AnyRelation } from "#relation.ts"
18
19
  import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
@@ -34,7 +35,7 @@ function collectImplied(name: string, relations: SchemaRelations): Set<string> {
34
35
  `schema ${name}: record key ${recordKey} holds relation ${member.name} — the key must equal the relation's declared name`
35
36
  )
36
37
  }
37
- if ("handles" in member.data) {
38
+ if (isClosedMember(member)) {
38
39
  implied.add(`${member.name}(id) -> ${member.name}`)
39
40
  continue
40
41
  }
@@ -75,33 +76,6 @@ function verifyMembership(name: string, relations: SchemaRelations, statement: S
75
76
  }
76
77
  }
77
78
 
78
- /** Finds a face's field descriptor by name, across both relation kinds. */
79
- function faceField(face: FaceData, fieldName: string): AnyField | undefined {
80
- const data = face.owner.data
81
- if ("handles" in data) {
82
- const column = data.columns.find(function byName(candidate) {
83
- return candidate.name === fieldName
84
- })
85
- return column?.field
86
- }
87
- const declared = data.fields.find(function byName(candidate) {
88
- return candidate.name === fieldName
89
- })
90
- return declared?.field
91
- }
92
-
93
- /**
94
- * The roster a field resolves handles through: present exactly on a closed
95
- * reference descriptor (the structural `closed` property — S1's
96
- * `ClosedIdField`), absent on every other field kind.
97
- */
98
- function rosterOf(field: AnyField | undefined): ClosedRoster | undefined {
99
- if (field !== undefined && "closed" in field) {
100
- return field.closed
101
- }
102
- return undefined
103
- }
104
-
105
79
  /** Flattens one binding's literal set into its literals. */
106
80
  function bindingLiterals(set: LiteralSetSpec): readonly LiteralSpec[] {
107
81
  if (set.kind === "one") {
@@ -122,7 +96,7 @@ function verifyBindingHandles(
122
96
  binding: { readonly field: string; readonly set: LiteralSetSpec },
123
97
  rendered: string
124
98
  ): void {
125
- const roster = rosterOf(faceField(face, binding.field))
99
+ const roster = rosterOf(sealedFieldOf(face.owner, binding.field))
126
100
  for (const literal of bindingLiterals(binding.set)) {
127
101
  if (literal.kind !== "handle") {
128
102
  continue
@@ -177,7 +151,7 @@ function closedTargetOf(statements: readonly Statement[], owner: string, field:
177
151
  source.projection[0] === field &&
178
152
  target.projection.length === 1 &&
179
153
  target.projection[0] === "id" &&
180
- "handles" in target.owner.data
154
+ isClosedMember(target.owner)
181
155
  ) {
182
156
  return target.owner.name
183
157
  }
@@ -227,11 +201,11 @@ function verifyClosedReferenceBinding(
227
201
  if (!spellsHandle) {
228
202
  return
229
203
  }
230
- const roster = rosterOf(faceField(face, binding.field))
204
+ const roster = rosterOf(sealedFieldOf(face.owner, binding.field))
231
205
  if (roster === undefined) {
232
206
  return
233
207
  }
234
- if ("handles" in face.owner.data && binding.field === "id") {
208
+ if (isClosedMember(face.owner) && binding.field === "id") {
235
209
  return
236
210
  }
237
211
  const resolved = closedTargetOf(statements, face.owner.name, binding.field)
package/src/spec.ts CHANGED
@@ -62,8 +62,9 @@ type LiteralSpec =
62
62
 
63
63
  /**
64
64
  * One σ binding's right side: a single literal or a literal set (read
65
- * disjunctively). The SDK's selection constructors make the degenerate sets
66
- * unwritable (`oneOf` demands two leading literals), so a lowered `many`
65
+ * disjunctively). The SDK's selection resolver refuses the degenerate sets
66
+ * (a membership array needs two members the empty set selects nothing,
67
+ * the one-element set is the bare literal respelled), so a lowered `many`
67
68
  * always carries ≥ 2 literals.
68
69
  */
69
70
  type LiteralSetSpec =