#![allow(clippy::arc_with_non_send_sync)]
mod common;
use common::TestStorage;
use linkrs_core::types::{EdgeTypeInfo, PropertyDef, SpaceInfo, TagInfo, VertexId};
use linkrs_core::vertex_edge_path::Tag;
use linkrs_core::{DataType, Edge, Value, Vertex};
use linkrs_metrics::StatsManager;
use linkrs_query::executor::base::ExecutionResult;
use linkrs_query::optimizer::{OptimizerEngine, PartitioningConfig};
use linkrs_query::pipeline::QueryPipelineManager;
use linkrs_query::storage::{GraphStorage, StorageSchemaOps, StorageWriter};
use parking_lot::RwLock;
use std::sync::Arc;
const SPACE: &str = "pp";
const TAG: &str = "Node";
const TAG2: &str = "Other";
const EDGE: &str = "Link";
const VERTEX_COUNT: i64 = 2000;
fn insert_vertices(storage: &Arc<RwLock<GraphStorage>>) {
let mut start = 0i64;
while start < VERTEX_COUNT {
let end = (start + 500).min(VERTEX_COUNT);
let vertices: Vec<Vertex> = (start..end)
.map(|i| {
Vertex::new(
VertexId::try_from_int64(i).expect("test vertex id"),
Tag::new(
TAG.to_string(),
vec![
("id".to_string(), Value::BigInt(i)),
("value".to_string(), Value::BigInt(i)),
("group_id".to_string(), Value::BigInt(i % 20)),
]
.into_iter()
.map(|(name, value)| (Arc::from(name.as_str()), value))
.collect(),
),
)
})
.collect();
storage
.write()
.batch_insert_vertices(SPACE, vertices)
.expect("insert vertices");
start = end;
}
}
fn insert_edges(storage: &Arc<RwLock<GraphStorage>>) {
let mut edges = Vec::with_capacity((VERTEX_COUNT * 2) as usize);
for src in 0..VERTEX_COUNT {
for k in 1..=2i64 {
let dst = (src + k) % VERTEX_COUNT;
edges.push(Edge {
src: VertexId::try_from_int64(src).expect("test vertex id"),
dst: VertexId::try_from_int64(dst).expect("test vertex id"),
edge_type: EDGE.to_string(),
ranking: 0,
props: Default::default(),
});
}
}
for chunk in edges.chunks(10_000) {
storage
.write()
.batch_insert_edges(SPACE, chunk.to_vec())
.expect("insert edges");
}
}
fn setup_storage() -> Arc<RwLock<GraphStorage>> {
let storage = TestStorage::new().expect("storage init").storage();
{
let mut guard = storage.write();
let mut space = SpaceInfo::new(SPACE.to_string()).with_vid_type(DataType::BigInt);
guard.create_space(&mut space).expect("create space");
guard
.create_tag(
SPACE,
&TagInfo::new(TAG.to_string()).with_properties(vec![
PropertyDef::new("id".to_string(), DataType::BigInt),
PropertyDef::new("value".to_string(), DataType::BigInt),
PropertyDef::new("group_id".to_string(), DataType::BigInt),
]),
)
.expect("create tag");
guard
.create_tag(
SPACE,
&TagInfo::new(TAG2.to_string()).with_properties(vec![
PropertyDef::new("id".to_string(), DataType::BigInt),
PropertyDef::new("value".to_string(), DataType::BigInt),
]),
)
.expect("create tag 2");
guard
.create_edge_type(
SPACE,
&EdgeTypeInfo::new(EDGE.to_string())
.with_src_tag(TAG.to_string())
.with_dst_tag(TAG.to_string()),
)
.expect("create edge type");
}
insert_vertices(&storage);
{
let mut start = 0i64;
while start < VERTEX_COUNT {
let end = (start + 500).min(VERTEX_COUNT);
let vertices: Vec<Vertex> = (start..end)
.map(|i| {
let vid = i + VERTEX_COUNT;
Vertex::new(
VertexId::try_from_int64(vid).expect("test vertex id"),
Tag::new(
TAG2.to_string(),
vec![
("id".to_string(), Value::BigInt(vid)),
("value".to_string(), Value::BigInt(i + 1000)),
]
.into_iter()
.map(|(name, value)| (Arc::from(name.as_str()), value))
.collect(),
),
)
})
.collect();
storage
.write()
.batch_insert_vertices(SPACE, vertices)
.expect("insert tag2 vertices");
start = end;
}
}
insert_edges(&storage);
storage
}
fn build_pipeline(
storage: &Arc<RwLock<GraphStorage>>,
workers: usize,
) -> QueryPipelineManager<GraphStorage> {
let mut engine = OptimizerEngine::default();
if workers > 1 {
engine.set_partitioning_config(PartitioningConfig {
enabled: true,
min_rows_per_partition: 400,
max_partitions: workers * 2,
vertex_id_range: Some(0i64..2 * VERTEX_COUNT),
max_workers: workers,
max_buffered_chunks: 10,
fixed_overhead_rows: 0,
per_partition_rows: 0,
exchange_row_per_mille: 0,
});
} else {
engine.set_partitioning_config(PartitioningConfig {
max_workers: 1,
..PartitioningConfig::default()
});
}
let stats = Arc::new(StatsManager::new());
let pipeline = QueryPipelineManager::with_optimizer(storage.clone(), stats, Arc::new(engine));
pipeline
.collect_statistics(SPACE, true)
.expect("collect statistics");
pipeline
}
fn space_info() -> SpaceInfo {
let mut info = SpaceInfo::new(SPACE.to_string());
info.space_id = 1;
info
}
fn query_rows(pipeline: &mut QueryPipelineManager<GraphStorage>, query: &str) -> Vec<Vec<Value>> {
let space = space_info();
match pipeline
.execute_query_with_space(query, Some(space))
.expect("query should succeed")
{
ExecutionResult::DataSet { data, .. } => data.rows,
ExecutionResult::Empty => vec![],
other => panic!("unexpected result: {:?}", other),
}
}
fn sorted(rows: &mut [Vec<Value>]) {
rows.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
}
#[test]
fn partitioned_count_query_matches_serial_results() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q1 = "MATCH (n:Node) WHERE n.value < 800 RETURN count(n)";
let serial_rows = query_rows(&mut serial, q1);
let parallel_rows = query_rows(&mut parallel, q1);
assert_eq!(serial_rows, parallel_rows, "Q1 (filter + count) mismatch");
let q2 = "MATCH (n:Node) WHERE n.value >= 500 RETURN n.group_id, count(*)";
let mut serial_rows = query_rows(&mut serial, q2);
let mut parallel_rows = query_rows(&mut parallel, q2);
sorted(&mut serial_rows);
sorted(&mut parallel_rows);
assert_eq!(serial_rows, parallel_rows, "Q2 (group-by count) mismatch");
let q3 = "MATCH (n:Node) RETURN count(*)";
let serial_rows = query_rows(&mut serial, q3);
let parallel_rows = query_rows(&mut parallel, q3);
assert_eq!(serial_rows, parallel_rows, "Q3 (plain count) mismatch");
}
#[test]
fn explain_analyze_reports_active_parallelism() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 2);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (n:Node) WHERE n.value < 800 RETURN count(n)",
);
assert_eq!(output.len(), 1, "EXPLAIN ANALYZE returns one plan string");
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("actual="),
"EXPLAIN ANALYZE should report actual workers, got:\n{plan}"
);
assert!(
!plan.contains("fallback_reason"),
"partitioned plan must not report a fallback reason, got:\n{plan}"
);
}
#[test]
fn explain_analyze_reports_fallback_reason_without_partitioning_config() {
let storage = setup_storage();
let mut engine = OptimizerEngine::default();
engine.set_partitioning_config(PartitioningConfig {
enabled: false,
max_workers: 2,
..PartitioningConfig::default()
});
let stats = Arc::new(StatsManager::new());
let mut pipeline =
QueryPipelineManager::with_optimizer(storage.clone(), stats, Arc::new(engine));
pipeline
.collect_statistics(SPACE, true)
.expect("collect statistics");
let output = query_rows(
&mut pipeline,
"EXPLAIN ANALYZE MATCH (n:Node) RETURN count(n)",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("fallback_reason"),
"fallback reason must be visible, got:\n{plan}"
);
}
#[test]
fn enrich_scan_slots_rule_widens_scan_output() {
let storage = setup_storage();
let mut pipeline = build_pipeline(&storage, 1);
let output = query_rows(
&mut pipeline,
"EXPLAIN FORMAT = DOT MATCH (n:Node) WHERE n.value < 50 RETURN n.name",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("projected"),
"EXPLAIN should report the scan's projected properties, got:\n{plan}"
);
let projected_line = plan
.lines()
.find(|line| line.contains("projected:"))
.unwrap_or_else(|| panic!("no projected line in plan:\n{plan}"));
assert!(
projected_line.contains("name"),
"scan should project RETURN column 'name', got: {projected_line}"
);
assert!(
projected_line.contains("value"),
"scan should project predicate column 'value' (enrich rule), got: {projected_line}"
);
}
#[test]
fn partitioned_edge_scan_matches_serial_results() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q1 = "LOOKUP ON EDGE Link";
let mut serial_rows = query_rows(&mut serial, q1);
let mut parallel_rows = query_rows(&mut parallel, q1);
sorted(&mut serial_rows);
sorted(&mut parallel_rows);
assert_eq!(serial_rows, parallel_rows, "E-Q1 (edge scan) mismatch");
let q2 = "LOOKUP ON EDGE Link YIELD Link.src";
let mut serial_rows = query_rows(&mut serial, q2);
let mut parallel_rows = query_rows(&mut parallel, q2);
sorted(&mut serial_rows);
sorted(&mut parallel_rows);
assert_eq!(
serial_rows, parallel_rows,
"E-Q2 (edge projection) mismatch"
);
}
#[test]
fn explain_analyze_edge_scan_reports_partition_shape() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 2);
let output = query_rows(&mut parallel, "EXPLAIN ANALYZE LOOKUP ON EDGE Link");
assert_eq!(output.len(), 1, "EXPLAIN ANALYZE returns one plan string");
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("Partitioning:"),
"edge-scan plan must describe its partition spec, got:\n{plan}"
);
assert!(
plan.contains("actual="),
"edge-scan plan must report actual workers, got:\n{plan}"
);
assert!(
!plan.contains("fallback_reason"),
"partitioned edge-scan plan must not report a fallback reason, got:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"edge-scan plan must execute on at least 2 workers, got actual={actual}:\n{plan}"
);
}
#[test]
fn anchored_hop_filter_is_pushed_below_expand() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q = "MATCH (a:Node)-[:Link]->(b:Node) WHERE a.value < 100 RETURN count(b)";
let serial_rows = query_rows(&mut serial, q);
let parallel_rows = query_rows(&mut parallel, q);
assert_eq!(serial_rows, parallel_rows, "anchored 1-hop mismatch");
assert_eq!(
serial_rows,
vec![vec![Value::BigInt(200)]],
"anchored 1-hop must expand only 100 anchors, got {serial_rows:?}"
);
let output = query_rows(
&mut serial,
"EXPLAIN MATCH (a:Node)-[:Link]->(b:Node) WHERE a.value < 100 RETURN count(b)",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("ExpandAll"),
"plan must contain ExpandAll, got:\n{plan}"
);
let scan_pos = plan.find("StorageScan").expect("anchor scan");
let filter_pos = scan_pos + plan[scan_pos..].find("Filter").expect("pushed filter");
let expand_pos = filter_pos + plan[filter_pos..].find("ExpandAll").expect("expand");
assert!(
scan_pos < filter_pos && filter_pos < expand_pos,
"anchor predicate must be evaluated before expansion, got:\n{plan}"
);
}
#[test]
fn unanchored_two_hop_returns_correct_count() {
let storage = setup_storage();
let mut pipeline = build_pipeline(&storage, 1);
let q = "MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) RETURN count(c)";
let rows = query_rows(&mut pipeline, q);
assert_eq!(rows, vec![vec![Value::BigInt(8000)]]);
}
#[test]
fn partitioned_union_of_independent_scans_matches_serial_results() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q1 = "MATCH (a:Node) WHERE a.value < 800 RETURN a.value \
UNION ALL \
MATCH (b:Other) WHERE b.value >= 1000 RETURN b.value";
let mut serial_rows = query_rows(&mut serial, q1);
let mut parallel_rows = query_rows(&mut parallel, q1);
sorted(&mut serial_rows);
sorted(&mut parallel_rows);
assert_eq!(serial_rows, parallel_rows, "U-Q1 (union all) mismatch");
}
#[test]
fn explain_analyze_union_reports_active_parallelism() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 2);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (a:Node) RETURN a.value \
UNION ALL \
MATCH (b:Other) RETURN b.value",
);
assert_eq!(output.len(), 1, "EXPLAIN ANALYZE returns one plan string");
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("actual="),
"EXPLAIN ANALYZE should report actual workers, got:\n{plan}"
);
assert!(
!plan.contains("fallback_reason"),
"partitioned union plan must not report a fallback reason, got:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"union plan must execute on at least 2 workers, got actual={actual}:\n{plan}"
);
}
#[test]
fn partitioned_anchored_traversal_matches_serial_results() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q = "MATCH (a:Node)-[:Link]->(b:Node) WHERE a.value < 100 RETURN count(b)";
let serial_rows = query_rows(&mut serial, q);
let parallel_rows = query_rows(&mut parallel, q);
assert_eq!(serial_rows, parallel_rows, "E4 anchored 1-hop mismatch");
assert_eq!(serial_rows, vec![vec![Value::BigInt(200)]]);
let q2 = "MATCH (a:Node)-[:Link]->(b:Node) WHERE a.value < 100 RETURN b.value, count(*)";
let mut serial_rows = query_rows(&mut serial, q2);
let mut parallel_rows = query_rows(&mut parallel, q2);
sorted(&mut serial_rows);
sorted(&mut parallel_rows);
assert_eq!(serial_rows, parallel_rows, "E4 grouped aggregate mismatch");
assert_eq!(
serial_rows.len(),
101,
"anchors 0..99 reach neighbors 1..101"
);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (a:Node)-[:Link]->(b:Node) WHERE a.value < 100 RETURN count(b)",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("below the partition threshold"),
"small-anchor traversal must fall back to serial, got:\n{plan}"
);
}
#[test]
fn explain_analyze_vertex_scan_reports_partition_spec() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 4);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (n:Node) WHERE n.value < 800 RETURN n.value",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("Partitioning:"),
"vertex scan plan must describe partition spec, got:\n{plan}"
);
assert!(
plan.contains("partitioned into"),
"vertex scan plan must show partition count, got:\n{plan}"
);
assert!(
plan.contains("ranges ["),
"vertex scan plan must show ranges, got:\n{plan}"
);
assert!(
plan.contains("actual="),
"vertex scan plan must report actual workers, got:\n{plan}"
);
assert!(
!plan.contains("fallback_reason"),
"partitioned vertex scan must not fallback, got:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"vertex scan must use at least 2 workers, got actual={actual}:\n{plan}"
);
}
#[test]
fn explain_analyze_union_plan_contains_exchange() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 2);
let output = query_rows(
&mut parallel,
"EXPLAIN MATCH (a:Node) RETURN a.value \
UNION ALL \
MATCH (b:Other) RETURN b.value",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("Exchange"),
"union plan must contain Exchange node, got:\n{plan}"
);
assert!(
plan.contains("Partitioning:"),
"union plan must describe partition spec, got:\n{plan}"
);
}
#[test]
fn explain_analyze_4_workers_vertex_scan_uses_all_workers() {
let storage = setup_storage();
let mut parallel = build_pipeline(&storage, 4);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (n:Node) RETURN count(n)",
);
let plan = output[0][0].to_string().unwrap_or_default();
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 4,
"4-worker config should use at least 4 workers for full scan, got actual={actual}:\n{plan}"
);
}
#[test]
fn plan_cache_replans_on_partition_config_change() {
let storage = setup_storage();
let q = "MATCH (n:Node) WHERE n.value < 800 RETURN count(n)";
let mut pipeline_2w = build_pipeline(&storage, 2);
let rows_2w = query_rows(&mut pipeline_2w, q);
assert_eq!(rows_2w, vec![vec![Value::BigInt(800)]]);
let mut pipeline_4w = build_pipeline(&storage, 4);
let rows_4w = query_rows(&mut pipeline_4w, q);
assert_eq!(rows_4w, vec![vec![Value::BigInt(800)]]);
assert_eq!(
rows_2w, rows_4w,
"cache replan must produce identical results"
);
let output = query_rows(
&mut pipeline_4w,
"EXPLAIN ANALYZE MATCH (n:Node) WHERE n.value < 800 RETURN count(n)",
);
let plan = output[0][0].to_string().unwrap_or_default();
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"replanned 4-worker query must use parallel execution, got actual={actual}:\n{plan}"
);
}
#[test]
fn feedback_does_not_learn_partition_bias_from_range_slices() {
let storage = setup_storage();
let mut pipeline = build_pipeline(&storage, 4);
let q = "MATCH (n:Node) WHERE n.value < 800 RETURN count(n)";
for _ in 0..4 {
let rows = query_rows(&mut pipeline, q);
assert_eq!(rows, vec![vec![Value::BigInt(800)]]);
}
let output = query_rows(&mut pipeline, &format!("EXPLAIN ANALYZE {q}"));
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
!plan.contains("below the partition threshold"),
"learned correction must not push the range query under the gate:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"range query must stay partitioned after feedback, got actual={actual}:\n{plan}"
);
}
#[test]
fn plan_cache_partition_fingerprint_changes_with_ranges() {
use linkrs_query::cache::plan_cache::PlanCacheKey;
use linkrs_query::planning::plan::execution_plan::{PartitionSource, PartitionSpec};
use std::ops::Range;
let source = PartitionSource::VertexId {
tag: "Node".to_string(),
};
let spec_a = PartitionSpec::try_new(
vec![
Range { start: 0, end: 500 },
Range {
start: 500,
end: 1000,
},
],
source.clone(),
Some(42),
)
.unwrap();
let spec_b = PartitionSpec::try_new(
vec![
Range { start: 0, end: 250 },
Range {
start: 250,
end: 500,
},
Range {
start: 500,
end: 750,
},
Range {
start: 750,
end: 1000,
},
],
source.clone(),
Some(42),
)
.unwrap();
let key_a = PlanCacheKey::from_query_with_partition("MATCH (n:Node) RETURN count(n)", &spec_a);
let key_b = PlanCacheKey::from_query_with_partition("MATCH (n:Node) RETURN count(n)", &spec_b);
assert_ne!(
key_a, key_b,
"different ranges must produce different cache keys"
);
let spec_c = PartitionSpec::try_new(
vec![
Range { start: 0, end: 500 },
Range {
start: 500,
end: 1000,
},
],
source.clone(),
Some(42),
)
.unwrap();
let key_c = PlanCacheKey::from_query_with_partition("MATCH (n:Node) RETURN count(n)", &spec_c);
assert_eq!(key_a, key_c, "same spec must produce same cache key");
}
#[test]
fn two_hop_id_only_seeds_feed_next_hop() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q = "MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) RETURN count(c)";
let serial_rows = query_rows(&mut serial, q);
let parallel_rows = query_rows(&mut parallel, q);
assert_eq!(
serial_rows, parallel_rows,
"A1.5 2-hop id_only chain mismatch"
);
assert_eq!(serial_rows, vec![vec![Value::BigInt(8000)]]);
let output = query_rows(
&mut serial,
"EXPLAIN MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) RETURN count(c)",
);
let plan = output[0][0].to_string().unwrap_or_default();
let expand_pos = plan.find("ExpandAll").expect("expand");
let info_after_first = &plan[expand_pos..];
assert!(
info_after_first.contains("mode:id_only"),
"hop1 must be id_only, got:\n{plan}"
);
assert!(
info_after_first.contains("mode:count_only"),
"hop2 must be count_only, got:\n{plan}"
);
}
#[test]
fn referenced_source_keeps_full_vertex() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let rows = query_rows(
&mut serial,
"MATCH (a:Node)-[:Link]->(b:Node) RETURN a.value",
);
assert_eq!(
rows.len(),
4000,
"1-hop with projected source must return 4000 rows"
);
let mut values: Vec<i64> = rows
.iter()
.map(|r| match &r[0] {
Value::BigInt(v) => *v,
other => panic!("unexpected source value: {other:?}"),
})
.collect();
values.sort_unstable();
let mut expected: Vec<i64> = (0..2000i64).flat_map(|i| [i, i]).collect();
expected.sort_unstable();
assert_eq!(
values, expected,
"projected source values must match the anchors"
);
}
#[test]
fn middle_var_referenced_blocks_id_only() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let q =
"MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) WHERE b.group_id < 10 RETURN count(c)";
let rows = query_rows(&mut serial, q);
assert_eq!(
rows,
vec![vec![Value::BigInt(4000)]],
"id_only must be blocked when b is referenced"
);
}
#[test]
fn parallel_unanchored_two_hop_matches_serial() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q = "MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) RETURN count(c)";
let serial_rows = query_rows(&mut serial, q);
let parallel_rows = query_rows(&mut parallel, q);
assert_eq!(
serial_rows, parallel_rows,
"C1 2-hop parallel count mismatch"
);
assert_eq!(serial_rows, vec![vec![Value::BigInt(8000)]]);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (a:Node)-[:Link]->(b:Node)-[:Link]->(c:Node) RETURN count(c)",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("Partitioning:"),
"2-hop plan must carry a partition spec, got:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"2-hop traversal must execute on at least 2 workers, got actual={actual}:\n{plan}"
);
}
#[test]
fn partitioned_equality_join_matches_serial_results() {
let storage = setup_storage();
let mut serial = build_pipeline(&storage, 1);
let mut parallel = build_pipeline(&storage, 2);
let q = "MATCH (a:Node),(b:Other) WHERE a.value = b.value RETURN count(*)";
let serial_rows = query_rows(&mut serial, q);
let parallel_rows = query_rows(&mut parallel, q);
assert_eq!(
serial_rows, parallel_rows,
"partitioned equality join mismatch"
);
assert_eq!(
serial_rows,
vec![vec![Value::BigInt(1000)]],
"equality join must produce exactly 1000 matches, got {serial_rows:?}"
);
let output = query_rows(
&mut parallel,
"EXPLAIN ANALYZE MATCH (a:Node),(b:Other) WHERE a.value = b.value RETURN count(*)",
);
let plan = output[0][0].to_string().unwrap_or_default();
if plan.contains("Partitioning:") {
assert!(
!plan.contains("fallback_reason"),
"partitioned equality join must not report a fallback reason, got:\n{plan}"
);
assert!(
plan.contains("actual="),
"partitioned equality join must report actual workers, got:\n{plan}"
);
}
}
#[test]
fn storage_self_proven_domain_enables_partitioning_without_config_range() {
let storage = setup_storage();
let mut engine = OptimizerEngine::default();
engine.set_partitioning_config(PartitioningConfig {
enabled: true,
min_rows_per_partition: 400,
max_partitions: 4,
vertex_id_range: None, max_workers: 2,
max_buffered_chunks: 10,
fixed_overhead_rows: 0,
per_partition_rows: 0,
exchange_row_per_mille: 0,
});
let stats = Arc::new(StatsManager::new());
let mut pipeline =
QueryPipelineManager::with_optimizer(storage.clone(), stats, Arc::new(engine));
pipeline
.collect_statistics(SPACE, true)
.expect("collect statistics");
let q = "MATCH (n:Node) WHERE n.value < 800 RETURN count(n)";
let serial_rows = query_rows(&mut pipeline, q);
assert_eq!(serial_rows, vec![vec![Value::BigInt(800)]]);
let output = query_rows(
&mut pipeline,
"EXPLAIN ANALYZE MATCH (n:Node) WHERE n.value < 800 RETURN count(n)",
);
let plan = output[0][0].to_string().unwrap_or_default();
assert!(
plan.contains("Partitioning:"),
"storage self-proven domain must enable partitioning, got:\n{plan}"
);
let actual = plan
.split("actual=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_digit()).next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
assert!(
actual >= 2,
"storage-driven partitioning must use real workers, got actual={actual}:\n{plan}"
);
}