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;
9pub use bind::BindValue;
10pub use column::Column;
11pub use condition::Condition;
12use sqlx::FromRow;
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: std::fmt::Debug> {
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, Out: Debug + FromRow<'a, Row>>(mut self, cols: Vec<&'static str>) -> QB<Out> {
89        if cols.is_empty() {
90            panic!("Cannot select empty column list. At least one column must be specified.");
91        }
92        self.base.columns = cols;
93        QB {
94            base: self.base,
95            eager: self.eager,
96            batch: self.batch,
97            filters: self.filters,
98            _marker: std::marker::PhantomData,
99        }
100    }
101
102    fn build_projections(&self) -> Vec<String> {
103        let mut projections = Vec::new();
104
105        for col in &self.base.columns {
106            let field = format!("{}.{}", self.base.alias, col);
107            let as_field = format_alised_col_name(&self.base.alias, col);
108            projections.push(format!("{} AS {}", field, as_field));
109        }
110
111        for join in &self.eager {
112            for col in &join.foreign_table.columns {
113                let field = format!("{}.{}", join.foreign_table.alias, col);
114                let as_field = format_alised_col_name(&join.foreign_table.alias, col);
115                projections.push(format!("{} AS {}", field, as_field));
116            }
117        }
118
119        projections
120    }
121
122    fn build_from_clause(&self) -> String {
123        format!(
124            "FROM {} AS {}",
125            with_quotes(self.base.name),
126            self.base.alias
127        )
128    }
129
130    pub fn filter(mut self, cond: Condition) -> Self {
131        self.filters.push(cond);
132        self
133    }
134
135    fn build_joins(&self) -> String {
136        let mut joins = String::new();
137
138        for join in &self.eager {
139            let other_table = format!(
140                "{} AS {}",
141                with_quotes(join.foreign_table.name),
142                join.foreign_table.alias
143            );
144
145            let jt = match join.join_type {
146                JoinType::Inner => "INNER JOIN",
147                JoinType::Left => "LEFT JOIN",
148            };
149
150            let on_base = format!("{}.{}", self.base.alias, join.on.0);
151            let on_other = format!("{}.{}", join.foreign_table.alias, join.on.1);
152
153            joins.push_str(&format!(
154                " {} {} ON {} = {}",
155                jt, other_table, on_base, on_other
156            ));
157        }
158
159        joins
160    }
161
162    pub fn build_query(&self) -> QueryBuilder<'static, Driver> {
163        let projections = self.build_projections().join(", ");
164        let from_clause = self.build_from_clause();
165        let joins = self.build_joins();
166
167        let mut builder = QueryBuilder::new("SELECT ");
168        builder.push(projections);
169        builder.push(" ");
170        builder.push(from_clause);
171        builder.push(" ");
172        builder.push(joins);
173
174        if !self.filters.is_empty() {
175            builder.push(" WHERE ");
176
177            for (i, cond) in self.filters.iter().enumerate() {
178                if i > 0 {
179                    builder.push(" AND ");
180                }
181
182                let mut parts = cond.sql.split('?');
183                if let Some(first) = parts.next() {
184                    builder.push(first);
185                }
186
187                for (val, part) in cond.values.iter().zip(parts) {
188                    val.bind(&mut builder);
189                    builder.push(part);
190                }
191            }
192        }
193
194        builder
195    }
196
197    pub fn to_sql(&self) -> String {
198        self.build_query().sql().to_string()
199    }
200}