Skip to main content

uqa_sql/semantics/partition/
order.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Partitions in the order of `PostgreSQL`'s partition descriptor, which numbers the objects derived on a partitioned table's partitions, such as the constraints a foreign key derives on the partitions it references.
8
9use super::PartitionContext;
10use crate::{
11    ast::{PartitionBound, PartitionRangeDatum},
12    SQLError,
13};
14use uqa_core::Value;
15
16#[cfg(test)]
17mod tests;
18
19#[derive(PartialEq, Eq, PartialOrd, Ord)]
20enum RangePoint {
21    MinValue,
22    Value(Value),
23    MaxValue,
24}
25
26/// A partition's place among its siblings, which share a partitioning strategy apart from the default partition.
27#[derive(PartialEq, Eq, PartialOrd, Ord)]
28enum Position {
29    /// A range partition by its lower bound.
30    Range(Vec<RangePoint>),
31    /// A list partition by its smallest value.
32    List(Value),
33    /// A hash partition by modulus, then remainder.
34    Hash(i32, i32),
35    /// A list partition that holds only NULL follows every list partition that holds a value.
36    NullList,
37    Default,
38}
39
40fn position(context: &PartitionContext<'_>, bound: &PartitionBound) -> Result<Position, SQLError> {
41    let evaluate = |expression| context.expressions.evaluate_bound(expression, &[]);
42    Ok(match bound {
43        PartitionBound::Default => Position::Default,
44        PartitionBound::Range { lower, .. } => Position::Range(
45            lower
46                .iter()
47                .map(|datum| {
48                    Ok(match datum {
49                        PartitionRangeDatum::MinValue => RangePoint::MinValue,
50                        PartitionRangeDatum::Value(expression) => {
51                            RangePoint::Value(evaluate(expression)?)
52                        }
53                        PartitionRangeDatum::MaxValue => RangePoint::MaxValue,
54                    })
55                })
56                .collect::<Result<_, SQLError>>()?,
57        ),
58        PartitionBound::List(values) => {
59            let mut smallest = None;
60            for value in values {
61                let value = evaluate(value)?;
62                if value != Value::Null && smallest.as_ref().is_none_or(|current| value < *current)
63                {
64                    smallest = Some(value);
65                }
66            }
67            smallest.map_or(Position::NullList, Position::List)
68        }
69        PartitionBound::Hash { modulus, remainder } => Position::Hash(*modulus, *remainder),
70    })
71}
72
73/// The names of `partitions`, which are siblings, in the order of `PostgreSQL`'s partition descriptor: range partitions by lower bound, list partitions by their smallest value followed by a partition that holds only NULL, hash partitions by modulus and remainder, and the default partition last.
74pub fn partition_bound_order(
75    context: &PartitionContext<'_>,
76    partitions: Vec<(String, PartitionBound)>,
77) -> Result<Vec<String>, SQLError> {
78    let mut positioned = partitions
79        .into_iter()
80        .map(|(name, bound)| Ok((position(context, &bound)?, name)))
81        .collect::<Result<Vec<_>, SQLError>>()?;
82    positioned.sort_by(|left, right| left.0.cmp(&right.0));
83    Ok(positioned.into_iter().map(|(_, name)| name).collect())
84}