Skip to main content

nautilus_core/
select.rs

1//! SELECT query AST and builder.
2
3use crate::column::ColumnMarker;
4use crate::error::{Error, Result};
5use crate::expr::Expr;
6
7/// A select list item that can be either a simple column or a computed expression.
8#[derive(Debug, Clone, PartialEq)]
9pub enum SelectItem {
10    /// A simple column reference.
11    Column(ColumnMarker),
12    /// A computed expression with an alias.
13    Computed {
14        /// The expression to compute.
15        expr: Expr,
16        /// The alias for the computed expression.
17        alias: String,
18    },
19}
20
21impl SelectItem {
22    /// Creates a SelectItem from a ColumnMarker.
23    pub fn column(marker: ColumnMarker) -> Self {
24        SelectItem::Column(marker)
25    }
26
27    /// Creates a computed SelectItem with an expression and alias.
28    pub fn computed(expr: Expr, alias: impl Into<String>) -> Self {
29        SelectItem::Computed {
30            expr,
31            alias: alias.into(),
32        }
33    }
34}
35
36impl From<ColumnMarker> for SelectItem {
37    fn from(marker: ColumnMarker) -> Self {
38        SelectItem::Column(marker)
39    }
40}
41
42/// Sort direction for ORDER BY clauses.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum OrderDir {
45    /// Ascending.
46    Asc,
47    /// Descending.
48    Desc,
49}
50
51/// ORDER BY clause item.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct OrderBy {
54    /// Column name.
55    pub column: String,
56    /// Sort direction.
57    pub direction: OrderDir,
58}
59
60impl OrderBy {
61    /// Creates a new ORDER BY clause.
62    pub fn new(column: impl Into<String>, direction: OrderDir) -> Self {
63        OrderBy {
64            column: column.into(),
65            direction,
66        }
67    }
68
69    /// Creates an ascending ORDER BY clause.
70    pub fn asc(column: impl Into<String>) -> Self {
71        OrderBy::new(column, OrderDir::Asc)
72    }
73
74    /// Creates a descending ORDER BY clause.
75    pub fn desc(column: impl Into<String>) -> Self {
76        OrderBy::new(column, OrderDir::Desc)
77    }
78}
79
80/// Reserved capacities for the `Vec`s maintained by a [`SelectBuilder`].
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82pub struct SelectCapacity {
83    /// Expected number of select-list items.
84    pub items: usize,
85    /// Expected number of JOIN clauses.
86    pub joins: usize,
87    /// Expected number of column-based `ORDER BY` items.
88    pub order_by_columns: usize,
89    /// Expected number of expression-based `ORDER BY` items.
90    pub order_by_exprs: usize,
91    /// Expected number of `GROUP BY` columns.
92    pub group_by: usize,
93    /// Expected number of `DISTINCT` columns.
94    pub distinct: usize,
95}
96
97/// One ORDER BY item in the original user-specified sequence.
98#[derive(Debug, Clone, PartialEq)]
99pub enum OrderByItem {
100    /// An ORDER BY over a plain column reference.
101    Column(OrderBy),
102    /// An ORDER BY over an arbitrary expression, such as an aggregate function.
103    Expr(Expr, OrderDir),
104}
105
106/// Type of JOIN operation.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum JoinType {
109    /// INNER JOIN — only matching rows from both tables.
110    Inner,
111    /// LEFT JOIN — all rows from the left table, matching rows from the right.
112    Left,
113}
114
115/// A JOIN clause attached to a SELECT query.
116#[derive(Debug, Clone, PartialEq)]
117pub struct JoinClause {
118    /// The type of join (INNER, LEFT).
119    pub join_type: JoinType,
120    /// The table to join.
121    pub table: String,
122    /// The ON condition expression.
123    pub on: Expr,
124    /// Select items (columns or computed expressions) from the joined table.
125    pub items: Vec<SelectItem>,
126}
127
128impl JoinClause {
129    /// Creates a new JOIN clause.
130    pub fn new(
131        join_type: JoinType,
132        table: impl Into<String>,
133        on: Expr,
134        items: Vec<SelectItem>,
135    ) -> Self {
136        Self {
137            join_type,
138            table: table.into(),
139            on,
140            items,
141        }
142    }
143}
144
145/// SELECT query AST node.
146#[derive(Debug, Clone, PartialEq)]
147pub struct Select {
148    /// Table name.
149    pub table: String,
150    /// Select items (columns or computed expressions).
151    pub items: Vec<SelectItem>,
152    /// JOIN clauses.
153    pub joins: Vec<JoinClause>,
154    /// WHERE clause.
155    pub filter: Option<Expr>,
156    /// ORDER BY clauses.
157    pub order_by: Vec<OrderBy>,
158    /// Row count to return (maps to SQL LIMIT).
159    ///
160    /// Positive values limit forward; negative values signal backward pagination
161    /// (callers must negate and reverse `ORDER BY` in application code).
162    /// The absolute value is used when building the SQL `LIMIT` clause.
163    pub take: Option<i32>,
164    /// Row offset to skip (maps to SQL OFFSET).
165    pub skip: Option<u32>,
166    /// GROUP BY columns.
167    pub group_by: Vec<ColumnMarker>,
168    /// Columns to deduplicate on.
169    ///
170    /// Non-empty activates deduplication:
171    /// - Postgres: `SELECT DISTINCT ON (cols)`
172    /// - SQLite / MySQL: plain `SELECT DISTINCT`
173    pub distinct: Vec<String>,
174    /// HAVING clause (post-GROUP BY filter).
175    pub having: Option<Expr>,
176    /// ORDER BY items preserved in their original mixed column/expression order.
177    pub order_by_items: Vec<OrderByItem>,
178    /// ORDER BY expression items (for aggregate functions, e.g. `COUNT(*) DESC`).
179    pub order_by_exprs: Vec<(Expr, OrderDir)>,
180}
181
182impl Select {
183    /// Creates a new SELECT query builder for the given table.
184    pub fn from_table(table: impl Into<String>) -> SelectBuilder {
185        SelectBuilder {
186            table: table.into(),
187            items: Vec::new(),
188            joins: Vec::new(),
189            filter: None,
190            order_by: Vec::new(),
191            take: None,
192            skip: None,
193            group_by: Vec::new(),
194            distinct: Vec::new(),
195            having: None,
196            order_by_items: Vec::new(),
197            order_by_exprs: Vec::new(),
198        }
199    }
200}
201
202/// Builder for SELECT queries.
203#[derive(Debug, Clone)]
204pub struct SelectBuilder {
205    table: String,
206    items: Vec<SelectItem>,
207    joins: Vec<JoinClause>,
208    filter: Option<Expr>,
209    order_by: Vec<OrderBy>,
210    take: Option<i32>,
211    skip: Option<u32>,
212    group_by: Vec<ColumnMarker>,
213    distinct: Vec<String>,
214    having: Option<Expr>,
215    order_by_items: Vec<OrderByItem>,
216    order_by_exprs: Vec<(Expr, OrderDir)>,
217}
218
219impl SelectBuilder {
220    /// Reserve capacity for the builder's internal vectors.
221    #[must_use]
222    pub fn with_capacity(mut self, capacity: SelectCapacity) -> Self {
223        self.items.reserve(capacity.items);
224        self.joins.reserve(capacity.joins);
225        self.order_by.reserve(capacity.order_by_columns);
226        self.group_by.reserve(capacity.group_by);
227        self.distinct.reserve(capacity.distinct);
228        self.order_by_items
229            .reserve(capacity.order_by_columns + capacity.order_by_exprs);
230        self.order_by_exprs.reserve(capacity.order_by_exprs);
231        self
232    }
233
234    /// Sets the select items.
235    #[must_use]
236    pub fn items(mut self, items: Vec<SelectItem>) -> Self {
237        self.items = items;
238        self
239    }
240
241    /// Adds a select item.
242    #[must_use]
243    pub fn item(mut self, item: SelectItem) -> Self {
244        self.items.push(item);
245        self
246    }
247
248    /// Adds a computed expression with an alias.
249    #[must_use]
250    pub fn computed(mut self, expr: Expr, alias: impl Into<String>) -> Self {
251        self.items.push(SelectItem::computed(expr, alias));
252        self
253    }
254
255    /// Adds a WHERE clause filter.
256    #[must_use]
257    pub fn filter(mut self, expr: Expr) -> Self {
258        self.filter = Some(expr);
259        self
260    }
261
262    /// Adds an ORDER BY clause.
263    #[must_use]
264    pub fn order_by(mut self, column: impl Into<String>, direction: OrderDir) -> Self {
265        let order = OrderBy::new(column, direction);
266        self.order_by.push(order.clone());
267        self.order_by_items.push(OrderByItem::Column(order));
268        self
269    }
270
271    /// Adds an ORDER BY ASC clause.
272    #[must_use]
273    pub fn order_by_asc(mut self, column: impl Into<String>) -> Self {
274        let order = OrderBy::asc(column);
275        self.order_by.push(order.clone());
276        self.order_by_items.push(OrderByItem::Column(order));
277        self
278    }
279
280    /// Adds an ORDER BY DESC clause.
281    #[must_use]
282    pub fn order_by_desc(mut self, column: impl Into<String>) -> Self {
283        let order = OrderBy::desc(column);
284        self.order_by.push(order.clone());
285        self.order_by_items.push(OrderByItem::Column(order));
286        self
287    }
288
289    /// Sets the row count (maps to SQL LIMIT).
290    ///
291    /// Pass a positive value for forward pagination. Negative values signal
292    /// backward pagination to callers; the dialect renders the absolute value.
293    #[must_use]
294    pub fn take(mut self, n: i32) -> Self {
295        self.take = Some(n);
296        self
297    }
298
299    /// Sets the row offset (maps to SQL OFFSET).
300    #[must_use]
301    pub fn skip(mut self, n: u32) -> Self {
302        self.skip = Some(n);
303        self
304    }
305
306    /// Adds a JOIN clause.
307    #[must_use]
308    pub fn join(mut self, clause: JoinClause) -> Self {
309        self.joins.push(clause);
310        self
311    }
312
313    /// Adds an INNER JOIN clause.
314    #[must_use]
315    pub fn inner_join(self, table: impl Into<String>, on: Expr, items: Vec<SelectItem>) -> Self {
316        self.join(JoinClause::new(JoinType::Inner, table, on, items))
317    }
318
319    /// Adds a LEFT JOIN clause.
320    #[must_use]
321    pub fn left_join(self, table: impl Into<String>, on: Expr, items: Vec<SelectItem>) -> Self {
322        self.join(JoinClause::new(JoinType::Left, table, on, items))
323    }
324
325    /// Adds a GROUP BY clause.
326    #[must_use]
327    pub fn group_by_column(mut self, column: ColumnMarker) -> Self {
328        self.group_by.push(column);
329        self
330    }
331
332    /// Adds multiple columns to the GROUP BY clause.
333    #[must_use]
334    pub fn group_by(mut self, columns: Vec<ColumnMarker>) -> Self {
335        self.group_by.extend(columns);
336        self
337    }
338
339    /// Sets the HAVING clause (post-GROUP BY filter).
340    #[must_use]
341    pub fn having(mut self, expr: Expr) -> Self {
342        self.having = Some(expr);
343        self
344    }
345
346    /// Adds an ORDER BY clause using an arbitrary expression (e.g. an aggregate function).
347    #[must_use]
348    pub fn order_by_expr(mut self, expr: Expr, direction: OrderDir) -> Self {
349        self.order_by_exprs.push((expr.clone(), direction));
350        self.order_by_items.push(OrderByItem::Expr(expr, direction));
351        self
352    }
353
354    /// Sets the columns to deduplicate on (SELECT DISTINCT / DISTINCT ON).
355    ///
356    /// - **Postgres**: emits `SELECT DISTINCT ON (col, ...)` and requires those
357    ///   columns to appear first in `ORDER BY` (callers should prepend them).
358    /// - **SQLite / MySQL**: emits plain `SELECT DISTINCT`.
359    #[must_use]
360    pub fn distinct(mut self, columns: Vec<String>) -> Self {
361        self.distinct = columns;
362        self
363    }
364
365    /// Builds the final SELECT query.
366    pub fn build(self) -> Result<Select> {
367        if self.table.is_empty() {
368            return Err(Error::MissingField("table".to_string()));
369        }
370
371        Ok(Select {
372            table: self.table,
373            items: self.items,
374            joins: self.joins,
375            filter: self.filter,
376            order_by: self.order_by,
377            take: self.take,
378            skip: self.skip,
379            group_by: self.group_by,
380            distinct: self.distinct,
381            having: self.having,
382            order_by_items: self.order_by_items,
383            order_by_exprs: self.order_by_exprs,
384        })
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use crate::expr::Expr;
392
393    #[test]
394    fn test_order_by() {
395        let asc = OrderBy::asc("id");
396        assert_eq!(asc.column, "id");
397        assert_eq!(asc.direction, OrderDir::Asc);
398
399        let desc = OrderBy::desc("created_at");
400        assert_eq!(desc.column, "created_at");
401        assert_eq!(desc.direction, OrderDir::Desc);
402    }
403
404    #[test]
405    fn test_simple_select() {
406        let query = Select::from_table("users").build().unwrap();
407
408        assert_eq!(query.table, "users");
409        assert!(query.items.is_empty());
410        assert!(query.joins.is_empty());
411        assert!(query.filter.is_none());
412        assert!(query.order_by.is_empty());
413        assert!(query.take.is_none());
414        assert!(query.skip.is_none());
415    }
416
417    #[test]
418    fn test_select_with_columns() {
419        let query = Select::from_table("users")
420            .item(SelectItem::from(ColumnMarker::new("users", "id")))
421            .item(SelectItem::from(ColumnMarker::new("users", "email")))
422            .build()
423            .unwrap();
424
425        assert_eq!(query.items.len(), 2);
426        if let SelectItem::Column(col) = &query.items[0] {
427            assert_eq!(col.table, "users");
428            assert_eq!(col.name, "id");
429        }
430        if let SelectItem::Column(col) = &query.items[1] {
431            assert_eq!(col.table, "users");
432            assert_eq!(col.name, "email");
433        }
434    }
435
436    #[test]
437    fn test_select_with_filter() {
438        let filter = Expr::column("age").ge(Expr::param(18i64));
439        let query = Select::from_table("users")
440            .filter(filter.clone())
441            .build()
442            .unwrap();
443
444        assert_eq!(query.filter, Some(filter));
445    }
446
447    #[test]
448    fn test_select_with_order_by() {
449        let query = Select::from_table("users")
450            .order_by_desc("created_at")
451            .order_by_asc("email")
452            .build()
453            .unwrap();
454
455        assert_eq!(query.order_by.len(), 2);
456        assert_eq!(query.order_by[0].column, "created_at");
457        assert_eq!(query.order_by[0].direction, OrderDir::Desc);
458        assert_eq!(query.order_by[1].column, "email");
459        assert_eq!(query.order_by[1].direction, OrderDir::Asc);
460    }
461
462    #[test]
463    fn test_select_with_take_and_skip() {
464        let query = Select::from_table("users")
465            .take(10)
466            .skip(20)
467            .build()
468            .unwrap();
469
470        assert_eq!(query.take, Some(10));
471        assert_eq!(query.skip, Some(20));
472    }
473
474    #[test]
475    fn test_complex_select() {
476        let filter = Expr::column("age")
477            .ge(Expr::param(18i64))
478            .and(Expr::column("email").like(Expr::param("%@gmail.com")));
479
480        let query = Select::from_table("users")
481            .items(vec![
482                SelectItem::from(ColumnMarker::new("users", "id")),
483                SelectItem::from(ColumnMarker::new("users", "email")),
484                SelectItem::from(ColumnMarker::new("users", "age")),
485            ])
486            .filter(filter)
487            .order_by_desc("id")
488            .take(10)
489            .build()
490            .unwrap();
491
492        assert_eq!(query.table, "users");
493        assert_eq!(query.items.len(), 3);
494        assert!(query.filter.is_some());
495        assert_eq!(query.order_by.len(), 1);
496        assert_eq!(query.take, Some(10));
497    }
498
499    #[test]
500    fn test_select_with_inner_join() {
501        let on = Expr::column("users__id").eq(Expr::column("posts__user_id"));
502        let query = Select::from_table("users")
503            .item(SelectItem::from(ColumnMarker::new("users", "id")))
504            .inner_join(
505                "posts",
506                on.clone(),
507                vec![
508                    SelectItem::from(ColumnMarker::new("posts", "id")),
509                    SelectItem::from(ColumnMarker::new("posts", "title")),
510                ],
511            )
512            .build()
513            .unwrap();
514
515        assert_eq!(query.joins.len(), 1);
516        assert_eq!(query.joins[0].join_type, JoinType::Inner);
517        assert_eq!(query.joins[0].table, "posts");
518        assert_eq!(query.joins[0].on, on);
519        assert_eq!(query.joins[0].items.len(), 2);
520    }
521
522    #[test]
523    fn test_select_with_left_join() {
524        let on = Expr::column("users__id").eq(Expr::column("posts__user_id"));
525        let query = Select::from_table("users")
526            .item(SelectItem::from(ColumnMarker::new("users", "id")))
527            .left_join(
528                "posts",
529                on,
530                vec![SelectItem::from(ColumnMarker::new("posts", "title"))],
531            )
532            .build()
533            .unwrap();
534
535        assert_eq!(query.joins.len(), 1);
536        assert_eq!(query.joins[0].join_type, JoinType::Left);
537        assert_eq!(query.joins[0].table, "posts");
538        assert_eq!(query.joins[0].items.len(), 1);
539    }
540
541    #[test]
542    fn test_select_with_multiple_joins() {
543        let query = Select::from_table("users")
544            .inner_join(
545                "posts",
546                Expr::column("users__id").eq(Expr::column("posts__user_id")),
547                vec![SelectItem::from(ColumnMarker::new("posts", "title"))],
548            )
549            .left_join(
550                "comments",
551                Expr::column("posts__id").eq(Expr::column("comments__post_id")),
552                vec![SelectItem::from(ColumnMarker::new("comments", "body"))],
553            )
554            .build()
555            .unwrap();
556
557        assert_eq!(query.joins.len(), 2);
558        assert_eq!(query.joins[0].join_type, JoinType::Inner);
559        assert_eq!(query.joins[0].table, "posts");
560        assert_eq!(query.joins[1].join_type, JoinType::Left);
561        assert_eq!(query.joins[1].table, "comments");
562    }
563}