Skip to main content

bottle_orm/
value_binding.rs

1//! # Value Binding Module
2//!
3//! This module provides type-safe value binding utilities for SQL queries.
4//! It handles conversion from Rust types to database-native types across
5//! different database drivers (PostgreSQL, MySQL, SQLite).
6//!
7//! ## Features
8//!
9//! - **Type-Safe Binding**: Automatic type detection and conversion
10//! - **Driver-Specific Optimization**: Uses native types when possible
11//! - **Temporal Type Support**: Specialized handling for DateTime types via temporal module
12//! - **UUID Support**: Handles all UUID versions (1-7)
13//! - **Error Handling**: Graceful fallback for parsing errors
14
15use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
16use sqlx::any::AnyArguments;
17use sqlx::Arguments;
18use uuid::Uuid;
19
20use crate::{database::Drivers, temporal, Error};
21
22// ============================================================================
23// Value Binding Trait
24// ============================================================================
25
26/// Extension trait for binding values to AnyArguments with driver-specific handling.
27pub trait ValueBinder {
28    /// Binds a value to the arguments based on its SQL type and database driver.
29    ///
30    /// # Arguments
31    ///
32    /// * `value_str` - String representation of the value
33    /// * `sql_type` - SQL type identifier (e.g., "INTEGER", "TEXT", "TIMESTAMPTZ")
34    /// * `driver` - Database driver being used
35    ///
36    /// # Returns
37    ///
38    /// `Ok(())` if binding succeeds, `Err(Error)` otherwise
39    fn bind_value(&mut self, value_str: &str, sql_type: &str, driver: &Drivers) -> Result<(), Error>;
40
41    /// Binds an integer value (i32).
42    fn bind_i32(&mut self, value: i32);
43
44    /// Binds a big integer value (i64).
45    fn bind_i64(&mut self, value: i64);
46
47    /// Binds a boolean value.
48    fn bind_bool(&mut self, value: bool);
49
50    /// Binds a floating-point value (f64).
51    fn bind_f64(&mut self, value: f64);
52
53    /// Binds a string value.
54    fn bind_string(&mut self, value: String);
55
56    /// Binds a UUID value.
57    fn bind_uuid(&mut self, value: Uuid, driver: &Drivers);
58
59    /// Binds a DateTime<Utc> value.
60    fn bind_datetime_utc(&mut self, value: DateTime<Utc>, driver: &Drivers);
61
62    /// Binds a DateTime<FixedOffset> value.
63    fn bind_datetime_fixed(&mut self, value: chrono::DateTime<chrono::FixedOffset>, driver: &Drivers);
64
65    /// Binds a NaiveDateTime value.
66    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers);
67
68    /// Binds a NaiveDate value.
69    fn bind_naive_date(&mut self, value: NaiveDate, driver: &Drivers);
70
71    /// Binds a NaiveTime value.
72    fn bind_naive_time(&mut self, value: NaiveTime, driver: &Drivers);
73}
74
75impl ValueBinder for AnyArguments<'_> {
76    fn bind_value(&mut self, value_str: &str, sql_type: &str, driver: &Drivers) -> Result<(), Error> {
77        match sql_type {
78            // ================================================================
79            // Integer Types
80            // ================================================================
81            "INTEGER" | "INT" | "SERIAL" | "serial" | "int4" => {
82                // Try parsing as i32 first, fallback to u32/i64 if needed but sql_type says INTEGER
83                if let Ok(val) = value_str.parse::<i32>() {
84                    self.bind_i32(val);
85                } else if let Ok(val) = value_str.parse::<u32>() {
86                    self.bind_i64(val as i64); // Map u32 to i64 to fit
87                } else {
88                    return Err(Error::Conversion(format!("Failed to parse integer: {}", value_str)));
89                }
90                Ok(())
91            }
92
93            "BIGINT" | "INT8" | "int8" | "BIGSERIAL" => {
94                if let Ok(val) = value_str.parse::<i64>() {
95                    self.bind_i64(val);
96                } else if let Ok(_val) = value_str.parse::<u64>() {
97                    // u64 might overflow i64, strictly speaking, but standard mapping in rust sqlx usually handles i64
98                    // We'll try to bind as i64 (unsafe cast) or string?
99                    // Best effort: bind as i64 (reinterpreting bits or clamping? No, let's just parse)
100                    // If it exceeds i64::MAX, it's an issue for standard SQL BIGINT (signed).
101                    // For now, parse as i64.
102                    let val = value_str
103                        .parse::<i64>()
104                        .map_err(|e| Error::Conversion(format!("Failed to parse i64: {}", e)))?;
105                    self.bind_i64(val);
106                } else {
107                    return Err(Error::Conversion(format!("Failed to parse i64: {}", value_str)));
108                }
109                Ok(())
110            }
111
112            "SMALLINT" | "INT2" | "int2" => {
113                let val: i16 =
114                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse i16: {}", e)))?;
115                let _ = self.add(val);
116                Ok(())
117            }
118
119            // ================================================================
120            // Boolean Type
121            // ================================================================
122            "BOOLEAN" | "BOOL" | "bool" => {
123                let val: bool =
124                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse bool: {}", e)))?;
125                self.bind_bool(val);
126                Ok(())
127            }
128
129            // ================================================================
130            // Floating-Point Types
131            // ================================================================
132            "DOUBLE PRECISION" | "FLOAT" | "float8" | "NUMERIC" | "DECIMAL" => {
133                let val: f64 =
134                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse f64: {}", e)))?;
135                self.bind_f64(val);
136                Ok(())
137            }
138
139            "REAL" | "float4" => {
140                let val: f32 =
141                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse f32: {}", e)))?;
142                let _ = self.add(val);
143                Ok(())
144            }
145
146            // ================================================================
147            // JSON Types
148            // ================================================================
149            "JSON" | "JSONB" | "json" | "jsonb" => {
150                // Determine driver-specific JSON handling
151                match driver {
152                    Drivers::Postgres => {
153                        // For Postgres, we can bind as serde_json::Value if sqlx supports it,
154                        // or bind as string/text but rely on Postgres casting `::JSONB` in the query string.
155                        // The QueryBuilder handles the `::JSONB` cast in the SQL string.
156                        // So we just bind the string representation here.
157                        self.bind_string(value_str.to_string());
158                    }
159                    _ => {
160                        self.bind_string(value_str.to_string());
161                    }
162                }
163                Ok(())
164            }
165
166            // ================================================================
167            // UUID Type
168            // ================================================================
169            "UUID" => {
170                let val =
171                    value_str.parse::<Uuid>().map_err(|e| Error::Conversion(format!("Failed to parse UUID: {}", e)))?;
172                self.bind_uuid(val, driver);
173                Ok(())
174            }
175
176            // ================================================================
177            // Temporal Types (DateTime, Date, Time)
178            // ================================================================
179            "TIMESTAMPTZ" | "DateTime" => {
180                // Try parsing as UTC first
181                if let Ok(val) = temporal::parse_datetime_utc(value_str) {
182                    self.bind_datetime_utc(val, driver);
183                } else if let Ok(val) = temporal::parse_datetime_fixed(value_str) {
184                    // Fallback to FixedOffset if UTC fails (though parse_datetime_utc handles fixed too)
185                    self.bind_datetime_fixed(val, driver);
186                } else {
187                    return Err(Error::Conversion(format!("Failed to parse DateTime: {}", value_str)));
188                }
189                Ok(())
190            }
191
192            "TIMESTAMP" | "NaiveDateTime" => {
193                let val = temporal::parse_naive_datetime(value_str)?;
194                self.bind_naive_datetime(val, driver);
195                Ok(())
196            }
197
198            "DATE" | "NaiveDate" => {
199                let val = temporal::parse_naive_date(value_str)?;
200                self.bind_naive_date(val, driver);
201                Ok(())
202            }
203
204            "TIME" | "NaiveTime" => {
205                let val = temporal::parse_naive_time(value_str)?;
206                self.bind_naive_time(val, driver);
207                Ok(())
208            }
209
210            // ================================================================
211            // Text and Default Types
212            // ================================================================
213            "TEXT" | "VARCHAR" | "CHAR" | "STRING" | _ => {
214                self.bind_string(value_str.to_string());
215                Ok(())
216            }
217        }
218    }
219
220    fn bind_i32(&mut self, value: i32) {
221        let _ = self.add(value);
222    }
223
224    fn bind_i64(&mut self, value: i64) {
225        let _ = self.add(value);
226    }
227
228    fn bind_bool(&mut self, value: bool) {
229        let _ = self.add(value);
230    }
231
232    fn bind_f64(&mut self, value: f64) {
233        let _ = self.add(value);
234    }
235
236    fn bind_string(&mut self, value: String) {
237        let _ = self.add(value);
238    }
239
240    fn bind_uuid(&mut self, value: Uuid, driver: &Drivers) {
241        match driver {
242            Drivers::Postgres => {
243                // PostgreSQL has native UUID support
244                // Convert to hyphenated string format
245                let _ = self.add(value.hyphenated().to_string());
246            }
247            Drivers::MySQL => {
248                // MySQL stores UUID as CHAR(36)
249                let _ = self.add(value.hyphenated().to_string());
250            }
251            Drivers::SQLite => {
252                // SQLite stores as TEXT
253                let _ = self.add(value.hyphenated().to_string());
254            }
255        }
256    }
257
258    fn bind_datetime_utc(&mut self, value: DateTime<Utc>, driver: &Drivers) {
259        let formatted = temporal::format_datetime_for_driver(&value, driver);
260        let _ = self.add(formatted);
261    }
262
263    fn bind_datetime_fixed(&mut self, value: chrono::DateTime<chrono::FixedOffset>, driver: &Drivers) {
264        let formatted = temporal::format_datetime_fixed_for_driver(&value, driver);
265        let _ = self.add(formatted);
266    }
267
268    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers) {
269        let formatted = temporal::format_naive_datetime_for_driver(&value, driver);
270        let _ = self.add(formatted);
271    }
272
273    fn bind_naive_date(&mut self, value: NaiveDate, _driver: &Drivers) {
274        // All drivers use ISO 8601 date format
275        let formatted = value.format("%Y-%m-%d").to_string();
276        let _ = self.add(formatted);
277    }
278
279    fn bind_naive_time(&mut self, value: NaiveTime, _driver: &Drivers) {
280        // All drivers use ISO 8601 time format
281        let formatted = value.format("%H:%M:%S%.6f").to_string();
282        let _ = self.add(formatted);
283    }
284}
285
286// ============================================================================
287// Convenience Functions
288// ============================================================================
289
290/// Binds a value to AnyArguments with automatic type detection and conversion.
291///
292/// This is a convenience function that wraps the ValueBinder trait.
293///
294/// # Arguments
295///
296/// * `args` - The AnyArguments to bind the value to
297/// * `value_str` - String representation of the value
298/// * `sql_type` - SQL type identifier
299/// * `driver` - Database driver
300///
301/// # Example
302///
303/// ```rust,ignore
304/// use bottle_orm::value_binding::bind_typed_value;
305/// use sqlx::any::AnyArguments;
306///
307/// let mut args = AnyArguments::default();
308/// bind_typed_value(&mut args, "42", "INTEGER", &Drivers::Postgres)?;
309/// bind_typed_value(&mut args, "2024-01-15T14:30:00+00:00", "TIMESTAMPTZ", &Drivers::Postgres)?;
310/// ```
311pub fn bind_typed_value(
312    args: &mut AnyArguments<'_>,
313    value_str: &str,
314    sql_type: &str,
315    driver: &Drivers,
316) -> Result<(), Error> {
317    args.bind_value(value_str, sql_type, driver)
318}
319
320/// Attempts to bind a value, falling back to string binding on error.
321///
322/// This is useful for cases where you want to be more lenient with type conversion.
323///
324/// # Arguments
325///
326/// * `args` - The AnyArguments to bind the value to
327/// * `value_str` - String representation of the value
328/// * `sql_type` - SQL type identifier
329/// * `driver` - Database driver
330pub fn bind_typed_value_or_string(args: &mut AnyArguments<'_>, value_str: &str, sql_type: &str, driver: &Drivers) {
331    if let Err(_) = args.bind_value(value_str, sql_type, driver) {
332        // Fallback: bind as string
333        let _ = args.add(value_str.to_string());
334    }
335}
336
337// ============================================================================
338// Type Detection
339// ============================================================================
340
341/// Detects if a SQL type requires special handling.
342pub fn requires_special_binding(sql_type: &str) -> bool {
343    matches!(
344        sql_type,
345        "UUID"
346            | "TIMESTAMPTZ"
347            | "DateTime"
348            | "TIMESTAMP"
349            | "NaiveDateTime"
350            | "DATE"
351            | "NaiveDate"
352            | "TIME"
353            | "NaiveTime"
354    )
355}
356
357/// Returns whether a SQL type is numeric.
358pub fn is_numeric_type(sql_type: &str) -> bool {
359    matches!(
360        sql_type,
361        "INTEGER"
362            | "INT"
363            | "BIGINT"
364            | "INT8"
365            | "SERIAL"
366            | "BIGSERIAL"
367            | "SMALLINT"
368            | "DOUBLE PRECISION"
369            | "FLOAT"
370            | "REAL"
371            | "NUMERIC"
372            | "DECIMAL"
373    )
374}
375
376/// Returns whether a SQL type is textual.
377pub fn is_text_type(sql_type: &str) -> bool {
378    matches!(sql_type, "TEXT" | "VARCHAR" | "CHAR" | "STRING")
379}