Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Plan: `.claude/plans/gremlin-composable-algebra-v1.md`.
| D-CMH-G1-1 | predicate methods + method references | Shipped (#1508) | composed `programs()` == hand conjunction; cycles, contract, cross-class refused |
| D-CMH-G1-2 | typed Gremlin steps → method body | Shipped (#1508) | == direct Quack program; both frontends == row oracle |
| D-CMH-G1-3 | Gremlin-shaped runtime declarations | Shipped (#1508) | loads after the build; effects refused at load |
| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` | In PR | row loop over the text at n ∈ {1,64,65,4097}; 6 disable runs red; R2IL refuses (no `GATHER`) |
| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` | Shipped (#1514; ConflictingUniverse fix follows) | row loop over the text at n ∈ {1,64,65,4097}; 6 disable runs red; R2IL refuses (no `GATHER`) |
| D-CMH-G1-4 | rest of the relation carrier (fan-out moves the anchor; `E()`, `outE/inV`) | Queued | #1311 bulk oracle + DuckDB through the method layer | #1311 bulk oracle + DuckDB through the method layer |
| D-CMH-G1-5 | FirstOfRun | Queued | insufficiency proof first; Quack + R2IL vs independent oracle |

Expand Down
8 changes: 5 additions & 3 deletions .claude/plans/gremlin-composable-algebra-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,15 @@ adapter, minus the relations that fan out.
and binder, and names errors `relation.field`. The key joins `required`, so
an unproven key refuses the method like any field it reads.
- A call names each target concept's population; another population, or
none, is `CallError::ForeignUniverse`.
none, is `CallError::ForeignUniverse`. Two relations that reach one target concept in
different populations are refused at preparation (`ConflictingUniverse`),
since one population per concept is all a call can name.
- **R2IL cannot express it.** The fold band has `VIA` but no `GATHER`: it can
read a target field through a key, but not test that the key names a live
row. A method with a relation condition returns `CallError::FrontendGap {
R2il, "GATHER" }`; methods without one still run on both frontends.

Evidence: `tests/relation.rs` (7 tests). FIXTURE: a synthetic `line -billed_to->
Evidence: `tests/relation.rs` (8 tests). FIXTURE: a synthetic `line -billed_to->
partner` pair (37 partners, 4 dangling key values, every 7th partner dead,
every 13th line dead); every answer is checked against a row loop over the
field text at n ∈ {1, 64, 65, 4097}. Six disable runs, each red on the test
Expand All @@ -280,6 +282,6 @@ emitted per condition.
| D-CMH-G1-1 | predicate methods + method references | Shipped (#1508) |
| D-CMH-G1-2 | typed Gremlin steps → method body | Shipped (#1508) |
| D-CMH-G1-3 | Gremlin-shaped runtime declarations | Shipped (#1508) |
| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` after it (§8) | In PR |
| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` after it (§8) | Shipped (#1514) |
| D-CMH-G1-4 | the rest of the carrier: `in`/M:N (anchor moves to child or edge rows), `E()`, `outE/inV`, `dedup` after a fan-out | Queued |
| D-CMH-G1-5 | FirstOfRun (insufficiency proof first; separate PR #3) | Queued |
12 changes: 10 additions & 2 deletions crates/lance-graph-glove-parity/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ pub enum PrepareError {
/// The relation supplies no foreign lane for this target field
/// (`relation.field`).
NoForeignLane(String),
/// Two relations the method reads reach this target concept in
/// different populations. A call names one population per concept.
ConflictingUniverse(u16),
}

/// Why a call did not run.
Expand Down Expand Up @@ -1136,8 +1139,13 @@ impl Glove<'_> {
for c in filter {
if let Cond::Reaches { rel } | Cond::Via { rel, .. } = *c {
let r = self.relation(rel)?;
if !reaches.iter().any(|(c, _)| *c == r.target.concept) {
reaches.push((r.target.concept, r.target.universe));
let (concept, universe) = (r.target.concept, r.target.universe);
match reaches.iter().find(|(c, _)| *c == concept) {
None => reaches.push((concept, universe)),
Some(&(_, u)) if u != universe => {
return Err(PrepareError::ConflictingUniverse(concept));
}
Some(_) => {}
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions crates/lance-graph-glove-parity/tests/relation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,66 @@ fn out_composes_and_keeps_the_anchor() {
let get = |k: u8| table.get(MethodKey([k; 16])).unwrap().as_ref().unwrap();
assert_eq!(get(1).programs(), get(2).programs());
}

/// FAILS IF: two relations to one target concept from different populations
/// prepare. A call names one population per concept, so the method could not
/// say which one the second relation's planes come from.
#[test]
fn two_populations_of_one_target_concept_are_refused() {
let fx = Fx::new();
let glove = |universe| Glove {
concept: PARTNER,
basis: &fx.partner_basis,
claims: &fx.partner_claims,
binder: &Erp,
table: "partner",
reading: READING,
universe,
relations: &[],
};
let (now, then) = (glove(PARTNER_U), glove(Universe(9)));
let lanes: &[(&str, u16)] = &[("country", 0)];
let rel = |name, target, plane| Relation {
name,
fk: "partner",
target,
plane,
lanes,
};
let prepare = |second| {
let rels = [rel("billed_to", &now, 0), rel("shipped_to", second, 1)];
let line = Glove {
concept: LINE,
basis: &fx.line_basis,
claims: &fx.line_claims,
binder: &Erp,
table: "line",
reading: READING,
universe: LINE_U,
relations: &rels,
};
let specs = [(
"m",
fx.spec(
1,
query(
&[
Cond::Reaches { rel: "billed_to" },
Cond::Reaches { rel: "shipped_to" },
],
&[Fold::Count],
),
),
)];
MethodTable::prepare(&line, &specs)
.get(MethodKey([1; 16]))
.unwrap()
.clone()
};
assert_eq!(
prepare(&then).unwrap_err(),
PrepareError::ConflictingUniverse(PARTNER)
);
// Silence twin: the same population through both relations prepares.
assert!(prepare(&now).is_ok());
}
Loading