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)]
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/// Type of JOIN operation.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum JoinType {
83    /// INNER JOIN — only matching rows from both tables.
84    Inner,
85    /// LEFT JOIN — all rows from the left table, matching rows from the right.
86    Left,
87}
88
89/// A JOIN clause attached to a SELECT query.
90#[derive(Debug, Clone, PartialEq)]
91pub struct JoinClause {
92    /// The type of join (INNER, LEFT).
93    pub join_type: JoinType,
94    /// The table to join.
95    pub table: String,
96    /// The ON condition expression.
97    pub on: Expr,
98    /// Select items (columns or computed expressions) from the joined table.
99    pub items: Vec<SelectItem>,
100}
101
102impl JoinClause {
103    /// Creates a new JOIN clause.
104    pub fn new(
105        join_type: JoinType,
106        table: impl Into<String>,
107        on: Expr,
108        items: Vec<SelectItem>,
109    ) -> Self {
110        Self {
111            join_type,
112            table: table.into(),
113            on,
114            items,
115        }
116    }
117}
118
119/// SELECT query AST node.
120#[derive(Debug, Clone, PartialEq)]
121pub struct Select {
122    /// Table name.
123    pub table: String,
124    /// Select items (columns or computed expressions).
125    pub items: Vec<SelectItem>,
126    /// JOIN clauses.
127    pub joins: Vec<JoinClause>,
128    /// WHERE clause.
129    pub filter: Option<Expr>,
130    /// ORDER BY clauses.
131    pub order_by: Vec<OrderBy>,
132    /// Row count to return (maps to SQL LIMIT).
133    ///
134    /// Positive values limit forward; negative values signal backward pagination
135    /// (callers must negate and reverse `ORDER BY` in application code).
136    /// The absolute value is used when building the SQL `LIMIT` clause.
137    pub take: Option<i32>,
138    /// Row offset to skip (maps to SQL OFFSET).
139    pub skip: Option<u32>,
140    /// GROUP BY columns.
141    pub group_by: Vec<ColumnMarker>,
142    /// Columns to deduplicate on.
143    ///
144    /// Non-empty activates deduplication:
145    /// - Postgres: `SELECT DISTINCT ON (cols)`
146    /// - SQLite / MySQL: plain `SELECT DISTINCT`
147    pub distinct: Vec<String>,
148    /// HAVING clause (post-GROUP BY filter).
149    pub having: Option<Expr>,
150    /// ORDER BY expression items (for aggregate functions, e.g. `COUNT(*) DESC`).
151    pub order_by_exprs: Vec<(Expr, OrderDir)>,
152}
153
154impl Select {
155    /// Creates a new SELECT query builder for the given table.
156    pub fn from_table(table: impl Into<String>) -> SelectBuilder {
157        SelectBuilder {
158            table: table.into(),
159            items: Vec::new(),
160            joins: Vec::new(),
161            filter: None,
162            order_by: Vec::new(),
163            take: None,
164            skip: None,
165            group_by: Vec::new(),
166            distinct: Vec::new(),
167            having: None,
168            order_by_exprs: Vec::new(),
169        }
170    }
171}
172
173/// Builder for SELECT queries.
174#[derive(Debug, Clone)]
175pub struct SelectBuilder {
176    table: String,
177    items: Vec<SelectItem>,
178    joins: Vec<JoinClause>,
179    filter: Option<Expr>,
180    order_by: Vec<OrderBy>,
181    take: Option<i32>,
182    skip: Option<u32>,
183    group_by: Vec<ColumnMarker>,
184    distinct: Vec<String>,
185    having: Option<Expr>,
186    order_by_exprs: Vec<(Expr, OrderDir)>,
187}
188
189impl SelectBuilder {
190    /// Sets the select items.
191    #[must_use]
192    pub fn items(mut self, items: Vec<SelectItem>) -> Self {
193        self.items = items;
194        self
195    }
196
197    /// Adds a select item.
198    #[must_use]
199    pub fn item(mut self, item: SelectItem) -> Self {
200        self.items.push(item);
201        self
202    }
203
204    /// Adds a computed expression with an alias.
205    #[must_use]
206    pub fn computed(mut self, expr: Expr, alias: impl Into<String>) -> Self {
207        self.items.push(SelectItem::computed(expr, alias));
208        self
209    }
210
211    /// Adds a WHERE clause filter.
212    #[must_use]
213    pub fn filter(mut self, expr: Expr) -> Self {
214        self.filter = Some(expr);
215        self
216    }
217
218    /// Adds an ORDER BY clause.
219    #[must_use]
220    pub fn order_by(mut self, column: impl Into<String>, direction: OrderDir) -> Self {
221        self.order_by.push(OrderBy::new(column, direction));
222        self
223    }
224
225    /// Adds an ORDER BY ASC clause.
226    #[must_use]
227    pub fn order_by_asc(mut self, column: impl Into<String>) -> Self {
228        self.order_by.push(OrderBy::asc(column));
229        self
230    }
231
232    /// Adds an ORDER BY DESC clause.
233    #[must_use]
234    pub fn order_by_desc(mut self, column: impl Into<String>) -> Self {
235        self.order_by.push(OrderBy::desc(column));
236        self
237    }
238
239    /// Sets the row count (maps to SQL LIMIT).
240    ///
241    /// Pass a positive value for forward pagination. Negative values signal
242    /// backward pagination to callers; the dialect renders the absolute value.
243    #[must_use]
244    pub fn take(mut self, n: i32) -> Self {
245        self.take = Some(n);
246        self
247    }
248
249    /// Sets the row offset (maps to SQL OFFSET).
250    #[must_use]
251    pub fn skip(mut self, n: u32) -> Self {
252        self.skip = Some(n);
253        self
254    }
255
256    /// Adds a JOIN clause.
257    #[must_use]
258    pub fn join(mut self, clause: JoinClause) -> Self {
259        self.joins.push(clause);
260        self
261    }
262
263    /// Adds an INNER JOIN clause.
264    #[must_use]
265    pub fn inner_join(self, table: impl Into<String>, on: Expr, items: Vec<SelectItem>) -> Self {
266        self.join(JoinClause::new(JoinType::Inner, table, on, items))
267    }
268
269    /// Adds a LEFT JOIN clause.
270    #[must_use]
271    pub fn left_join(self, table: impl Into<String>, on: Expr, items: Vec<SelectItem>) -> Self {
272        self.join(JoinClause::new(JoinType::Left, table, on, items))
273    }
274
275    /// Adds a GROUP BY clause.
276    #[must_use]
277    pub fn group_by_column(mut self, column: ColumnMarker) -> Self {
278        self.group_by.push(column);
279        self
280    }
281
282    /// Adds multiple columns to the GROUP BY clause.
283    #[must_use]
284    pub fn group_by(mut self, columns: Vec<ColumnMarker>) -> Self {
285        self.group_by.extend(columns);
286        self
287    }
288
289    /// Sets the HAVING clause (post-GROUP BY filter).
290    #[must_use]
291    pub fn having(mut self, expr: Expr) -> Self {
292        self.having = Some(expr);
293        self
294    }
295
296    /// Adds an ORDER BY clause using an arbitrary expression (e.g. an aggregate function).
297    #[must_use]
298    pub fn order_by_expr(mut self, expr: Expr, direction: OrderDir) -> Self {
299        self.order_by_exprs.push((expr, direction));
300        self
301    }
302
303    /// Sets the columns to deduplicate on (SELECT DISTINCT / DISTINCT ON).
304    ///
305    /// - **Postgres**: emits `SELECT DISTINCT ON (col, ...)` and requires those
306    ///   columns to appear first in `ORDER BY` (callers should prepend them).
307    /// - **SQLite / MySQL**: emits plain `SELECT DISTINCT`.
308    #[must_use]
309    pub fn distinct(mut self, columns: Vec<String>) -> Self {
310        self.distinct = columns;
311        self
312    }
313
314    /// Builds the final SELECT query.
315    pub fn build(self) -> Result<Select> {
316        if self.table.is_empty() {
317            return Err(Error::MissingField("table".to_string()));
318        }
319
320        Ok(Select {
321            table: self.table,
322            items: self.items,
323            joins: self.joins,
324            filter: self.filter,
325            order_by: self.order_by,
326            take: self.take,
327            skip: self.skip,
328            group_by: self.group_by,
329            distinct: self.distinct,
330            having: self.having,
331            order_by_exprs: self.order_by_exprs,
332        })
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::expr::Expr;
340
341    #[test]
342    fn test_order_by() {
343        let asc = OrderBy::asc("id");
344        assert_eq!(asc.column, "id");
345        assert_eq!(asc.direction, OrderDir::Asc);
346
347        let desc = OrderBy::desc("created_at");
348        assert_eq!(desc.column, "created_at");
349        assert_eq!(desc.direction, OrderDir::Desc);
350    }
351
352    #[test]
353    fn test_simple_select() {
354        let query = Select::from_table("users").build().unwrap();
355
356        assert_eq!(query.table, "users");
357        assert!(query.items.is_empty());
358        assert!(query.joins.is_empty());
359        assert!(query.filter.is_none());
360        assert!(query.order_by.is_empty());
361        assert!(query.take.is_none());
362        assert!(query.skip.is_none());
363    }
364
365    #[test]
366    fn test_select_with_columns() {
367        let query = Select::from_table("users")
368            .item(SelectItem::from(ColumnMarker::new("users", "id")))
369            .item(SelectItem::from(ColumnMarker::new("users", "email")))
370            .build()
371            .unwrap();
372
373        assert_eq!(query.items.len(), 2);
374        if let SelectItem::Column(col) = &query.items[0] {
375            assert_eq!(col.table, "users");
376            assert_eq!(col.name, "id");
377        }
378        if let SelectItem::Column(col) = &query.items[1] {
379            assert_eq!(col.table, "users");
380            assert_eq!(col.name, "email");
381        }
382    }
383
384    #[test]
385    fn test_select_with_filter() {
386        let filter = Expr::column("age").ge(Expr::param(18i64));
387        let query = Select::from_table("users")
388            .filter(filter.clone())
389            .build()
390            .unwrap();
391
392        assert_eq!(query.filter, Some(filter));
393    }
394
395    #[test]
396    fn test_select_with_order_by() {
397        let query = Select::from_table("users")
398            .order_by_desc("created_at")
399            .order_by_asc("email")
400            .build()
401            .unwrap();
402
403        assert_eq!(query.order_by.len(), 2);
404        assert_eq!(query.order_by[0].column, "created_at");
405        assert_eq!(query.order_by[0].direction, OrderDir::Desc);
406        assert_eq!(query.order_by[1].column, "email");
407        assert_eq!(query.order_by[1].direction, OrderDir::Asc);
408    }
409
410    #[test]
411    fn test_select_with_take_and_skip() {
412        let query = Select::from_table("users")
413            .take(10)
414            .skip(20)
415            .build()
416            .unwrap();
417
418        assert_eq!(query.take, Some(10));
419        assert_eq!(query.skip, Some(20));
420    }
421
422    #[test]
423    fn test_complex_select() {
424        let filter = Expr::column("age")
425            .ge(Expr::param(18i64))
426            .and(Expr::column("email").like(Expr::param("%@gmail.com")));
427
428        let query = Select::from_table("users")
429            .items(vec![
430                SelectItem::from(ColumnMarker::new("users", "id")),
431                SelectItem::from(ColumnMarker::new("users", "email")),
432                SelectItem::from(ColumnMarker::new("users", "age")),
433            ])
434            .filter(filter)
435            .order_by_desc("id")
436            .take(10)
437            .build()
438            .unwrap();
439
440        assert_eq!(query.table, "users");
441        assert_eq!(query.items.len(), 3);
442        assert!(query.filter.is_some());
443        assert_eq!(query.order_by.len(), 1);
444        assert_eq!(query.take, Some(10));
445    }
446
447    #[test]
448    fn test_select_with_inner_join() {
449        let on = Expr::column("users__id").eq(Expr::column("posts__user_id"));
450        let query = Select::from_table("users")
451            .item(SelectItem::from(ColumnMarker::new("users", "id")))
452            .inner_join(
453                "posts",
454                on.clone(),
455                vec![
456                    SelectItem::from(ColumnMarker::new("posts", "id")),
457                    SelectItem::from(ColumnMarker::new("posts", "title")),
458                ],
459            )
460            .build()
461            .unwrap();
462
463        assert_eq!(query.joins.len(), 1);
464        assert_eq!(query.joins[0].join_type, JoinType::Inner);
465        assert_eq!(query.joins[0].table, "posts");
466        assert_eq!(query.joins[0].on, on);
467        assert_eq!(query.joins[0].items.len(), 2);
468    }
469
470    #[test]
471    fn test_select_with_left_join() {
472        let on = Expr::column("users__id").eq(Expr::column("posts__user_id"));
473        let query = Select::from_table("users")
474            .item(SelectItem::from(ColumnMarker::new("users", "id")))
475            .left_join(
476                "posts",
477                on,
478                vec![SelectItem::from(ColumnMarker::new("posts", "title"))],
479            )
480            .build()
481            .unwrap();
482
483        assert_eq!(query.joins.len(), 1);
484        assert_eq!(query.joins[0].join_type, JoinType::Left);
485        assert_eq!(query.joins[0].table, "posts");
486        assert_eq!(query.joins[0].items.len(), 1);
487    }
488
489    #[test]
490    fn test_select_with_multiple_joins() {
491        let query = Select::from_table("users")
492            .inner_join(
493                "posts",
494                Expr::column("users__id").eq(Expr::column("posts__user_id")),
495                vec![SelectItem::from(ColumnMarker::new("posts", "title"))],
496            )
497            .left_join(
498                "comments",
499                Expr::column("posts__id").eq(Expr::column("comments__post_id")),
500                vec![SelectItem::from(ColumnMarker::new("comments", "body"))],
501            )
502            .build()
503            .unwrap();
504
505        assert_eq!(query.joins.len(), 2);
506        assert_eq!(query.joins[0].join_type, JoinType::Inner);
507        assert_eq!(query.joins[0].table, "posts");
508        assert_eq!(query.joins[1].join_type, JoinType::Left);
509        assert_eq!(query.joins[1].table, "comments");
510    }
511}