use std::sync::Arc;
use rushdb_storage::{ReadView, ScanBounds, ScanProjection};
use rushdb_types::ids::TenantId;
use crate::{Direction, QueryError, QuerySnapshot};
pub(crate) trait Adjacency {
fn neighbours(
&self,
node: u64,
forward: bool,
out: &mut Vec<(u64, u64)>,
) -> Result<(), QueryError>;
fn undirected(&self) -> bool;
}
fn row_directions(direction: Direction, forward: bool) -> &'static [bool] {
match (direction, forward) {
(Direction::Outgoing, true) | (Direction::Incoming, false) => &[true],
(Direction::Incoming, true) | (Direction::Outgoing, false) => &[false],
(Direction::Both, _) => &[true, false],
}
}
fn both_directions(
node: u64,
out: &mut Vec<(u64, u64)>,
mut read: impl FnMut(bool, &mut Vec<(u64, u64)>) -> Result<(), QueryError>,
) -> Result<(), QueryError> {
read(true, out)?;
let before = out.len();
read(false, out)?;
let mut at = before;
while at < out.len() {
let (other, rel) = out[at];
if other == node && out[..before].contains(&(node, rel)) {
out.remove(at);
} else {
at += 1;
}
}
Ok(())
}
pub(crate) enum TypeAdjacency {
Resident {
graph: Arc<ResidentGraph>,
direction: Direction,
},
Store(StoreAdjacency),
}
impl TypeAdjacency {
pub(crate) fn open(
snap: &QuerySnapshot,
tenant: TenantId,
rel_type: Option<&str>,
direction: Direction,
cancel: &dyn Fn() -> Result<(), QueryError>,
) -> Result<Self, QueryError> {
let key = graph_key(tenant, rel_type);
let ci = snap.commit_index();
let cache = snap.graphs();
if let Some(graph) = cache
.get(&key, ci)
.and_then(|graph| graph.downcast::<ResidentGraph>().ok())
{
return Ok(Self::Resident { graph, direction });
}
if cache.rent(&key, ci, 0, build_rows(snap, tenant)?) {
let graph = ResidentGraph::keep(snap, tenant, rel_type, cancel)?;
return Ok(Self::Resident { graph, direction });
}
Ok(Self::Store(StoreAdjacency {
snap: Arc::clone(snap),
tenant,
rel_type: rel_type.map(str::to_owned),
direction,
rows: std::cell::Cell::new(0),
}))
}
}
impl Adjacency for TypeAdjacency {
fn neighbours(
&self,
node: u64,
forward: bool,
out: &mut Vec<(u64, u64)>,
) -> Result<(), QueryError> {
match self {
Self::Resident { graph, direction } => {
let read = |outgoing: bool, out: &mut Vec<(u64, u64)>| {
out.extend_from_slice(graph.edges(node, outgoing));
Ok(())
};
if *direction == Direction::Both {
both_directions(node, out, read)
} else {
read(row_directions(*direction, forward)[0], out)
}
}
Self::Store(store) => store.neighbours(node, forward, out),
}
}
fn undirected(&self) -> bool {
match self {
Self::Resident { direction, .. } => *direction == Direction::Both,
Self::Store(store) => store.direction == Direction::Both,
}
}
}
fn build_rows(snap: &ReadView, tenant: TenantId) -> Result<u64, QueryError> {
Ok(rushdb_lmpg::derived::tenant_usage(snap, tenant)?.relationships)
}
fn graph_key(tenant: TenantId, rel_type: Option<&str>) -> Vec<u8> {
format!("{tenant:?}|{rel_type:?}").into_bytes()
}
pub(crate) struct StoreAdjacency {
snap: QuerySnapshot,
tenant: TenantId,
rel_type: Option<String>,
direction: Direction,
rows: std::cell::Cell<u64>,
}
impl StoreAdjacency {
fn scan(&self, outgoing: bool, node: u64, out: &mut Vec<(u64, u64)>) -> Result<(), QueryError> {
let dir: &[u8] = if outgoing { b"out" } else { b"in" };
let prefix = match self.rel_type.as_deref() {
Some(rel_type) => {
rushdb_lmpg::reader::keys_ext::adj_prefix_typed(self.tenant, dir, node, rel_type)
}
None => rushdb_types::keys::key(self.tenant, &[b"adj", dir, &node.to_be_bytes()]),
};
let mut cursor = self
.snap
.scan(ScanBounds::prefix(&prefix), ScanProjection::Keys)?;
let mut rows = 1u64;
loop {
let batch = cursor.next_batch(4_096, 1 << 20)?;
for row in batch.rows {
let (other, rel) = adjacency_tail(&row.key)?;
out.push((other, rel));
rows += 1;
}
if batch.done {
break;
}
}
self.rows.set(self.rows.get().saturating_add(rows));
Ok(())
}
fn neighbours(
&self,
node: u64,
forward: bool,
out: &mut Vec<(u64, u64)>,
) -> Result<(), QueryError> {
if self.direction == Direction::Both {
return both_directions(node, out, |outgoing, out| self.scan(outgoing, node, out));
}
self.scan(row_directions(self.direction, forward)[0], node, out)
}
}
impl Drop for StoreAdjacency {
fn drop(&mut self) {
let rows = self.rows.get();
if rows == 0 {
return;
}
if let Ok(build) = build_rows(&self.snap, self.tenant) {
let _ = self.snap.graphs().rent(
&graph_key(self.tenant, self.rel_type.as_deref()),
self.snap.commit_index(),
rows,
build,
);
}
}
}
fn adjacency_tail(key: &[u8]) -> Result<(u64, u64), QueryError> {
let Some(tail) = key.len().checked_sub(17).map(|at| &key[at..]) else {
return Err(rushdb_storage::StorageError::CorruptLog.into());
};
let other = u64::from_be_bytes(tail[..8].try_into().unwrap_or_default());
let rel = u64::from_be_bytes(tail[9..17].try_into().unwrap_or_default());
Ok((other, rel))
}
pub(crate) struct ResidentGraph {
outgoing: Rows,
incoming: Rows,
}
struct Rows {
nodes: Vec<u64>,
start: Vec<usize>,
edges: Vec<(u64, u64)>,
}
impl Rows {
fn from_sorted(triples: &[(u64, u64, u64)]) -> Self {
let mut rows = Rows {
nodes: Vec::new(),
start: Vec::new(),
edges: Vec::with_capacity(triples.len()),
};
for &(node, other, rel) in triples {
if rows.nodes.last() != Some(&node) {
rows.nodes.push(node);
rows.start.push(rows.edges.len());
}
rows.edges.push((other, rel));
}
rows.start.push(rows.edges.len());
rows
}
fn of(&self, node: u64) -> &[(u64, u64)] {
match self.nodes.binary_search(&node) {
Ok(at) => &self.edges[self.start[at]..self.start[at + 1]],
Err(_) => &[],
}
}
fn bytes(&self) -> usize {
self.nodes.len() * 8 + self.start.len() * 8 + self.edges.len() * 16
}
}
impl ResidentGraph {
pub(crate) fn keep(
snap: &ReadView,
tenant: TenantId,
rel_type: Option<&str>,
cancel: &dyn Fn() -> Result<(), QueryError>,
) -> Result<Arc<Self>, QueryError> {
let key = graph_key(tenant, rel_type);
let ci = snap.commit_index();
if let Some(graph) = snap
.graphs()
.get(&key, ci)
.and_then(|graph| graph.downcast::<ResidentGraph>().ok())
{
return Ok(graph);
}
let graph = Arc::new(Self::build(snap, tenant, rel_type, cancel)?);
let bytes = graph.outgoing.bytes() + graph.incoming.bytes();
snap.graphs().insert(
key,
ci,
Arc::clone(&graph) as rushdb_storage::graph_cache::Graph,
bytes,
);
Ok(graph)
}
fn build(
snap: &ReadView,
tenant: TenantId,
rel_type: Option<&str>,
cancel: &dyn Fn() -> Result<(), QueryError>,
) -> Result<Self, QueryError> {
let prefix = rushdb_types::keys::key(tenant, &[b"adj", b"out", b""]);
let mut cursor = snap.scan(ScanBounds::prefix(&prefix), ScanProjection::Keys)?;
let mut triples: Vec<(u64, u64, u64)> = Vec::new();
loop {
cancel()?;
let batch = cursor.next_batch(4_096, 1 << 20)?;
for row in batch.rows {
let Some(rest) = row.key.strip_prefix(prefix.as_slice()) else {
continue;
};
if rest.len() < 8 + 1 + 1 + 18 {
return Err(rushdb_storage::StorageError::CorruptLog.into());
}
if rel_type.is_some_and(|wanted| wanted.as_bytes() != &rest[9..rest.len() - 18]) {
continue;
}
let source = u64::from_be_bytes(rest[..8].try_into().unwrap_or_default());
let (target, rel) = adjacency_tail(rest)?;
triples.push((source, target, rel));
}
if batch.done {
break;
}
}
triples.sort_unstable();
let outgoing = Rows::from_sorted(&triples);
for triple in &mut triples {
*triple = (triple.1, triple.0, triple.2);
}
triples.sort_unstable();
let incoming = Rows::from_sorted(&triples);
Ok(Self { outgoing, incoming })
}
pub(crate) fn edges(&self, node: u64, outgoing: bool) -> &[(u64, u64)] {
if outgoing {
self.outgoing.of(node)
} else {
self.incoming.of(node)
}
}
pub(crate) fn sources(&self) -> &[u64] {
&self.outgoing.nodes
}
}