Skip to main content

datafusion_physical_expr/
partitioning.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//! [`Partitioning`] and [`Distribution`] for `ExecutionPlans`
19
20use crate::{
21    EquivalenceProperties, PhysicalExpr, equivalence::ProjectionMapping,
22    expressions::UnKnownColumn, physical_exprs_contains, physical_exprs_equal,
23};
24pub use datafusion_common::SplitPoint;
25use datafusion_common::{Result, validate_range_split_points};
26use datafusion_physical_expr_common::physical_expr::format_physical_expr_list;
27use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
28#[cfg(feature = "proto")]
29use datafusion_physical_expr_common::sort_expr::{
30    sort_exprs_try_from_proto, sort_exprs_try_to_proto,
31};
32use std::fmt;
33use std::fmt::Display;
34use std::sync::Arc;
35
36/// Output partitioning supported by [`ExecutionPlan`]s.
37///
38/// Calling [`ExecutionPlan::execute`] produce one or more independent streams of
39/// [`RecordBatch`]es in parallel, referred to as partitions. The streams are Rust
40/// `async` [`Stream`]s (a special kind of future). The number of output
41/// partitions varies based on the input and the operation performed.
42///
43/// For example, an `ExecutionPlan` that has output partitioning of 3 will
44/// produce 3 distinct output streams as the result of calling
45/// `ExecutionPlan::execute(0)`, `ExecutionPlan::execute(1)`, and
46/// `ExecutionPlan::execute(2)`, as shown below:
47///
48/// ```text
49///                                                   ...         ...        ...
50///               ...                                  ▲           ▲           ▲
51///                                                    │           │           │
52///                ▲                                   │           │           │
53///                │                                   │           │           │
54///                │                               ┌───┴────┐  ┌───┴────┐  ┌───┴────┐
55///     ┌────────────────────┐                     │ Stream │  │ Stream │  │ Stream │
56///     │   ExecutionPlan    │                     │  (0)   │  │  (1)   │  │  (2)   │
57///     └────────────────────┘                     └────────┘  └────────┘  └────────┘
58///                ▲                                   ▲           ▲           ▲
59///                │                                   │           │           │
60///     ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─                          │           │           │
61///             Input        │                         │           │           │
62///     └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─                          │           │           │
63///                ▲                               ┌ ─ ─ ─ ─   ┌ ─ ─ ─ ─   ┌ ─ ─ ─ ─
64///                │                                 Input  │    Input  │    Input  │
65///                │                               │ Stream    │ Stream    │ Stream
66///                                                   (0)   │     (1)   │     (2)   │
67///               ...                              └ ─ ▲ ─ ─   └ ─ ▲ ─ ─   └ ─ ▲ ─ ─
68///                                                    │           │           │
69///                                                    │           │           │
70///                                                    │           │           │
71///
72/// ExecutionPlan with 1 input                      3 (async) streams, one for each
73/// that has 3 partitions, which itself             output partition
74/// has 3 output partitions
75/// ```
76///
77/// It is common (but not required) that an `ExecutionPlan` has the same number
78/// of input partitions as output partitions. However, some plans have different
79/// numbers such as the `RepartitionExec` that redistributes batches from some
80/// number of inputs to some number of outputs
81///
82/// ```text
83///               ...                                     ...         ...        ...
84///
85///                                                        ▲           ▲           ▲
86///                ▲                                       │           │           │
87///                │                                       │           │           │
88///       ┌────────┴───────────┐                           │           │           │
89///       │  RepartitionExec   │                      ┌────┴───┐  ┌────┴───┐  ┌────┴───┐
90///       └────────────────────┘                      │ Stream │  │ Stream │  │ Stream │
91///                ▲                                  │  (0)   │  │  (1)   │  │  (2)   │
92///                │                                  └────────┘  └────────┘  └────────┘
93///                │                                       ▲           ▲           ▲
94///                ...                                     │           │           │
95///                                                        └──────────┐│┌──────────┘
96///                                                                   │││
97///                                                                   │││
98/// RepartitionExec with 1 input
99/// partition and 3 output partitions                 3 (async) streams, that internally
100///                                                    pull from the same input stream
101///                                                                  ...
102/// ```
103///
104/// # Additional Examples
105///
106/// A simple `FileScanExec` might produce one output stream (partition) for each
107/// file (note the actual DataFusion file scanners can read individual files in
108/// parallel, potentially producing multiple partitions per file)
109///
110/// Plans such as `SortPreservingMerge` produce a single output stream
111/// (1 output partition) by combining some number of input streams (input partitions)
112///
113/// Plans such as `FilterExec` produce the same number of output streams
114/// (partitions) as input streams (partitions).
115///
116/// [`RecordBatch`]: arrow::record_batch::RecordBatch
117/// [`ExecutionPlan::execute`]: https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html#tymethod.execute
118/// [`ExecutionPlan`]: https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html
119/// [`Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html
120#[derive(Debug, Clone)]
121pub enum Partitioning {
122    /// Allocate batches using a round-robin algorithm and the specified number of partitions
123    RoundRobinBatch(usize),
124    /// Allocate rows based on a hash of one of more expressions and the specified number of
125    /// partitions
126    Hash(Vec<Arc<dyn PhysicalExpr>>, usize),
127    /// Partition rows by source-declared ranges
128    Range(RangePartitioning),
129    /// Unknown partitioning scheme with a known number of partitions
130    UnknownPartitioning(usize),
131}
132
133impl Display for Partitioning {
134    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
135        match self {
136            Partitioning::RoundRobinBatch(size) => write!(f, "RoundRobinBatch({size})"),
137            Partitioning::Hash(phy_exprs, size) => {
138                let phy_exprs_str = phy_exprs
139                    .iter()
140                    .map(|e| format!("{e}"))
141                    .collect::<Vec<String>>()
142                    .join(", ");
143                write!(f, "Hash([{phy_exprs_str}], {size})")
144            }
145            Partitioning::Range(range) => write!(f, "{range}"),
146            Partitioning::UnknownPartitioning(size) => {
147                write!(f, "UnknownPartitioning({size})")
148            }
149        }
150    }
151}
152
153/// Physical range partitioning.
154///
155/// [`RangePartitioning`] describes an ordered key space with split points.
156///
157/// - `ordering` defines the partitioning key and ordering.
158/// - `split_points` define the boundaries between adjacent partitions.
159///
160/// Comparisons use the lexicographic order defined by `ordering`, including
161/// `ASC`/`DESC` and null ordering. Split points must be strictly ordered
162/// according to that ordering, and each split point must have one value per
163/// ordering expression. See [`SplitPoint`] for the shared boundary convention.
164///
165/// Like other user-specified data properties such as sortedness, if a source
166/// declares range partitioning, it is responsible for placing each row in the
167/// partition described by the split points. DataFusion will not validate this is
168/// upheld.
169///
170/// For a single range key:
171///
172/// ```text
173/// ordering = [date ASC NULLS LAST]
174/// split_points = [
175///   (2022-01-01),
176///   (2023-01-01),
177/// ]
178///
179/// partition 0: date before 2022-01-01
180/// partition 1: date between 2022-01-01 (inclusive) and 2023-01-01 (exclusive)
181/// partition 2: date at/after 2023-01-01
182/// ```
183///
184/// The same model extends to compound keys.
185/// For `ordering = [time ASC, city ASC]`, split points are ordered
186/// lexicographically by `(time, city)`:
187///
188/// ```text
189/// ordering = [time ASC NULLS LAST, city ASC NULLS LAST]
190/// split_points = [
191///   (2022, Allston),
192///   (2023, Allston),
193/// ]
194///
195/// partition 0: keys before  (2022, Allston)
196/// partition 1: keys between (2022, Allston) and (2023, Allston)
197/// partition 2: keys at/after (2023, Allston)
198/// ```
199///
200/// NOTE: Optimizer and execution behavior for this partitioning is intentionally
201/// not implemented and will be introduced incrementally. See
202/// <https://github.com/apache/datafusion/issues/22395>.
203#[derive(Debug, Clone, PartialEq)]
204pub struct RangePartitioning {
205    /// Ordered partitioning key.
206    ordering: LexOrdering,
207    /// Boundaries between adjacent partitions.
208    split_points: Vec<SplitPoint>,
209}
210
211impl RangePartitioning {
212    /// Creates range partitioning metadata without validating split points.
213    ///
214    /// Use [`Self::try_new`] to validate the contract documented on
215    /// [`RangePartitioning`].
216    pub fn new(ordering: LexOrdering, split_points: Vec<SplitPoint>) -> Self {
217        Self {
218            ordering,
219            split_points,
220        }
221    }
222
223    /// Creates range partitioning metadata and validates split point shape and
224    /// ordering.
225    pub fn try_new(ordering: LexOrdering, split_points: Vec<SplitPoint>) -> Result<Self> {
226        validate_range_split_points(
227            &split_points,
228            &ordering
229                .iter()
230                .map(|sort_expr| sort_expr.options)
231                .collect::<Vec<_>>(),
232        )?;
233        Ok(Self::new(ordering, split_points))
234    }
235
236    /// Returns the ordering that defines the range key.
237    pub fn ordering(&self) -> &LexOrdering {
238        &self.ordering
239    }
240
241    /// Returns the ordered split points between partitions.
242    pub fn split_points(&self) -> &[SplitPoint] {
243        &self.split_points
244    }
245
246    /// Returns the number of partitions.
247    pub fn partition_count(&self) -> usize {
248        self.split_points.len() + 1
249    }
250
251    /// Calculates the range partitioning after applying the given projection.
252    ///
253    /// Returns `None` if any range key cannot be projected or if projection
254    /// collapses distinct range keys into duplicate output expressions.
255    fn project(
256        &self,
257        mapping: &ProjectionMapping,
258        input_eq_properties: &EquivalenceProperties,
259    ) -> Option<Self> {
260        let exprs = self
261            .ordering
262            .iter()
263            .map(|sort_expr| Arc::clone(&sort_expr.expr))
264            .collect::<Vec<_>>();
265        let projected_exprs = input_eq_properties
266            .project_expressions(&exprs, mapping)
267            .collect::<Option<Vec<_>>>()?;
268        let sort_exprs = self
269            .ordering
270            .iter()
271            .zip(projected_exprs)
272            .map(|(sort_expr, expr)| PhysicalSortExpr::new(expr, sort_expr.options))
273            .collect::<Vec<_>>();
274        let ordering = LexOrdering::new(sort_exprs)?;
275        if ordering.len() != self.ordering.len() {
276            return None;
277        }
278
279        Some(Self {
280            ordering,
281            split_points: self.split_points.clone(),
282        })
283    }
284}
285
286impl Display for RangePartitioning {
287    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
288        let split_points = format_range_split_points(&self.split_points);
289        write!(
290            f,
291            "Range([{}], [{}], {})",
292            self.ordering,
293            split_points,
294            self.partition_count()
295        )
296    }
297}
298
299fn format_range_split_points(split_points: &[SplitPoint]) -> String {
300    split_points
301        .iter()
302        .map(ToString::to_string)
303        .collect::<Vec<_>>()
304        .join(", ")
305}
306
307fn equivalent_exprs(
308    left: &[Arc<dyn PhysicalExpr>],
309    right: &[Arc<dyn PhysicalExpr>],
310    eq_properties: &EquivalenceProperties,
311) -> bool {
312    if physical_exprs_equal(left, right) {
313        return true;
314    }
315
316    let eq_groups = eq_properties.eq_group();
317    if eq_groups.is_empty() {
318        return false;
319    }
320
321    let normalized_left = normalize_exprs(left, eq_properties);
322    let normalized_right = normalize_exprs(right, eq_properties);
323
324    physical_exprs_equal(&normalized_left, &normalized_right)
325}
326
327fn normalize_exprs(
328    exprs: &[Arc<dyn PhysicalExpr>],
329    eq_properties: &EquivalenceProperties,
330) -> Vec<Arc<dyn PhysicalExpr>> {
331    let eq_groups = eq_properties.eq_group();
332    exprs
333        .iter()
334        .map(|expr| eq_groups.normalize_expr(Arc::clone(expr)))
335        .collect()
336}
337
338/// Represents how a [`Partitioning`] satisfies a [`Distribution`] requirement.
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum PartitioningSatisfaction {
341    /// The partitioning does not satisfy the distribution requirement
342    NotSatisfied,
343    /// The partitioning exactly matches the distribution requirement
344    Exact,
345    /// The partitioning satisfies the distribution requirement via subset logic
346    Subset,
347}
348
349impl PartitioningSatisfaction {
350    pub fn is_satisfied(&self) -> bool {
351        matches!(self, Self::Exact | Self::Subset)
352    }
353
354    pub fn is_subset(&self) -> bool {
355        *self == Self::Subset
356    }
357}
358
359impl Partitioning {
360    /// Returns the number of partitions in this partitioning scheme
361    pub fn partition_count(&self) -> usize {
362        use Partitioning::*;
363        match self {
364            RoundRobinBatch(n) | Hash(_, n) | UnknownPartitioning(n) => *n,
365            Range(range) => range.partition_count(),
366        }
367    }
368
369    /// Returns true if `subset_exprs` is a subset of `exprs`.
370    /// For example: Hash(a, b) is subset of Hash(a) since a partition with all occurrences of
371    /// a distinct (a) must also contain all occurrences of a distinct (a, b) with the same (a).
372    fn is_subset_partitioning(
373        subset_exprs: &[Arc<dyn PhysicalExpr>],
374        superset_exprs: &[Arc<dyn PhysicalExpr>],
375    ) -> bool {
376        // Require strict subset: fewer expressions, not equal
377        if subset_exprs.is_empty() || subset_exprs.len() >= superset_exprs.len() {
378            return false;
379        }
380
381        subset_exprs
382            .iter()
383            .all(|subset_expr| physical_exprs_contains(superset_exprs, subset_expr))
384    }
385
386    #[deprecated(since = "52.0.0", note = "Use satisfaction instead")]
387    pub fn satisfy(
388        &self,
389        required: &Distribution,
390        eq_properties: &EquivalenceProperties,
391    ) -> bool {
392        self.satisfaction(required, eq_properties, false)
393            == PartitioningSatisfaction::Exact
394    }
395
396    /// Returns how this [`Partitioning`] satisfies the partitioning scheme mandated
397    /// by the `required` [`Distribution`].
398    #[expect(
399        deprecated,
400        reason = "HashPartitioned is accepted during the KeyPartitioned migration"
401    )]
402    pub fn satisfaction(
403        &self,
404        required: &Distribution,
405        eq_properties: &EquivalenceProperties,
406        allow_subset: bool,
407    ) -> PartitioningSatisfaction {
408        match required {
409            Distribution::UnspecifiedDistribution => PartitioningSatisfaction::Exact,
410            Distribution::SinglePartition if self.partition_count() == 1 => {
411                PartitioningSatisfaction::Exact
412            }
413            // When partition count is 1, key partitioning is satisfied.
414            Distribution::HashPartitioned(_) | Distribution::KeyPartitioned(_)
415                if self.partition_count() == 1 =>
416            {
417                PartitioningSatisfaction::Exact
418            }
419            Distribution::HashPartitioned(required_exprs)
420            | Distribution::KeyPartitioned(required_exprs) => match self {
421                // Here we do not check the partition count for hash partitioning and assumes the partition count
422                // and hash functions in the system are the same. In future if we plan to support storage partition-wise joins,
423                // then we need to have the partition count and hash functions validation.
424                Partitioning::Hash(partition_exprs, _) => Self::key_satisfaction(
425                    partition_exprs,
426                    required_exprs,
427                    eq_properties,
428                    allow_subset,
429                ),
430                Partitioning::Range(range) => {
431                    let partition_exprs = range
432                        .ordering()
433                        .iter()
434                        .map(|sort_expr| Arc::clone(&sort_expr.expr))
435                        .collect::<Vec<_>>();
436                    Self::key_satisfaction(
437                        &partition_exprs,
438                        required_exprs,
439                        eq_properties,
440                        allow_subset,
441                    )
442                }
443                Partitioning::RoundRobinBatch(_)
444                | Partitioning::UnknownPartitioning(_) => {
445                    PartitioningSatisfaction::NotSatisfied
446                }
447            },
448            Distribution::SinglePartition => PartitioningSatisfaction::NotSatisfied,
449        }
450    }
451
452    fn key_satisfaction(
453        partition_exprs: &[Arc<dyn PhysicalExpr>],
454        required_exprs: &[Arc<dyn PhysicalExpr>],
455        eq_properties: &EquivalenceProperties,
456        allow_subset: bool,
457    ) -> PartitioningSatisfaction {
458        if partition_exprs.is_empty() || required_exprs.is_empty() {
459            return PartitioningSatisfaction::NotSatisfied;
460        }
461
462        if equivalent_exprs(required_exprs, partition_exprs, eq_properties) {
463            return PartitioningSatisfaction::Exact;
464        }
465
466        let eq_groups = eq_properties.eq_group();
467        if !eq_groups.is_empty() {
468            if allow_subset {
469                let normalized_partition_exprs =
470                    normalize_exprs(partition_exprs, eq_properties);
471                let normalized_required_exprs =
472                    normalize_exprs(required_exprs, eq_properties);
473                if Self::is_subset_partitioning(
474                    &normalized_partition_exprs,
475                    &normalized_required_exprs,
476                ) {
477                    return PartitioningSatisfaction::Subset;
478                }
479            }
480        } else if allow_subset
481            && Self::is_subset_partitioning(partition_exprs, required_exprs)
482        {
483            return PartitioningSatisfaction::Subset;
484        }
485
486        PartitioningSatisfaction::NotSatisfied
487    }
488
489    /// Calculate the output partitioning after applying the given projection.
490    pub fn project(
491        &self,
492        mapping: &ProjectionMapping,
493        input_eq_properties: &EquivalenceProperties,
494    ) -> Self {
495        match self {
496            Partitioning::Hash(exprs, part) => {
497                let normalized_exprs = input_eq_properties
498                    .project_expressions(exprs, mapping)
499                    .zip(exprs)
500                    .map(|(proj_expr, expr)| {
501                        proj_expr.unwrap_or_else(|| {
502                            Arc::new(UnKnownColumn::new(&expr.to_string()))
503                        })
504                    })
505                    .collect();
506                Partitioning::Hash(normalized_exprs, *part)
507            }
508            Partitioning::Range(range) => {
509                if let Some(projected) = range.project(mapping, input_eq_properties) {
510                    Partitioning::Range(projected)
511                } else {
512                    Partitioning::UnknownPartitioning(range.partition_count())
513                }
514            }
515            Partitioning::RoundRobinBatch(_) | Partitioning::UnknownPartitioning(_) => {
516                self.clone()
517            }
518        }
519    }
520}
521
522/// Protobuf conversions for [`Partitioning`].
523///
524/// Child expressions (hash keys, range orderings) and `ScalarValue` split
525/// points are (de)serialized through the expression-level context, so this is
526/// the single copy of the partitioning wire format: `RepartitionExec` and
527/// `datafusion-proto`'s central serializer route through it, and the remaining
528/// per-plan migrations (`FileScanConfig` and friends) are meant to do the same
529/// rather than grow another copy.
530///
531/// [`protobuf::Partitioning`]: datafusion_proto_models::protobuf::Partitioning
532#[cfg(feature = "proto")]
533impl Partitioning {
534    /// Serialize this partitioning into its protobuf representation.
535    pub fn try_to_proto(
536        &self,
537        ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
538    ) -> Result<datafusion_proto_models::protobuf::Partitioning> {
539        use datafusion_proto_models::protobuf;
540
541        let partition_method = match self {
542            Partitioning::RoundRobinBatch(n) => {
543                protobuf::partitioning::PartitionMethod::RoundRobin(wire_partition_count(
544                    *n,
545                )?)
546            }
547            Partitioning::Hash(exprs, n) => {
548                protobuf::partitioning::PartitionMethod::Hash(
549                    protobuf::PhysicalHashRepartition {
550                        hash_expr: ctx.encode_children_expressions(exprs)?,
551                        partition_count: wire_partition_count(*n)?,
552                    },
553                )
554            }
555            Partitioning::Range(range) => {
556                let sort_expr = sort_exprs_try_to_proto(range.ordering().iter(), ctx)?;
557                let split_point = range
558                    .split_points()
559                    .iter()
560                    .map(|split_point| {
561                        let value = split_point
562                            .values()
563                            .iter()
564                            .map(|value| value.try_into().map_err(Into::into))
565                            .collect::<Result<Vec<_>>>()?;
566                        Ok(protobuf::PhysicalRangeSplitPoint { value })
567                    })
568                    .collect::<Result<Vec<_>>>()?;
569                protobuf::partitioning::PartitionMethod::Range(
570                    protobuf::PhysicalRangePartitioning {
571                        sort_expr,
572                        split_point,
573                    },
574                )
575            }
576            Partitioning::UnknownPartitioning(n) => {
577                protobuf::partitioning::PartitionMethod::Unknown(wire_partition_count(
578                    *n,
579                )?)
580            }
581        };
582        Ok(protobuf::Partitioning {
583            partition_method: Some(partition_method),
584        })
585    }
586
587    /// Reconstruct a [`Partitioning`] from its protobuf representation.
588    ///
589    /// Returns `Ok(None)` when the message carries no `partition_method`, which
590    /// the wire format uses to mean "no output partitioning declared"; callers
591    /// for which it is required should turn that into their own error.
592    pub fn try_from_proto(
593        node: &datafusion_proto_models::protobuf::Partitioning,
594        ctx: &datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx<'_>,
595    ) -> Result<Option<Self>> {
596        use datafusion_common::{ScalarValue, internal_datafusion_err, internal_err};
597        use datafusion_proto_models::protobuf;
598
599        let Some(partition_method) = node.partition_method.as_ref() else {
600            return Ok(None);
601        };
602        let partitioning = match partition_method {
603            protobuf::partitioning::PartitionMethod::RoundRobin(n) => {
604                Partitioning::RoundRobinBatch(partition_count(*n)?)
605            }
606            protobuf::partitioning::PartitionMethod::Hash(hash) => {
607                let exprs = hash
608                    .hash_expr
609                    .iter()
610                    .map(|expr| ctx.decode(expr))
611                    .collect::<Result<Vec<_>>>()?;
612                Partitioning::Hash(exprs, partition_count(hash.partition_count)?)
613            }
614            protobuf::partitioning::PartitionMethod::Unknown(n) => {
615                Partitioning::UnknownPartitioning(partition_count(*n)?)
616            }
617            protobuf::partitioning::PartitionMethod::Range(range) => {
618                let sort_exprs = sort_exprs_try_from_proto(&range.sort_expr, ctx)?;
619                let sort_expr_count = sort_exprs.len();
620                let ordering = LexOrdering::new(sort_exprs).ok_or_else(|| {
621                    internal_datafusion_err!(
622                        "Range partitioning requires non-empty ordering"
623                    )
624                })?;
625                if ordering.len() != sort_expr_count {
626                    return internal_err!(
627                        "Range partitioning ordering must not contain duplicate expressions"
628                    );
629                }
630                let split_points = range
631                    .split_point
632                    .iter()
633                    .map(|split_point| {
634                        let values = split_point
635                            .value
636                            .iter()
637                            .map(|value| ScalarValue::try_from(value).map_err(Into::into))
638                            .collect::<Result<Vec<_>>>()?;
639                        Ok(SplitPoint::new(values))
640                    })
641                    .collect::<Result<Vec<_>>>()?;
642                Partitioning::Range(RangePartitioning::try_new(ordering, split_points)?)
643            }
644        };
645        Ok(Some(partitioning))
646    }
647}
648
649/// Narrow a wire partition count to `usize`.
650#[cfg(feature = "proto")]
651fn partition_count(count: u64) -> Result<usize> {
652    usize::try_from(count).map_err(|_| {
653        datafusion_common::internal_datafusion_err!(
654            "Partition count {count} exceeds usize::MAX"
655        )
656    })
657}
658
659/// Widen a partition count to its `u64` wire representation.
660///
661/// The mirror of [`partition_count`]: an out-of-range count is an error on both
662/// sides rather than a silent truncation on the way out.
663#[cfg(feature = "proto")]
664fn wire_partition_count(count: usize) -> Result<u64> {
665    u64::try_from(count).map_err(|_| {
666        datafusion_common::internal_datafusion_err!(
667            "Partition count {count} exceeds u64::MAX"
668        )
669    })
670}
671
672impl PartialEq for Partitioning {
673    fn eq(&self, other: &Partitioning) -> bool {
674        match (self, other) {
675            (
676                Partitioning::RoundRobinBatch(count1),
677                Partitioning::RoundRobinBatch(count2),
678            ) if count1 == count2 => true,
679            (Partitioning::Hash(exprs1, count1), Partitioning::Hash(exprs2, count2))
680                if physical_exprs_equal(exprs1, exprs2) && (count1 == count2) =>
681            {
682                true
683            }
684            (Partitioning::Range(left), Partitioning::Range(right)) => left == right,
685            _ => false,
686        }
687    }
688}
689
690/// How data is distributed amongst partitions. See [`Partitioning`] for more
691/// details.
692#[derive(Debug, Clone)]
693pub enum Distribution {
694    /// Unspecified distribution
695    UnspecifiedDistribution,
696    /// A single partition is required
697    SinglePartition,
698    /// Deprecated historical name for [`Distribution::KeyPartitioned`].
699    /// See <https://github.com/apache/datafusion/issues/23236> for details.
700    #[deprecated(since = "55.0.0", note = "Use Distribution::KeyPartitioned")]
701    HashPartitioned(Vec<Arc<dyn PhysicalExpr>>),
702    /// Requires children to be distributed in such a way that the same
703    /// values of the keys end up in the same partition
704    KeyPartitioned(Vec<Arc<dyn PhysicalExpr>>),
705}
706
707#[expect(
708    deprecated,
709    reason = "HashPartitioned is accepted during the KeyPartitioned migration"
710)]
711impl Distribution {
712    /// Creates a `Partitioning` that satisfies this `Distribution`
713    pub fn create_partitioning(self, partition_count: usize) -> Partitioning {
714        match self {
715            Distribution::UnspecifiedDistribution => {
716                Partitioning::UnknownPartitioning(partition_count)
717            }
718            Distribution::SinglePartition => Partitioning::UnknownPartitioning(1),
719            Distribution::HashPartitioned(expr) | Distribution::KeyPartitioned(expr) => {
720                Partitioning::Hash(expr, partition_count)
721            }
722        }
723    }
724}
725
726#[expect(
727    deprecated,
728    reason = "HashPartitioned display is preserved during the KeyPartitioned migration"
729)]
730impl Display for Distribution {
731    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
732        match self {
733            Distribution::UnspecifiedDistribution => write!(f, "Unspecified"),
734            Distribution::SinglePartition => write!(f, "SinglePartition"),
735            Distribution::HashPartitioned(exprs) => {
736                write!(f, "HashPartitioned[{}])", format_physical_expr_list(exprs))
737            }
738            Distribution::KeyPartitioned(exprs) => {
739                write!(f, "KeyPartitioned[{}])", format_physical_expr_list(exprs))
740            }
741        }
742    }
743}
744
745#[cfg(test)]
746mod tests {
747
748    use super::*;
749    use crate::expressions::Column;
750    use crate::projection::ProjectionTargets;
751
752    use arrow::compute::SortOptions;
753    use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
754    use datafusion_common::{Result, ScalarValue};
755
756    struct PartitioningTestFixture {
757        schema: SchemaRef,
758        cols: Vec<Arc<dyn PhysicalExpr>>,
759        eq_properties: EquivalenceProperties,
760    }
761
762    impl PartitioningTestFixture {
763        fn new(fields: Vec<(&str, DataType)>) -> Result<Self> {
764            let schema = Arc::new(Schema::new(
765                fields
766                    .iter()
767                    .map(|(name, data_type)| Field::new(*name, data_type.clone(), false))
768                    .collect::<Vec<_>>(),
769            ));
770            let cols = fields
771                .iter()
772                .map(|(name, _)| {
773                    Ok(Arc::new(Column::new_with_schema(name, &schema)?)
774                        as Arc<dyn PhysicalExpr>)
775                })
776                .collect::<Result<_>>()?;
777            let eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
778
779            Ok(Self {
780                schema,
781                cols,
782                eq_properties,
783            })
784        }
785
786        fn int64(names: &[&str]) -> Result<Self> {
787            Self::new(names.iter().map(|name| (*name, DataType::Int64)).collect())
788        }
789
790        fn col(&self, index: usize) -> Arc<dyn PhysicalExpr> {
791            Arc::clone(&self.cols[index])
792        }
793
794        fn cols(
795            &self,
796            indices: impl IntoIterator<Item = usize>,
797        ) -> Vec<Arc<dyn PhysicalExpr>> {
798            indices.into_iter().map(|index| self.col(index)).collect()
799        }
800
801        fn hash_partitioning(
802            &self,
803            indices: impl IntoIterator<Item = usize>,
804            partition_count: usize,
805        ) -> Partitioning {
806            Partitioning::Hash(self.cols(indices), partition_count)
807        }
808
809        fn key_distribution(
810            &self,
811            indices: impl IntoIterator<Item = usize>,
812        ) -> Distribution {
813            Distribution::KeyPartitioned(self.cols(indices))
814        }
815
816        fn range_sort_expr(
817            &self,
818            index: usize,
819            options: SortOptions,
820        ) -> PhysicalSortExpr {
821            PhysicalSortExpr::new(self.col(index), options)
822        }
823
824        fn range_ordering(
825            &self,
826            indices: impl IntoIterator<Item = usize>,
827        ) -> LexOrdering {
828            LexOrdering::new(
829                indices
830                    .into_iter()
831                    .map(|index| PhysicalSortExpr::new_default(self.col(index))),
832            )
833            .expect("ordering must not be empty")
834        }
835
836        fn range(
837            &self,
838            indices: impl IntoIterator<Item = usize>,
839            split_points: Vec<SplitPoint>,
840        ) -> RangePartitioning {
841            RangePartitioning::try_new(self.range_ordering(indices), split_points)
842                .expect("test range partitioning should be valid")
843        }
844
845        fn range_partitioning(
846            &self,
847            indices: impl IntoIterator<Item = usize>,
848            split_points: Vec<SplitPoint>,
849        ) -> Partitioning {
850            Partitioning::Range(self.range(indices, split_points))
851        }
852
853        fn range_partitioning_with_ordering(
854            &self,
855            ordering: LexOrdering,
856            split_points: Vec<SplitPoint>,
857        ) -> Partitioning {
858            Partitioning::Range(
859                RangePartitioning::try_new(ordering, split_points)
860                    .expect("test range partitioning should be valid"),
861            )
862        }
863    }
864
865    fn assert_satisfaction(
866        desc: &str,
867        partitioning: &Partitioning,
868        required: &Distribution,
869        eq_properties: &EquivalenceProperties,
870        expected_with_subset: PartitioningSatisfaction,
871        expected_without_subset: PartitioningSatisfaction,
872    ) {
873        assert_eq!(
874            partitioning.satisfaction(required, eq_properties, true),
875            expected_with_subset,
876            "Failed for {desc} with subset enabled"
877        );
878        assert_eq!(
879            partitioning.satisfaction(required, eq_properties, false),
880            expected_without_subset,
881            "Failed for {desc} with subset disabled"
882        );
883    }
884
885    #[test]
886    #[expect(
887        deprecated,
888        reason = "test intentionally covers deprecated HashPartitioned compatibility"
889    )]
890    fn partitioning_satisfy_distribution() -> Result<()> {
891        let fixture = PartitioningTestFixture::new(vec![
892            ("column_1", DataType::Int64),
893            ("column_2", DataType::Utf8),
894        ])?;
895
896        let distribution_types = vec![
897            Distribution::UnspecifiedDistribution,
898            Distribution::SinglePartition,
899            Distribution::HashPartitioned(fixture.cols([0, 1])),
900            fixture.key_distribution([0, 1]),
901        ];
902
903        let single_partition = Partitioning::UnknownPartitioning(1);
904        let unspecified_partition = Partitioning::UnknownPartitioning(10);
905        let round_robin_partition = Partitioning::RoundRobinBatch(10);
906        let hash_partition1 = fixture.hash_partitioning([0, 1], 10);
907        let hash_partition2 = fixture.hash_partitioning([1, 0], 10);
908
909        for distribution in distribution_types {
910            let result = (
911                single_partition
912                    .satisfaction(&distribution, &fixture.eq_properties, true)
913                    .is_satisfied(),
914                unspecified_partition
915                    .satisfaction(&distribution, &fixture.eq_properties, true)
916                    .is_satisfied(),
917                round_robin_partition
918                    .satisfaction(&distribution, &fixture.eq_properties, true)
919                    .is_satisfied(),
920                hash_partition1
921                    .satisfaction(&distribution, &fixture.eq_properties, true)
922                    .is_satisfied(),
923                hash_partition2
924                    .satisfaction(&distribution, &fixture.eq_properties, true)
925                    .is_satisfied(),
926            );
927
928            match distribution {
929                Distribution::UnspecifiedDistribution => {
930                    assert_eq!(result, (true, true, true, true, true))
931                }
932                Distribution::SinglePartition => {
933                    assert_eq!(result, (true, false, false, false, false))
934                }
935                Distribution::HashPartitioned(_) | Distribution::KeyPartitioned(_) => {
936                    assert_eq!(result, (true, false, false, true, false))
937                }
938            }
939        }
940
941        Ok(())
942    }
943
944    #[test]
945    #[expect(
946        deprecated,
947        reason = "test intentionally covers deprecated HashPartitioned compatibility"
948    )]
949    fn deprecated_hash_partitioned_matches_key_partitioned() -> Result<()> {
950        let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
951        let partitioning = fixture.hash_partitioning([0, 1], 4);
952        let hash_distribution = Distribution::HashPartitioned(fixture.cols([0, 1]));
953        let key_distribution = fixture.key_distribution([0, 1]);
954
955        assert_eq!(
956            partitioning.satisfaction(&hash_distribution, &fixture.eq_properties, false),
957            partitioning.satisfaction(&key_distribution, &fixture.eq_properties, false)
958        );
959        assert_eq!(
960            hash_distribution.create_partitioning(4),
961            key_distribution.create_partitioning(4)
962        );
963
964        Ok(())
965    }
966
967    #[test]
968    fn hash_partitioning_key_distribution_satisfaction() -> Result<()> {
969        let fixture = PartitioningTestFixture::int64(&["a", "b", "c"])?;
970        let unknown: Arc<dyn PhysicalExpr> = Arc::new(UnKnownColumn::new("dropped"));
971
972        let test_cases = vec![
973            (
974                "exact: KeyPartitioned([a, b]) satisfied by Hash([a, b])",
975                fixture.hash_partitioning([0, 1], 4),
976                fixture.key_distribution([0, 1]),
977                PartitioningSatisfaction::Exact,
978                PartitioningSatisfaction::Exact,
979            ),
980            (
981                "subset: KeyPartitioned([a, b]) satisfied by Hash([a])",
982                fixture.hash_partitioning([0], 4),
983                fixture.key_distribution([0, 1]),
984                PartitioningSatisfaction::Subset,
985                PartitioningSatisfaction::NotSatisfied,
986            ),
987            (
988                "subset: KeyPartitioned([a, b, c]) satisfied by Hash([b])",
989                fixture.hash_partitioning([1], 4),
990                fixture.key_distribution([0, 1, 2]),
991                PartitioningSatisfaction::Subset,
992                PartitioningSatisfaction::NotSatisfied,
993            ),
994            (
995                "subset reordered: KeyPartitioned([a, b, c]) satisfied by Hash([b, a])",
996                fixture.hash_partitioning([1, 0], 4),
997                fixture.key_distribution([0, 1, 2]),
998                PartitioningSatisfaction::Subset,
999                PartitioningSatisfaction::NotSatisfied,
1000            ),
1001            (
1002                "superset: KeyPartitioned([a]) not satisfied by Hash([a, b])",
1003                fixture.hash_partitioning([0, 1], 4),
1004                fixture.key_distribution([0]),
1005                PartitioningSatisfaction::NotSatisfied,
1006                PartitioningSatisfaction::NotSatisfied,
1007            ),
1008            (
1009                "superset: KeyPartitioned([a, b]) not satisfied by Hash([a, b, c])",
1010                fixture.hash_partitioning([0, 1, 2], 4),
1011                fixture.key_distribution([0, 1]),
1012                PartitioningSatisfaction::NotSatisfied,
1013                PartitioningSatisfaction::NotSatisfied,
1014            ),
1015            (
1016                "partial overlap: KeyPartitioned([a, b]) not satisfied by Hash([a, c])",
1017                fixture.hash_partitioning([0, 2], 4),
1018                fixture.key_distribution([0, 1]),
1019                PartitioningSatisfaction::NotSatisfied,
1020                PartitioningSatisfaction::NotSatisfied,
1021            ),
1022            (
1023                "no overlap: KeyPartitioned([b, c]) not satisfied by Hash([a])",
1024                fixture.hash_partitioning([0], 4),
1025                fixture.key_distribution([1, 2]),
1026                PartitioningSatisfaction::NotSatisfied,
1027                PartitioningSatisfaction::NotSatisfied,
1028            ),
1029            (
1030                "unknown partition expr",
1031                Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
1032                fixture.key_distribution([0, 1]),
1033                PartitioningSatisfaction::NotSatisfied,
1034                PartitioningSatisfaction::NotSatisfied,
1035            ),
1036            (
1037                "unknown required expr",
1038                fixture.hash_partitioning([0, 1], 4),
1039                Distribution::KeyPartitioned(vec![Arc::clone(&unknown)]),
1040                PartitioningSatisfaction::NotSatisfied,
1041                PartitioningSatisfaction::NotSatisfied,
1042            ),
1043            (
1044                "same unknown expr",
1045                Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
1046                Distribution::KeyPartitioned(vec![Arc::clone(&unknown)]),
1047                PartitioningSatisfaction::NotSatisfied,
1048                PartitioningSatisfaction::NotSatisfied,
1049            ),
1050            (
1051                "unknown partition expr is not a valid subset",
1052                Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
1053                Distribution::KeyPartitioned(vec![Arc::clone(&unknown), fixture.col(0)]),
1054                PartitioningSatisfaction::NotSatisfied,
1055                PartitioningSatisfaction::NotSatisfied,
1056            ),
1057            (
1058                "empty hash partitioning",
1059                Partitioning::Hash(vec![], 4),
1060                fixture.key_distribution([0]),
1061                PartitioningSatisfaction::NotSatisfied,
1062                PartitioningSatisfaction::NotSatisfied,
1063            ),
1064            (
1065                "empty key distribution",
1066                fixture.hash_partitioning([0], 4),
1067                Distribution::KeyPartitioned(vec![]),
1068                PartitioningSatisfaction::NotSatisfied,
1069                PartitioningSatisfaction::NotSatisfied,
1070            ),
1071        ];
1072
1073        for (desc, partition, required, expected_with_subset, expected_without_subset) in
1074            test_cases
1075        {
1076            assert_satisfaction(
1077                desc,
1078                &partition,
1079                &required,
1080                &fixture.eq_properties,
1081                expected_with_subset,
1082                expected_without_subset,
1083            );
1084        }
1085
1086        Ok(())
1087    }
1088
1089    fn int_split_point(values: impl IntoIterator<Item = i64>) -> SplitPoint {
1090        SplitPoint::new(
1091            values
1092                .into_iter()
1093                .map(|value| ScalarValue::Int64(Some(value)))
1094                .collect(),
1095        )
1096    }
1097
1098    fn assert_range_try_new_error(
1099        ordering: LexOrdering,
1100        split_points: Vec<SplitPoint>,
1101        expected: &str,
1102    ) {
1103        let error = RangePartitioning::try_new(ordering, split_points)
1104            .unwrap_err()
1105            .to_string();
1106        assert!(error.contains(expected), "{error}");
1107    }
1108
1109    #[test]
1110    fn test_range_partitioning_metadata() -> Result<()> {
1111        let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
1112
1113        let range_partitioning =
1114            fixture.range([0], vec![int_split_point([10]), int_split_point([20])]);
1115        assert_eq!(range_partitioning.ordering()[0].to_string(), "a@0 ASC");
1116        assert_eq!(
1117            range_partitioning.split_points(),
1118            &[int_split_point([10]), int_split_point([20])]
1119        );
1120        let partitioning = Partitioning::Range(range_partitioning);
1121
1122        assert_eq!(partitioning.partition_count(), 3);
1123        assert_eq!(
1124            partitioning.to_string(),
1125            "Range([a@0 ASC], [(10), (20)], 3)"
1126        );
1127
1128        Ok(())
1129    }
1130
1131    #[test]
1132    fn test_range_partitioning_try_new_validates_split_points() -> Result<()> {
1133        let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
1134        let asc_a = fixture.range_ordering([0]);
1135        let ordering_ab = fixture.range_ordering([0, 1]);
1136
1137        assert_range_try_new_error(
1138            ordering_ab.clone(),
1139            vec![int_split_point([10])],
1140            "split point 0 has width 1, but ordering has width 2",
1141        );
1142
1143        RangePartitioning::try_new(
1144            [fixture.range_sort_expr(0, SortOptions::new(true, false))].into(),
1145            vec![int_split_point([20]), int_split_point([10])],
1146        )?;
1147
1148        assert_range_try_new_error(
1149            asc_a,
1150            vec![int_split_point([20]), int_split_point([10])],
1151            "split points must be strictly ordered",
1152        );
1153
1154        assert_range_try_new_error(
1155            [fixture.range_sort_expr(0, SortOptions::new(false, false))].into(),
1156            vec![
1157                SplitPoint::new(vec![ScalarValue::Int64(None)]),
1158                int_split_point([10]),
1159            ],
1160            "split points must be strictly ordered",
1161        );
1162
1163        RangePartitioning::try_new(
1164            ordering_ab.clone(),
1165            vec![int_split_point([10, 20]), int_split_point([10, 30])],
1166        )?;
1167
1168        assert_range_try_new_error(
1169            ordering_ab,
1170            vec![int_split_point([10, 30]), int_split_point([10, 20])],
1171            "split points must be strictly ordered",
1172        );
1173
1174        Ok(())
1175    }
1176
1177    #[test]
1178    fn test_range_partitioning_project_preserves_or_degrades() -> Result<()> {
1179        let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
1180        let range_partitioning = fixture.range_partitioning_with_ordering(
1181            [fixture.range_sort_expr(1, SortOptions::new(true, false))].into(),
1182            vec![int_split_point([10])],
1183        );
1184
1185        let keep_b_mapping = ProjectionMapping::from_indices(&[1], &fixture.schema)?;
1186        let projected =
1187            range_partitioning.project(&keep_b_mapping, &fixture.eq_properties);
1188        assert_eq!(
1189            projected.to_string(),
1190            "Range([b@0 DESC NULLS LAST], [(10)], 2)"
1191        );
1192
1193        let drop_b_mapping = ProjectionMapping::from_indices(&[0], &fixture.schema)?;
1194        let projected =
1195            range_partitioning.project(&drop_b_mapping, &fixture.eq_properties);
1196        let Partitioning::UnknownPartitioning(partition_count) = projected else {
1197            panic!("expected UnknownPartitioning, got {projected:?}");
1198        };
1199        assert_eq!(partition_count, 2);
1200
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_range_partitioning_project_degrades_if_ordering_collapses() -> Result<()> {
1206        let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
1207        let target: Arc<dyn PhysicalExpr> = Arc::new(Column::new("x", 0));
1208        let range_partitioning =
1209            fixture.range_partitioning([0, 1], vec![int_split_point([10, 100])]);
1210        let mapping = ProjectionMapping::from_iter([
1211            (
1212                fixture.col(0),
1213                ProjectionTargets::from(vec![(Arc::clone(&target), 0)]),
1214            ),
1215            (
1216                fixture.col(1),
1217                ProjectionTargets::from(vec![(Arc::clone(&target), 0)]),
1218            ),
1219        ]);
1220
1221        let projected = range_partitioning.project(&mapping, &fixture.eq_properties);
1222        let Partitioning::UnknownPartitioning(partition_count) = projected else {
1223            panic!("expected UnknownPartitioning, got {projected:?}");
1224        };
1225        assert_eq!(partition_count, 2);
1226
1227        Ok(())
1228    }
1229
1230    #[test]
1231    fn range_partitioning_key_distribution_satisfaction() -> Result<()> {
1232        let fixture = PartitioningTestFixture::int64(&["a", "b", "c"])?;
1233        let range_a = fixture.range_partitioning([0], vec![int_split_point([10])]);
1234        let range_ab =
1235            fixture.range_partitioning([0, 1], vec![int_split_point([10, 100])]);
1236
1237        assert_satisfaction(
1238            "exact single key",
1239            &range_a,
1240            &fixture.key_distribution([0]),
1241            &fixture.eq_properties,
1242            PartitioningSatisfaction::Exact,
1243            PartitioningSatisfaction::Exact,
1244        );
1245        assert_satisfaction(
1246            "exact compound key",
1247            &range_ab,
1248            &fixture.key_distribution([0, 1]),
1249            &fixture.eq_properties,
1250            PartitioningSatisfaction::Exact,
1251            PartitioningSatisfaction::Exact,
1252        );
1253        assert_satisfaction(
1254            "subset key",
1255            &range_a,
1256            &fixture.key_distribution([0, 1]),
1257            &fixture.eq_properties,
1258            PartitioningSatisfaction::Subset,
1259            PartitioningSatisfaction::NotSatisfied,
1260        );
1261        assert_satisfaction(
1262            "incompatible key",
1263            &range_a,
1264            &fixture.key_distribution([1]),
1265            &fixture.eq_properties,
1266            PartitioningSatisfaction::NotSatisfied,
1267            PartitioningSatisfaction::NotSatisfied,
1268        );
1269
1270        let mut eq_properties = fixture.eq_properties.clone();
1271        eq_properties.add_equal_conditions(fixture.col(0), fixture.col(2))?;
1272        assert_satisfaction(
1273            "equivalent subset key",
1274            &range_a,
1275            &fixture.key_distribution([1, 2]),
1276            &eq_properties,
1277            PartitioningSatisfaction::Subset,
1278            PartitioningSatisfaction::NotSatisfied,
1279        );
1280
1281        let mut eq_properties = fixture.eq_properties.clone();
1282        eq_properties.add_equal_conditions(fixture.col(0), fixture.col(1))?;
1283        assert_satisfaction(
1284            "equivalent exact key",
1285            &range_a,
1286            &fixture.key_distribution([1]),
1287            &eq_properties,
1288            PartitioningSatisfaction::Exact,
1289            PartitioningSatisfaction::Exact,
1290        );
1291
1292        Ok(())
1293    }
1294}
1295
1296#[cfg(all(test, feature = "proto"))]
1297mod ordering_proto_tests {
1298    use std::sync::Arc;
1299
1300    use arrow::compute::SortOptions;
1301    use arrow::datatypes::{DataType, Field, Schema};
1302    use datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx;
1303    use datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx;
1304    use datafusion_physical_expr_common::sort_expr::{
1305        LexRequirement, PhysicalSortExpr, PhysicalSortRequirement,
1306        sort_exprs_try_from_proto, sort_exprs_try_to_proto,
1307    };
1308
1309    use crate::expressions::Column;
1310    use crate::proto_test_util::{StubDecoder, StubEncoder};
1311
1312    fn schema() -> Schema {
1313        Schema::new(vec![Field::new("a", DataType::Int32, false)])
1314    }
1315
1316    fn sort_expr(descending: bool, nulls_first: bool) -> PhysicalSortExpr {
1317        PhysicalSortExpr::new(
1318            Arc::new(Column::new("a", 0)),
1319            SortOptions {
1320                descending,
1321                nulls_first,
1322            },
1323        )
1324    }
1325
1326    #[test]
1327    fn sort_exprs_round_trip_preserves_options_and_order() {
1328        let encoder = StubEncoder::ok();
1329        let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
1330        let exprs = vec![sort_expr(true, false), sort_expr(false, true)];
1331
1332        let nodes = sort_exprs_try_to_proto(&exprs, &encode_ctx).unwrap();
1333        // `asc` is the inverse of `descending` on the wire.
1334        assert_eq!(
1335            nodes
1336                .iter()
1337                .map(|node| (node.asc, node.nulls_first))
1338                .collect::<Vec<_>>(),
1339            vec![(false, false), (true, true)]
1340        );
1341
1342        let schema = schema();
1343        let decoder = StubDecoder::ok();
1344        let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
1345        let decoded = sort_exprs_try_from_proto(&nodes, &decode_ctx).unwrap();
1346        assert_eq!(
1347            decoded.iter().map(|expr| expr.options).collect::<Vec<_>>(),
1348            exprs.iter().map(|expr| expr.options).collect::<Vec<_>>()
1349        );
1350    }
1351
1352    #[test]
1353    fn sort_exprs_accepts_owned_requirements() {
1354        let encoder = StubEncoder::ok();
1355        let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
1356        let requirement = LexRequirement::from([PhysicalSortRequirement::new(
1357            Arc::new(Column::new("a", 0)),
1358            Some(SortOptions {
1359                descending: true,
1360                nulls_first: true,
1361            }),
1362        )]);
1363
1364        let nodes = sort_exprs_try_to_proto(
1365            requirement
1366                .iter()
1367                .map(|req| PhysicalSortExpr::from(req.clone())),
1368            &encode_ctx,
1369        )
1370        .unwrap();
1371
1372        assert_eq!(nodes.len(), 1);
1373        assert!(!nodes[0].asc);
1374        assert!(nodes[0].nulls_first);
1375    }
1376
1377    #[test]
1378    fn sort_exprs_propagate_encode_errors() {
1379        let encoder = StubEncoder::failing_on(2);
1380        let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
1381        let exprs = vec![sort_expr(false, false), sort_expr(true, true)];
1382
1383        let err = sort_exprs_try_to_proto(&exprs, &encode_ctx).unwrap_err();
1384        assert!(err.to_string().contains("stub encode failure on call 2"));
1385    }
1386
1387    #[test]
1388    fn sort_exprs_reject_missing_inner_expr() {
1389        let encoder = StubEncoder::ok();
1390        let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
1391        let mut nodes =
1392            sort_exprs_try_to_proto(&[sort_expr(false, false)], &encode_ctx).unwrap();
1393        nodes[0].expr = None;
1394
1395        let schema = schema();
1396        let decoder = StubDecoder::ok();
1397        let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
1398        let err = sort_exprs_try_from_proto(&nodes, &decode_ctx).unwrap_err();
1399        assert!(
1400            err.to_string()
1401                .contains("PhysicalSortExpr is missing required field 'expr'")
1402        );
1403    }
1404}
1405
1406/// Partition counts are `usize` in memory and `u64` on the wire, so every
1407/// counted [`Partitioning`] variant crosses a width boundary in both
1408/// directions. These pin that neither crossing wraps or panics.
1409#[cfg(all(test, feature = "proto"))]
1410mod partition_count_proto_tests {
1411    use std::sync::Arc;
1412
1413    use arrow::datatypes::{DataType, Field, Schema};
1414    use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
1415    use datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx;
1416    use datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx;
1417    use datafusion_proto_models::protobuf;
1418
1419    use super::{Partitioning, partition_count, wire_partition_count};
1420    use crate::expressions::Column;
1421    use crate::proto_test_util::{StubDecoder, StubEncoder, column_node};
1422
1423    fn partitioning_node(
1424        method: protobuf::partitioning::PartitionMethod,
1425    ) -> protobuf::Partitioning {
1426        protobuf::Partitioning {
1427            partition_method: Some(method),
1428        }
1429    }
1430
1431    /// The counted variants, each carrying `count`. `Range` is excluded: it
1432    /// derives its partition count from its split points rather than reading
1433    /// one off the wire.
1434    fn counted_methods(count: u64) -> Vec<protobuf::partitioning::PartitionMethod> {
1435        use protobuf::partitioning::PartitionMethod;
1436
1437        vec![
1438            PartitionMethod::RoundRobin(count),
1439            PartitionMethod::Unknown(count),
1440            PartitionMethod::Hash(protobuf::PhysicalHashRepartition {
1441                hash_expr: vec![column_node("a")],
1442                partition_count: count,
1443            }),
1444        ]
1445    }
1446
1447    #[test]
1448    fn partition_count_round_trips_at_the_usize_ceiling() {
1449        // `usize::MAX` is the largest count that can exist in memory, so it has
1450        // to widen onto the wire and narrow back unchanged.
1451        let wire = wire_partition_count(usize::MAX).unwrap();
1452        assert_eq!(wire, u64::try_from(usize::MAX).unwrap());
1453        assert_eq!(partition_count(wire).unwrap(), usize::MAX);
1454    }
1455
1456    #[test]
1457    fn out_of_range_partition_count_is_reported_not_wrapped() {
1458        // A count wider than the target's `usize` can only be reached by
1459        // decoding on a narrower host than the one that encoded. That used to
1460        // wrap (`as usize`) or panic (`unwrap`); it is an error now. On a
1461        // 64-bit target every `u64` fits, so the same input has to decode
1462        // losslessly instead of being rejected.
1463        let narrowed = partition_count(u64::MAX);
1464
1465        #[cfg(target_pointer_width = "64")]
1466        assert_eq!(narrowed.unwrap(), usize::MAX);
1467
1468        #[cfg(not(target_pointer_width = "64"))]
1469        assert!(
1470            narrowed
1471                .unwrap_err()
1472                .to_string()
1473                .contains("Partition count 18446744073709551615 exceeds usize::MAX")
1474        );
1475    }
1476
1477    #[test]
1478    fn try_from_proto_narrows_every_counted_variant() {
1479        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1480        let decoder = StubDecoder::ok();
1481        let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
1482
1483        for method in counted_methods(u64::MAX) {
1484            let decoded =
1485                Partitioning::try_from_proto(&partitioning_node(method), &decode_ctx);
1486
1487            #[cfg(target_pointer_width = "64")]
1488            assert_eq!(decoded.unwrap().unwrap().partition_count(), usize::MAX);
1489
1490            #[cfg(not(target_pointer_width = "64"))]
1491            assert!(
1492                decoded
1493                    .unwrap_err()
1494                    .to_string()
1495                    .contains("exceeds usize::MAX")
1496            );
1497        }
1498    }
1499
1500    #[test]
1501    fn try_to_proto_widens_every_counted_variant() {
1502        use protobuf::partitioning::PartitionMethod;
1503
1504        let encoder = StubEncoder::ok();
1505        let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
1506        let hash_key: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
1507
1508        let encoded = [
1509            Partitioning::RoundRobinBatch(usize::MAX),
1510            Partitioning::UnknownPartitioning(usize::MAX),
1511            Partitioning::Hash(vec![hash_key], usize::MAX),
1512        ]
1513        .iter()
1514        .map(|partitioning| {
1515            match partitioning
1516                .try_to_proto(&encode_ctx)
1517                .unwrap()
1518                .partition_method
1519            {
1520                Some(PartitionMethod::RoundRobin(n) | PartitionMethod::Unknown(n)) => n,
1521                Some(PartitionMethod::Hash(hash)) => hash.partition_count,
1522                other => panic!("expected a counted partition method, got {other:?}"),
1523            }
1524        })
1525        .collect::<Vec<_>>();
1526
1527        // Every variant widens to the same wire value, with no truncation.
1528        assert_eq!(encoded, vec![u64::try_from(usize::MAX).unwrap(); 3]);
1529    }
1530}