sqlorm_core/qb/
mod.rs

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