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::Arguments;
17use sqlx::any::AnyArguments;
18use uuid::Uuid;
19
20use crate::{Error, database::Drivers, temporal};
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.parse::<i64>().map_err(|e| Error::Conversion(format!("Failed to parse i64: {}", e)))?;
103                     self.bind_i64(val);
104                 } else {
105                    return Err(Error::Conversion(format!("Failed to parse i64: {}", value_str)));
106                 }
107                Ok(())
108            }
109
110            "SMALLINT" | "INT2" | "int2" => {
111                let val: i16 = value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse i16: {}", e)))?;
112                let _ = self.add(val);
113                Ok(())
114            }
115
116            // ================================================================
117            // Boolean Type
118            // ================================================================
119            "BOOLEAN" | "BOOL" | "bool" => {
120                let val: bool =
121                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse bool: {}", e)))?;
122                self.bind_bool(val);
123                Ok(())
124            }
125
126            // ================================================================
127            // Floating-Point Types
128            // ================================================================
129            "DOUBLE PRECISION" | "FLOAT" | "float8" | "NUMERIC" | "DECIMAL" => {
130                let val: f64 =
131                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse f64: {}", e)))?;
132                self.bind_f64(val);
133                Ok(())
134            }
135            
136            "REAL" | "float4" => {
137                let val: f32 =
138                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse f32: {}", e)))?;
139                 let _ = self.add(val);
140                Ok(())
141            }
142
143            // ================================================================
144            // JSON Types
145            // ================================================================
146            "JSON" | "JSONB" | "json" | "jsonb" => {
147                // Determine driver-specific JSON handling
148                match driver {
149                    Drivers::Postgres => {
150                        // For Postgres, we can bind as serde_json::Value if sqlx supports it,
151                        // or bind as string/text but rely on Postgres casting `::JSONB` in the query string.
152                        // The QueryBuilder handles the `::JSONB` cast in the SQL string.
153                        // So we just bind the string representation here.
154                        self.bind_string(value_str.to_string());
155                    }
156                    _ => {
157                        self.bind_string(value_str.to_string());
158                    }
159                }
160                Ok(())
161            }
162
163            // ================================================================
164            // UUID Type
165            // ================================================================
166            "UUID" => {
167                let val =
168                    value_str.parse::<Uuid>().map_err(|e| Error::Conversion(format!("Failed to parse UUID: {}", e)))?;
169                self.bind_uuid(val, driver);
170                Ok(())
171            }
172
173            // ================================================================
174            // Temporal Types (DateTime, Date, Time)
175            // ================================================================
176            "TIMESTAMPTZ" | "DateTime" => {
177                // Try parsing as UTC first
178                if let Ok(val) = temporal::parse_datetime_utc(value_str) {
179                    self.bind_datetime_utc(val, driver);
180                } else if let Ok(val) = temporal::parse_datetime_fixed(value_str) {
181                    // Fallback to FixedOffset if UTC fails (though parse_datetime_utc handles fixed too)
182                    self.bind_datetime_fixed(val, driver);
183                } else {
184                     return Err(Error::Conversion(format!("Failed to parse DateTime: {}", value_str)));
185                }
186                Ok(())
187            }
188
189            "TIMESTAMP" | "NaiveDateTime" => {
190                let val = temporal::parse_naive_datetime(value_str)?;
191                self.bind_naive_datetime(val, driver);
192                Ok(())
193            }
194
195            "DATE" | "NaiveDate" => {
196                let val = temporal::parse_naive_date(value_str)?;
197                self.bind_naive_date(val, driver);
198                Ok(())
199            }
200
201            "TIME" | "NaiveTime" => {
202                let val = temporal::parse_naive_time(value_str)?;
203                self.bind_naive_time(val, driver);
204                Ok(())
205            }
206
207            // ================================================================
208            // Text and Default Types
209            // ================================================================
210            "TEXT" | "VARCHAR" | "CHAR" | "STRING" | _ => {
211                self.bind_string(value_str.to_string());
212                Ok(())
213            }
214        }
215    }
216
217    fn bind_i32(&mut self, value: i32) {
218        let _ = self.add(value);
219    }
220
221    fn bind_i64(&mut self, value: i64) {
222        let _ = self.add(value);
223    }
224
225    fn bind_bool(&mut self, value: bool) {
226        let _ = self.add(value);
227    }
228
229    fn bind_f64(&mut self, value: f64) {
230        let _ = self.add(value);
231    }
232
233    fn bind_string(&mut self, value: String) {
234        let _ = self.add(value);
235    }
236
237    fn bind_uuid(&mut self, value: Uuid, driver: &Drivers) {
238        match driver {
239            Drivers::Postgres => {
240                // PostgreSQL has native UUID support
241                // Convert to hyphenated string format
242                let _ = self.add(value.hyphenated().to_string());
243            }
244            Drivers::MySQL => {
245                // MySQL stores UUID as CHAR(36)
246                let _ = self.add(value.hyphenated().to_string());
247            }
248            Drivers::SQLite => {
249                // SQLite stores as TEXT
250                let _ = self.add(value.hyphenated().to_string());
251            }
252        }
253    }
254
255    fn bind_datetime_utc(&mut self, value: DateTime<Utc>, driver: &Drivers) {
256        let formatted = temporal::format_datetime_for_driver(&value, driver);
257        let _ = self.add(formatted);
258    }
259
260    fn bind_datetime_fixed(&mut self, value: chrono::DateTime<chrono::FixedOffset>, driver: &Drivers) {
261        let formatted = temporal::format_datetime_fixed_for_driver(&value, driver);
262        let _ = self.add(formatted);
263    }
264
265    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers) {
266        let formatted = temporal::format_naive_datetime_for_driver(&value, driver);
267        let _ = self.add(formatted);
268    }
269
270    fn bind_naive_date(&mut self, value: NaiveDate, _driver: &Drivers) {
271        // All drivers use ISO 8601 date format
272        let formatted = value.format("%Y-%m-%d").to_string();
273        let _ = self.add(formatted);
274    }
275
276    fn bind_naive_time(&mut self, value: NaiveTime, _driver: &Drivers) {
277        // All drivers use ISO 8601 time format
278        let formatted = value.format("%H:%M:%S%.6f").to_string();
279        let _ = self.add(formatted);
280    }
281}
282
283// ============================================================================
284// Convenience Functions
285// ============================================================================
286
287/// Binds a value to AnyArguments with automatic type detection and conversion.
288///
289/// This is a convenience function that wraps the ValueBinder trait.
290///
291/// # Arguments
292///
293/// * `args` - The AnyArguments to bind the value to
294/// * `value_str` - String representation of the value
295/// * `sql_type` - SQL type identifier
296/// * `driver` - Database driver
297///
298/// # Example
299///
300/// ```rust,ignore
301/// use bottle_orm::value_binding::bind_typed_value;
302/// use sqlx::any::AnyArguments;
303///
304/// let mut args = AnyArguments::default();
305/// bind_typed_value(&mut args, "42", "INTEGER", &Drivers::Postgres)?;
306/// bind_typed_value(&mut args, "2024-01-15T14:30:00+00:00", "TIMESTAMPTZ", &Drivers::Postgres)?;
307/// ```
308pub fn bind_typed_value(
309    args: &mut AnyArguments<'_>,
310    value_str: &str,
311    sql_type: &str,
312    driver: &Drivers,
313) -> Result<(), Error> {
314    args.bind_value(value_str, sql_type, driver)
315}
316
317/// Attempts to bind a value, falling back to string binding on error.
318///
319/// This is useful for cases where you want to be more lenient with type conversion.
320///
321/// # Arguments
322///
323/// * `args` - The AnyArguments to bind the value to
324/// * `value_str` - String representation of the value
325/// * `sql_type` - SQL type identifier
326/// * `driver` - Database driver
327pub fn bind_typed_value_or_string(args: &mut AnyArguments<'_>, value_str: &str, sql_type: &str, driver: &Drivers) {
328    if let Err(_) = args.bind_value(value_str, sql_type, driver) {
329        // Fallback: bind as string
330        let _ = args.add(value_str.to_string());
331    }
332}
333
334// ============================================================================
335// Type Detection
336// ============================================================================
337
338/// Detects if a SQL type requires special handling.
339pub fn requires_special_binding(sql_type: &str) -> bool {
340    matches!(
341        sql_type,
342        "UUID"
343            | "TIMESTAMPTZ"
344            | "DateTime"
345            | "TIMESTAMP"
346            | "NaiveDateTime"
347            | "DATE"
348            | "NaiveDate"
349            | "TIME"
350            | "NaiveTime"
351    )
352}
353
354/// Returns whether a SQL type is numeric.
355pub fn is_numeric_type(sql_type: &str) -> bool {
356    matches!(
357        sql_type,
358        "INTEGER"
359            | "INT"
360            | "BIGINT"
361            | "INT8"
362            | "SERIAL"
363            | "BIGSERIAL"
364            | "SMALLINT"
365            | "DOUBLE PRECISION"
366            | "FLOAT"
367            | "REAL"
368            | "NUMERIC"
369            | "DECIMAL"
370    )
371}
372
373/// Returns whether a SQL type is textual.
374pub fn is_text_type(sql_type: &str) -> bool {
375    matches!(sql_type, "TEXT" | "VARCHAR" | "CHAR" | "STRING")
376}