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        value_sql: &'static str,
32    },
33}
34
35impl OrderClause {
36    pub const fn column(column: &'static str, direction: SortDirection) -> Self {
37        Self {
38            target: OrderTarget::Column(column),
39            direction,
40            null_order: NullOrder::Last,
41        }
42    }
43
44    pub const fn relation_scalar(
45        parent_table: &'static str,
46        parent_column: &'static str,
47        related_table: &'static str,
48        related_column: &'static str,
49        value_sql: &'static str,
50        direction: SortDirection,
51    ) -> Self {
52        Self {
53            target: OrderTarget::RelationScalar {
54                parent_table,
55                parent_column,
56                related_table,
57                related_column,
58                value_sql,
59            },
60            direction,
61            null_order: NullOrder::Last,
62        }
63    }
64
65    /// Place NULL values *before* non-NULL ones for this clause. Use on
66    /// scheduler / outbox listings where "no scheduled time yet" should
67    /// sort ahead of every retry-scheduled row.
68    pub fn nulls_first(mut self) -> Self {
69        self.null_order = NullOrder::First;
70        self
71    }
72
73    /// Place NULL values *after* non-NULL ones (the framework default).
74    /// Mostly useful when overriding a programmatically-built clause
75    /// that previously asked for `nulls_first`.
76    pub fn nulls_last(mut self) -> Self {
77        self.null_order = NullOrder::Last;
78        self
79    }
80
81    pub fn is_relation_scalar(&self) -> bool {
82        matches!(self.target, OrderTarget::RelationScalar { .. })
83    }
84
85    pub fn targets_column(&self, column: &str) -> bool {
86        matches!(self.target, OrderTarget::Column(candidate) if candidate == column)
87    }
88
89    pub fn direction(&self) -> SortDirection {
90        self.direction
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SortDirection {
96    Asc,
97    Desc,
98}