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}