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}
56
57impl OrderClause {
58 pub const fn column(column: &'static str, direction: SortDirection) -> Self {
59 Self {
60 target: OrderTarget::Column(column),
61 direction,
62 null_order: NullOrder::Last,
63 }
64 }
65
66 /// Not `const` (unlike [`OrderClause::column`]): `value_sql` is folded
67 /// from the traversed relation path at call time rather than baked in
68 /// at macro-expansion time. See [`crate::order_value_sql`].
69 pub fn relation_scalar(
70 parent_table: &'static str,
71 parent_column: &'static str,
72 related_table: &'static str,
73 related_column: &'static str,
74 value_sql: String,
75 direction: SortDirection,
76 ) -> Self {
77 Self {
78 target: OrderTarget::RelationScalar {
79 parent_table,
80 parent_column,
81 related_table,
82 related_column,
83 value_sql,
84 },
85 direction,
86 null_order: NullOrder::Last,
87 }
88 }
89
90 /// Place NULL values *before* non-NULL ones for this clause. Use on
91 /// scheduler / outbox listings where "no scheduled time yet" should
92 /// sort ahead of every retry-scheduled row.
93 pub fn nulls_first(mut self) -> Self {
94 self.null_order = NullOrder::First;
95 self
96 }
97
98 /// Place NULL values *after* non-NULL ones (the framework default).
99 /// Mostly useful when overriding a programmatically-built clause
100 /// that previously asked for `nulls_first`.
101 pub fn nulls_last(mut self) -> Self {
102 self.null_order = NullOrder::Last;
103 self
104 }
105
106 pub fn is_relation_scalar(&self) -> bool {
107 matches!(self.target, OrderTarget::RelationScalar { .. })
108 }
109
110 pub fn targets_column(&self, column: &str) -> bool {
111 matches!(self.target, OrderTarget::Column(candidate) if candidate == column)
112 }
113
114 pub fn direction(&self) -> SortDirection {
115 self.direction
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub enum SortDirection {
122 Asc,
123 Desc,
124}