use crate::optimizer::cost::SelectivityEstimator;
use crate::optimizer::cost_based::row_estimates::{
estimate_node_output_rows_corrected, estimate_node_output_rows_logical,
};
use crate::optimizer::cost_based::traversal::rewrite_children;
use crate::optimizer::cost_based::traversal_logical::rewrite_children_logical;
use crate::optimizer::cost_based::{
SortContext, SortEliminationDecision, SortEliminationOptimizer,
};
use crate::optimizer::stats::feedback::cardinality::CardinalityFeedbackManager;
use crate::optimizer::stats::StatsView;
use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
use crate::planning::plan::core::nodes::operation::sort_node::TopNNode;
use crate::planning::plan::logical::logical_node_traits::LogicalSingleInputNode;
use crate::planning::plan::logical::LogicalNodeEnum;
use crate::planning::plan::PlanNodeEnum;
pub fn rewrite_sort_with_limits(
node: &PlanNodeEnum,
optimizer: &SortEliminationOptimizer,
stats: &StatsView,
selectivity: &SelectivityEstimator,
cardinality: &CardinalityFeedbackManager,
notes: &mut Vec<String>,
) -> PlanNodeEnum {
use PlanNodeEnum::*;
if let Limit(limit) = node {
if let Sort(sort) = limit.input() {
let offset = limit.offset();
let count = limit.count();
if offset >= 0 && count > 0 {
let input_rows = estimate_node_output_rows_corrected(
sort.input(),
stats,
selectivity,
cardinality,
);
let context =
SortContext::new(sort.clone(), input_rows).with_limit(count + offset.max(0));
match optimizer.optimize_with_memory(&context, None) {
SortEliminationDecision::ConvertToTopN { reason, .. } => {
let topn = TopNNode::new(
sort.input().clone(),
sort.sort_items().to_vec(),
count + offset.max(0),
);
if let Ok(topn) = topn {
notes.push(format!(
"sort: convert sort+limit -> topn (limit={}, offset={}, reason={:?})",
count, offset, reason
));
let mut new_limit = limit.clone();
new_limit.set_input(PlanNodeEnum::TopN(topn));
return Limit(new_limit);
}
}
SortEliminationDecision::KeepSort { reason, .. } => {
notes.push(format!(
"sort: keep sort+limit (limit={}, offset={}, reason={:?})",
count, offset, reason
));
}
}
}
}
}
let mut closure = |child: &PlanNodeEnum| {
rewrite_sort_with_limits(child, optimizer, stats, selectivity, cardinality, notes)
};
rewrite_children(node, &mut closure)
}
pub fn rewrite_sort_with_limits_logical(
node: &LogicalNodeEnum,
optimizer: &SortEliminationOptimizer,
stats: &StatsView,
selectivity: &SelectivityEstimator,
notes: &mut Vec<String>,
) -> LogicalNodeEnum {
use LogicalNodeEnum::*;
if let Limit(limit) = node {
if let Sort(sort) = limit.input() {
let offset = limit.offset;
let count = limit.count;
if offset >= 0 && count > 0 {
let input_rows =
estimate_node_output_rows_logical(sort.input(), stats, selectivity);
let total = count + offset.max(0);
if optimizer
.check_topn_conversion_cost(&sort.sort_items, total, input_rows)
.is_some()
{
let topn =
crate::planning::plan::logical::logical_nodes::operation::LogicalTopNNode {
id: crate::planning::plan::core::node_id_generator::next_node_id(),
input: Some(Box::new(sort.input().clone())),
sort_items: sort.sort_items.clone(),
limit: total,
output_var: sort.output_var.clone(),
col_names: sort.col_names.clone(),
column_types: sort.column_types.clone(),
};
notes.push(format!(
"sort: convert sort+limit -> topn (limit={}, offset={})",
count, offset,
));
let mut new_limit = limit.clone();
new_limit.set_input(LogicalNodeEnum::TopN(topn));
return Limit(new_limit);
}
notes.push(format!(
"sort: keep sort+limit (limit={}, offset={})",
count, offset,
));
}
}
}
let mut closure = |child: &LogicalNodeEnum| {
rewrite_sort_with_limits_logical(child, optimizer, stats, selectivity, notes)
};
rewrite_children_logical(node, &mut closure)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizer::cost::CostCalculator;
use crate::optimizer::stats::{StatisticsManager, TagStatistics};
use crate::planning::plan::core::nodes::access::graph_scan_node::ScanVerticesNode;
use crate::planning::plan::core::nodes::operation::sort_node::{LimitNode, SortItem, SortNode};
use std::sync::Arc;
fn test_optimizer() -> SortEliminationOptimizer {
let stats_manager = Arc::new(StatisticsManager::new());
let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
SortEliminationOptimizer::new(cost_calculator)
}
fn setup(vertex_count: u64) -> (SortEliminationOptimizer, Arc<StatisticsManager>) {
let manager = Arc::new(StatisticsManager::new());
let mut tag_stats = TagStatistics::new("person".to_string());
tag_stats.vertex_count = vertex_count;
manager.update_tag_stats("test", tag_stats);
(test_optimizer(), manager)
}
fn build_limit_sort(offset: i64, count: i64) -> PlanNodeEnum {
let mut scan = ScanVerticesNode::new(1, "test");
scan.set_tag("person");
let sort = SortNode::new(
PlanNodeEnum::ScanVertices(scan),
vec![SortItem::column_asc("x".to_string())],
)
.expect("sort should build");
let limit =
LimitNode::new(PlanNodeEnum::Sort(sort), offset, count).expect("limit should build");
PlanNodeEnum::Limit(limit)
}
#[test]
fn converts_offset_limit_sort_to_topn() {
let (optimizer, manager) = setup(100_000);
let view = StatsView::new(&manager, Some("test"));
let selectivity = SelectivityEstimator::new(manager.clone());
let plan = build_limit_sort(10, 100);
let mut notes = Vec::new();
let rewritten = rewrite_sort_with_limits(
&plan,
&optimizer,
&view,
&selectivity,
&CardinalityFeedbackManager::new(),
&mut notes,
);
let PlanNodeEnum::Limit(limit) = &rewritten else {
panic!("expected limit at root");
};
assert!(matches!(limit.input(), PlanNodeEnum::TopN(_)));
if let PlanNodeEnum::TopN(topn) = limit.input() {
assert_eq!(topn.limit(), 110);
}
assert_eq!(notes.len(), 1);
assert!(notes[0].contains("convert sort+limit -> topn"));
}
#[test]
fn sort_without_limit_is_left_untouched() {
let (optimizer, manager) = setup(100_000);
let view = StatsView::new(&manager, Some("test"));
let selectivity = SelectivityEstimator::new(manager.clone());
let mut scan = ScanVerticesNode::new(1, "test");
scan.set_tag("person");
let sort = SortNode::new(
PlanNodeEnum::ScanVertices(scan),
vec![SortItem::column_asc("x".to_string())],
)
.expect("sort should build");
let mut notes = Vec::new();
let rewritten = rewrite_sort_with_limits(
&PlanNodeEnum::Sort(sort),
&optimizer,
&view,
&selectivity,
&CardinalityFeedbackManager::new(),
&mut notes,
);
assert!(matches!(rewritten, PlanNodeEnum::Sort(_)));
assert!(notes.is_empty());
}
}