Skip to content

Commit c2f4eb3

Browse files
authored
Add rewrite context to MV cost function (#38)
* Add rewrite context to MV cost fn * Clarify rewrite root tables
1 parent a16868e commit c2f4eb3

1 file changed

Lines changed: 182 additions & 15 deletions

File tree

src/rewrite/exploitation.rs

Lines changed: 182 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,55 @@ use crate::materialized::cast_to_materialized;
4141
use super::normal_form::SpjNormalForm;
4242
use super::QueryRewriteOptions;
4343

44+
/// Logical rewrite metadata propagated alongside equivalent candidate plans.
45+
#[derive(Debug, Clone, Default, PartialEq, PartialOrd, Eq, Hash)]
46+
pub struct RewriteContext {
47+
root_table_refs: Vec<String>,
48+
}
49+
50+
impl RewriteContext {
51+
/// Create a new rewrite context from the root table refs visible during rewrite.
52+
pub fn new(root_table_refs: Vec<String>) -> Self {
53+
Self { root_table_refs }
54+
}
55+
56+
/// Returns the root table refs that produced this rewrite opportunity.
57+
pub fn root_table_refs(&self) -> &[String] {
58+
&self.root_table_refs
59+
}
60+
}
61+
62+
/// Inputs provided to a cost function when selecting the best candidate plan.
63+
pub struct CostContext<'a> {
64+
candidate_plans: Box<dyn Iterator<Item = &'a dyn ExecutionPlan> + 'a>,
65+
rewrite_context: &'a RewriteContext,
66+
}
67+
68+
impl<'a> CostContext<'a> {
69+
/// Create a new cost context.
70+
pub fn new(
71+
candidate_plans: Box<dyn Iterator<Item = &'a dyn ExecutionPlan> + 'a>,
72+
rewrite_context: &'a RewriteContext,
73+
) -> Self {
74+
Self {
75+
candidate_plans,
76+
rewrite_context,
77+
}
78+
}
79+
80+
/// Consume the context and return the candidate plans iterator.
81+
pub fn into_candidate_plans(self) -> Box<dyn Iterator<Item = &'a dyn ExecutionPlan> + 'a> {
82+
self.candidate_plans
83+
}
84+
85+
/// Returns rewrite metadata for the current candidate set.
86+
pub fn rewrite_context(&self) -> &RewriteContext {
87+
self.rewrite_context
88+
}
89+
}
90+
4491
/// A cost function. Used to evaluate the best physical plan among multiple equivalent choices.
45-
pub type CostFn = Arc<
46-
dyn for<'a> Fn(Box<dyn Iterator<Item = &'a dyn ExecutionPlan> + 'a>) -> Vec<f64> + Send + Sync,
47-
>;
92+
pub type CostFn = Arc<dyn for<'a> Fn(CostContext<'a>) -> Vec<f64> + Send + Sync>;
4893

4994
/// A logical optimizer that generates candidate logical plans in the form of [`OneOf`] nodes.
5095
#[derive(Debug)]
@@ -186,9 +231,10 @@ impl TreeNodeRewriter for ViewMatchingRewriter<'_> {
186231
} else {
187232
Ok(Transformed::new(
188233
LogicalPlan::Extension(Extension {
189-
node: Arc::new(OneOf {
190-
branches: Some(node).into_iter().chain(candidates).collect_vec(),
191-
}),
234+
node: Arc::new(OneOf::with_rewrite_context(
235+
Some(node).into_iter().chain(candidates).collect_vec(),
236+
RewriteContext::new(vec![table_reference.to_string()]),
237+
)),
192238
}),
193239
true,
194240
TreeNodeRecursion::Jump,
@@ -241,9 +287,9 @@ impl ExtensionPlanner for ViewExploitationPlanner {
241287
physical_inputs: &[Arc<dyn ExecutionPlan>],
242288
_session_state: &SessionState,
243289
) -> Result<Option<Arc<dyn ExecutionPlan>>> {
244-
if node.as_any().downcast_ref::<OneOf>().is_none() {
290+
let Some(one_of) = node.as_any().downcast_ref::<OneOf>() else {
245291
return Ok(None);
246-
}
292+
};
247293

248294
// Compare schemas ignoring nullability differences.
249295
// Different table types (FileScanTable, LiveTable, MV) may expose
@@ -282,6 +328,7 @@ impl ExtensionPlanner for ViewExploitationPlanner {
282328
physical_inputs.to_vec(),
283329
None,
284330
Arc::clone(&self.cost),
331+
one_of.rewrite_context().clone(),
285332
)?)))
286333
}
287334
}
@@ -291,12 +338,29 @@ impl ExtensionPlanner for ViewExploitationPlanner {
291338
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash)]
292339
pub struct OneOf {
293340
branches: Vec<LogicalPlan>,
341+
rewrite_context: RewriteContext,
294342
}
295343

