Skip to main content

toolu_orm_query/select/
builder.rs

1//! Core [`SelectBuilder`] struct with SQL generation methods.
2//!
3//! # Public API
4//!
5//! - [`SelectBuilder`] — fluent builder for SELECT queries
6//!
7//! # Usage
8//!
9//! ```ignore
10//! let (sql, params) = SelectBuilder::new("users")
11//!     .columns_raw(&["id", "name"])
12//!     .to_sql();
13//! ```
14
15use toolu_orm_core::dialect::Dialect;
16use toolu_orm_core::expr::{Expr, JoinCondition, OrderBy};
17use toolu_orm_core::query_column::ColumnRef;
18use toolu_orm_core::value::Value;
19
20use crate::where_clause::{append_where_for, impl_filter};
21
22// ── JoinClause ────────────────────────────────────────────────────────────────
23
24pub(super) struct JoinClause {
25  pub join_type: &'static str,
26  pub table: String,
27  pub condition: JoinCondition,
28}
29
30// ── SelectBuilder ─────────────────────────────────────────────────────────────
31
32pub struct SelectBuilder {
33  pub(super) table: String,
34  pub(super) columns: Vec<String>,
35  pub(super) filters: Vec<Expr>,
36  pub(super) joins: Vec<JoinClause>,
37  pub(super) order_bys: Vec<OrderBy>,
38  pub(super) limit_val: Option<i64>,
39  pub(super) offset_val: Option<i64>,
40  pub(super) column_exprs: Vec<(String, String)>,
41  pub(super) is_raw: bool,
42}
43
44impl_filter!(SelectBuilder);
45
46impl SelectBuilder {
47  pub fn new(table: &str) -> Self {
48    Self {
49      table: table.to_owned(),
50      columns: Vec::new(),
51      filters: Vec::new(),
52      joins: Vec::new(),
53      order_bys: Vec::new(),
54      limit_val: None,
55      offset_val: None,
56      column_exprs: Vec::new(),
57      is_raw: false,
58    }
59  }
60
61  pub fn raw() -> Self {
62    Self {
63      table: String::new(),
64      columns: Vec::new(),
65      filters: Vec::new(),
66      joins: Vec::new(),
67      order_bys: Vec::new(),
68      limit_val: None,
69      offset_val: None,
70      column_exprs: Vec::new(),
71      is_raw: true,
72    }
73  }
74
75  pub fn columns_raw(mut self, cols: &[&str]) -> Self {
76    self.columns = cols.iter().map(|c| (*c).to_owned()).collect();
77    self
78  }
79
80  pub fn columns_typed(mut self, cols: &[&dyn ColumnRef]) -> Self {
81    self.columns = cols.iter().map(|c| c.name().to_owned()).collect();
82    self
83  }
84
85  pub fn join(mut self, table: &str, on: JoinCondition) -> Self {
86    self.joins.push(JoinClause {
87      join_type: "INNER JOIN",
88      table: table.to_owned(),
89      condition: on,
90    });
91    self
92  }
93
94  pub fn left_join(mut self, table: &str, on: JoinCondition) -> Self {
95    self.joins.push(JoinClause {
96      join_type: "LEFT JOIN",
97      table: table.to_owned(),
98      condition: on,
99    });
100    self
101  }
102
103  pub fn order_by(mut self, ob: OrderBy) -> Self {
104    self.order_bys.push(ob);
105    self
106  }
107
108  pub fn limit(mut self, n: i64) -> Self {
109    self.limit_val = Some(n);
110    self
111  }
112
113  pub fn offset(mut self, n: i64) -> Self {
114    self.offset_val = Some(n);
115    self
116  }
117
118  pub fn column_expr(mut self, expr: &str, alias: &str) -> Self {
119    self.column_exprs.push((expr.to_owned(), alias.to_owned()));
120    self
121  }
122
123  pub fn to_sql_for(&self, dialect: Dialect) -> (String, Vec<Value>) {
124    let mut sql = String::new();
125    let mut params: Vec<Value> = Vec::new();
126
127    sql.push_str("SELECT ");
128    sql.push_str(&self.build_select_list());
129
130    if !self.is_raw {
131      sql.push_str(&format!(r#" FROM "{}""#, self.table));
132      self.append_joins(&mut sql);
133    }
134
135    append_where_for(&self.filters, &mut sql, &mut params, dialect);
136    self.append_order_by(&mut sql);
137    self.append_limit_offset_for(&mut sql, &mut params, dialect);
138
139    (sql, params)
140  }
141
142  pub fn to_sql(&self) -> (String, Vec<Value>) {
143    self.to_sql_for(Dialect::CURRENT)
144  }
145
146  pub fn to_count_sql_for(&self, dialect: Dialect) -> (String, Vec<Value>) {
147    let mut sql = String::new();
148    let mut params: Vec<Value> = Vec::new();
149
150    sql.push_str(&format!(r#"SELECT COUNT(*) FROM "{}""#, self.table));
151    self.append_joins(&mut sql);
152    append_where_for(&self.filters, &mut sql, &mut params, dialect);
153
154    (sql, params)
155  }
156
157  pub fn to_count_sql(&self) -> (String, Vec<Value>) {
158    self.to_count_sql_for(Dialect::CURRENT)
159  }
160
161  pub fn to_exists_sql_for(&self, dialect: Dialect) -> (String, Vec<Value>) {
162    let mut inner = String::new();
163    let mut params: Vec<Value> = Vec::new();
164
165    inner.push_str(&format!(r#"SELECT 1 FROM "{}""#, self.table));
166    self.append_joins(&mut inner);
167    append_where_for(&self.filters, &mut inner, &mut params, dialect);
168
169    (format!("SELECT EXISTS({inner})"), params)
170  }
171
172  pub fn to_exists_sql(&self) -> (String, Vec<Value>) {
173    self.to_exists_sql_for(Dialect::CURRENT)
174  }
175
176  pub fn table_name(&self) -> &str {
177    &self.table
178  }
179
180  // ── Helpers ────────────────────────────────────────────────────────────────
181
182  fn build_select_list(&self) -> String {
183    if self.is_raw && !self.column_exprs.is_empty() {
184      self
185        .column_exprs
186        .iter()
187        .map(|(expr, alias)| format!(r#"{expr} AS "{alias}""#))
188        .collect::<Vec<_>>()
189        .join(", ")
190    } else {
191      self
192        .columns
193        .iter()
194        .map(|c| format!(r#""{c}""#))
195        .collect::<Vec<_>>()
196        .join(", ")
197    }
198  }
199
200  fn append_joins(&self, sql: &mut String) {
201    for join in &self.joins {
202      sql.push_str(&format!(
203        r#" {} "{}" ON {}"#,
204        join.join_type,
205        join.table,
206        join.condition.to_sql()
207      ));
208    }
209  }
210
211  fn append_order_by(&self, sql: &mut String) {
212    if self.order_bys.is_empty() {
213      return;
214    }
215    let parts: Vec<String> = self.order_bys.iter().map(|ob| ob.to_sql()).collect();
216    sql.push_str(&format!(" ORDER BY {}", parts.join(", ")));
217  }
218
219  fn append_limit_offset_for(&self, sql: &mut String, params: &mut Vec<Value>, dialect: Dialect) {
220    if let Some(limit) = self.limit_val {
221      let idx = params.len() + 1;
222      params.push(Value::Integer(limit));
223      sql.push_str(&format!(" LIMIT {}", dialect.param(idx)));
224    }
225    if let Some(offset) = self.offset_val {
226      let idx = params.len() + 1;
227      params.push(Value::Integer(offset));
228      sql.push_str(&format!(" OFFSET {}", dialect.param(idx)));
229    }
230  }
231}