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}