@bjornpagen/bumbledb 0.11.0 → 0.12.2

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.
@@ -114,7 +114,7 @@ type HeadShape = Readonly<Record<string, ClassedField>> | undefined
114
114
 
115
115
  /**
116
116
  * One finished rule as a plain value: the runtime data plus the inferred
117
- * row/params carrier (and, for an interior or recursive rule, the head
117
+ * row/params carrier (and, for an interior or rec rule, the head
118
118
  * record of classed slots an `.interior(name)` join pairs against).
119
119
  * `.rule(...)` consumes it.
120
120
  */
@@ -265,7 +265,7 @@ interface InteriorRuleChain<
265
265
  find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
266
266
  }
267
267
 
268
- /** The rule builder a `recursive("reach", { base, rec })` arm receives. */
268
+ /** The rule builder a `.reach("reach", { base, rec })` arm receives. */
269
269
  interface RecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
270
270
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
271
271
  relation: R,
@@ -278,10 +278,10 @@ interface RecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClass
278
278
  }
279
279
 
280
280
  /**
281
- * The chain of a recursive arm. `.interior("reach", …)` is the self-atom
281
+ * The chain of a rec arm. `.interior("reach", …)` is the self-atom
282
282
  * on rec arms (and a prior interior on either list). `find` takes bound
283
283
  * variables only — aggregates and the measure are unrepresentable in a
284
- * recursive head.
284
+ * rec head.
285
285
  */
286
286
  interface RecRuleChain<
287
287
  Rels extends SchemaRelations,
@@ -303,18 +303,31 @@ interface RecRuleChain<
303
303
  }
304
304
 
305
305
  /** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
306
- interface QueryData {
307
- /** Named interiors in declaration order (DAG). */
308
- readonly interiors: readonly InteriorData[]
309
- /** The optional linear rec. */
310
- readonly rec: RecData | null
311
- /** The main rules in written order (multiple rules = set union). */
312
- readonly rules: readonly RuleData[]
313
- /** The head columns (every rule derives the same head; written order = answer column order). */
314
- readonly finds: readonly FindColumn[]
315
- /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
316
- readonly params: readonly ParamEntry[]
317
- }
306
+ type QueryData =
307
+ | {
308
+ readonly kind: "cq"
309
+ /** Named interiors in declaration order (DAG). */
310
+ readonly interiors: readonly InteriorData[]
311
+ /** The main rules in written order (multiple rules = set union). */
312
+ readonly rules: readonly RuleData[]
313
+ /** The head columns (every rule derives the same head; written order = answer column order). */
314
+ readonly finds: readonly FindColumn[]
315
+ /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
316
+ readonly params: readonly ParamEntry[]
317
+ }
318
+ | {
319
+ readonly kind: "reach"
320
+ /** Named interiors in declaration order (DAG). */
321
+ readonly interiors: readonly InteriorData[]
322
+ /** The linear rec (base and rec arms nonempty by type). */
323
+ readonly rec: RecData
324
+ /** The main rules in written order (multiple rules = set union). */
325
+ readonly rules: readonly RuleData[]
326
+ /** The head columns (every rule derives the same head; written order = answer column order). */
327
+ readonly finds: readonly FindColumn[]
328
+ /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
329
+ readonly params: readonly ParamEntry[]
330
+ }
318
331
 
319
332
  /**
320
333
  * An inert query value. `Row` is the inferred answer-row object type;
@@ -335,8 +348,8 @@ interface Query<
335
348
  ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
336
349
  /** Construction error: interiors precede main rules. Uncallable after `.rule()`. */
337
350
  interior(name: string, ...builds: never[]): never
338
- /** Construction error: recursive precedes main rules. Uncallable after `.rule()`. */
339
- recursive(name: string, arms: never): never
351
+ /** Construction error: reach precedes main rules. Uncallable after `.rule()`. */
352
+ reach(name: string, arms: never): never
340
353
  readonly [inferred]?: { readonly row: Row; readonly params: Params }
