Skip to main content

datafusion_physical_plan/
distribution_requirements.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Input distribution requirements for physical execution plans.
19
20use datafusion_common::{Result, internal_err};
21use datafusion_physical_expr::{Distribution, Partitioning, PartitioningSatisfaction};
22
23use crate::execution_plan::{ExecutionPlan, ExecutionPlanProperties, InvariantLevel};
24
25/// Distribution requirements for an [`ExecutionPlan`]'s inputs.
26///
27/// [`InputDistributionRequirements`] describes what distribution an operator
28/// requires from each child.
29///
30/// - [`Self::new`] describes independent per-child requirements.
31/// - [`Self::co_partitioned`] additionally requires child partitions with the
32///   same index to cover compatible key ranges.
33///
34/// For a single-input aggregate:
35///
36/// ```text
37/// AggregateExec
38///   child 0 requirement: KeyPartitioned(group_exprs)
39/// ```
40///
41/// each input partition can aggregate its own key domain independently.
42///
43/// For a partitioned join:
44///
45/// ```text
46/// HashJoinExec
47///   child 0 requirement: KeyPartitioned(left_keys)
48///   child 1 requirement: KeyPartitioned(right_keys)
49///
50///   partition 0: join(left partition 0, right partition 0)
51///   partition 1: join(left partition 1, right partition 1)
52///   partition 2: join(left partition 2, right partition 2)
53/// ```
54///
55/// each child must satisfy its own key requirement. In addition, matching
56/// partition indexes must be safe to process together.
57#[non_exhaustive]
58#[derive(Debug, Clone)]
59pub struct InputDistributionRequirements {
60    /// Per-child distribution requirements, indexed by child position.
61    children: Vec<ChildDistributionRequirement>,
62    /// Child indexes that must also have compatible partition layouts.
63    co_partitioned: Option<Vec<usize>>,
64}
65
66/// Options for checking child distribution satisfaction.
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68pub struct ChildSatisfactionOptions {
69    allow_subset: bool,
70}
71
72impl ChildSatisfactionOptions {
73    /// Create default satisfaction options.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Allow a child partitioning whose key expressions are a subset of the
79    /// required key expressions to satisfy the requirement.
80    pub fn with_allow_subset(mut self, allow_subset: bool) -> Self {
81        self.allow_subset = allow_subset;
82        self
83    }
84
85    /// Whether subset satisfaction is enabled.
86    pub fn allow_subset(&self) -> bool {
87        self.allow_subset
88    }
89}
90
91impl InputDistributionRequirements {
92    /// Create independent per-child requirements.
93    pub fn new(per_child: Vec<Distribution>) -> Self {
94        let children = per_child
95            .into_iter()
96            .map(|distribution| ChildDistributionRequirement { distribution })
97            .collect();
98
99        Self {
100            children,
101            co_partitioned: None,
102        }
103    }
104
105    /// Create a requirement that all children are co-partitioned.
106    ///
107    /// Each child must satisfy its own [`Distribution`]. Matching partition
108    /// indexes are processed together:
109    ///
110    /// ```text
111    /// left:  Range(left.a ASC,  split_points=[10, 20])
112    /// right: Range(right.x ASC, split_points=[10, 20])
113    ///
114    /// partition 0 from both sides contains keys before 10
115    /// partition 1 from both sides contains keys in [10, 20)
116    /// partition 2 from both sides contains keys at/after 20
117    /// ```
118    ///
119    /// If the split points differ, partition `i` from one side no longer covers
120    /// the same key range as partition `i` from the other side.
121    pub fn co_partitioned(per_child: Vec<Distribution>) -> Self {
122        debug_assert!(
123            per_child.len() >= 2,
124            "co-partitioned distribution requirements need at least two children"
125        );
126        let co_partitioned = (0..per_child.len()).collect();
127        let mut result = Self::new(per_child);
128        result.co_partitioned = Some(co_partitioned);
129        result
130    }
131
132    /// Return the per-child distribution requirements.
133    pub fn per_child_distributions(
134        &self,
135    ) -> impl ExactSizeIterator<Item = &Distribution> + '_ {
136        self.children.iter().map(|child| &child.distribution)
137    }
138
139    /// Return the distribution requirement for a child.
140    pub fn child_distribution(&self, child_idx: usize) -> Option<&Distribution> {
141        self.children
142            .get(child_idx)
143            .map(|child| &child.distribution)
144    }
145
146    /// Return the per-child distribution requirements.
147    ///
148    /// WARNING: This intentionally drops any grouped relationship.
149    pub fn into_per_child(self) -> Vec<Distribution> {
150        self.children
151            .into_iter()
152            .map(|child| child.distribution)
153            .collect()
154    }
155
156    /// Returns how a child satisfies its distribution requirement.
157    ///
158    /// This preserves the requirement set's satisfaction policy.
159    pub fn child_satisfaction(
160        &self,
161        child_idx: usize,
162        child: &dyn ExecutionPlan,
163        options: ChildSatisfactionOptions,
164    ) -> Result<PartitioningSatisfaction> {
165        let Some(requirement) = self.children.get(child_idx) else {
166            return internal_err!(
167                "missing distribution requirement for child {child_idx}"
168            );
169        };
170
171        Ok(child.output_partitioning().satisfaction(
172            &requirement.distribution,
173            child.equivalence_properties(),
174            options.allow_subset(),
175        ))
176    }
177
178    /// Return child indexes whose co-partitioning requirements are
179    /// unsatisfied by the provided candidate children.
180    ///
181    /// Independent per-child requirements are intentionally ignored here, use
182    /// [`Self::child_satisfaction`] for those checks. An empty result means all
183    /// co-partitioning requirements are satisfied.
184    #[doc(hidden)]
185    pub fn unsatisfied_co_partitioned_children(
186        &self,
187        plan_name: &str,
188        children: &[&dyn ExecutionPlan],
189    ) -> Result<Vec<usize>> {
190        self.validate_shape(plan_name, children.len())?;
191
192        let Some(co_partitioned) = &self.co_partitioned else {
193            return Ok(vec![]);
194        };
195        if self.co_partitioning_satisfied(co_partitioned, children) {
196            return Ok(vec![]);
197        }
198
199        Ok(co_partitioned.clone())
200    }
201
202    /// Validate the requirements against a plan's children.
203    pub(crate) fn check_invariants<P: ExecutionPlan + ?Sized>(
204        &self,
205        plan: &P,
206        check: InvariantLevel,
207    ) -> Result<()> {
208        let children = plan.children();
209        self.validate_shape(plan.name(), children.len())?;
210
211        let children = children
212            .into_iter()
213            .map(|child| child.as_ref())
214            .collect::<Vec<_>>();
215        if matches!(check, InvariantLevel::Executable)
216            && let Some(co_partitioned) = &self.co_partitioned
217            && !self.co_partitioning_satisfied(co_partitioned, &children)
218        {
219            return internal_err!(
220                "{} requires children {:?} to be co-partitioned",
221                plan.name(),
222                co_partitioned
223            );
224        }
225
226        Ok(())
227    }
228
229    fn validate_shape(&self, plan_name: &str, children_len: usize) -> Result<()> {
230        if self.children.len() != children_len {
231            return internal_err!(
232                "{plan_name}::input_distribution_requirements returned incorrect child count: {} != {}",
233                self.children.len(),
234                children_len
235            );
236        }
237
238        if let Some(co_partitioned) = &self.co_partitioned {
239            if co_partitioned.len() < 2 {
240                return internal_err!(
241                    "{plan_name} has invalid co-partitioning requirement: at least two children are required"
242                );
243            }
244            let mut seen = vec![false; self.children.len()];
245            for &child in co_partitioned {
246                validate_child_index(plan_name, child, self.children.len(), &mut seen)?;
247                if matches!(
248                    self.children[child].distribution,
249                    Distribution::UnspecifiedDistribution
250                ) {
251                    return internal_err!(
252                        "{plan_name} has invalid co-partitioning requirement: child {child} has unspecified distribution"
253                    );
254                }
255            }
256        }
257
258        Ok(())
259    }
260
261    fn co_partitioning_satisfied(
262        &self,
263        co_partitioned: &[usize],
264        children: &[&dyn ExecutionPlan],
265    ) -> bool {
266        let first_idx = co_partitioned[0];
267        let first_requirement = &self.children[first_idx];
268        let first = children[first_idx];
269        let first_partitioning = first.output_partitioning();
270
271        if !first_partitioning
272            .satisfaction(
273                &first_requirement.distribution,
274                first.equivalence_properties(),
275                false,
276            )
277            .is_satisfied()
278        {
279            return false;
280        }
281
282        for &child_idx in co_partitioned.iter().skip(1) {
283            let requirement = &self.children[child_idx];
284            let child = children[child_idx];
285            if !child
286                .output_partitioning()
287                .satisfaction(
288                    &requirement.distribution,
289                    child.equivalence_properties(),
290                    false,
291                )
292                .is_satisfied()
293                || !compatible_co_partitioning_layout(
294                    first_partitioning,
295                    child.output_partitioning(),
296                )
297            {
298                return false;
299            }
300        }
301
302        true
303    }
304}
305
306/// A distribution requirement for a single child.
307#[derive(Debug, Clone)]
308struct ChildDistributionRequirement {
309    distribution: Distribution,
310}
311
312fn validate_child_index(
313    plan_name: &str,
314    child_idx: usize,
315    child_count: usize,
316    seen: &mut [bool],
317) -> Result<()> {
318    if child_idx >= child_count {
319        return internal_err!(
320            "{plan_name} has invalid distribution requirement: child index {child_idx} out of bounds"
321        );
322    }
323    if seen[child_idx] {
324        return internal_err!(
325            "{plan_name} has invalid distribution requirement: child {child_idx} appears more than once"
326        );
327    }
328    seen[child_idx] = true;
329    Ok(())
330}
331
332fn compatible_co_partitioning_layout(
333    first_partitioning: &Partitioning,
334    other_partitioning: &Partitioning,
335) -> bool {
336    if first_partitioning.partition_count() == 1
337        && other_partitioning.partition_count() == 1
338    {
339        return true;
340    }
341
342    if first_partitioning.partition_count() != other_partitioning.partition_count() {
343        return false;
344    }
345
346    match (first_partitioning, other_partitioning) {
347        (Partitioning::Hash(_, _), Partitioning::Hash(_, _)) => true,
348        (Partitioning::Range(left), Partitioning::Range(right)) => {
349            left.split_points() == right.split_points()
350                && left.ordering().len() == right.ordering().len()
351                && left
352                    .ordering()
353                    .iter()
354                    .zip(right.ordering())
355                    .all(|(left, right)| left.options == right.options)
356        }
357        _ => false,
358    }
359}