sqlorm_core/qb/
mod.rs

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