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;
8use crate::format_alised_col_name;
9use crate::selectable::Selectable;
10pub use bind::BindValue;
11pub use column::Column;
12pub use condition::Condition;
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> {
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    _marker: std::marker::PhantomData<T>,
35}
36#[derive(Clone, Debug)]
37/// Static information about a table used to build queries.
38pub struct TableInfo {
39    /// Database table name.
40    pub name: &'static str,
41    /// SQL alias to use for the table in the query.
42    pub alias: String,
43    /// Columns to project for this table.
44    pub columns: Vec<&'static str>,
45}
46
47#[derive(Clone, Debug)]
48/// Join type for related tables.
49pub enum JoinType {
50    Inner,
51    Left,
52}
53
54#[derive(Clone, Debug)]
55/// Specification for joining a related table.
56pub struct JoinSpec {
57    /// The join type.
58    pub join_type: JoinType,
59    /// Relation name.
60    pub relation_name: &'static str,
61    /// The joined table metadata.
62    pub foreign_table: TableInfo,
63    /// Join key mapping as (base_pk, foreign_fk).
64    pub on: (&'static str, &'static str),
65}
66
67impl<T: std::fmt::Debug> QB<T> {
68    pub fn new(base: TableInfo) -> QB<T> {
69        QB {
70            base,
71            eager: Vec::new(),
72            batch: Vec::new(),
73            filters: Vec::new(),
74            _marker: std::marker::PhantomData,
75        }
76    }
77
78    pub fn join_eager(mut self, spec: JoinSpec) -> Self {
79        self.eager.push(spec);
80        self
81    }
82
83    pub fn join_batch(mut self, spec: JoinSpec) -> Self {
84        self.batch.push(spec);
85        self
86    }
87
88    pub fn select<'a, S: Selectable>(mut self, cols: S) -> QB<S::Row> {
89        let cols = cols.collect();
90        if cols.is_empty() {
91            panic!("Cannot select empty column list. At least one column must be specified.");
92        }
93        self.base.columns = cols;
94        QB {
95            base: self.base,
96            eager: self.eager,
97            batch: self.batch,
98            filters: self.filters,
99            _marker: std::marker::PhantomData,
100        }
101    }
102
103    fn build_projections(&self) -> Vec<String> {
104        let mut projections = Vec::new();
105
106        for col in &self.base.columns {
107            let field = format!("{}.{}", self.base.alias, col);
108            let as_field = format_alised_col_name(&self.base.alias, col);
109            projections.push(format!("{} AS {}", field, as_field));
110        }
111
112        for join in &self.eager {
113            for col in &join.foreign_table.columns {
114                let field = format!("{}.{}", join.foreign_table.alias, col);
115                let as_field = format_alised_col_name(&join.foreign_table.alias, col);
116                projections.push(format!("{} AS {}", field, as_field));
117            }
118        }
119
120        projections
121    }
122
123    fn build_from_clause(&self) -> String {
124        format!(
125            "FROM {} AS {}",
126            with_quotes(self.base.name),
127            self.base.alias
128        )
129    }
130
131    pub fn filter(mut self, cond: Condition) -> Self {
132        self.filters.push(cond);
133        self
134    }
135
136    fn build_joins(&self) -> String {
137        let mut joins = String::new();
138
139        for join in &self.eager {
140            let other_table = format!(
141                "{} AS {}",
142                with_quotes(join.foreign_table.name),
143                join.foreign_table.alias
144            );
145
146            let jt = match join.join_type {
147                JoinType::Inner => "INNER JOIN",
148                JoinType::Left => "LEFT JOIN",
149            };
150
151            let on_base = format!("{}.{}", self.base.alias, join.on.0);
152            let on_other = format!("{}.{}", join.foreign_table.alias, join.on.1);
153
154            joins.push_str(&format!(
155                " {} {} ON {} = {}",
156                jt, other_table, on_base, on_other
157            ));
158        }
159
160        joins
161    }
162
163    pub fn build_query(&self) -> QueryBuilder<'static, Driver> {
164        let projections = self.build_projections().join(", ");
165        let from_clause = self.build_from_clause();
166        let joins = self.build_joins();
167
168        let mut builder = QueryBuilder::new("SELECT ");
169        builder.push(projections);
170        builder.push(" ");
171        builder.push(from_clause);
172        builder.push(" ");
173        builder.push(joins);
174
175        if !self.filters.is_empty() {
176            builder.push(" WHERE ");
177
178            for (i, cond) in self.filters.iter().enumerate() {
179                if i > 0 {
180                    builder.push(" AND ");
181                }
182
183                let mut parts = cond.sql.split('?');
184                if let Some(first) = parts.next() {
185                    builder.push(first);
186                }
187
188                for (val, part) in cond.values.iter().zip(parts) {
189                    val.bind(&mut builder);
190                    builder.push(part);
191                }
192            }
193        }
194
195        builder
196    }
197
198    pub fn to_sql(&self) -> String {
199        self.build_query().sql().to_string()
200    }
201}