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            // Array Types (e.g., TEXT[], INTEGER[])
212            // ================================================================
213            s if s.ends_with("[]") => {
214                match driver {
215                    Drivers::Postgres => {
216                        // For PostgreSQL, we rely on string representation + cast in query builder
217                        self.bind_string(value_str.to_string());
218                    }
219                    _ => {
220                        // MySQL and SQLite don't have native arrays, stored as JSON/Text
221                        self.bind_string(value_str.to_string());
222                    }
223                }
224                Ok(())
225            }
226
227            // ================================================================
228            // Text and Default Types
229            // ================================================================
230            "TEXT" | "VARCHAR" | "CHAR" | "STRING" | _ => {
231                self.bind_string(value_str.to_string());
232                Ok(())
233            }
234        }
235    }
236
237    fn bind_i32(&mut self, value: i32) {
238        let _ = self.add(value);
239    }
240
241    fn bind_i64(&mut self, value: i64) {
242        let _ = self.add(value);
243    }
244
245    fn bind_bool(&mut self, value: bool) {
246        let _ = self.add(value);
247    }
248
249    fn bind_f64(&mut self, value: f64) {
250        let _ = self.add(value);
251    }
252
253    fn bind_string(&mut self, value: String) {
254        let _ = self.add(value);
255    }
256
257    fn bind_uuid(&mut self, value: Uuid, driver: &Drivers) {
258        match driver {
259            Drivers::Postgres => {
260                // PostgreSQL has native UUID support
261                // Convert to hyphenated string format
262                let _ = self.add(value.hyphenated().to_string());
263            }
264            Drivers::MySQL => {
265                // MySQL stores UUID as CHAR(36)
266                let _ = self.add(value.hyphenated().to_string());
267            }
268            Drivers::SQLite => {
269                // SQLite stores as TEXT
270                let _ = self.add(value.hyphenated().to_string());
271            }
272        }
273    }
274
275    fn bind_datetime_utc(&mut self, value: DateTime<Utc>, driver: &Drivers) {
276        let formatted = temporal::format_datetime_for_driver(&value, driver);
277        let _ = self.add(formatted);
278    }
279
280    fn bind_datetime_fixed(&mut self, value: chrono::DateTime<chrono::FixedOffset>, driver: &Drivers) {
281        let formatted = temporal::format_datetime_fixed_for_driver(&value, driver);
282        let _ = self.add(formatted);
283    }
284
285    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers) {
286        let formatted = temporal::format_naive_datetime_for_driver(&value, driver);
287        let _ = self.add(formatted);
288    }
289
290    fn bind_naive_date(&mut self, value: NaiveDate, _driver: &Drivers) {
291        // All drivers use ISO 8601 date format
292        let formatted = value.format("%Y-%m-%d").to_string();
293        let _ = self.add(formatted);
294    }
295
296    fn bind_naive_time(&mut self, value: NaiveTime, _driver: &Drivers) {
297        // All drivers use ISO 8601 time format
298        let formatted = value.format("%H:%M:%S%.6f").to_string();
299        let _ = self.add(formatted);
300    }
301}
302
303// ============================================================================
304// Convenience Functions
305// ============================================================================
306
307/// Binds a value to AnyArguments with automatic type detection and conversion.
308///
309/// This is a convenience function that wraps the ValueBinder trait.
310///
311/// # Arguments
312///
313/// * `args` - The AnyArguments to bind the value to
314/// * `value_str` - String representation of the value
315/// * `sql_type` - SQL type identifier
316/// * `driver` - Database driver
317///
318/// # Example
319///
320/// ```rust,ignore
321/// use bottle_orm::value_binding::bind_typed_value;
322/// use sqlx::any::AnyArguments;
323///
324/// let mut args = AnyArguments::default();
325/// bind_typed_value(&mut args, "42", "INTEGER", &Drivers::Postgres)?;
326/// bind_typed_value(&mut args, "2024-01-15T14:30:00+00:00", "TIMESTAMPTZ", &Drivers::Postgres)?;
327/// ```
328pub fn bind_typed_value(
329    args: &mut AnyArguments<'_>,
330    value_str: &str,
331    sql_type: &str,
332    driver: &Drivers,
333) -> Result<(), Error> {
334    args.bind_value(value_str, sql_type, driver)
335}
336
337/// Attempts to bind a value, falling back to string binding on error.
338///
339/// This is useful for cases where you want to be more lenient with type conversion.
340///
341/// # Arguments
342///
343/// * `args` - The AnyArguments to bind the value to
344/// * `value_str` - String representation of the value
345/// * `sql_type` - SQL type identifier
346/// * `driver` - Database driver
347pub fn bind_typed_value_or_string(args: &mut AnyArguments<'_>, value_str: &str, sql_type: &str, driver: &Drivers) {
348    if let Err(_) = args.bind_value(value_str, sql_type, driver) {
349        // Fallback: bind as string
350        let _ = args.add(value_str.to_string());
351    }
352}
353
354// ============================================================================
355// Type Detection
356// ============================================================================
357
358/// Detects if a SQL type requires special handling.
359pub fn requires_special_binding(sql_type: &str) -> bool {
360    matches!(
361        sql_type,
362        "UUID"
363            | "TIMESTAMPTZ"
364            | "DateTime"
365            | "TIMESTAMP"
366            | "NaiveDateTime"
367            | "DATE"
368            | "NaiveDate"
369            | "TIME"
370            | "NaiveTime"
371    )
372}
373
374/// Returns whether a SQL type is numeric.
375pub fn is_numeric_type(sql_type: &str) -> bool {
376    matches!(
377        sql_type,
378        "INTEGER"
379            | "INT"
380            | "BIGINT"
381            | "INT8"
382            | "SERIAL"
383            | "BIGSERIAL"
384            | "SMALLINT"
385            | "DOUBLE PRECISION"
386            | "FLOAT"
387            | "REAL"
388            | "NUMERIC"
389            | "DECIMAL"
390    )
391}
392
393/// Returns whether a SQL type is textual.
394pub fn is_text_type(sql_type: &str) -> bool {
395    matches!(sql_type, "TEXT" | "VARCHAR" | "CHAR" | "STRING")
396}