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}