Skip to main content

nodedb_physical/physical_plan/
sort_key.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Physical sort key: one ORDER BY term as dispatched to the Data Plane.
4
5/// A single ORDER BY term.
6///
7/// The key is an expression, not a column name. `ORDER BY 100 / weight`,
8/// `ORDER BY UPPER(name)`, and `ORDER BY qty * price` all sort by a value that
9/// exists nowhere in the stored row, so a key that could only name a column
10/// would have to drop them — answering a query that declared a sort with rows
11/// in storage order, and reporting success while doing it.
12///
13/// A bare `ORDER BY col` is simply the `SqlExpr::Column` case and still
14/// resolves by direct field lookup.
15#[derive(
16    Debug,
17    Clone,
18    PartialEq,
19    serde::Serialize,
20    serde::Deserialize,
21    zerompk::ToMessagePack,
22    zerompk::FromMessagePack,
23)]
24pub struct SortKeySpec {
25    /// Expression evaluated against each row to produce its sort value.
26    pub expr: nodedb_query::expr::SqlExpr,
27    /// `true` = ascending, `false` = descending.
28    pub ascending: bool,
29    /// Where NULLs go: `true` places them before all non-NULL values,
30    /// `false` after.
31    ///
32    /// This is independent of `ascending` — SQL's `NULLS FIRST` / `NULLS LAST`
33    /// is absolute and is not flipped by the sort direction. When the query
34    /// does not say, the planner fills in PostgreSQL's default (`ASC` → NULLS
35    /// LAST, `DESC` → NULLS FIRST).
36    pub nulls_first: bool,
37}
38
39impl SortKeySpec {
40    /// Sort by a bare column name, with PostgreSQL's default NULL placement
41    /// for the given direction.
42    pub fn column(name: impl Into<String>, ascending: bool) -> Self {
43        Self {
44            expr: nodedb_query::expr::SqlExpr::Column(name.into()),
45            ascending,
46            nulls_first: !ascending,
47        }
48    }
49
50    /// Order two rows that may be NULL on this key.
51    ///
52    /// `Some(ordering)` when at least one side is NULL — decided entirely by
53    /// `nulls_first`, never reversed by the direction. `None` when both sides
54    /// carry a value and the caller must compare them.
55    pub fn order_nulls(&self, a_is_null: bool, b_is_null: bool) -> Option<std::cmp::Ordering> {
56        use std::cmp::Ordering;
57        match (a_is_null, b_is_null) {
58            (true, true) => Some(Ordering::Equal),
59            (true, false) => Some(if self.nulls_first {
60                Ordering::Less
61            } else {
62                Ordering::Greater
63            }),
64            (false, true) => Some(if self.nulls_first {
65                Ordering::Greater
66            } else {
67                Ordering::Less
68            }),
69            (false, false) => None,
70        }
71    }
72
73    /// Apply the sort direction to a comparison of two non-NULL values.
74    pub fn direct(&self, ordering: std::cmp::Ordering) -> std::cmp::Ordering {
75        if self.ascending {
76            ordering
77        } else {
78            ordering.reverse()
79        }
80    }
81
82    /// The column this key sorts by, when the key is a bare column reference.
83    ///
84    /// Storage layers that can only push a *stored column* down to an index
85    /// use this to decide whether the key is pushable; a computed key returns
86    /// `None` and must be sorted after the rows are materialized.
87    pub fn as_column(&self) -> Option<&str> {
88        match &self.expr {
89            nodedb_query::expr::SqlExpr::Column(name) => Some(name.as_str()),
90            _ => None,
91        }
92    }
93}