Skip to main content

ferriorm_runtime/
query.rs

1//! Parameterized SQL query builder.
2//!
3//! [`SqlBuilder`] constructs SQL strings with properly indexed parameter
4//! placeholders. It supports both PostgreSQL (`$1`, `$2`, ...) and SQLite
5//! (`?`) placeholder styles, selected automatically based on the active
6//! [`DatabaseClient`]. The builder also provides safe identifier quoting to
7//! prevent SQL injection in table and column names.
8
9use crate::client::DatabaseClient;
10
11/// Determines the SQL placeholder style.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ParamStyle {
14    /// PostgreSQL: `$1`, `$2`, etc.
15    Dollar,
16    /// SQLite/MySQL: `?`
17    QuestionMark,
18}
19
20impl ParamStyle {
21    pub fn from_client(client: &DatabaseClient) -> Self {
22        match client {
23            #[cfg(feature = "postgres")]
24            DatabaseClient::Postgres(_) => Self::Dollar,
25            #[cfg(feature = "sqlite")]
26            DatabaseClient::Sqlite(_) => Self::QuestionMark,
27        }
28    }
29}
30
31/// A safe parameterized SQL builder.
32///
33/// Tracks parameter bindings and builds SQL strings with placeholders.
34/// Supports both PostgreSQL (`$1`) and SQLite (`?`) styles.
35///
36/// Note: the generated client code uses `sqlx::QueryBuilder` directly for its
37/// queries. This builder is available for advanced use cases where manual SQL
38/// construction with database-aware placeholders is needed.
39#[derive(Debug)]
40pub struct SqlBuilder {
41    sql: String,
42    param_count: usize,
43    style: ParamStyle,
44}
45
46impl SqlBuilder {
47    pub fn new(style: ParamStyle) -> Self {
48        Self {
49            sql: String::with_capacity(256),
50            param_count: 0,
51            style,
52        }
53    }
54
55    pub fn for_client(client: &DatabaseClient) -> Self {
56        Self::new(ParamStyle::from_client(client))
57    }
58
59    /// Append raw SQL text.
60    pub fn push(&mut self, sql: &str) {
61        self.sql.push_str(sql);
62    }
63
64    /// Append a single character.
65    pub fn push_char(&mut self, c: char) {
66        self.sql.push(c);
67    }
68
69    /// Append a parameter placeholder and increment the counter.
70    /// Returns the parameter index (1-based).
71    pub fn push_param(&mut self) -> usize {
72        self.param_count += 1;
73        match self.style {
74            ParamStyle::Dollar => {
75                self.sql.push('$');
76                self.sql.push_str(&self.param_count.to_string());
77            }
78            ParamStyle::QuestionMark => {
79                self.sql.push('?');
80            }
81        }
82        self.param_count
83    }
84
85    /// Append a quoted identifier (table or column name).
86    pub fn push_identifier(&mut self, name: &str) {
87        self.sql.push('"');
88        // Escape any double quotes in the name
89        for c in name.chars() {
90            if c == '"' {
91                self.sql.push('"');
92            }
93            self.sql.push(c);
94        }
95        self.sql.push('"');
96    }
97
98    /// Get the current parameter count.
99    pub fn param_count(&self) -> usize {
100        self.param_count
101    }
102
103    /// Get the current parameter style.
104    pub fn style(&self) -> ParamStyle {
105        self.style
106    }
107
108    /// Consume the builder and return the SQL string.
109    pub fn build(self) -> String {
110        self.sql
111    }
112
113    /// Get the SQL string by reference.
114    pub fn sql(&self) -> &str {
115        &self.sql
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_postgres_params() {
125        let mut b = SqlBuilder::new(ParamStyle::Dollar);
126        b.push("SELECT * FROM ");
127        b.push_identifier("users");
128        b.push(" WHERE ");
129        b.push_identifier("email");
130        b.push(" = ");
131        b.push_param();
132        b.push(" AND ");
133        b.push_identifier("age");
134        b.push(" > ");
135        b.push_param();
136
137        assert_eq!(
138            b.build(),
139            r#"SELECT * FROM "users" WHERE "email" = $1 AND "age" > $2"#
140        );
141    }
142
143    #[test]
144    fn test_sqlite_params() {
145        let mut b = SqlBuilder::new(ParamStyle::QuestionMark);
146        b.push("SELECT * FROM ");
147        b.push_identifier("users");
148        b.push(" WHERE ");
149        b.push_identifier("email");
150        b.push(" = ");
151        b.push_param();
152
153        assert_eq!(b.build(), r#"SELECT * FROM "users" WHERE "email" = ?"#);
154    }
155}