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 NaiveDateTime value.
63    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers);
64
65    /// Binds a NaiveDate value.
66    fn bind_naive_date(&mut self, value: NaiveDate, driver: &Drivers);
67
68    /// Binds a NaiveTime value.
69    fn bind_naive_time(&mut self, value: NaiveTime, driver: &Drivers);
70}
71
72impl ValueBinder for AnyArguments<'_> {
73    fn bind_value(&mut self, value_str: &str, sql_type: &str, driver: &Drivers) -> Result<(), Error> {
74        match sql_type {
75            // ================================================================
76            // Integer Types
77            // ================================================================
78            "INTEGER" | "INT" | "SERIAL" | "serial" | "int4" => {
79                let val: i32 =
80                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse i32: {}", e)))?;
81                self.bind_i32(val);
82                Ok(())
83            }
84
85            "BIGINT" | "INT8" | "int8" | "BIGSERIAL" => {
86                let val: i64 =
87                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse i64: {}", e)))?;
88                self.bind_i64(val);
89                Ok(())
90            }
91
92            // ================================================================
93            // Boolean Type
94            // ================================================================
95            "BOOLEAN" | "BOOL" | "bool" => {
96                let val: bool =
97                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse bool: {}", e)))?;
98                self.bind_bool(val);
99                Ok(())
100            }
101
102            // ================================================================
103            // Floating-Point Types
104            // ================================================================
105            "DOUBLE PRECISION" | "FLOAT" | "float8" | "REAL" | "NUMERIC" | "DECIMAL" => {
106                let val: f64 =
107                    value_str.parse().map_err(|e| Error::Conversion(format!("Failed to parse f64: {}", e)))?;
108                self.bind_f64(val);
109                Ok(())
110            }
111
112            // ================================================================
113            // UUID Type
114            // ================================================================
115            "UUID" => {
116                let val =
117                    value_str.parse::<Uuid>().map_err(|e| Error::Conversion(format!("Failed to parse UUID: {}", e)))?;
118                self.bind_uuid(val, driver);
119                Ok(())
120            }
121
122            // ================================================================
123            // Temporal Types (DateTime, Date, Time)
124            // ================================================================
125            "TIMESTAMPTZ" | "DateTime" => {
126                let val = temporal::parse_datetime_utc(value_str)?;
127                self.bind_datetime_utc(val, driver);
128                Ok(())
129            }
130
131            "TIMESTAMP" | "NaiveDateTime" => {
132                let val = temporal::parse_naive_datetime(value_str)?;
133                self.bind_naive_datetime(val, driver);
134                Ok(())
135            }
136
137            "DATE" | "NaiveDate" => {
138                let val = temporal::parse_naive_date(value_str)?;
139                self.bind_naive_date(val, driver);
140                Ok(())
141            }
142
143            "TIME" | "NaiveTime" => {
144                let val = temporal::parse_naive_time(value_str)?;
145                self.bind_naive_time(val, driver);
146                Ok(())
147            }
148
149            // ================================================================
150            // Text and Default Types
151            // ================================================================
152            "TEXT" | "VARCHAR" | "CHAR" | "STRING" | _ => {
153                self.bind_string(value_str.to_string());
154                Ok(())
155            }
156        }
157    }
158
159    fn bind_i32(&mut self, value: i32) {
160        let _ = self.add(value);
161    }
162
163    fn bind_i64(&mut self, value: i64) {
164        let _ = self.add(value);
165    }
166
167    fn bind_bool(&mut self, value: bool) {
168        let _ = self.add(value);
169    }
170
171    fn bind_f64(&mut self, value: f64) {
172        let _ = self.add(value);
173    }
174
175    fn bind_string(&mut self, value: String) {
176        let _ = self.add(value);
177    }
178
179    fn bind_uuid(&mut self, value: Uuid, driver: &Drivers) {
180        match driver {
181            Drivers::Postgres => {
182                // PostgreSQL has native UUID support
183                // Convert to hyphenated string format
184                let _ = self.add(value.hyphenated().to_string());
185            }
186            Drivers::MySQL => {
187                // MySQL stores UUID as CHAR(36)
188                let _ = self.add(value.hyphenated().to_string());
189            }
190            Drivers::SQLite => {
191                // SQLite stores as TEXT
192                let _ = self.add(value.hyphenated().to_string());
193            }
194        }
195    }
196
197    fn bind_datetime_utc(&mut self, value: DateTime<Utc>, driver: &Drivers) {
198        let formatted = temporal::format_datetime_for_driver(&value, driver);
199        let _ = self.add(formatted);
200    }
201
202    fn bind_naive_datetime(&mut self, value: NaiveDateTime, driver: &Drivers) {
203        let formatted = temporal::format_naive_datetime_for_driver(&value, driver);
204        let _ = self.add(formatted);
205    }
206
207    fn bind_naive_date(&mut self, value: NaiveDate, _driver: &Drivers) {
208        // All drivers use ISO 8601 date format
209        let formatted = value.format("%Y-%m-%d").to_string();
210        let _ = self.add(formatted);
211    }
212
213    fn bind_naive_time(&mut self, value: NaiveTime, _driver: &Drivers) {
214        // All drivers use ISO 8601 time format
215        let formatted = value.format("%H:%M:%S%.6f").to_string();
216        let _ = self.add(formatted);
217    }
218}
219
220// ============================================================================
221// Convenience Functions
222// ============================================================================
223
224/// Binds a value to AnyArguments with automatic type detection and conversion.
225///
226/// This is a convenience function that wraps the ValueBinder trait.
227///
228/// # Arguments
229///
230/// * `args` - The AnyArguments to bind the value to
231/// * `value_str` - String representation of the value
232/// * `sql_type` - SQL type identifier
233/// * `driver` - Database driver
234///
235/// # Example
236///
237/// ```rust,ignore
238/// use bottle_orm::value_binding::bind_typed_value;
239/// use sqlx::any::AnyArguments;
240///
241/// let mut args = AnyArguments::default();
242/// bind_typed_value(&mut args, "42", "INTEGER", &Drivers::Postgres)?;
243/// bind_typed_value(&mut args, "2024-01-15T14:30:00+00:00", "TIMESTAMPTZ", &Drivers::Postgres)?;
244/// ```
245pub fn bind_typed_value(
246    args: &mut AnyArguments<'_>,
247    value_str: &str,
248    sql_type: &str,
249    driver: &Drivers,
250) -> Result<(), Error> {
251    args.bind_value(value_str, sql_type, driver)
252}
253
254/// Attempts to bind a value, falling back to string binding on error.
255///
256/// This is useful for cases where you want to be more lenient with type conversion.
257///
258/// # Arguments
259///
260/// * `args` - The AnyArguments to bind the value to
261/// * `value_str` - String representation of the value
262/// * `sql_type` - SQL type identifier
263/// * `driver` - Database driver
264pub fn bind_typed_value_or_string(args: &mut AnyArguments<'_>, value_str: &str, sql_type: &str, driver: &Drivers) {
265    if let Err(_) = args.bind_value(value_str, sql_type, driver) {
266        // Fallback: bind as string
267        let _ = args.add(value_str.to_string());
268    }
269}
270
271// ============================================================================
272// Type Detection
273// ============================================================================
274
275/// Detects if a SQL type requires special handling.
276pub fn requires_special_binding(sql_type: &str) -> bool {
277    matches!(
278        sql_type,
279        "UUID"
280            | "TIMESTAMPTZ"
281            | "DateTime"
282            | "TIMESTAMP"
283            | "NaiveDateTime"
284            | "DATE"
285            | "NaiveDate"
286            | "TIME"
287            | "NaiveTime"
288    )
289}
290
291/// Returns whether a SQL type is numeric.
292pub fn is_numeric_type(sql_type: &str) -> bool {
293    matches!(
294        sql_type,
295        "INTEGER"
296            | "INT"
297            | "BIGINT"
298            | "INT8"
299            | "SERIAL"
300            | "BIGSERIAL"
301            | "SMALLINT"
302            | "DOUBLE PRECISION"
303            | "FLOAT"
304            | "REAL"
305            | "NUMERIC"
306            | "DECIMAL"
307    )
308}
309
310/// Returns whether a SQL type is textual.
311pub fn is_text_type(sql_type: &str) -> bool {
312    matches!(sql_type, "TEXT" | "VARCHAR" | "CHAR" | "STRING")
313}