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}