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