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