Skip to content

Supply Chain Management with Uni (Rust)

BOM explosion, cost rollup, and supplier risk analysis using Uni's native Rust API.

:dep uni-db = { path = "../../../crates/uni" }
:dep tokio = { version = "1", features = ["full"] }
:dep serde_json = "1"
use uni_db::{Uni, DataType, IndexType, ScalarType, VectorMetric, VectorAlgo, VectorIndexCfg};
use std::collections::HashMap;
use serde_json::json;

// Helper macro to run async code in evcxr
macro_rules! run {
    ($e:expr) => {
        tokio::runtime::Runtime::new().unwrap().block_on($e)
    };
}
let db_path = "./supply_chain_db";

// Clean up any existing database
if std::path::Path::new(db_path).exists() {
    std::fs::remove_dir_all(db_path).unwrap();
}

let db = run!(Uni::open(db_path).build()).unwrap();
println!("Opened database at {}", db_path);

1. Define Schema

Parts (with name), Suppliers, and Products, along with ASSEMBLED_FROM and SUPPLIED_BY relationships.

run!(async {
    db.schema()
        .label("Part")
            .property("name", DataType::String)
            .property("sku",  DataType::String)
            .property("cost", DataType::Float64)
            .index("sku", IndexType::Scalar(ScalarType::Hash))
        .label("Supplier")
            .property("name", DataType::String)
        .label("Product")
            .property("name",  DataType::String)
            .property("price", DataType::Float64)
        .edge_type("ASSEMBLED_FROM", &["Product", "Part"], &["Part"])
        .edge_type("SUPPLIED_BY",    &["Part"],             &["Supplier"])
        .apply()
        .await
}).unwrap();

println!("Schema created successfully");

2. Ingest Data

7 parts, 3 suppliers, 2 products — same topology as the Python notebook.

// 7 parts
let part_props = vec![
    HashMap::from([("name".to_string(), json!("Resistor 10K")),    ("sku".to_string(), json!("RES-10K")),   ("cost".to_string(), json!(0.05))]),
    HashMap::from([("name".to_string(), json!("Capacitor 100uF")), ("sku".to_string(), json!("CAP-100UF")), ("cost".to_string(), json!(0.08))]),
    HashMap::from([("name".to_string(), json!("Motherboard X1")),  ("sku".to_string(), json!("MB-X1")),     ("cost".to_string(), json!(50.0))]),
    HashMap::from([("name".to_string(), json!("OLED Screen")),     ("sku".to_string(), json!("SCR-OLED")),  ("cost".to_string(), json!(30.0))]),
    HashMap::from([("name".to_string(), json!("Battery 4000mAh")), ("sku".to_string(), json!("BAT-4000")),  ("cost".to_string(), json!(15.0))]),
    HashMap::from([("name".to_string(), json!("ARM Processor")),   ("sku".to_string(), json!("PROC-ARM")),  ("cost".to_string(), json!(80.0))]),
    HashMap::from([("name".to_string(), json!("LCD Screen")),      ("sku".to_string(), json!("SCR-LCD")),   ("cost".to_string(), json!(20.0))]),
];

let part_vids = run!(db.bulk_insert_vertices("Part", part_props)).unwrap();
let (res10k, cap100uf, mbx1, scr_oled, bat4000, proc_arm, scr_lcd) =
    (part_vids[0], part_vids[1], part_vids[2], part_vids[3], part_vids[4], part_vids[5], part_vids[6]);

// 3 suppliers
let sup_props = vec![
    HashMap::from([("name".to_string(), json!("ResistorWorld"))]),
    HashMap::from([("name".to_string(), json!("ScreenTech"))]),
    HashMap::from([("name".to_string(), json!("CoreComponents"))]),
];
let sup_vids = run!(db.bulk_insert_vertices("Supplier", sup_props)).unwrap();
let (resistor_world, screen_tech, core_components) = (sup_vids[0], sup_vids[1], sup_vids[2]);

// 2 products
let prod_props = vec![
    HashMap::from([("name".to_string(), json!("Smartphone X")), ("price".to_string(), json!(599.0))]),
    HashMap::from([("name".to_string(), json!("TabletPro 10")), ("price".to_string(), json!(799.0))]),
];
let prod_vids = run!(db.bulk_insert_vertices("Product", prod_props)).unwrap();
let (smartphone, tablet) = (prod_vids[0], prod_vids[1]);

