oxide_sql_core/builder/
value.rs

1//! SQL values and parameter handling.
2//!
3//! This module provides safe handling of SQL values to prevent SQL injection.
4
5/// A SQL value that can be used as a parameter.
6///
7/// All values are properly escaped or parameterized to prevent SQL injection.
8#[derive(Debug, Clone, PartialEq)]
9pub enum SqlValue {
10    /// NULL value.
11    Null,
12    /// Boolean value.
13    Bool(bool),
14    /// Integer value.
15    Int(i64),
16    /// Float value.
17    Float(f64),
18    /// Text value.
19    Text(String),
20    /// Binary blob value.
21    Blob(Vec<u8>),
22}
23
24impl SqlValue {
25    /// Returns the SQL representation for inline use (escaped).
26    ///
27    /// **Warning**: Prefer using parameterized queries instead.
28    #[must_use]
29    pub fn to_sql_inline(&self) -> String {
30        match self {
31            Self::Null => String::from("NULL"),
32            Self::Bool(b) => {
33                if *b {
34                    String::from("TRUE")
35                } else {
36                    String::from("FALSE")
37                }
38            }
39            Self::Int(n) => format!("{n}"),
40            Self::Float(f) => format!("{f}"),
41            Self::Text(s) => {
42                // Escape single quotes by doubling them
43                let escaped = s.replace('\'', "''");
44                format!("'{escaped}'")
45            }
46            Self::Blob(b) => {
47                let hex: String = b.iter().map(|byte| format!("{byte:02X}")).collect();
48                format!("X'{hex}'")
49            }
50        }
51    }
52
53    /// Returns the parameter placeholder.
54    #[must_use]
55    pub const fn placeholder() -> &'static str {
56        "?"
57    }
58}
59
60/// Trait for types that can be converted to SQL values.
61pub trait ToSqlValue {
62    /// Converts the value to a `SqlValue`.
63    fn to_sql_value(self) -> SqlValue;
64}
65
66impl ToSqlValue for SqlValue {
67    fn to_sql_value(self) -> SqlValue {
68        self
69    }
70}
71
72impl ToSqlValue for bool {
73    fn to_sql_value(self) -> SqlValue {
74        SqlValue::Bool(self)
75    }
76}
77
78impl ToSqlValue for i64 {
79    fn to_sql_value(self) -> SqlValue {
80        SqlValue::Int(self)
81    }
82}
83
84impl ToSqlValue for i32 {
85    fn to_sql_value(self) -> SqlValue {
86        SqlValue::Int(i64::from(self))
87    }
88}
89
90impl ToSqlValue for i16 {
91    fn to_sql_value(self) -> SqlValue {
92        SqlValue::Int(i64::from(self))
93    }
94}
95
96impl ToSqlValue for i8 {
97    fn to_sql_value(self) -> SqlValue {
98        SqlValue::Int(i64::from(self))
99    }
100}
101
102impl ToSqlValue for u32 {
103    fn to_sql_value(self) -> SqlValue {
104        SqlValue::Int(i64::from(self))
105    }
106}
107
108impl ToSqlValue for u16 {
109    fn to_sql_value(self) -> SqlValue {
110        SqlValue::Int(i64::from(self))
111    }
112}
113
114impl ToSqlValue for u8 {
115    fn to_sql_value(self) -> SqlValue {
116        SqlValue::Int(i64::from(self))
117    }
118}
119
120impl ToSqlValue for f64 {
121    fn to_sql_value(self) -> SqlValue {
122        SqlValue::Float(self)
123    }
124}
125
126impl ToSqlValue for f32 {
127    fn to_sql_value(self) -> SqlValue {
128        SqlValue::Float(f64::from(self))
129    }
130}
131
132impl ToSqlValue for String {
133    fn to_sql_value(self) -> SqlValue {
134        SqlValue::Text(self)
135    }
136}
137
138impl ToSqlValue for &str {
139    fn to_sql_value(self) -> SqlValue {
140        SqlValue::Text(String::from(self))
141    }
142}
143
144impl<T: ToSqlValue> ToSqlValue for Option<T> {
145    fn to_sql_value(self) -> SqlValue {
146        match self {
147            Some(v) => v.to_sql_value(),
148            None => SqlValue::Null,
149        }
150    }
151}
152
153impl ToSqlValue for Vec<u8> {
154    fn to_sql_value(self) -> SqlValue {
155        SqlValue::Blob(self)
156    }
157}
158
159impl ToSqlValue for &[u8] {
160    fn to_sql_value(self) -> SqlValue {
161        SqlValue::Blob(self.to_vec())
162    }
163}
164
165// From implementations for Into<SqlValue> support in typed builders
166
167impl From<bool> for SqlValue {
168    fn from(value: bool) -> Self {
169        Self::Bool(value)
170    }
171}
172
173impl From<i64> for SqlValue {
174    fn from(value: i64) -> Self {
175        Self::Int(value)
176    }
177}
178
179impl From<i32> for SqlValue {
180    fn from(value: i32) -> Self {
181        Self::Int(i64::from(value))
182    }
183}
184
185impl From<i16> for SqlValue {
186    fn from(value: i16) -> Self {
187        Self::Int(i64::from(value))
188    }
189}
190
191impl From<i8> for SqlValue {
192    fn from(value: i8) -> Self {
193        Self::Int(i64::from(value))
194    }
195}
196
197impl From<u32> for SqlValue {
198    fn from(value: u32) -> Self {
199        Self::Int(i64::from(value))
200    }
201}
202
203impl From<u16> for SqlValue {
204    fn from(value: u16) -> Self {
205        Self::Int(i64::from(value))
206    }
207}
208
209impl From<u8> for SqlValue {
210    fn from(value: u8) -> Self {
211        Self::Int(i64::from(value))
212    }
213}
214
215impl From<f64> for SqlValue {
216    fn from(value: f64) -> Self {
217        Self::Float(value)
218    }
219}
220
221impl From<f32> for SqlValue {
222    fn from(value: f32) -> Self {
223        Self::Float(f64::from(value))
224    }
225}
226
227impl From<String> for SqlValue {
228    fn from(value: String) -> Self {
229        Self::Text(value)
230    }
231}
232
233impl From<&str> for SqlValue {
234    fn from(value: &str) -> Self {
235        Self::Text(String::from(value))
236    }
237}
238
239impl<T: Into<SqlValue>> From<Option<T>> for SqlValue {
240    fn from(value: Option<T>) -> Self {
241        match value {
242            Some(v) => v.into(),
243            None => Self::Null,
244        }
245    }
246}
247
248impl From<Vec<u8>> for SqlValue {
249    fn from(value: Vec<u8>) -> Self {
250        Self::Blob(value)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_sql_value_inline_null() {
260        assert_eq!(SqlValue::Null.to_sql_inline(), "NULL");
261    }
262
263    #[test]
264    fn test_sql_value_inline_bool() {
265        assert_eq!(SqlValue::Bool(true).to_sql_inline(), "TRUE");
266        assert_eq!(SqlValue::Bool(false).to_sql_inline(), "FALSE");
267    }
268
269    #[test]
270    fn test_sql_value_inline_int() {
271        assert_eq!(SqlValue::Int(42).to_sql_inline(), "42");
272        assert_eq!(SqlValue::Int(-100).to_sql_inline(), "-100");
273    }
274
275    #[test]
276    fn test_sql_value_inline_text() {
277        assert_eq!(
278            SqlValue::Text(String::from("hello")).to_sql_inline(),
279            "'hello'"
280        );
281    }
282
283    #[test]
284    fn test_sql_value_inline_text_escaping() {
285        // Single quotes are escaped by doubling
286        assert_eq!(
287            SqlValue::Text(String::from("it's")).to_sql_inline(),
288            "'it''s'"
289        );
290        assert_eq!(
291            SqlValue::Text(String::from("O'Brien")).to_sql_inline(),
292            "'O''Brien'"
293        );
294    }
295
296    #[test]
297    fn test_sql_injection_prevention() {
298        // Attempt SQL injection
299        let malicious = "'; DROP TABLE users; --";
300        let value = SqlValue::Text(String::from(malicious));
301        let escaped = value.to_sql_inline();
302        // The single quote is escaped, preventing the injection
303        assert_eq!(escaped, "'''; DROP TABLE users; --'");
304    }
305
306    #[test]
307    fn test_sql_value_inline_blob() {
308        assert_eq!(
309            SqlValue::Blob(vec![0x48, 0x45, 0x4C, 0x4C, 0x4F]).to_sql_inline(),
310            "X'48454C4C4F'"
311        );
312    }
313
314    #[test]
315    fn test_to_sql_value_conversions() {
316        assert_eq!(true.to_sql_value(), SqlValue::Bool(true));
317        assert_eq!(42_i32.to_sql_value(), SqlValue::Int(42));
318        assert_eq!(2.5_f64.to_sql_value(), SqlValue::Float(2.5));
319        assert_eq!(
320            "hello".to_sql_value(),
321            SqlValue::Text(String::from("hello"))
322        );
323        assert_eq!(None::<i32>.to_sql_value(), SqlValue::Null);
324        assert_eq!(Some(42_i32).to_sql_value(), SqlValue::Int(42));
325    }
326}