Locy Use Case: Fraud Risk Propagation (Rust)¶
Propagate account risk backward over transfer edges and isolate clean accounts.
This notebook uses schema-first mode and mirrors the Python flow using the Rust API (uni_db).
How To Read This Notebook¶
- Define schema first, then load data.
- Keep Locy rules declarative and focused.
- Read output rows together with materialization stats.
1) Setup¶
Initialize an in-memory database and import DataType for schema definitions.
In [ ]:
Copied!
use uni_db::{DataType, Uni, Result};
let db = Uni::in_memory().build().await?;
use uni_db::{DataType, Uni, Result};
let db = Uni::in_memory().build().await?;
2) Define Schema (Recommended)¶
Define labels, typed properties, and edge types before inserting graph facts.
In [ ]:
Copied!
db.schema()
.label("Account")
.property("id", DataType::String)
.property("flagged", DataType::Boolean)
.edge_type("TRANSFER", &["Account"], &["Account"])
.apply()
.await?;
println!("Schema created");
db.schema()
.label("Account")
.property("id", DataType::String)
.property("flagged", DataType::Boolean)
.edge_type("TRANSFER", &["Account"], &["Account"])
.apply()
.await?;
println!("Schema created");
3) Seed Graph Data¶
Insert the minimal graph needed for the scenario.
In [ ]:
Copied!
db.execute("CREATE (:Account {id: 'A1', flagged: true})").await?;
db.execute("CREATE (:Account {id: 'A2', flagged: false})").await?;
db.execute("CREATE (:Account {id: 'A3', flagged: false})").await?;
db.execute("CREATE (:Account {id: 'A4', flagged: false})").await?;
db.execute("MATCH (a1:Account {id:'A1'}), (a2:Account {id:'A2'}) CREATE (a1)-[:TRANSFER]->(a2)").await?;
db.execute("MATCH (a2:Account {id:'A2'}), (a3:Account {id:'A3'}) CREATE (a2)-[:TRANSFER]->(a3)").await?;
db.execute("MATCH (a4:Account {id:'A4'}), (a3:Account {id:'A3'}) CREATE (a4)-[:TRANSFER]->(a3)").await?;
println!("Seeded graph data");
db.execute("CREATE (:Account {id: 'A1', flagged: true})").await?;
db.execute("CREATE (:Account {id: 'A2', flagged: false})").await?;
db.execute("CREATE (:Account {id: 'A3', flagged: false})").await?;
db.execute("CREATE (:Account {id: 'A4', flagged: false})").await?;
db.execute("MATCH (a1:Account {id:'A1'}), (a2:Account {id:'A2'}) CREATE (a1)-[:TRANSFER]->(a2)").await?;
db.execute("MATCH (a2:Account {id:'A2'}), (a3:Account {id:'A3'}) CREATE (a2)-[:TRANSFER]->(a3)").await?;
db.execute("MATCH (a4:Account {id:'A4'}), (a3:Account {id:'A3'}) CREATE (a4)-[:TRANSFER]->(a3)").await?;
println!("Seeded graph data");
4) Locy Program¶
Rules derive relations, then QUERY ... WHERE ... RETURN ... projects the final answer.
In [ ]:
Copied!
let program = r#"CREATE RULE risky_seed AS\nMATCH (a:Account)\nWHERE a.flagged = true\nYIELD KEY a\n\nCREATE RULE risky AS\nMATCH (a:Account)\nWHERE a IS risky_seed\nYIELD KEY a\n\nCREATE RULE risky AS\nMATCH (a:Account)-[:TRANSFER]->(b:Account)\nWHERE b IS risky\nYIELD KEY a\n\nCREATE RULE clean AS\nMATCH (a:Account)\nWHERE a IS NOT risky\nYIELD KEY a\n\nQUERY risky WHERE a.id = a.id RETURN a.id AS risky_account\nQUERY clean WHERE a.id = a.id RETURN a.id AS clean_account"#;
let program = r#"CREATE RULE risky_seed AS\nMATCH (a:Account)\nWHERE a.flagged = true\nYIELD KEY a\n\nCREATE RULE risky AS\nMATCH (a:Account)\nWHERE a IS risky_seed\nYIELD KEY a\n\nCREATE RULE risky AS\nMATCH (a:Account)-[:TRANSFER]->(b:Account)\nWHERE b IS risky\nYIELD KEY a\n\nCREATE RULE clean AS\nMATCH (a:Account)\nWHERE a IS NOT risky\nYIELD KEY a\n\nQUERY risky WHERE a.id = a.id RETURN a.id AS risky_account\nQUERY clean WHERE a.id = a.id RETURN a.id AS clean_account"#;
5) Evaluate¶
Evaluate the Locy program and inspect stats/rows.
In [ ]:
Copied!
let result = db.locy().evaluate(program).await?;
println!("Derived relations: {:?}", result.derived.keys().collect::<Vec<_>>());
println!("Iterations: {}", result.stats().total_iterations);
println!("Queries executed: {}", result.stats().queries_executed);
for (name, rows) in &result.derived {
println!("{}: {} row(s)", name, rows.len());
}
if let Some(rows) = result.rows() {
println!("Rows: {:?}", rows);
}
let result = db.locy().evaluate(program).await?;
println!("Derived relations: {:?}", result.derived.keys().collect::>());
println!("Iterations: {}", result.stats().total_iterations);
println!("Queries executed: {}", result.stats().queries_executed);
for (name, rows) in &result.derived {
println!("{}: {} row(s)", name, rows.len());
}
if let Some(rows) = result.rows() {
println!("Rows: {:?}", rows);
}
6) What To Expect¶
Use these checks to validate output after evaluation:
A1is risky by seed;A2andA4become risky by backward propagation throughTRANSFER.A3should remain incleanbecause it does not transfer to a risky account.- Two query result blocks should appear: one for
risky, one forclean.
Notes¶
- Rust notebooks are included for API parity and learning.
- In this docs build, Rust notebooks are rendered without execution.