sqlorm_core/qb/
mod.rs

1mod bind;
2mod column;
3pub mod condition;
4use std::fmt::Debug;
5
6#[cfg(any(feature = "postgres", feature = "sqlite"))]
7use crate::driver::{Driver, Row};
8use crate::format_alised_col_name;
9pub use bind::BindValue;
10pub use column::Column;
11pub use condition::Condition;
12use sqlx::FromRow;
13use sqlx::QueryBuilder;
14
15/// Quote identifiers appropriately for the target database
16/// Both PostgreSQL and SQLite support double quotes for identifiers
17pub fn with_quotes(s: &str) -> String {
18    // Double quotes work for both PostgreSQL and SQLite
19    // This ensures consistent behavior across databases
20    format!("\"{}\"", s)
21}
22
23#[derive(Debug)]
24/// Query builder for composing SELECT statements with optional joins and filters.
25pub struct QB<T: std::fmt::Debug> {
26    /// Base table information and selected columns.
27    pub base: TableInfo,
28    /// Eager joins that project columns from related tables.
29    pub eager: Vec<JoinSpec>,
30    /// Batch joins for has-many relations.
31    pub batch: Vec<JoinSpec>,
32    /// WHERE clause conditions combined with AND.
33    pub filters: Vec<Condition>,
34    /// Debug flag to enable query debugging output.
35    pub debug_enabled: bool,
36    _marker: std::marker::PhantomData<T>,
37}
38#[derive(Clone, Debug)]
39/// Static information about a table used to build queries.
40pub struct TableInfo {
41    /// Database table name.
42    pub name: &'static str,
43    /// SQL alias to use for the table in the query.
44    pub alias: String,
45    /// Columns to project for this table.
46    pub columns: Vec<&'static str>,
47}
48
49#[derive(Clone, Debug)]
50/// Join type for related tables.
51pub enum JoinType {
52    Inner,
53    Left,
54}
55
56#[derive(Clone, Debug)]
57/// Specification for joining a related table.
58pub struct JoinSpec {
59    /// The join type.
60    pub join_type: JoinType,
61    /// Relation name.
62    pub relation_name: &'static str,
63    /// The joined table metadata.
64    pub foreign_table: TableInfo,
65    /// Join key mapping as (base_pk, foreign_fk).
66    pub on: (&'static str, &'static str),
67}
68
69impl<T: std::fmt::Debug> QB<T> {
70    pub fn new(base: TableInfo) -> QB<T> {
71        QB {
72            base,
73            eager: Vec::new(),
74            batch: Vec::new(),
75            filters: Vec::new(),
76            debug_enabled: false,
77            _marker: std::marker::PhantomData,
78        }
79    }
80
81    pub fn join_eager(mut self, spec: JoinSpec) -> Self {
82        self.eager.push(spec);
83        self
84    }
85
86    pub fn join_batch(mut self, spec: JoinSpec) -> Self {
87        self.batch.push(spec);
88        self
89    }
90
91    pub fn select<'a, Out: Debug + FromRow<'a, Row>>(mut self, cols: Vec<&'static str>) -> QB<Out> {
92        if cols.is_empty() {
93            panic!("Cannot select empty column list. At least one column must be specified.");
94        }
95        self.base.columns = cols;
96        QB {
97            base: self.base,
98            eager: self.eager,
99            batch: self.batch,
100            filters: self.filters,
101            debug_enabled: self.debug_enabled,
102            _marker: std::marker::PhantomData,
103        }
104    }
105
106    fn build_projections(&self) -> Vec<String> {
107        let mut projections = Vec::new();
108
109        for col in &self.base.columns {
110            let field = format!("{}.{}", self.base.alias, col);
111            let as_field = format_alised_col_name(&self.base.alias, col);
112            projections.push(format!("{} AS {}", field, as_field));
113        }
114
115        for join in &self.eager {
116            for col in &join.foreign_table.columns {
117                let field = format!("{}.{}", join.foreign_table.alias, col);
118                let as_field = format_alised_col_name(&join.foreign_table.alias, col);
119                projections.push(format!("{} AS {}", field, as_field));
120            }
121        }
122
123        projections
124    }
125
126    fn build_from_clause(&self) -> String {
127        format!(
128            "FROM {} AS {}",
129            with_quotes(self.base.name),
130            self.base.alias
131        )
132    }
133
134    pub fn filter(mut self, cond: Condition) -> Self {
135        self.filters.push(cond);
136        self
137    }
138
139    /// Enable debug mode for this query, which will print the generated SQL and bind values.
140    pub fn debug(mut self) -> Self {
141        self.debug_enabled = true;
142        self
143    }
144
145    /// Print debug information about the query if debug mode is enabled.
146    pub fn debug_query(&self) {
147        if self.debug_enabled {
148            let query_builder = self.build_query();
149            let sql = query_builder.sql();
150            
151            println!("[SQLOrm Debug] SQL: {}", sql);
152            
153            if !self.filters.is_empty() {
154                println!("[SQLOrm Debug] Parameters:");
155                let mut param_index = 0;
156                for condition in &self.filters {
157                    for value in &condition.values {
158                        param_index += 1;
159                        println!("[SQLOrm Debug]   ${}: {:?}", param_index, value);
160                    }
161                }
162            }
163        }
164    }
165
166    fn build_joins(&self) -> String {
167        let mut joins = String::new();
168
169        for join in &self.eager {
170            let other_table = format!(
171                "{} AS {}",
172                with_quotes(join.foreign_table.name),
173                join.foreign_table.alias
174            );
175
176            let jt = match join.join_type {
177                JoinType::Inner => "INNER JOIN",
178                JoinType::Left => "LEFT JOIN",
179            };
180
181            let on_base = format!("{}.{}", self.base.alias, join.on.0);
182            let on_other = format!("{}.{}", join.foreign_table.alias, join.on.1);
183
184            joins.push_str(&format!(
185                " {} {} ON {} = {}",
186                jt, other_table, on_base, on_other
187            ));
188        }
189
190        joins
191    }
192
193    pub fn build_query(&self) -> QueryBuilder<'static, Driver> {
194        let projections = self.build_projections().join(", ");
195        let from_clause = self.build_from_clause();
196        let joins = self.build_joins();
197
198        let mut builder = QueryBuilder::new("SELECT ");
199        builder.push(projections);
200        builder.push(" ");
201        builder.push(from_clause);
202        builder.push(" ");
203        builder.push(joins);
204
205        if !self.filters.is_empty() {
206            builder.push(" WHERE ");
207
208            for (i, cond) in self.filters.iter().enumerate() {
209                if i > 0 {
210                    builder.push(" AND ");
211                }
212
213                let mut parts = cond.sql.split('?');
214                if let Some(first) = parts.next() {
215                    builder.push(first);
216                }
217
218                for (val, part) in cond.values.iter().zip(parts) {
219                    val.bind(&mut builder);
220                    builder.push(part);
221                }
222            }
223        }
224
225        builder
226    }
227
228    pub fn to_sql(&self) -> String {
229        self.build_query().sql().to_string()
230    }
231}