use rusqlite::Connection;
pub const DEFAULT_DEPTH_LIMIT: u32 = 20;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WalkResult {
pub receipt_ids: Vec<String>,
pub depth_reached: u32,
pub truncated: bool,
pub limit: u32,
}
pub fn forward_receipt_lineage(
conn: &Connection,
parent_receipt_id: &str,
depth_limit: u32,
row_limit: u32,
) -> rusqlite::Result<WalkResult> {
let sql = "
WITH RECURSIVE downstream(receipt_id, parent_receipt_id, depth) AS (
SELECT receipt_id, parent_receipt_id, 0 AS depth
FROM receipt_lineage_statements
WHERE parent_receipt_id = ?1
UNION ALL
SELECT s.receipt_id, s.parent_receipt_id, d.depth + 1
FROM receipt_lineage_statements s
JOIN downstream d ON s.parent_receipt_id = d.receipt_id
WHERE d.depth + 1 < ?2
)
SELECT receipt_id, depth FROM downstream
ORDER BY depth, receipt_id
LIMIT ?3
";
let mut stmt = conn.prepare(sql)?;
let mut rows = stmt.query(rusqlite::params![
parent_receipt_id,
depth_limit as i64,
row_limit as i64,
])?;
let mut ids = Vec::new();
let mut deepest = 0u32;
while let Some(row) = rows.next()? {
let id: String = row.get(0)?;
let depth: i64 = row.get(1)?;
deepest = deepest.max(depth as u32);
ids.push(id);
}
let truncated = ids.len() as u32 >= row_limit || deepest + 1 >= depth_limit;
Ok(WalkResult {
receipt_ids: ids,
depth_reached: deepest,
truncated,
limit: depth_limit,
})
}
pub fn reverse_receipt_lineage(
conn: &Connection,
child_receipt_id: &str,
depth_limit: u32,
row_limit: u32,
) -> rusqlite::Result<WalkResult> {
let sql = "
WITH RECURSIVE upstream(receipt_id, parent_receipt_id, depth) AS (
SELECT receipt_id, parent_receipt_id, 0 AS depth
FROM receipt_lineage_statements
WHERE receipt_id = ?1
UNION ALL
SELECT s.receipt_id, s.parent_receipt_id, u.depth + 1
FROM receipt_lineage_statements s
JOIN upstream u ON s.receipt_id = u.parent_receipt_id
WHERE u.depth + 1 < ?2
)
SELECT parent_receipt_id, depth FROM upstream
WHERE parent_receipt_id IS NOT NULL
ORDER BY depth, parent_receipt_id
LIMIT ?3
";
let mut stmt = conn.prepare(sql)?;
let mut rows = stmt.query(rusqlite::params![
child_receipt_id,
depth_limit as i64,
row_limit as i64,
])?;
let mut ids = Vec::new();
let mut deepest = 0u32;
while let Some(row) = rows.next()? {
let id: String = row.get(0)?;
let depth: i64 = row.get(1)?;
deepest = deepest.max(depth as u32);
ids.push(id);
}
let truncated = ids.len() as u32 >= row_limit || deepest + 1 >= depth_limit;
Ok(WalkResult {
receipt_ids: ids,
depth_reached: deepest,
truncated,
limit: depth_limit,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use rusqlite::Connection;
fn fresh_conn() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE receipt_lineage_statements (
receipt_id TEXT NOT NULL,
parent_receipt_id TEXT,
PRIMARY KEY (receipt_id, parent_receipt_id)
);",
)
.unwrap();
conn
}
fn add_edge(conn: &Connection, child: &str, parent: &str) {
conn.execute(
"INSERT INTO receipt_lineage_statements (receipt_id, parent_receipt_id) VALUES (?1, ?2)",
rusqlite::params![child, parent],
)
.unwrap();
}
#[test]
fn forward_walks_descendants() {
let conn = fresh_conn();
add_edge(&conn, "b", "a");
add_edge(&conn, "c", "b");
let r = forward_receipt_lineage(&conn, "a", 20, 100).unwrap();
assert!(r.receipt_ids.contains(&"b".to_string()));
assert!(r.receipt_ids.contains(&"c".to_string()));
}
#[test]
fn reverse_walks_ancestors() {
let conn = fresh_conn();
add_edge(&conn, "b", "a");
add_edge(&conn, "c", "b");
let r = reverse_receipt_lineage(&conn, "c", 20, 100).unwrap();
assert!(r.receipt_ids.contains(&"b".to_string()));
assert!(r.receipt_ids.contains(&"a".to_string()));
}
#[test]
fn depth_limit_bounds_results() {
let conn = fresh_conn();
for i in 0..10 {
add_edge(&conn, &format!("r{}", i + 1), &format!("r{}", i));
}
let r = forward_receipt_lineage(&conn, "r0", 3, 100).unwrap();
assert!(r.receipt_ids.len() <= 3);
}
}