From 42348c46fdaa87c23f0d1ca0fb81989f88f72591 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 04:06:24 +0000 Subject: [PATCH 1/2] lance-graph-xlog cube: add_relation for derived relations under the cube's frame A rule's output or a fixpoint delta enters the cube as one more relation: its ids are looked up once in the shared map, existing relations are not touched, an id outside the map or a stale frame is refused. Needed for recursive and derived-input rules in XLOG's plans. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMNZqKXSaeb9g2ypywF --- ...6-11-01-xlog-resident-hypercube-context.md | 3 ++ crates/lance-graph-xlog/src/cube.rs | 15 ++++++++ crates/lance-graph-xlog/tests/cube.rs | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/.claude/board/entries/2026-11-01-xlog-resident-hypercube-context.md b/.claude/board/entries/2026-11-01-xlog-resident-hypercube-context.md index bcfca12dd..3e9cf6fb2 100644 --- a/.claude/board/entries/2026-11-01-xlog-resident-hypercube-context.md +++ b/.claude/board/entries/2026-11-01-xlog-resident-hypercube-context.md @@ -18,6 +18,9 @@ What `lance_graph_xlog::cube` does (VERIFIED-IN-CODE, `src/cube.rs`): place tuples are built). Each re-checks the frame. - Any relation joins any other on either column from the same lanes: lanes are stored per relation, not per join orientation. +- `add_relation` adds a derived relation (a rule's output, a fixpoint delta) + under the same frame, looking up only its own ids (TEST-PINNED, + `an_added_relation_joins_like_a_base_one`). TEST-PINNED (`tests/cube.rs`, 6 tests): diff --git a/crates/lance-graph-xlog/src/cube.rs b/crates/lance-graph-xlog/src/cube.rs index 7689a941c..babd0a3f4 100644 --- a/crates/lance-graph-xlog/src/cube.rs +++ b/crates/lance-graph-xlog/src/cube.rs @@ -156,6 +156,21 @@ impl Cube { }) } + /// Add a relation under the cube's frame: a rule's output or a fixpoint + /// delta. Each id of the new tuples is looked up once in the shared map; + /// relations already in the cube are not touched. Its ids must be in the + /// map, so a derived relation over the base relations' ids always fits. + pub fn add_relation( + &mut self, + table: &MapTable, + tuples: &[(u32, u32)], + ) -> Result { + let map = self.map(table)?; + let r = rel(tuples, map)?; + self.rels.push(r); + Ok((self.rels.len() - 1) as RelId) + } + pub fn stats(&self) -> CubeStats { self.stats.get() } diff --git a/crates/lance-graph-xlog/tests/cube.rs b/crates/lance-graph-xlog/tests/cube.rs index 64507cc11..4a86f6054 100644 --- a/crates/lance-graph-xlog/tests/cube.rs +++ b/crates/lance-graph-xlog/tests/cube.rs @@ -276,3 +276,39 @@ fn an_empty_domain_answers_empty() { assert_eq!(cube.support(&table, j), Ok(vec![])); assert_eq!(cube.materialize_rows(&table, j), Ok(vec![])); } + +/// A relation added after construction joins the base relations like any +/// other, looks up only its own ids, and is refused under a stale frame or +/// with an id outside the map. +#[test] +fn an_added_relation_joins_like_a_base_one() { + let rels: [&[(u32, u32)]; 2] = [&R, &S]; + let mut table = table_over(&rels); + let mut cube = Cube::new(&table, &table.seal(), TILE, &rels).unwrap(); + let map_lookups = || table.map(&table.seal(), TILE).unwrap().lookups(); + let before = map_lookups(); + + // Every pair of R, read as a derived relation over the same ids. + let derived: Vec<(u32, u32)> = R.iter().map(|&(x, y)| (y, x)).collect(); + let d = cube.add_relation(&table, &derived).unwrap(); + assert_eq!(map_lookups(), before + 2 * derived.len() as u64); + + let l = cube.scan(d).unwrap(); + let r = cube.scan(0).unwrap(); + let j = cube.join(l, 0, r, 1).unwrap(); + let map = table.map(&table.seal(), TILE).unwrap(); + let got: BTreeMap = nonzero(map, &cube.bag_count(&table, j).unwrap()) + .into_iter() + .collect(); + assert_eq!(got, counts(&oracle(&derived, 0, None, &R, 1, None))); + + assert!(matches!( + cube.add_relation(&table, &[(1, 1000)]), + Err(CubeRefusal::Domain(_)) + )); + table.advance(1, 1, vec![]); + assert!(matches!( + cube.add_relation(&table, &derived), + Err(CubeRefusal::Map(MapRead::Stale(_))) + )); +} From 3f71f6f6df13c2889f7f3ec637616bd92b2e3cec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 04:06:38 +0000 Subject: [PATCH 2/2] board: regenerate the entries index (stale on main after the #1909 merge) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMNZqKXSaeb9g2ypywF --- .claude/board/entries/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index aee7cfba8..f072e3465 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -540 entries, 2026-08-06 .. 2026-11-01. +541 entries, 2026-08-06 .. 2026-11-01. | date | entry id | finding | file | |---|---|---|---|