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
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
2 changes: 1 addition & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|---|
Expand Down
15 changes: 15 additions & 0 deletions crates/lance-graph-xlog/src/cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RelId, CubeRefusal> {
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()
}
Expand Down
36 changes: 36 additions & 0 deletions crates/lance-graph-xlog/tests/cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64, i64> = 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(_)))
));
}
Loading