Skip to main content

cratestack_sql/
order.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct OrderClause {
3    pub target: OrderTarget,
4    pub direction: SortDirection,
5    pub null_order: NullOrder,
6}
7
8/// Where NULLs sort relative to non-NULL values. PostgreSQL's default is
9/// `NULLS LAST` for `ASC` and `NULLS FIRST` for `DESC`; SQLite's default
10/// is `NULLS FIRST` for both. CrateStack pins the framework default to
11/// `NULLS LAST` so listings stay deterministic across backends and so
12/// soft-deleted rows (typed `Option<DateTime>` that surface as `None`
13/// for visible rows) don't muscle their way to the top of every listing.
14/// Override per-clause via [`OrderClause::nulls_first`] when scheduler /
15/// outbox queries want fresh-as-null tasks at the head of the queue.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum NullOrder {
18    First,
19    #[default]
20    Last,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum OrderTarget {
25    Column(&'static str),
26    RelationScalar {
27        parent_table: &'static str,
28        parent_column: &'static str,
29        related_table: &'static str,
30        related_column: &'static str,
31        /// Owned rather than `&'static str`: the correlated-subquery chain
32        /// is folded from the traversed relation path at call time (see
33        /// [`crate::order_value_sql`]). It used to be baked in per path at
34        /// macro-expansion time, which is exactly what made codegen
35        /// exponential in relation-graph connectivity (cratestack#252).
36        value_sql: String,
37    },
38}
39
40impl OrderClause {
41    pub const fn column(column: &'static str, direction: SortDirection) -> Self {
42        Self {
43            target: OrderTarget::Column(column),
44            direction,
45            null_order: NullOrder::Last,
46        }
47    }
48
49    /// Not `const` (unlike [`OrderClause::column`]): `value_sql` is folded
50    /// from the traversed relation path at call time rather than baked in
51    /// at macro-expansion time. See [`crate::order_value_sql`].
52    pub fn relation_scalar(
53        parent_table: &'static str,
54        parent_column: &'static str,
55        related_table: &'static str,
56        related_column: &'static str,
57        value_sql: String,
58        direction: SortDirection,
59    ) -> Self {
60        Self {
61            target: OrderTarget::RelationScalar {
62                parent_table,
63                parent_column,
64                related_table,
65                related_column,
66                value_sql,
67            },
68            direction,
69            null_order: NullOrder::Last,
70        }
71    }
72
73    /// Place NULL values *before* non-NULL ones for this clause. Use on
74    /// scheduler / outbox listings where "no scheduled time yet" should
75    /// sort ahead of every retry-scheduled row.
76    pub fn nulls_first(mut self) -> Self {
77        self.null_order = NullOrder::First;
78        self
79    }
80
81    /// Place NULL values *after* non-NULL ones (the framework default).
82    /// Mostly useful when overriding a programmatically-built clause
83    /// that previously asked for `nulls_first`.
84    pub fn nulls_last(mut self) -> Self {
85        self.null_order = NullOrder::Last;
86        self
87    }
88
89    pub fn is_relation_scalar(&self) -> bool {
90        matches!(self.target, OrderTarget::RelationScalar { .. })
91    }
92
93    pub fn targets_column(&self, column: &str) -> bool {
94        matches!(self.target, OrderTarget::Column(candidate) if candidate == column)
95    }
96
97    pub fn direction(&self) -> SortDirection {
98        self.direction
99    }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum SortDirection {
104    Asc,
105    Desc,
106}