use std::time::Instant;
use lora_database::{Database, ExecuteOptions, ResultFormat};
const ITEM_COUNT: usize = 500_000;
const CATEGORY_COUNT: usize = 50_000;
const EDGE_COUNT: usize = ITEM_COUNT;
const BULK_BATCH: usize = 2_000;
fn rows() -> Option<ExecuteOptions> {
Some(ExecuteOptions {
format: ResultFormat::Rows,
})
}
fn first_row_int(json: &serde_json::Value, key: &str) -> i64 {
let value = &json["rows"][0][key];
value
.as_i64()
.unwrap_or_else(|| panic!("expected integer at rows[0].{key}, got {value}"))
}
fn time<R>(label: &str, f: impl FnOnce() -> R) -> R {
let start = Instant::now();
let result = f();
let elapsed = start.elapsed();
eprintln!(" [{label}] {:?}", elapsed);
result
}
fn build_super_large_graph(db: &Database<lora_database::InMemoryGraph>) {
let mut i = 0;
while i < CATEGORY_COUNT {
let end = (i + BULK_BATCH).min(CATEGORY_COUNT);
db.execute(
&format!(
"UNWIND list.range({i}, {}) AS k \
CREATE (:Category {{idx: k, name: 'cat_' + type.cast(k, STRING)}})",
end - 1
),
rows(),
)
.unwrap();
i = end;
}
let mut i = 0;
while i < ITEM_COUNT {
let end = (i + BULK_BATCH).min(ITEM_COUNT);
db.execute(
&format!(
"UNWIND list.range({i}, {}) AS k \
CREATE (:Item {{id: k, value: k, kind: k % 10}})",
end - 1
),
rows(),
)
.unwrap();
i = end;
}
let mut i = 0;
while i < EDGE_COUNT {
let end = (i + BULK_BATCH).min(EDGE_COUNT);
db.execute(
&format!(
"UNWIND list.range({i}, {}) AS k \
MATCH (it:Item {{id: k}}), (c:Category {{idx: k % {CATEGORY_COUNT}}}) \
CREATE (it)-[:BELONGS_TO]->(c)",
end - 1
),
rows(),
)
.unwrap();
i = end;
}
}
#[test]
#[ignore = "super-large database build (~minute); run with --ignored"]
fn super_large_database_query_correctness() {
let db = Database::in_memory();
eprintln!(
"building super-large graph: {} items, {} categories, {} edges",
ITEM_COUNT, CATEGORY_COUNT, EDGE_COUNT
);
time("build", || build_super_large_graph(&db));
assert_eq!(db.node_count(), ITEM_COUNT + CATEGORY_COUNT);
assert_eq!(db.relationship_count(), EDGE_COUNT);
let total_nodes = time("MATCH (n) RETURN count(n)", || {
let result = db
.execute("MATCH (n) RETURN count(n) AS c", rows())
.unwrap();
first_row_int(&serde_json::to_value(&result).unwrap(), "c")
});
assert_eq!(total_nodes as usize, ITEM_COUNT + CATEGORY_COUNT);
let item_count = time("MATCH (n:Item) RETURN count(n)", || {
let result = db
.execute("MATCH (n:Item) RETURN count(n) AS c", rows())
.unwrap();
first_row_int(&serde_json::to_value(&result).unwrap(), "c")
});
assert_eq!(item_count as usize, ITEM_COUNT);
let target = ITEM_COUNT - 1; let by_id = time("MATCH (n:Item {id: target}) RETURN n.value", || {
db.execute(
&format!("MATCH (n:Item {{id: {target}}}) RETURN n.value AS v"),
rows(),
)
.unwrap()
});
let json = serde_json::to_value(&by_id).unwrap();
assert_eq!(json["rows"].as_array().unwrap().len(), 1);
assert_eq!(first_row_int(&json, "v") as usize, target);
let lower = ITEM_COUNT / 10;
let upper = lower * 2;
let in_range = time("range filter [10%, 20%]", || {
let result = db
.execute(
&format!(
"MATCH (n:Item) WHERE n.value >= {lower} AND n.value < {upper} \
RETURN count(n) AS c"
),
rows(),
)
.unwrap();
first_row_int(&serde_json::to_value(&result).unwrap(), "c")
});
assert_eq!(in_range as usize, upper - lower);
let buckets = time("aggregation by kind", || {
db.execute(
"MATCH (n:Item) RETURN n.kind AS kind, count(n) AS c ORDER BY kind",
rows(),
)
.unwrap()
});
let json = serde_json::to_value(&buckets).unwrap();
let bucket_rows = json["rows"].as_array().unwrap();
assert_eq!(bucket_rows.len(), 10, "expected exactly 10 kind buckets");
let mut sum = 0i64;
for (i, row) in bucket_rows.iter().enumerate() {
assert_eq!(
row["kind"].as_i64().unwrap() as usize,
i,
"buckets must come back in ORDER BY kind ascending"
);
let count = row["c"].as_i64().unwrap();
sum += count;
assert_eq!(count as usize, ITEM_COUNT / 10);
}
assert_eq!(sum as usize, ITEM_COUNT);
let cat_idx = CATEGORY_COUNT / 2; let expected_per_cat = ITEM_COUNT / CATEGORY_COUNT;
let traversal_count = time("incoming-edge count for one category", || {
let result = db
.execute(
&format!(
"MATCH (it:Item)-[:BELONGS_TO]->(c:Category {{idx: {cat_idx}}}) \
RETURN count(it) AS c"
),
rows(),
)
.unwrap();
first_row_int(&serde_json::to_value(&result).unwrap(), "c")
});
assert_eq!(
traversal_count as usize, expected_per_cat,
"each category should have exactly {expected_per_cat} items"
);
let top = time("ORDER BY value DESC LIMIT 5", || {
db.execute(
"MATCH (n:Item) RETURN n.id AS id ORDER BY n.value DESC LIMIT 5",
rows(),
)
.unwrap()
});
let json = serde_json::to_value(&top).unwrap();
let top_rows = json["rows"].as_array().unwrap();
assert_eq!(top_rows.len(), 5);
for (i, row) in top_rows.iter().enumerate() {
assert_eq!(
row["id"].as_i64().unwrap() as usize,
ITEM_COUNT - 1 - i,
"top-N row {i} should be id {}",
ITEM_COUNT - 1 - i
);
}
eprintln!(
"all assertions passed against {}-node / {}-edge graph",
db.node_count(),
db.relationship_count()
);
}