// Smartphone X assembly
run!(db.bulk_insert_edges("ASSEMBLED_FROM", vec![
    (smartphone, mbx1,     HashMap::new()),
    (smartphone, scr_oled, HashMap::new()),
    (smartphone, bat4000,  HashMap::new()),
    (smartphone, proc_arm, HashMap::new()),
    (mbx1,       res10k,   HashMap::new()),
    (mbx1,       cap100uf, HashMap::new()),
])).unwrap();

// TabletPro 10 assembly
run!(db.bulk_insert_edges("ASSEMBLED_FROM", vec![
    (tablet, mbx1,    HashMap::new()),
    (tablet, scr_lcd, HashMap::new()),
    (tablet, bat4000, HashMap::new()),
    (tablet, proc_arm, HashMap::new()),
])).unwrap();

// Supply relationships
run!(db.bulk_insert_edges("SUPPLIED_BY", vec![
    (res10k,   resistor_world,  HashMap::new()),
    (cap100uf, resistor_world,  HashMap::new()),
    (scr_oled, screen_tech,     HashMap::new()),
    (scr_lcd,  screen_tech,     HashMap::new()),
    (mbx1,     core_components, HashMap::new()),
    (bat4000,  core_components, HashMap::new()),
    (proc_arm, core_components, HashMap::new()),
])).unwrap();

run!(db.flush()).unwrap();
println!("Data ingested and flushed");

3. BOM Explosion

Which products are affected if RES-10K is defective? Traverses the assembly hierarchy upward.

let query = r#"
    MATCH (defective:Part {sku: 'RES-10K'})
    MATCH (product:Product)-[:ASSEMBLED_FROM*]->(defective)
    RETURN product.name AS name, product.price AS price
    ORDER BY product.price DESC
"#;

let results = run!(db.query(query)).unwrap();
println!("Products affected by defective RES-10K:");
for row in &results.rows {
    println!("  {:?}", row);
}
assert!(results.rows.len() == 2, "Expected 2 affected products, got {}", results.rows.len());

Bounded vs Unbounded Paths: [*] performs unbounded traversal (defaults to 100 hops max), ideal for BOM explosion where you want every affected product regardless of depth. Use [*1..5] to cap traversal at a known depth, as shown in the queries below.

4. Full BOM Listing

Every part in Smartphone X with its cost, ordered by cost descending.

let query_parts = r#"
    MATCH (p:Product {name: 'Smartphone X'})-[:ASSEMBLED_FROM*1..5]->(part:Part)
    RETURN part.name AS part_name, part.sku AS sku, part.cost AS cost
    ORDER BY cost DESC
"#;

let results = run!(db.query(query_parts)).unwrap();
println!("Smartphone X BOM:");
for row in &results.rows {
    println!("  {:?}", row);
}

5. Cost Rollup

Total BOM cost per product — GROUP BY product with SUM of part costs.

let query_rollup = r#"
    MATCH (p:Product)-[:ASSEMBLED_FROM*1..5]->(part:Part)
    RETURN p.name AS product, SUM(part.cost) AS total_bom_cost
    ORDER BY total_bom_cost DESC
"#;

let results = run!(db.query(query_rollup)).unwrap();
println!("BOM cost rollup per product:");
for row in &results.rows {
    println!("  {:?}", row);
}
assert!(results.rows.len() == 2, "Expected 2 rows, got {}", results.rows.len());

6. Supply Chain Risk

Which supplier is critical to the most products?

let query_risk = r#"
    MATCH (p:Product)-[:ASSEMBLED_FROM*1..5]->(part:Part)-[:SUPPLIED_BY]->(s:Supplier)
    RETURN s.name AS supplier, COUNT(DISTINCT p) AS products_at_risk
    ORDER BY products_at_risk DESC
"#;

let results = run!(db.query(query_risk)).unwrap();
println!("Supplier risk analysis:");
for row in &results.rows {
    println!("  {:?}", row);
}
// CoreComponents supplies mbx1, bat4000, proc_arm — used by both products
println!("Top supplier: {:?}", results.rows[0]);