diff --git a/rust-core/verisim-planner/src/explain.rs b/rust-core/verisim-planner/src/explain.rs index 95153a4..d87c448 100644 --- a/rust-core/verisim-planner/src/explain.rs +++ b/rust-core/verisim-planner/src/explain.rs @@ -254,6 +254,8 @@ mod tests { }, optimization_hint: Some("HNSW ANN search (k=10)".to_string()), pushed_predicates: vec!["Similarity { k: 10 }".to_string()], + projections: vec![], + early_limit: None, }, PlanStep { step: 2, @@ -266,6 +268,8 @@ mod tests { io_cost: 135.0, cpu_cost: 90.0, }, + projections: vec![], + early_limit: None, optimization_hint: Some("Graph traversal: relates_to (depth=2)".to_string()), pushed_predicates: vec!["Traversal { predicate: relates_to }".to_string()], }, @@ -334,6 +338,8 @@ mod tests { }, optimization_hint: Some("ZKP verification — expensive".to_string()), pushed_predicates: vec![], + projections: vec![], + early_limit: None, }], strategy: ExecutionStrategy::Sequential, total_cost: CostEstimate { diff --git a/rust-core/verisim-planner/src/optimizer.rs b/rust-core/verisim-planner/src/optimizer.rs index 1953dac..dfc986e 100644 --- a/rust-core/verisim-planner/src/optimizer.rs +++ b/rust-core/verisim-planner/src/optimizer.rs @@ -125,6 +125,12 @@ impl Planner { cost: cost.clone(), optimization_hint: hint.clone(), pushed_predicates, + // Carry both through to the physical plan. Dropping them here + // was not merely lossy: CostModel::estimate discounts a node + // that has an early_limit, so the plan was priced for a + // pushdown the executor could never see. + projections: node.projections.clone(), + early_limit: node.early_limit, }); cost_estimates.push(cost.clone()); @@ -352,4 +358,71 @@ mod tests { assert!(explain.text_output.contains("Strategy")); assert!(explain.text_output.contains("vector")); } + + /// Projection and early-limit pushdown must survive optimization. + /// + /// Before 2026-07-28 both were computed on the logical node and dropped at + /// the physical boundary. That was worse than lossy: `CostModel::estimate` + /// discounts a node carrying an `early_limit` (scaling `selectivity` and + /// taking `time_ms` to as low as 50% of base), so plans were priced for a + /// pushdown the executor could not see. Nothing failed loudly — the plan + /// just quietly cost less than it ran. + #[test] + fn test_pushdown_survives_optimization() { + let logical = LogicalPlan { + source: QuerySource::Octad, + nodes: vec![ + PlanNode { + modality: Modality::Vector, + conditions: vec![ConditionKind::Similarity { k: 10 }], + projections: vec!["id".to_string(), "label".to_string()], + early_limit: Some(25), + }, + PlanNode { + modality: Modality::Graph, + conditions: vec![ConditionKind::Traversal { + predicate: "relates_to".to_string(), + depth: Some(2), + }], + projections: vec!["id".to_string()], + early_limit: None, + }, + ], + post_processing: vec![], + }; + + let planner = Planner::new(PlannerConfig::default()); + let physical = planner.optimize(&logical).expect("optimize"); + + // Match on modality rather than position: the optimizer reorders by + // execution priority, so asserting on steps[0] would be asserting on + // the sort order, not on the pushdown. + let vector_step = physical + .steps + .iter() + .find(|s| s.modality == Modality::Vector) + .expect("vector step present"); + assert_eq!( + vector_step.projections, + vec!["id".to_string(), "label".to_string()], + "projection pushdown was dropped at the physical boundary" + ); + assert_eq!( + vector_step.early_limit, + Some(25), + "early-limit pushdown was dropped, but the cost model already \ + discounted the plan for it" + ); + + let graph_step = physical + .steps + .iter() + .find(|s| s.modality == Modality::Graph) + .expect("graph step present"); + assert_eq!(graph_step.projections, vec!["id".to_string()]); + assert_eq!( + graph_step.early_limit, None, + "a node without an early limit must not acquire one" + ); + } } diff --git a/rust-core/verisim-planner/src/plan.rs b/rust-core/verisim-planner/src/plan.rs index 3ec177b..1fff77c 100644 --- a/rust-core/verisim-planner/src/plan.rs +++ b/rust-core/verisim-planner/src/plan.rs @@ -113,6 +113,24 @@ pub struct PlanStep { pub optimization_hint: Option, /// Pushed-down predicates. pub pushed_predicates: Vec, + /// Fields to project, carried through from [`PlanNode::projections`] + /// (empty = all fields). + /// + /// Until 2026-07-28 this was absent, so projection pushdown was computed + /// on the logical node and then discarded at the physical boundary — the + /// executor had no way to see it. + #[serde(default)] + pub projections: Vec, + /// Early row limit pushed down to the store, carried through from + /// [`PlanNode::early_limit`]. + /// + /// Also absent until 2026-07-28, which was the more consequential half of + /// the same gap: [`crate::cost::CostModel::estimate`] *already* discounts a node + /// carrying an early limit (it scales `selectivity` and takes `time_ms` + /// down to as low as 50% of base), so a plan was scored cheaper for a + /// pushdown the physical plan could not actually perform. + #[serde(default)] + pub early_limit: Option, } /// An optimized physical plan ready for execution. @@ -182,6 +200,8 @@ mod tests { }, optimization_hint: Some("HNSW ANN".to_string()), pushed_predicates: vec!["k=10".to_string()], + projections: vec![], + early_limit: None, }], strategy: ExecutionStrategy::Sequential, total_cost: CostEstimate { diff --git a/rust-core/verisim-planner/src/prepared.rs b/rust-core/verisim-planner/src/prepared.rs index df8b8b4..966709c 100644 --- a/rust-core/verisim-planner/src/prepared.rs +++ b/rust-core/verisim-planner/src/prepared.rs @@ -755,6 +755,8 @@ mod tests { }, optimization_hint: Some("depth-limited BFS".to_string()), pushed_predicates: vec!["relates_to".to_string()], + projections: vec![], + early_limit: None, }], strategy: ExecutionStrategy::Sequential, total_cost: CostEstimate { diff --git a/rust-core/verisim-planner/src/profiler.rs b/rust-core/verisim-planner/src/profiler.rs index 36a2b72..eba6feb 100644 --- a/rust-core/verisim-planner/src/profiler.rs +++ b/rust-core/verisim-planner/src/profiler.rs @@ -463,6 +463,8 @@ mod tests { }, optimization_hint: Some("HNSW ANN search (k=10)".to_string()), pushed_predicates: vec!["Similarity { k: 10 }".to_string()], + projections: vec![], + early_limit: None, }, PlanStep { step: 2, @@ -475,6 +477,8 @@ mod tests { io_cost: 135.0, cpu_cost: 90.0, }, + projections: vec![], + early_limit: None, optimization_hint: Some("Graph traversal: relates_to (depth=2)".to_string()), pushed_predicates: vec!["Traversal { predicate: relates_to }".to_string()], }, diff --git a/rust-core/verisim-planner/src/slow_query.rs b/rust-core/verisim-planner/src/slow_query.rs index bcd29c5..290f3ea 100644 --- a/rust-core/verisim-planner/src/slow_query.rs +++ b/rust-core/verisim-planner/src/slow_query.rs @@ -321,6 +321,8 @@ mod tests { }, optimization_hint: None, pushed_predicates: vec![], + projections: vec![], + early_limit: None, }) .collect(), strategy: if steps.len() >= 2 {