bottle_orm/
temporal.rs

1//! # Temporal Type Conversion Module
2//!
3//! This module provides specialized handling for temporal types (DateTime, NaiveDateTime, etc.)
4//! across different database drivers. It optimizes the conversion between Rust chrono types
5//! and native database types for PostgreSQL, MySQL, and SQLite.
6//!
7//! ## Key Features
8//!
9//! - **Native Type Support**: Uses database-native types when possible instead of string conversion
10//! - **Driver-Specific Optimization**: Tailored conversion for each database driver
11//! - **Timezone Handling**: Proper timezone conversion for DateTime<Utc>
12//! - **Format Consistency**: Ensures consistent date/time formats across drivers
13//!
14//! ## Supported Types
15//!
16//! - `DateTime<Utc>` - Timestamp with timezone (UTC)
17//! - `NaiveDateTime` - Timestamp without timezone
18//! - `NaiveDate` - Date only (year, month, day)
19//! - `NaiveTime` - Time only (hour, minute, second)
20
21use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
22use sqlx::Arguments;
23use sqlx::any::AnyArguments;
24
25use crate::Error;
26use crate::database::Drivers;
27
28// ============================================================================
29// DateTime<Utc> Conversion
30// ============================================================================
31
32/// Binds a `DateTime<Utc>` value to a SQL query based on the database driver.
33///
34/// # Arguments
35///
36/// * `query_args` - The SQLx AnyArguments to bind the value to
37/// * `value` - The DateTime<Utc> value to bind
38/// * `driver` - The database driver being used
39///
40/// # Database-Specific Behavior
41///
42/// ## PostgreSQL
43/// - Uses native TIMESTAMPTZ type
44/// - Stores as UTC timestamp
45/// - Format: RFC 3339 (ISO 8601)
46///
47/// ## MySQL
48/// - Converts to TIMESTAMP type
49/// - Stores as UTC, converts based on session timezone
50/// - Note: Limited to dates before 2038-01-19 (Y2038 problem)
51///
52/// ## SQLite
53/// - Stores as TEXT in ISO 8601 format
54/// - Format: "YYYY-MM-DD HH:MM:SS.SSS+00:00"
55pub fn bind_datetime_utc(
56    query_args: &mut AnyArguments<'_>,
57    value: &DateTime<Utc>,
58    driver: &Drivers,
59) -> Result<(), Error> {
60    match driver {
61        Drivers::Postgres => {
62            // PostgreSQL has native TIMESTAMPTZ support
63            // SQLx handles the conversion automatically
64            let _ = query_args.add(value.to_rfc3339());
65        }
66        Drivers::MySQL => {
67            // MySQL TIMESTAMP: stores in UTC, displays in session timezone
68            // Format: "YYYY-MM-DD HH:MM:SS"
69            let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
70            let _ = query_args.add(formatted);
71        }
72        Drivers::SQLite => {
73            // SQLite stores as TEXT in ISO 8601 format
74            // Using RFC 3339 for maximum compatibility
75            let _ = query_args.add(value.to_rfc3339());
76        }
77    }
78    Ok(())
79}
80
81/// Parses a string into a `DateTime<Utc>`.
82///
83/// Attempts to parse the string using RFC 3339 format first,
84/// then falls back to other common formats.
85pub fn parse_datetime_utc(value: &str) -> Result<DateTime<Utc>, Error> {
86    value.parse::<DateTime<Utc>>().map_err(|e| Error::Conversion(format!("Failed to parse DateTime<Utc>: {}", e)))
87}
88
89// ============================================================================
90// NaiveDateTime Conversion
91// ============================================================================
92
93/// Binds a `NaiveDateTime` value to a SQL query based on the database driver.
94///
95/// # Arguments
96///
97/// * `query_args` - The SQLx AnyArguments to bind the value to
98/// * `value` - The NaiveDateTime value to bind
99/// * `driver` - The database driver being used
100///
101/// # Database-Specific Behavior
102///
103/// ## PostgreSQL
104/// - Uses TIMESTAMP (without timezone) type
105/// - Stores as-is without timezone conversion
106///
107/// ## MySQL
108/// - Uses DATETIME type (no Y2038 limit)
109/// - Stores without timezone information
110/// - Range: '1000-01-01 00:00:00' to '9999-12-31 23:59:59'
111///
112/// ## SQLite
113/// - Stores as TEXT in ISO 8601 format
114/// - Format: "YYYY-MM-DD HH:MM:SS.SSS"
115pub fn bind_naive_datetime(
116    query_args: &mut AnyArguments<'_>,
117    value: &NaiveDateTime,
118    driver: &Drivers,
119) -> Result<(), Error> {
120    match driver {
121        Drivers::Postgres => {
122            // PostgreSQL TIMESTAMP (without timezone)
123            // Format: "YYYY-MM-DD HH:MM:SS.SSSSSS"
124            let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
125            let _ = query_args.add(formatted);
126        }
127        Drivers::MySQL => {
128            // MySQL DATETIME
129            // Format: "YYYY-MM-DD HH:MM:SS.SSSSSS"
130            let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
131            let _ = query_args.add(formatted);
132        }
133        Drivers::SQLite => {
134            // SQLite TEXT format
135            // Using ISO 8601 format
136            let formatted = value.format("%Y-%m-%d %H:%M:%S%.f").to_string();
137            let _ = query_args.add(formatted);
138        }
139    }
140    Ok(())
141}
142
143/// Parses a string into a `NaiveDateTime`.
144pub fn parse_naive_datetime(value: &str) -> Result<NaiveDateTime, Error> {
145    value.parse::<NaiveDateTime>().map_err(|e| Error::Conversion(format!("Failed to parse NaiveDateTime: {}", e)))
146}
147
148// ============================================================================
149// NaiveDate Conversion
150// ============================================================================
151
152/// Binds a `NaiveDate` value to a SQL query based on the database driver.
153///
154/// # Arguments
155///
156/// * `query_args` - The SQLx AnyArguments to bind the value to
157/// * `value` - The NaiveDate value to bind
158/// * `driver` - The database driver being used
159///
160/// # Database-Specific Behavior
161///
162/// All drivers use standard DATE type with format "YYYY-MM-DD"
163pub fn bind_naive_date(query_args: &mut AnyArguments<'_>, value: &NaiveDate, driver: &Drivers) -> Result<(), Error> {
164    match driver {
165        Drivers::Postgres | Drivers::MySQL | Drivers::SQLite => {
166            // All databases use ISO 8601 date format: YYYY-MM-DD
167            let formatted = value.format("%Y-%m-%d").to_string();
168            let _ = query_args.add(formatted);
169        }
170    }
171    Ok(())
172}
173
174/// Parses a string into a `NaiveDate`.
175pub fn parse_naive_date(value: &str) -> Result<NaiveDate, Error> {
176    value.parse::<NaiveDate>().map_err(|e| Error::Conversion(format!("Failed to parse NaiveDate: {}", e)))
177}
178
179// ============================================================================
180// NaiveTime Conversion
181// ============================================================================
182
183/// Binds a `NaiveTime` value to a SQL query based on the database driver.
184///
185/// # Arguments
186///
187/// * `query_args` - The SQLx AnyArguments to bind the value to
188/// * `value` - The NaiveTime value to bind
189/// * `driver` - The database driver being used
190///
191/// # Database-Specific Behavior
192///
193/// All drivers use standard TIME type with format "HH:MM:SS.ffffff"
194pub fn bind_naive_time(query_args: &mut AnyArguments<'_>, value: &NaiveTime, driver: &Drivers) -> Result<(), Error> {
195    match driver {
196        Drivers::Postgres | Drivers::MySQL | Drivers::SQLite => {
197            // All databases use ISO 8601 time format: HH:MM:SS.ffffff
198            let formatted = value.format("%H:%M:%S%.6f").to_string();
199            let _ = query_args.add(formatted);
200        }
201    }
202    Ok(())
203}
204
205/// Parses a string into a `NaiveTime`.
206pub fn parse_naive_time(value: &str) -> Result<NaiveTime, Error> {
207    value.parse::<NaiveTime>().map_err(|e| Error::Conversion(format!("Failed to parse NaiveTime: {}", e)))
208}
209
210// ============================================================================
211// Generic Temporal Binding
212// ============================================================================
213
214/// Binds a temporal value to a SQL query based on its SQL type.
215///
216/// This is a convenience function that dispatches to the appropriate
217/// type-specific binding function based on the SQL type string.
218///
219/// # Arguments
220///
221/// * `query_args` - The SQLx AnyArguments to bind the value to
222/// * `value_str` - The string representation of the temporal value
223/// * `sql_type` - The SQL type of the column
224/// * `driver` - The database driver being used
225pub fn bind_temporal_value(
226    query_args: &mut AnyArguments<'_>,
227    value_str: &str,
228    sql_type: &str,
229    driver: &Drivers,
230) -> Result<(), Error> {
231    match sql_type {
232        "TIMESTAMPTZ" | "DateTime" => {
233            let value = parse_datetime_utc(value_str)?;
234            bind_datetime_utc(query_args, &value, driver)
235        }
236        "TIMESTAMP" | "NaiveDateTime" => {
237            let value = parse_naive_datetime(value_str)?;
238            bind_naive_datetime(query_args, &value, driver)
239        }
240        "DATE" | "NaiveDate" => {
241            let value = parse_naive_date(value_str)?;
242            bind_naive_date(query_args, &value, driver)
243        }
244        "TIME" | "NaiveTime" => {
245            let value = parse_naive_time(value_str)?;
246            bind_naive_time(query_args, &value, driver)
247        }
248        _ => Err(Error::Conversion(format!("Unknown temporal SQL type: {}", sql_type))),
249    }
250}
251
252// ============================================================================
253// Utility Functions
254// ============================================================================
255
256/// Returns the appropriate SQL type cast string for temporal types in PostgreSQL.
257///
258/// # Arguments
259///
260/// * `sql_type` - The SQL type identifier
261///
262/// # Returns
263///
264/// The PostgreSQL type cast string (e.g., "::TIMESTAMPTZ")
265pub fn get_postgres_type_cast(sql_type: &str) -> &'static str {
266    match sql_type {
267        "TIMESTAMPTZ" | "DateTime" => "::TIMESTAMPTZ",
268        "TIMESTAMP" | "NaiveDateTime" => "::TIMESTAMP",
269        "DATE" | "NaiveDate" => "::DATE",
270        "TIME" | "NaiveTime" => "::TIME",
271        _ => "",
272    }
273}
274
275/// Checks if a SQL type is a temporal type.
276pub fn is_temporal_type(sql_type: &str) -> bool {
277    matches!(
278        sql_type,
279        "TIMESTAMPTZ" | "DateTime" | "TIMESTAMP" | "NaiveDateTime" | "DATE" | "NaiveDate" | "TIME" | "NaiveTime"
280    )
281}
282
283// ============================================================================
284// Format Conversion Utilities
285// ============================================================================
286
287/// Converts a `DateTime<Utc>` to the format expected by a specific driver.
288///
289/// This is useful for debugging or when you need the string representation
290/// without actually binding to a query.
291pub fn format_datetime_for_driver(value: &DateTime<Utc>, driver: &Drivers) -> String {
292    match driver {
293        Drivers::Postgres | Drivers::SQLite => value.to_rfc3339(),
294        Drivers::MySQL => value.format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
295    }
296}
297
298/// Converts a `NaiveDateTime` to the format expected by a specific driver.
299pub fn format_naive_datetime_for_driver(value: &NaiveDateTime, driver: &Drivers) -> String {
300    match driver {
301        Drivers::Postgres | Drivers::MySQL => value.format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
302        Drivers::SQLite => value.format("%Y-%m-%d %H:%M:%S%.f").to_string(),
303    }
304}