296344
impl OneOf {
297345
/// Create a new OneOf node with the given branches.
298346
pub fn new(branches: Vec<LogicalPlan>) -> Self {
299-
Self { branches }
347+
Self::with_rewrite_context(branches, RewriteContext::default())
348+
}
349+
350+
/// Create a new OneOf node with the given branches and rewrite context.
351+
pub fn with_rewrite_context(
352+
branches: Vec<LogicalPlan>,
353+
rewrite_context: RewriteContext,
354+
) -> Self {
355+
Self {
356+
branches,
357+
rewrite_context,
358+
}
359+
}
360+
361+
/// Returns logical rewrite metadata for this candidate set.
362+
pub fn rewrite_context(&self) -> &RewriteContext {
363+
&self.rewrite_context
300364
}
301365
}
302366

@@ -333,7 +397,10 @@ impl UserDefinedLogicalNodeCore for OneOf {
333397
_exprs: Vec<datafusion::prelude::Expr>,
334398
inputs: Vec<LogicalPlan>,
335399
) -> Result<Self> {
336-
Ok(Self { branches: inputs })
400+
Ok(Self {
401+
branches: inputs,
402+
rewrite_context: self.rewrite_context.clone(),
403+
})
337404
}
338405
}
339406

@@ -349,6 +416,7 @@ pub struct OneOfExec {
349416
best: usize,
350417
// Cost function to use in optimization
351418
cost: CostFn,
419+
rewrite_context: RewriteContext,
352420
}
353421

354422
impl std::fmt::Debug for OneOfExec {
@@ -357,6 +425,7 @@ impl std::fmt::Debug for OneOfExec {
357425
.field("candidates", &self.candidates)
358426
.field("required_input_ordering", &self.required_input_ordering)
359427
.field("best", &self.best)
428+
.field("rewrite_context", &self.rewrite_context)
360429
.finish_non_exhaustive()
361430
}
362431
}
@@ -367,23 +436,28 @@ impl OneOfExec {
367436
candidates: Vec<Arc<dyn ExecutionPlan>>,
368437
required_input_ordering: Option<OrderingRequirements>,
369438
cost: CostFn,
439+
rewrite_context: RewriteContext,
370440
) -> Result<Self> {
371441
if candidates.is_empty() {
372442
return Err(DataFusionError::Plan(
373443
"can't create OneOfExec with empty children".to_string(),
374444
));
375445
}
376446

377-
let best = cost(Box::new(candidates.iter().map(|c| c.as_ref())))
378-
.iter()
379-
.position_min_by_key(|&cost| OrderedFloat(*cost))
380-
.unwrap();
447+
let best = cost(CostContext::new(
448+
Box::new(candidates.iter().map(|c| c.as_ref())),
449+
&rewrite_context,
450+
))
451+
.iter()
452+
.position_min_by_key(|&cost| OrderedFloat(*cost))
453+
.unwrap();
381454

382455
Ok(Self {
383456
candidates,
384457
required_input_ordering,
385458
best,
386459
cost,
460+
rewrite_context,
387461
})
388462
}
389463

@@ -393,6 +467,11 @@ impl OneOfExec {
393467
Arc::clone(&self.candidates[self.best])
394468
}
395469

470+
/// Returns rewrite metadata for this candidate set.
471+
pub fn rewrite_context(&self) -> &RewriteContext {
472+
&self.rewrite_context
473+
}
474+
396475
/// Modify this plan's required input ordering.
397476
/// Used for sort pushdown
398477
pub fn with_required_input_ordering(self, requirement: Option<OrderingRequirements>) -> Self {
@@ -444,6 +523,7 @@ impl ExecutionPlan for OneOfExec {
444523
children,
445524
self.required_input_ordering.clone(),
446525
Arc::clone(&self.cost),
526+
self.rewrite_context.clone(),
447527
)?))
448528
}
449529

