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