Skip to content

Commit daa80a9

Browse files
adityamukhoclaude
andauthored
feat: selective B+Tree lookup in filter_facts_for_query (#208) (#246)
* docs: add Wave 1 performance design spec (#208, #202, #203, #204) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Wave 1 implementation plans for #208 and #202/#203/#204 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: promote get_facts_by_entity/attribute to production code (#208) Move get_facts_by_entity, get_facts_by_attribute, and get_facts_by_entity_attribute from #[cfg(test)] to a production impl block so they can be called from the query executor. Also promote the resolve_fact_ref and next_string_prefix helper functions to production scope. The five remaining test-only helpers stay in #[cfg(test)]. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add selective lookup correctness tests (#208) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: selective B+Tree lookup in filter_facts_for_query (#208) Add `selective_fact_fetch` to `DatalogExecutor` and wire it into the `(None, None)` arm of `filter_facts_for_query`. For rule-free queries with ≤4 distinct bound entities/attributes, this skips the full `get_all_facts()` scan in favour of targeted index lookups. Patterns inside `not`/`not-join`/`or`/`or-join` bodies are included via `collect_all_patterns` so the snapshot is always complete. Rule queries continue to use the full scan since rules require the entire fact base. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * bench: add btree_lookup/entity_point and attribute_scan benchmarks (#208) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: apply rustfmt formatting to #208 changes * fix: resolve clippy lints in promoted storage methods (#208) * fix: collapse nested if in next_string_prefix per clippy --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2f156f0 commit daa80a9

7 files changed

Lines changed: 2290 additions & 33 deletions

File tree

benches/helpers/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,27 @@ pub fn populate_with_dept(n: usize, dept_count: usize) -> Arc<Minigraf> {
331331
Arc::new(db)
332332
}
333333

334+
/// In-memory DB with `n` entities, each with `:val` and `:name` attributes.
335+
/// Suitable for point-lookup and attribute-scan benchmarks.
336+
///
337+
/// Schema:
338+
/// `:e{i} :val {i}` for i in 0..n
339+
/// `:e{i} :name "entity{i}"` for i in 0..n
340+
pub fn populate_with_names(n: usize) -> Arc<Minigraf> {
341+
let db = Minigraf::in_memory().unwrap();
342+
for batch_start in (0..n).step_by(50) {
343+
let batch_end = (batch_start + 50).min(n);
344+
let mut cmd = String::from("(transact [");
345+
for i in batch_start..batch_end {
346+
cmd.push_str(&format!("[:e{i} :val {i}]", i = i));
347+
cmd.push_str(&format!(r#"[:e{i} :name "entity{i}"]"#, i = i));
348+
}
349+
cmd.push_str("])");
350+
db.execute(&cmd).unwrap();
351+
}
352+
Arc::new(db)
353+
}
354+
334355
/// In-memory DB with `n` value facts, `dup_fraction`% of values are duplicates.
335356
/// Each entity has `:val` integer where `dup_fraction`% map to same values.
336357
///

benches/minigraf_bench.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,44 @@ fn bench_retract(c: &mut Criterion) {
14651465
}
14661466
}
14671467

1468+
// ── B+Tree selective lookup (Issue #208) ─────────────────────────────────────
1469+
1470+
fn bench_btree_lookup(c: &mut Criterion) {
1471+
const SCALES: &[(&str, usize)] = &[("1k", 1_000), ("10k", 10_000), ("100k", 100_000)];
1472+
1473+
// entity_point: single known entity literal — should be O(1) with selective lookup.
1474+
{
1475+
let mut group = c.benchmark_group("btree_lookup/entity_point");
1476+
group.sample_size(10);
1477+
for &(label, n) in SCALES {
1478+
group.bench_with_input(BenchmarkId::from_parameter(label), &n, |b, &n| {
1479+
let db = helpers::populate_with_names(n);
1480+
b.iter(|| {
1481+
db.execute(r#"(query [:find ?n :where [:e0 :name ?n]])"#)
1482+
.unwrap()
1483+
});
1484+
});
1485+
}
1486+
group.finish();
1487+
}
1488+
1489+
// attribute_scan: all entities via a single bound attribute.
1490+
{
1491+
let mut group = c.benchmark_group("btree_lookup/attribute_scan");
1492+
group.sample_size(10);
1493+
for &(label, n) in SCALES {
1494+
group.bench_with_input(BenchmarkId::from_parameter(label), &n, |b, &n| {
1495+
let db = helpers::populate_with_names(n);
1496+
b.iter(|| {
1497+
db.execute("(query [:find ?e ?n :where [?e :name ?n]])")
1498+
.unwrap()
1499+
});
1500+
});
1501+
}
1502+
group.finish();
1503+
}
1504+
}
1505+
14681506
criterion_group!(
14691507
benches,
14701508
bench_insert,
@@ -1488,5 +1526,6 @@ criterion_group!(
14881526
bench_concurrent_btree_scan,
14891527
bench_prepared,
14901528
bench_retract,
1529+
bench_btree_lookup,
14911530
);
14921531
criterion_main!(benches);

0 commit comments

Comments
 (0)