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}