341
354
  }
342
355
 
@@ -353,37 +366,38 @@ type QueryRow<Q extends AnyQuery> = RowOf<Q>
353
366
  type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
354
367
 
355
368
  /**
356
- * The entry value of `query(S)`: interiors, then optional recursive, then
357
- * the first `.rule` mints the query. `interior` / `recursive` exist only
358
- * while `Rec` is `null`; `.recursive()` moves the type parameter.
369
+ * The entry value of `query(S)`: interiors, then reach (moves to
370
+ * {@link QueryReachStart}), then the first `.rule` mints the query.
371
+ * `interior` / `reach` exist only on this CQ start.
359
372
  */
360
373
  type QueryStart<
361
374
  Rels extends SchemaRelations,
362
375
  Classes extends SchemaClasses = SchemaClasses,
363
- P extends ParamsRecord = Record<never, never>,
364
- Rec extends RecData | null = null
376
+ P extends ParamsRecord = Record<never, never>
365
377
  > = {
366
378
  rule<RV extends AnyRuleValue>(
367
379
  build: (r: QueryRuleScope<Rels, Classes>) => RV
368
380
  ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
369
- } & (Rec extends null
370
- ? {
371
- interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
372
- name: string,
373
- ...builds: Builds
374
- ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null>
375
- recursive<
376
- const Base extends readonly RecBuild<Rels, Classes>[],
377
- const Step extends readonly RecBuild<Rels, Classes>[]
378
- >(
379
- name: string,
380
- arms: { readonly base: Base; readonly rec: Step }
381
- ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData>
382
- }
383
- : {
384
- interior: never
385
- recursive: never
386
- })
381
+ interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
382
+ name: string,
383
+ ...builds: Builds
384
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>>
385
+ reach<const Base extends readonly RecBuild<Rels, Classes>[], const Step extends readonly RecBuild<Rels, Classes>[]>(
386
+ name: string,
387
+ arms: { readonly base: Base; readonly rec: Step }
388
+ ): QueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>>
389
+ }
390
+
391
+ /** After `.reach()`: interior/reach are unrepresentable; only `.rule` remains. */
392
+ type QueryReachStart<
393
+ Rels extends SchemaRelations,
394
+ Classes extends SchemaClasses = SchemaClasses,
395
+ P extends ParamsRecord = Record<never, never>
396
+ > = {
397
+ rule<RV extends AnyRuleValue>(
398
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
399
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
400
+ }
387
401
 
388
402
  /** The frozen constructor vocabulary every rule builder spreads. */
389
403
  const termOps: TermOps = Object.freeze({
@@ -1080,10 +1094,9 @@ interface RawScope extends TermOps {
1080
1094
  }
1081
1095
 
1082
1096
  /** The declared derived tables a chain may name. */
1083
- interface DerivedEnv {
1084
- readonly interiors: readonly InteriorData[]
1085
- readonly rec: RecHandle | RecHead | RecData | null
1086
- }
1097
+ type DerivedEnv =
1098
+ | { readonly interiors: readonly InteriorData[] }
1099
+ | { readonly interiors: readonly InteriorData[]; readonly rec: RecHandle | RecHead | RecData }
1087
1100
 
1088
1101
  /** Which rule family a chain builds — plus the schema's runtime class map and theory value (the join judge's authority). */
1089
1102
  type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & DerivedEnv &
@@ -1102,12 +1115,17 @@ function contextLabel(context: ChainContext): string {
1102
1115
  case "interior":
1103
1116
  return `interior ${context.self} rule`
1104
1117
  case "rec-base":
1105
- return `recursive ${context.self.name} base`
1118
+ return `rec ${context.self.name} base`
1106
1119
  case "rec-arm":
1107
- return `recursive ${context.self.name} rec`
1120
+ return `rec ${context.self.name} rec`
1108
1121
  }
1109
1122
  }
1110
1123
 
1124
+ /** RecHandle is name-only staging; RecHead/RecData carry the sealed finds. */
1125
+ function isRecHead(rec: RecHandle | RecHead | RecData): rec is RecHead {
1126
+ return Array.isArray((rec as RecHead).finds)
1127
+ }
1128
+
1111
1129
  /** Resolves a derived-table name against the context's visible tables. */
1112
1130
  function lookupDerived(context: ChainContext, name: string): DerivedTable {
1113
1131
  const interior = context.interiors.find(function byName(candidate) {
@@ -1121,19 +1139,18 @@ function lookupDerived(context: ChainContext, name: string): DerivedTable {
1121
1139
  }
1122
1140
  return interior
1123
1141
  }
1124
- if (context.rec !== null && context.rec.name === name) {
1142
+ const rec = "rec" in context ? context.rec : undefined
1143
+ if (rec !== undefined && rec.name === name) {
1125
1144
  if (context.kind === "interior") {
1126
1145
  throw errors.new(`interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`)
1127
1146
  }
1128
1147
  if (context.kind === "rec-base") {
1129
- throw errors.new(
1130
- `recursive ${context.rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`
1131
- )
1148
+ throw errors.new(`rec ${rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`)
1132
1149
  }
1133
- if (!("finds" in context.rec)) {
1134
- throw errors.new(`recursive ${context.rec.name}: rec arms resolve the rec head after base arms seal it`)
1150
+ if (!isRecHead(rec)) {
1151
+ throw errors.new(`rec ${rec.name}: rec arms resolve the rec head after base arms seal it`)
1135
1152
  }
1136
- return context.rec
1153
+ return rec
1137
1154
  }
1138
1155
  throw errors.new(`${contextLabel(context)}: no derived table named ${name} is in scope`)
1139
1156
  }
@@ -1161,7 +1178,7 @@ function notInteriorAdvance(
1161
1178
  ): RuleBuildState {
1162
1179
  if (context.kind === "rec-base" || context.kind === "rec-arm") {
1163
1180
  throw errors.new(
1164
- `recursive ${context.self.name}: a recursive rule negates no table — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished table's fold belongs in the main rules`
1181
+ `rec ${context.self.name}: a rec rule negates no table — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished table's fold belongs in the main rules`
1165
1182
  )
1166
1183
  }
1167
1184
  return advanceInterior(state, lookupDerived(context, name), bindings, "negatedInterior")
@@ -1176,9 +1193,9 @@ function findColumns(context: ChainContext, entries: Readonly<Record<string, unk
1176
1193
  continue
1177
1194
  }
1178
1195
  if (derivedHead && !(isTerm(entry) && entry[term] === "var")) {
1179
- const who = context.kind === "interior" ? `interior ${context.self}` : `recursive ${context.self.name}`
1196
+ const who = context.kind === "interior" ? `interior ${context.self}` : `rec ${context.self.name}`
1180
1197
  throw errors.new(
1181
- `${who}: a recursive head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`
1198
+ `${who}: a rec head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`
1182
1199
  )
1183
1200
  }
1184
1201
  columns.push(findColumnOf(name, entry))
@@ -1295,7 +1312,7 @@ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaCl
1295
1312
  ...env
1296
1313
  })
1297
1314
  if (!isTypedScope<RecRuleScope<Rels, Classes>>(raw)) {
1298
- throw errors.new("recursive rule builder construction incomplete")
1315
+ throw errors.new("rec rule builder construction incomplete")
1299
1316
  }
1300
1317
  return raw
1301
1318
  }
@@ -1342,7 +1359,7 @@ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1342
1359
  */
1343
1360
  function paramRegistryOf(
1344
1361
  interiors: readonly InteriorData[],
1345
- rec: RecData | null,
1362
+ rec: RecData | undefined,
1346
1363
  rules: readonly RuleData[]
1347
1364
  ): readonly ParamEntry[] {
1348
1365
  const order: string[] = []
@@ -1398,7 +1415,7 @@ function paramRegistryOf(
1398
1415
  fold(rule.paramUses)
1399
1416
  }
1400
1417
  }
1401
- if (rec !== null) {
1418
+ if (rec !== undefined) {
1402
1419
  for (const rule of rec.base) {
1403
1420
  fold(rule.paramUses)
1404
1421
  }
@@ -1449,7 +1466,7 @@ interface RawQuery {
1449
1466
  readonly data: QueryData
1450
1467
  rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1451
1468
  interior(name: string, ...builds: never[]): never
1452
- recursive(name: string, arms: never): never
1469
+ reach(name: string, arms: never): never
1453
1470
  }
1454
1471
 
1455
1472
  /** Asserts every rule in a list derives the same head (name, aggregate shape, closed slice, class). */
@@ -1495,12 +1512,13 @@ function afterMainError(what: string): Error {
1495
1512
  * Assembles the runtime query value over completed rules: every rule must
1496
1513
  * derive the SAME head (name and aggregate shape, position for position —
1497
1514
  * the decode labels and the engine's alignment rule agree), and the param
1498
- * registry folds in query-walk order.
1515
+ * registry folds in query-walk order. CQ lowering does not mention rec;
1516
+ * Reach carries RecData by value.
1499
1517
  */
1500
1518
  function makeRawQuery(
1501
1519
  theory: AnySchema,
1502
1520
  interiors: readonly InteriorData[],
1503
- rec: RecData | null,
1521
+ rec: RecData | undefined,
1504
1522
  rules: readonly RuleData[]
1505
1523
  ): RawQuery {
1506
1524
  assertAlignedHeads("a query", rules)
@@ -1508,14 +1526,27 @@ function makeRawQuery(
1508
1526
  if (first === undefined) {
1509
1527
  throw errors.new("a query needs at least one rule")
1510
1528
  }
1511
- const env: DerivedEnv = { interiors, rec }
1512
- const data: QueryData = Object.freeze({
1513
- interiors: Object.freeze([...interiors]),
1514
- rec,
1515
- rules: Object.freeze([...rules]),
1516
- finds: first.finds,
1517
- params: paramRegistryOf(interiors, rec, rules)
1518
- })
1529
+ const env: DerivedEnv = rec === undefined ? { interiors } : { interiors, rec }
1530
+ const frozenInteriors = Object.freeze([...interiors])
1531
+ const frozenRules = Object.freeze([...rules])
1532
+ const params = paramRegistryOf(interiors, rec, rules)
1533
+ const data: QueryData =
1534
+ rec === undefined
1535
+ ? Object.freeze({
1536
+ kind: "cq" as const,
1537
+ interiors: frozenInteriors,
1538
+ rules: frozenRules,
1539
+ finds: first.finds,
1540
+ params
1541
+ })
1542
+ : Object.freeze({
1543
+ kind: "reach" as const,
1544
+ interiors: frozenInteriors,
1545
+ rec,
1546
+ rules: frozenRules,
1547
+ finds: first.finds,
1548
+ params
1549
+ })
1519
1550
  const value: RawQuery = {
1520
1551
  schema: theory,
1521
1552
  data,
@@ -1526,8 +1557,8 @@ function makeRawQuery(
1526
1557
  interior() {
1527
1558
  throw afterMainError("interior")
1528
1559
  },
1529
- recursive() {
1530
- throw afterMainError("recursive")
1560
+ reach() {
1561
+ throw afterMainError("reach")
1531
1562
  }
1532
1563
  }
1533
1564
  Object.freeze(value)
@@ -1537,7 +1568,7 @@ function makeRawQuery(
1537
1568
  function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1538
1569
  theory: Schema<Rels, Classes>,
1539
1570
  interiors: readonly InteriorData[],
1540
- rec: RecData | null,
1571
+ rec: RecData | undefined,
1541
1572
  rules: readonly RuleData[]
1542
1573
  ): Query<Rels, Row, P, Classes> {
1543
1574
  return makeRawQuery(theory, interiors, rec, rules) as unknown as Query<Rels, Row, P, Classes>
@@ -1573,24 +1604,24 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1573
1604
  recBuilds: readonly RecBuild<Rels, Classes>[]
1574
1605
  ): RecData {
1575
1606
  if (baseBuilds.length === 0) {
1576
- throw errors.new(`query: recursive ${name} has no base arms`)
1607
+ throw errors.new(`query: rec ${name} has no base arms`)
1577
1608
  }
1578
1609
  if (recBuilds.length === 0) {
1579
- throw errors.new(`query: recursive ${name} has no rec arms`)
1610
+ throw errors.new(`query: rec ${name} has no rec arms`)
1580
1611
  }
1581
1612
  const handle: RecHandle = Object.freeze({ name })
1582
1613
  const baseEnv: DerivedEnv = { interiors, rec: handle }
1583
1614
  const base = baseBuilds.map(function buildBase(buildOne) {
1584
1615
  return buildOne(makeRecRuleScope<Rels, Classes>(theory, baseEnv, handle, "rec-base")).rule
1585
1616
  })
1586
- assertAlignedHeads(`recursive ${name}`, base)
1617
+ assertAlignedHeads(`rec ${name}`, base)
1587
1618
  const first = base[0]
1588
1619
  if (first === undefined) {
1589
- throw errors.new(`query: recursive ${name} has no base arms`)
1620
+ throw errors.new(`query: rec ${name} has no base arms`)
1590
1621
  }
1591
1622
  const firstFind = first.finds[0]
1592
1623
  if (firstFind === undefined) {
1593
- throw errors.new(`query: recursive ${name} has no head`)
1624
+ throw errors.new(`query: rec ${name} has no head`)
1594
1625
  }
1595
1626
  const finds: RecHead["finds"] = [firstFind, ...first.finds.slice(1)]
1596
1627
  const head: RecHead = Object.freeze({ name, finds })
@@ -1598,10 +1629,10 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1598
1629
  const rec = recBuilds.map(function buildRec(buildOne) {
1599
1630
  return buildOne(makeRecRuleScope<Rels, Classes>(theory, recEnv, head, "rec-arm")).rule
1600
1631
  })
1601
- assertAlignedHeads(`recursive ${name}`, [...base, ...rec])
1632
+ assertAlignedHeads(`rec ${name}`, [...base, ...rec])
1602
1633
  const firstRec = rec[0]
1603
1634
  if (firstRec === undefined) {
1604
- throw errors.new(`query: recursive ${name} has no rec arms`)
1635
+ throw errors.new(`query: rec ${name} has no rec arms`)
1605
1636
  }
1606
1637
  const sealedBase: RecData["base"] = [first, ...base.slice(1)]
1607
1638
  const sealedRec: RecData["rec"] = [firstRec, ...rec.slice(1)]
@@ -1614,24 +1645,17 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1614
1645
  return recData
1615
1646
  }
1616
1647
 
1617
- /** Builds the query start (interiors, then optional rec, then the first main rule). */
1618
- function makeQueryStart<
1619
- Rels extends SchemaRelations,
1620
- Classes extends SchemaClasses,
1621
- P extends ParamsRecord,
1622
- Rec extends RecData | null
1623
- >(theory: Schema<Rels, Classes>, interiors: readonly InteriorData[], rec: Rec): QueryStart<Rels, Classes, P, Rec> {
1624
- const env: DerivedEnv = { interiors, rec }
1648
+ /** Builds the CQ query start (interiors, then reach or the first main rule). */
1649
+ function makeQueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses, P extends ParamsRecord>(
1650
+ theory: Schema<Rels, Classes>,
1651
+ interiors: readonly InteriorData[]
1652
+ ): QueryStart<Rels, Classes, P> {
1653
+ const env: DerivedEnv = { interiors }
1625
1654
  const start = {
1626
1655
  interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
1627
1656
  name: string,
1628
1657
  ...builds: Builds
1629
- ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null> {
1630
- if (rec !== null) {
1631
- throw errors.new(
1632
- "query: interior after recursive is unwritable — declaration order is interiors, then rec, then main"
1633
- )
1634
- }
1658
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>> {
1635
1659
  if (
1636
1660
  interiors.some(function sameName(interior) {
1637
1661
  return interior.name === name
@@ -1643,35 +1667,48 @@ function makeQueryStart<
1643
1667
  throw errors.new("query: an interior needs a name")
1644
1668
  }
1645
1669
  const data = collectInterior(theory, env, name, builds)
1646
- return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null>(theory, [...interiors, data], null)
1670
+ return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>>(theory, [...interiors, data])
1647
1671
  },
1648
- recursive<
1649
- const Base extends readonly RecBuild<Rels, Classes>[],
1650
- const Step extends readonly RecBuild<Rels, Classes>[]
1651
- >(
1672
+ reach<const Base extends readonly RecBuild<Rels, Classes>[], const Step extends readonly RecBuild<Rels, Classes>[]>(
1652
1673
  name: string,
1653
1674
  arms: { readonly base: Base; readonly rec: Step }
1654
- ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData> {
1655
- if (rec !== null) {
1656
- throw errors.new("query: a second recursive is unwritable — this cut admits one linear rec")
1657
- }
1675
+ ): QueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>> {
1658
1676
  if (
1659
1677
  interiors.some(function sameName(interior) {
1660
1678
  return interior.name === name
1661
1679
  })
1662
1680
  ) {
1663
- throw errors.new(`query: interior and recursive share the name ${name}`)
1681
+ throw errors.new(`query: interior and rec share the name ${name}`)
1664
1682
  }
1665
1683
  if (name.length === 0) {
1666
- throw errors.new("query: recursive needs a name")
1684
+ throw errors.new("query: reach needs a name")
1667
1685
  }
1668
1686
  const data = collectRec(theory, interiors, name, arms.base, arms.rec)
1669
- return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData>(
1687
+ return makeQueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>>(
1670
1688
  theory,
1671
1689
  interiors,
1672
1690
  data
1673
1691
  )
1674
1692
  },
1693
+ rule<RV extends AnyRuleValue>(
1694
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
1695
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1696
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1697
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, undefined, [built.rule])
1698
+ }
1699
+ }
1700
+ Object.freeze(start)
1701
+ return start as unknown as QueryStart<Rels, Classes, P>
1702
+ }
1703
+
1704
+ /** Builds the Reach query start (rec sealed; only the first main rule remains). */
1705
+ function makeQueryReachStart<Rels extends SchemaRelations, Classes extends SchemaClasses, P extends ParamsRecord>(
1706
+ theory: Schema<Rels, Classes>,
1707
+ interiors: readonly InteriorData[],
1708
+ rec: RecData
1709
+ ): QueryReachStart<Rels, Classes, P> {
1710
+ const env: DerivedEnv = { interiors, rec }
1711
+ const start = {
1675
1712
  rule<RV extends AnyRuleValue>(
1676
1713
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1677
1714
  ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
@@ -1680,12 +1717,12 @@ function makeQueryStart<
1680
1717
  }
1681
1718
  }
1682
1719
  Object.freeze(start)
1683
- return start as unknown as QueryStart<Rels, Classes, P, Rec>
1720
+ return start as unknown as QueryReachStart<Rels, Classes, P>
1684
1721
  }
1685
1722
 
1686
1723
  /**
1687
1724
  * Opens a query over a schema: `query(S).rule(r => ...)`, optionally with
1688
- * `interior` / `recursive` first. Each `.rule` adds one conjunctive rule;
1725
+ * `interior` / `reach` first. Each `.rule` adds one conjunctive rule;
1689
1726
  * multiple rules are the set union. The schema's law-computed class map and
1690
1727
  * theory value ride into every rule builder — the join walls compare
1691
1728
  * against the mint slots off it.
@@ -1693,7 +1730,7 @@ function makeQueryStart<
1693
1730
  function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1694
1731
  theory: Schema<Rels, Classes>
1695
1732
  ): QueryStart<Rels, Classes> {
1696
- return makeQueryStart<Rels, Classes, Record<never, never>, null>(theory, [], null)
1733
+ return makeQueryStart<Rels, Classes, Record<never, never>>(theory, [])
1697
1734
  }
1698
1735
 
1699
1736
  /**
@@ -2088,9 +2125,9 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
2088
2125
  }
2089
2126
 
2090
2127
  /**
2091
- * Lowers a query value to the bridge's `QueryIr` — pure and stable: interiors
2092
- * in declaration order, optional rec, then main. Every registered param must
2093
- * carry a field anchor by now.
2128
+ * Lowers a query value to the bridge's `QueryIr` — pure and stable:
2129
+ * interiors in declaration order, then CQ or Reach, then main. Every
2130
+ * registered param must carry a field anchor by now.
2094
2131
  */
2095
2132
  function lowerQuery(q: AnyQuery): ParsedQuery {
2096
2133
  const theory = q.schema
@@ -2102,7 +2139,7 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
2102
2139
  q.data.interiors.forEach(function assignInteriorId(interior, index) {
2103
2140
  interiorIds.set(interior.name, index)
2104
2141
  })
2105
- if (q.data.rec !== null) {
2142
+ if (q.data.kind === "reach") {
2106
2143
  interiorIds.set(q.data.rec.name, q.data.interiors.length)
2107
2144
  }
2108
2145
  const paramIds = new Map<string, number>()
@@ -2117,31 +2154,35 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
2117
2154
  params.set(entry.name, entry)
2118
2155
  })
2119
2156
  const ctx: LowerContext = { theory, relationIds, interiorIds, paramIds, params }
2157
+ const interiors = q.data.interiors.map(function lowerInterior(interior) {
2158
+ return {
2159
+ head: interior.finds.map(headTermOf),
2160
+ rules: interior.rules.map(function lowerInteriorRule(rule) {
2161
+ return lowerRule(ctx, rule)
2162
+ })
2163
+ }
2164
+ })
2165
+ const head = q.data.finds.map(headTermOf)
2166
+ const rules = q.data.rules.map(function lowerMainRule(rule) {
2167
+ return lowerRule(ctx, rule)
2168
+ })
2169
+ if (q.data.kind === "cq") {
2170
+ return parseQueryIr({ kind: "cq", interiors, head, rules })
2171
+ }
2120
2172
  return parseQueryIr({
2121
- interiors: q.data.interiors.map(function lowerInterior(interior) {
2122
- return {
2123
- head: interior.finds.map(headTermOf),
2124
- rules: interior.rules.map(function lowerInteriorRule(rule) {
2125
- return lowerRule(ctx, rule)
2126
- })
2127
- }
2128
- }),
2129
- rec:
2130
- q.data.rec === null
2131
- ? null
2132
- : {
2133
- head: q.data.rec.finds.map(headTermOf),
2134
- base: q.data.rec.base.map(function lowerBase(rule) {
2135
- return lowerRule(ctx, rule)
2136
- }),
2137
- rec: q.data.rec.rec.map(function lowerRecArm(rule) {
2138
- return lowerRule(ctx, rule)
2139
- })
2140
- },
2141
- head: q.data.finds.map(headTermOf),
2142
- rules: q.data.rules.map(function lowerMainRule(rule) {
2143
- return lowerRule(ctx, rule)
2144
- })
2173
+ kind: "reach",
2174
+ interiors,
2175
+ rec: {
2176
+ head: q.data.rec.finds.map(headTermOf),
2177
+ base: q.data.rec.base.map(function lowerBase(rule) {
2178
+ return lowerRule(ctx, rule)
2179
+ }),
2180
+ rec: q.data.rec.rec.map(function lowerRecArm(rule) {
2181
+ return lowerRule(ctx, rule)
2182
+ })
2183
+ },
2184
+ head,
2185
+ rules
2145
2186
  })
2146
2187
  }
2147
2188
 
@@ -2156,6 +2197,7 @@ export type {
2156
2197
  Query,
2157
2198
  QueryData,
2158
2199
  QueryParams,
2200
+ QueryReachStart,
2159
2201
  QueryRelation,
2160
2202
  QueryRow,
2161
2203
  QueryRuleChain,
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Host shape parse for the wire `QueryIr`: rec/main nonempty, aggregate
3
- * finds split (Count has no `over`; folds require it), head/find
4
- * alignment. The engine validator remains the one roster authority —
5
- * this parse refuses only shape the host type can see.
2
+ * Host shape parse for the wire `QueryIr`: CQ/Reach eliminator, rec/main
3
+ * nonempty, aggregate finds split (Count has no `over`; folds require
4
+ * it), head/find alignment. The engine validator remains the one roster
5
+ * authority — this parse refuses only shape the host type can see.
6
6
  */
7
7
 
8
8
  import * as errors from "@superbuilders/errors"
@@ -17,7 +17,7 @@ function parseQueryIr(ir: QueryIr): ParsedQuery {
17
17
  ir.interiors.forEach(function checkInterior(interior, index) {
18
18
  align(`interior ${index}`, interior.head, interior.rules)
19
19
  })
20
- if (ir.rec !== null) {
20
+ if (ir.kind === "reach") {
21
21
  if (ir.rec.base.length === 0) {
22
22
  throw errors.new("parseQueryIr: rec base is empty")
23
23
  }
package/src/relation.ts CHANGED
@@ -20,6 +20,16 @@ import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
20
20
  /** Flattens an intersection into one displayed object type (hover legibility). */
21
21
  type Flatten<T> = { [K in keyof T]: T[K] }
22
22
 
23
+ /**
24
+ * An optional property that may be omitted OR explicitly `undefined`.
25
+ * `Partial<T>` under `exactOptionalPropertyTypes` is omit-only (`key?: T`),
26
+ * which rejects `key: T | undefined` — insert must accept both (omit-to-mint,
27
+ * and a host binding that is already `bigint | undefined`).
28
+ */
29
+ type ExactOptional<T> = {
30
+ [K in keyof T]?: T[K] | undefined
31
+ }
32
+
23
33
  /**
24
34
  * Resolves one selection entry to its lowered literal set: a plain ARRAY
25
35
  * (detected by `Array.isArray` — no field's value type is an array;
@@ -180,9 +190,12 @@ type FreshKeys<R extends AnyRelation> = {
180
190
  /**
181
191
  * The inferred row object type of a relation as INSERTED: fresh fields
182
192
  * optional — omitted, the engine mints; supplied, identity is preserved
183
- * (the ETL resupply idiom).
193
+ * (the ETL resupply idiom). Explicit `undefined` is the same as omit
194
+ * (`exactOptionalPropertyTypes`: `id?: bigint | undefined`, not omit-only).
184
195
  */
185
- type InsertFact<R extends AnyRelation> = Flatten<Omit<Fact<R>, FreshKeys<R>> & Partial<Pick<Fact<R>, FreshKeys<R>>>>
196
+ type InsertFact<R extends AnyRelation> = Flatten<
197
+ Omit<Fact<R>, FreshKeys<R>> & ExactOptional<Pick<Fact<R>, FreshKeys<R>>>
198
+ >
186
199
 
187
200
  /**
188
201
  * Declares one relation: `relation("Account", { id: u64.fresh,