@@ -473,7 +553,10 @@ impl ExecutionPlan for OneOfExec {
473553

474554
impl DisplayAs for OneOfExec {
475555
fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
476-
let costs = (self.cost)(Box::new(self.children().iter().map(|arc| arc.as_ref())));
556+
let costs = (self.cost)(CostContext::new(
557+
Box::new(self.children().iter().map(|arc| arc.as_ref())),
558+
&self.rewrite_context,
559+
));
477560
match t {
478561
DisplayFormatType::Default | DisplayFormatType::Verbose => {
479562
write!(
@@ -595,3 +678,87 @@ mod tests_nullability {
595678
assert!(!schemas_equal_ignoring_nullability(&a, &b));
596679
}
597680
}
681+
682+
#[cfg(test)]
683+
mod tests_rewrite_context {
684+
use super::*;
685+
use arrow_schema::Schema;
686+
use datafusion::physical_plan::empty::EmptyExec;
687+
use datafusion_expr::LogicalPlanBuilder;
688+
use std::sync::Mutex;
689+
690+
#[test]
691+
fn one_of_preserves_rewrite_context_when_rebuilt() {
692+
let plan = LogicalPlanBuilder::empty(false)
693+
.build()
694+
.expect("empty plan");
695+
let one_of = OneOf::with_rewrite_context(
696+
vec![plan.clone()],
697+
RewriteContext::new(vec!["catalog.schema.root_table".to_string()]),
698+
);
699+
700+
let rebuilt =
701+
UserDefinedLogicalNodeCore::with_exprs_and_inputs(&one_of, vec![], vec![plan])
702+
.expect("rebuild one_of");
703+
704+
assert_eq!(
705+
rebuilt.rewrite_context().root_table_refs(),
706+
["catalog.schema.root_table".to_string()]
707+
);
708+
}
709+
710+
#[test]
711+
fn one_of_exec_passes_rewrite_context_to_cost_function() {
712+
let seen = Arc::new(Mutex::new(Vec::<String>::new()));
713+
let seen_clone = Arc::clone(&seen);
714+
let cost: CostFn = Arc::new(move |ctx| {
715+
*seen_clone.lock().expect("lock seen") =
716+
ctx.rewrite_context().root_table_refs().to_vec();
717+
ctx.into_candidate_plans().map(|_| 1.0).collect()
718+
});
719+
let context = RewriteContext::new(vec!["catalog.schema.root_table".to_string()]);
720+
let schema = Arc::new(Schema::empty());
721+
let candidates = vec![
722+
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>,
723+
Arc::new(EmptyExec::new(schema)) as Arc<dyn ExecutionPlan>,
724+
];
725+
726+
let exec =
727+
OneOfExec::try_new(candidates, None, cost, context.clone()).expect("one_of exec");
728+
729+
assert_eq!(exec.rewrite_context(), &context);
730+
assert_eq!(*seen.lock().expect("lock seen"), context.root_table_refs());
731+
}
732+
733+
#[test]
734+
fn one_of_exec_with_new_children_preserves_rewrite_context() {
735+
let cost: CostFn = Arc::new(|ctx| ctx.into_candidate_plans().map(|_| 1.0).collect());
736+
let context = RewriteContext::new(vec!["catalog.schema.root_table".to_string()]);
737+
let schema = Arc::new(Schema::empty());
738+
let exec = Arc::new(
739+
OneOfExec::try_new(
740+
vec![
741+
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>,
742+
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>,
743+
],
744+
None,
745+
cost,
746+
context.clone(),
747+
)
748+
.expect("one_of exec"),
749+
);
750+
751+
let rebuilt = exec
752+
.with_new_children(vec![
753+
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>,
754+
Arc::new(EmptyExec::new(schema)) as Arc<dyn ExecutionPlan>,
755+
])
756+
.expect("rebuild exec");
757+
let rebuilt = rebuilt
758+
.as_any()
759+
.downcast_ref::<OneOfExec>()
760+
.expect("expected OneOfExec");
761+
762+
assert_eq!(rebuilt.rewrite_context(), &context);
763+
}
764+
}

0 commit comments

Comments
 (0)