Skip to main content

postrust_graphql/input/
order_by.rs

1//! Ordering and distinct input types.
2//!
3//! Hasura orders with `order_by: [<table>_order_by!]` where each entry maps a
4//! column to a direction drawn from one shared enum:
5//!
6//! ```graphql
7//! author(order_by: [{name: asc_nulls_last}, {id: desc}]) { id }
8//! ```
9//!
10//! A list rather than a single object, because ordering is ordered: `{name:
11//! asc, id: desc}` is one object whose two keys have no defined precedence,
12//! and the client that wrote it meant name first.
13//!
14//! The direction enum carries null placement, which the previous
15//! `orderBy: ["name.asc"]` strings could not express at all. In PostgreSQL
16//! nulls sort last ascending and first descending, so `asc` and
17//! `asc_nulls_last` mean the same thing and `asc_nulls_first` does not --
18//! which is exactly the case a client reaches for the enum to say.
19//!
20//! `distinct_on` takes columns from a generated enum rather than strings, for
21//! the same reason the boolean expressions do: an unknown column should be
22//! refused by the client, not by the database.
23
24use crate::schema::object::TableObjectType;
25use crate::schema::relationship::RelationshipField;
26use async_graphql::dynamic::{Enum, EnumItem, InputObject, InputValue, TypeRef};
27use std::collections::{HashMap, HashSet};
28
29/// The name of the shared direction enum.
30pub const DIRECTION_ENUM: &str = "order_by";
31
32/// Every direction, with its SQL.
33const DIRECTIONS: &[(&str, &str)] = &[
34    ("asc", "ASC"),
35    ("asc_nulls_first", "ASC NULLS FIRST"),
36    ("asc_nulls_last", "ASC NULLS LAST"),
37    ("desc", "DESC"),
38    ("desc_nulls_first", "DESC NULLS FIRST"),
39    ("desc_nulls_last", "DESC NULLS LAST"),
40];
41
42/// The name of a table's ordering input.
43pub fn order_by_type_name(base_name: &str) -> String {
44    format!("{}_order_by", base_name)
45}
46
47/// The name of a table's column enum.
48pub fn select_column_type_name(base_name: &str) -> String {
49    format!("{}_select_column", base_name)
50}
51
52/// The name of the input for ordering a row by an aggregate of its children.
53pub fn aggregate_order_by_type_name(base_name: &str) -> String {
54    format!("{}_aggregate_order_by", base_name)
55}
56
57/// The name of the input for ordering by one aggregate function's results.
58pub fn function_order_by_type_name(base_name: &str, function: &str) -> String {
59    format!("{}_{}_order_by", base_name, function)
60}
61
62/// The SQL for a direction, or `None` if it is not one.
63pub fn direction_sql(name: &str) -> Option<&'static str> {
64    DIRECTIONS
65        .iter()
66        .find(|(enum_name, _)| *enum_name == name)
67        .map(|(_, sql)| *sql)
68}
69
70/// The shared direction enum.
71pub fn direction_enum() -> Enum {
72    let mut direction = Enum::new(DIRECTION_ENUM)
73        .description("Sort direction, with where nulls go. `asc` is `asc_nulls_last`.");
74    for (name, _) in DIRECTIONS {
75        direction = direction.item(EnumItem::new(*name));
76    }
77    direction
78}
79
80/// The ordering input and column enum for every table.
81pub fn build_inputs(
82    object_types: &HashMap<String, TableObjectType>,
83    relationship_fields: &HashMap<String, Vec<RelationshipField>>,
84) -> (Vec<InputObject>, Vec<Enum>) {
85    let mut inputs = Vec::new();
86    let mut enums = vec![direction_enum()];
87
88    for (type_name, object) in object_types {
89        let local_type_name = object.local_name.as_str();
90        // A table with no columns would produce an enum with no members, which
91        // no GraphQL schema may contain.
92        if object.fields.is_empty() {
93            continue;
94        }
95
96        let mut input = InputObject::new(order_by_type_name(local_type_name)).description(format!(
97            "Order rows of {} by a column, by a related row's column, or by an \
98             aggregate of its children.",
99            local_type_name
100        ));
101        let mut columns = Enum::new(select_column_type_name(local_type_name))
102            .description(format!("A column of {}.", local_type_name));
103
104        let mut taken: HashSet<String> = HashSet::new();
105        for field in &object.fields {
106            if !taken.insert(field.name.clone()) {
107                continue;
108            }
109            input = input.field(InputValue::new(&field.name, TypeRef::named(DIRECTION_ENUM)));
110            columns = columns.item(EnumItem::new(&field.name));
111        }
112
113        // Ordering by something the row points at. One row contributes a
114        // column, so ordering by it is ordering by that column; many rows
115        // contribute a count or a statistic, so ordering by them is ordering
116        // by an aggregate. That is the whole difference, and it is why the two
117        // sides take different inputs.
118        for relationship in relationship_fields.get(type_name).into_iter().flatten() {
119            if relationship.is_list {
120                let field = format!("{}_aggregate", relationship.name);
121                if taken.insert(field.clone()) {
122                    input = input.field(InputValue::new(
123                        &field,
124                        TypeRef::named(aggregate_order_by_type_name(
125                            &relationship.target_local_name,
126                        )),
127                    ));
128                }
129            } else if taken.insert(relationship.name.clone()) {
130                input = input.field(InputValue::new(
131                    &relationship.name,
132                    TypeRef::named(order_by_type_name(&relationship.target_local_name)),
133                ));
134            }
135        }
136
137        inputs.push(input);
138        enums.push(columns);
139
140        // What a parent may order its children by.
141        let mut aggregate = InputObject::new(aggregate_order_by_type_name(local_type_name))
142            .description(format!(
143                "Order by an aggregate of {} rows.",
144                local_type_name
145            ))
146            .field(InputValue::new("count", TypeRef::named(DIRECTION_ENUM)));
147
148        for (function, _, columns) in crate::schema::aggregate::functions_for(object) {
149            let function_type = function_order_by_type_name(local_type_name, function);
150            let mut per_column = InputObject::new(&function_type).description(format!(
151                "Order by the `{}` of a {} column.",
152                function, local_type_name
153            ));
154            for column in &columns {
155                per_column =
156                    per_column.field(InputValue::new(column, TypeRef::named(DIRECTION_ENUM)));
157            }
158            inputs.push(per_column);
159            aggregate = aggregate.field(InputValue::new(function, TypeRef::named(&function_type)));
160        }
161        inputs.push(aggregate);
162    }
163
164    (inputs, enums)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use pretty_assertions::assert_eq;
171
172    #[test]
173    fn a_tables_ordering_is_named_after_it() {
174        assert_eq!(order_by_type_name("author"), "author_order_by");
175        assert_eq!(select_column_type_name("author"), "author_select_column");
176    }
177
178    #[test]
179    fn plain_asc_places_nulls_where_postgres_does() {
180        // Not `ASC NULLS LAST` spelled out: the point is that the default and
181        // the explicit spelling produce the same order, so the plain form is
182        // left to the database.
183        assert_eq!(direction_sql("asc"), Some("ASC"));
184        assert_eq!(direction_sql("asc_nulls_last"), Some("ASC NULLS LAST"));
185        assert_eq!(direction_sql("asc_nulls_first"), Some("ASC NULLS FIRST"));
186    }
187
188    #[test]
189    fn a_direction_that_is_not_one_has_no_sql() {
190        assert_eq!(direction_sql("sideways"), None);
191        assert_eq!(direction_sql("ASC"), None);
192    }
193}