Skip to main content

cratestack_sql/
order.rs

1use crate::filter::VectorMetric;
2
3// Not `Eq`: `OrderTarget::VectorDistance` carries a `Vec<f32>` query
4// vector, and `f32` has no sound total-equality impl (NaN != NaN) —
5// same reason `FilterExpr`/`SpatialFilter` stop at `PartialEq`.
6#[derive(Debug, Clone, PartialEq)]
7pub struct OrderClause {
8    pub target: OrderTarget,
9    pub direction: SortDirection,
10    pub null_order: NullOrder,
11}
12
13/// Where NULLs sort relative to non-NULL values. PostgreSQL's default is
14/// `NULLS LAST` for `ASC` and `NULLS FIRST` for `DESC`; SQLite's default
15/// is `NULLS FIRST` for both. CrateStack pins the framework default to
16/// `NULLS LAST` so listings stay deterministic across backends and so
17/// soft-deleted rows (typed `Option<DateTime>` that surface as `None`
18/// for visible rows) don't muscle their way to the top of every listing.
19/// Override per-clause via [`OrderClause::nulls_first`] when scheduler /
20/// outbox queries want fresh-as-null tasks at the head of the queue.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum NullOrder {
23    First,
24    #[default]
25    Last,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum OrderTarget {
30    Column(&'static str),
31    RelationScalar {
32        parent_table: &'static str,
33        parent_column: &'static str,
34        related_table: &'static str,
35        related_column: &'static str,
36        /// Owned rather than `&'static str`: the correlated-subquery chain
37        /// is folded from the traversed relation path at call time (see
38        /// [`crate::order_value_sql`]). It used to be baked in per path at
39        /// macro-expansion time, which is exactly what made codegen
40        /// exponential in relation-graph connectivity (cratestack#252).
41        value_sql: String,
42    },
43    /// Order by distance to a query vector on a `Vector(n)` column (see
44    /// `docs/design/extensions.md` §6/§7, cratestack#163). Built via
45    /// `FieldRef::distance_to(...).asc()`/`.desc()` or the
46    /// `order_by_distance` shorthand. PG-only (pgvector) — the
47    /// embedded rusqlite backend doesn't ship pgvector, so its
48    /// renderer fails loud, mirroring how `FilterExpr::Spatial` is
49    /// handled there.
50    VectorDistance {
51        column: &'static str,
52        metric: VectorMetric,
53        query_vector: Vec<f32>,
54    },
55    /// Order by `ST_Distance(col::geography, point::geography)` — great-
56    /// circle metres to a reference point (cratestack#842 item 5).
57    /// Built via `FieldRef::order_by_distance_to(point)`.
58    ///
59    /// This is the ordering half of the pair whose filtering half is
60    /// [`crate::SpatialFilter::DWithinGeographyPoint`]: `DWithin` picks
61    /// the rows inside a radius, this sorts them nearest-first, so
62    /// "closest N within X metres" no longer needs the distance
63    /// recomputed in application code after the radius filter returns.
64    ///
65    /// PG-only (PostGIS) — the embedded rusqlite backend doesn't ship
66    /// SpatiaLite, so its renderer fails loud, exactly as it does for
67    /// `FilterExpr::Spatial`.
68    #[cfg(feature = "postgis")]
69    SpatialDistance {
70        column: &'static str,
71        lng: f64,
72        lat: f64,
73    },
74}
75
76impl OrderClause {
77    pub const fn column(column: &'static str, direction: SortDirection) -> Self {
78        Self {
79            target: OrderTarget::Column(column),
80            direction,
81            null_order: NullOrder::Last,
82        }
83    }
84
85    /// Not `const` (unlike [`OrderClause::column`]): `value_sql` is folded
86    /// from the traversed relation path at call time rather than baked in
87    /// at macro-expansion time. See [`crate::order_value_sql`].
88    pub fn relation_scalar(
89        parent_table: &'static str,
90        parent_column: &'static str,
91        related_table: &'static str,
92        related_column: &'static str,
93        value_sql: String,
94        direction: SortDirection,
95    ) -> Self {
96        Self {
97            target: OrderTarget::RelationScalar {
98                parent_table,
99                parent_column,
100                related_table,
101                related_column,
102                value_sql,
103            },
104            direction,
105            null_order: NullOrder::Last,
106        }
107    }
108
109    /// Place NULL values *before* non-NULL ones for this clause. Use on
110    /// scheduler / outbox listings where "no scheduled time yet" should
111    /// sort ahead of every retry-scheduled row.
112    pub fn nulls_first(mut self) -> Self {
113        self.null_order = NullOrder::First;
114        self
115    }
116
117    /// Place NULL values *after* non-NULL ones (the framework default).
118    /// Mostly useful when overriding a programmatically-built clause
119    /// that previously asked for `nulls_first`.
120    pub fn nulls_last(mut self) -> Self {
121        self.null_order = NullOrder::Last;
122        self
123    }
124
125    pub fn is_relation_scalar(&self) -> bool {
126        matches!(self.target, OrderTarget::RelationScalar { .. })
127    }
128
129    pub fn targets_column(&self, column: &str) -> bool {
130        matches!(self.target, OrderTarget::Column(candidate) if candidate == column)
131    }
132
133    pub fn direction(&self) -> SortDirection {
134        self.direction
135    }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub enum SortDirection {
141    Asc,
142    Desc,
143}