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};
8pub use bind::BindValue;
9pub use column::Column;
10pub use condition::Condition;
11use sqlx::FromRow;
12use sqlx::QueryBuilder;
13
14/// Quote identifiers appropriately for the target database
15/// Both PostgreSQL and SQLite support double quotes for identifiers
16fn with_quotes(s: &str) -> String {
17    // Double quotes work for both PostgreSQL and SQLite
18    // This ensures consistent behavior across databases
19    format!("\"{}\"", s)
20}
21
22#[derive(Debug)]
23/// Query builder for composing SELECT statements with optional joins and filters.
24pub struct QB<T: std::fmt::Debug> {
25    /// Base table information and selected columns.
26    pub base: TableInfo,
27    /// Eager joins that project columns from related tables.
28    pub eager: Vec<JoinSpec>,
29    /// Batch joins for has-many relations.
30    pub batch: Vec<JoinSpec>,
31    /// WHERE clause conditions combined with AND.
32    pub filters: Vec<Condition>,
33    _marker: std::marker::PhantomData<T>,
34}
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            projections.push(format!(
107                "{}.{} AS {}{}",
108                self.base.alias, col, self.base.alias, col
109            ));
110        }
111
112        for join in &self.eager {
113            for col in &join.foreign_table.columns {
114                projections.push(format!(
115                    "{}.{} AS {}{}",
116                    join.foreign_table.alias, col, join.foreign_table.alias, col
117                ));
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}