Skip to main content

keelson_core/clause/
select.rs

1use crate::expr::{Expr, IntoExpr, IntoExprList};
2use crate::writer::{Expression, SqlWriter};
3
4/// The projection: `SELECT` **`a, b, c`**, or `*` when nothing was asked for.
5///
6/// Preload columns are kept apart from the ones the caller selected so that a
7/// preloader can be an ordinary query mod while the mapper still knows how many of
8/// the returned columns belong to the root object — that is what
9/// [`count_select_cols`](Self::count_select_cols) is for. They render as one list,
10/// preloads last.
11///
12/// This is the one clause whose "absent" rendering is not empty: a `SELECT` with
13/// no list is not a statement, and `*` is what the grammar asks for. Absence is
14/// therefore still observable through [`is_empty`](Self::is_empty).
15#[derive(Debug, Clone, Default)]
16pub struct SelectList {
17    /// What the caller selected.
18    pub columns: Vec<Expr>,
19    /// Columns a preloader added, rendered after [`columns`](Self::columns).
20    pub preload_columns: Vec<Expr>,
21}
22
23impl SelectList {
24    /// How many columns the caller selected, ignoring preloads.
25    pub fn count_select_cols(&self) -> usize {
26        self.columns.len()
27    }
28
29    /// Replace the selected columns.
30    pub fn set_select(&mut self, columns: impl IntoExprList) {
31        self.columns = columns.into_expr_list();
32    }
33
34    /// Add to the selected columns.
35    pub fn append_select(&mut self, columns: impl IntoExprList) {
36        self.columns.extend(columns.into_expr_list());
37    }
38
39    /// Replace the preload columns.
40    pub fn set_preload_select(&mut self, columns: impl IntoExprList) {
41        self.preload_columns = columns.into_expr_list();
42    }
43
44    /// Add to the preload columns.
45    pub fn append_preload_select(&mut self, columns: impl IntoExprList) {
46        self.preload_columns.extend(columns.into_expr_list());
47    }
48
49    /// Add one column.
50    pub fn append_column(&mut self, column: impl IntoExpr) {
51        self.columns.push(column.into_expr());
52    }
53
54    /// Whether nothing was selected, so that `*` is what will be written.
55    pub fn is_empty(&self) -> bool {
56        self.columns.is_empty() && self.preload_columns.is_empty()
57    }
58}
59
60impl Expression for SelectList {
61    fn write_sql(&self, w: &mut SqlWriter<'_>) {
62        if self.is_empty() {
63            w.push_str("*");
64            return;
65        }
66        w.write_iter(
67            self.columns.iter().chain(self.preload_columns.iter()),
68            "",
69            ", ",
70            "",
71        );
72    }
73}
74
75/// A statement with a projection.
76pub trait HasSelectList {
77    /// The projection to modify.
78    fn select_list_mut(&mut self) -> &mut SelectList;
79}
80
81impl HasSelectList for SelectList {
82    fn select_list_mut(&mut self) -> &mut SelectList {
83        self
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use keelson_sqlcheck::testing::assert_frag_sql;
90
91    use super::*;
92    use crate::dialect::testing::Numbered;
93    use crate::expr::{Chain, quote};
94    use crate::writer::build;
95
96    fn sql(list: &SelectList) -> String {
97        build(&Numbered, list).expect("render").0
98    }
99
100    #[test]
101    fn an_empty_projection_is_a_star() {
102        assert_frag_sql("SELECT {} FROM users", &sql(&SelectList::default()), "*");
103        assert!(SelectList::default().is_empty());
104    }
105
106    #[test]
107    fn preloads_render_after_the_selected_columns() {
108        let mut list = SelectList::default();
109        list.append_select((quote("age"), quote("name")));
110        list.append_preload_select(quote(("posts", "id")));
111
112        // Two tables in the frame so the preload's qualified column resolves; the
113        // unqualified ones are chosen to be unambiguous across both.
114        assert_frag_sql(
115            "SELECT {} FROM users, posts",
116            &sql(&list),
117            r#""age", "name", "posts"."id""#,
118        );
119        assert_eq!(
120            list.count_select_cols(),
121            2,
122            "a preload column is not part of the root projection"
123        );
124    }
125
126    #[test]
127    fn preloads_alone_still_suppress_the_star() {
128        let mut list = SelectList::default();
129        list.set_preload_select(quote(("posts", "id")));
130        assert_frag_sql("SELECT {} FROM posts", &sql(&list), r#""posts"."id""#);
131        assert!(!list.is_empty());
132    }
133
134    #[test]
135    fn a_column_can_be_any_expression() {
136        let mut list = SelectList::default();
137        list.append_column(quote("id"));
138        list.append_column(Expr::func("count", "*").as_("n"));
139        list.append_column("1 + 1");
140        // The GROUP BY is the frame's, not the projection's: a select list mixing
141        // an aggregate with a plain column needs one to be legal.
142        assert_frag_sql(
143            r#"SELECT {} FROM users GROUP BY "id""#,
144            &sql(&list),
145            r#""id", count(*) AS "n", 1 + 1"#,
146        );
147    }
148}