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, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
22use sqlx::Arguments;
23use sqlx::any::AnyArguments;
24
25use crate::Error;
26use crate::database::Drivers;
27
28// ============================================================================
29// DateTime<Utc> and DateTime<FixedOffset> Conversion
30// ============================================================================
31
32/// Binds a `DateTime<Utc>` value to a SQL query based on the database driver.
33pub fn bind_datetime_utc(
34 query_args: &mut AnyArguments<'_>,
35 value: &DateTime<Utc>,
36 driver: &Drivers,
37) -> Result<(), Error> {
38 match driver {
39 Drivers::Postgres => {
40 let _ = query_args.add(value.to_rfc3339());
41 }
42 Drivers::MySQL => {
43 let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
44 let _ = query_args.add(formatted);
45 }
46 Drivers::SQLite => {
47 let _ = query_args.add(value.to_rfc3339());
48 }
49 }
50 Ok(())
51}
52
53/// Binds a `DateTime<FixedOffset>` value.
54pub fn bind_datetime_fixed(
55 query_args: &mut AnyArguments<'_>,
56 value: &DateTime<FixedOffset>,
57 driver: &Drivers,
58) -> Result<(), Error> {
59 // Convert to UTC for consistency across drivers that enforce UTC
60 // or keep offset depending on driver capabilities.
61 // For simplicity and consistency with existing logic, we bind as string.
62 match driver {
63 Drivers::Postgres => {
64 // Postgres handles offsets fine in TIMESTAMPTZ
65 let _ = query_args.add(value.to_rfc3339());
66 }
67 Drivers::MySQL => {
68 // MySQL converts to UTC for TIMESTAMP storage anyway
69 let value_utc: DateTime<Utc> = value.with_timezone(&Utc);
70 let formatted = value_utc.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
71 let _ = query_args.add(formatted);
72 }
73 Drivers::SQLite => {
74 // SQLite uses text, so RFC3339 with offset is fine
75 let _ = query_args.add(value.to_rfc3339());
76 }
77 }
78 Ok(())
79}
80
81/// Parses a string into a `DateTime<Utc>`.
82///
83/// Tries strict `DateTime<Utc>` parsing first. If that fails, tries parsing as
84/// `DateTime<FixedOffset>` and converting to UTC. This supports inputs with
85/// arbitrary timezones (e.g. "+02:00").
86pub fn parse_datetime_utc(value: &str) -> Result<DateTime<Utc>, Error> {
87 // Try direct UTC parsing
88 if let Ok(dt) = value.parse::<DateTime<Utc>>() {
89 return Ok(dt);
90 }
91
92 // Try FixedOffset parsing and convert to UTC
93 if let Ok(dt) = DateTime::parse_from_rfc3339(value) {
94 return Ok(dt.with_timezone(&Utc));
95 }
96
97 // Try parsing without timezone (Naive) and assume UTC
98 // This handles "YYYY-MM-DD HH:MM:SS" formats common in MySQL/SQLite
99 if let Ok(naive) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f") {
100 return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
101 }
102
103 if let Ok(naive) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
104 return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
105 }
106
107 Err(Error::Conversion(format!("Failed to parse DateTime<Utc> from '{}'", value)))
108}
109
110/// Parses a string into a `DateTime<FixedOffset>`.
111pub fn parse_datetime_fixed(value: &str) -> Result<DateTime<FixedOffset>, Error> {
112 if let Ok(dt) = DateTime::parse_from_rfc3339(value) {
113 return Ok(dt);
114 }
115
116 // If it lacks timezone info (Naive), we generally assume UTC for safety
117 if let Ok(naive) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f") {
118 // Create a FixedOffset of +00:00 (UTC)
119 let offset = FixedOffset::east_opt(0).unwrap();
120 return Ok(DateTime::from_naive_utc_and_offset(naive, offset));
121 }
122
123 Err(Error::Conversion(format!("Failed to parse DateTime<FixedOffset> from '{}'", value)))
124}
125
126// ============================================================================
127// NaiveDateTime Conversion
128// ============================================================================
129
130/// Binds a `NaiveDateTime` value to a SQL query based on the database driver.
131///
132/// # Arguments
133///
134/// * `query_args` - The SQLx AnyArguments to bind the value to
135/// * `value` - The NaiveDateTime value to bind
136/// * `driver` - The database driver being used
137///
138/// # Database-Specific Behavior
139///
140/// ## PostgreSQL
141/// - Uses TIMESTAMP (without timezone) type
142/// - Stores as-is without timezone conversion
143///
144/// ## MySQL
145/// - Uses DATETIME type (no Y2038 limit)
146/// - Stores without timezone information
147/// - Range: '1000-01-01 00:00:00' to '9999-12-31 23:59:59'
148///
149/// ## SQLite
150/// - Stores as TEXT in ISO 8601 format
151/// - Format: "YYYY-MM-DD HH:MM:SS.SSS"
152pub fn bind_naive_datetime(
153 query_args: &mut AnyArguments<'_>,
154 value: &NaiveDateTime,
155 driver: &Drivers,
156) -> Result<(), Error> {
157 match driver {
158 Drivers::Postgres => {
159 // PostgreSQL TIMESTAMP (without timezone)
160 // Format: "YYYY-MM-DD HH:MM:SS.SSSSSS"
161 let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
162 let _ = query_args.add(formatted);
163 }
164 Drivers::MySQL => {
165 // MySQL DATETIME
166 // Format: "YYYY-MM-DD HH:MM:SS.SSSSSS"
167 let formatted = value.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
168 let _ = query_args.add(formatted);
169 }
170 Drivers::SQLite => {
171 // SQLite TEXT format
172 // Using ISO 8601 format
173 let formatted = value.format("%Y-%m-%d %H:%M:%S%.f").to_string();
174 let _ = query_args.add(formatted);
175 }
176 }
177 Ok(())
178}
179
180/// Parses a string into a `NaiveDateTime`.
181pub fn parse_naive_datetime(value: &str) -> Result<NaiveDateTime, Error> {
182 // Try default parsing
183 if let Ok(dt) = value.parse::<NaiveDateTime>() {
184 return Ok(dt);
185 }
186
187 // Try formats common in different DBs
188 // YYYY-MM-DD HH:MM:SS
189 if let Ok(dt) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
190 return Ok(dt);
191 }
192
193 // YYYY-MM-DD HH:MM:SS.f
194 if let Ok(dt) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f") {
195 return Ok(dt);
196 }
197
198 // YYYY-MM-DD HH:MM (no seconds)
199 if let Ok(dt) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M") {
200 return Ok(dt);
201 }
202
203 // RFC 3339 (T separator)
204 if let Ok(dt) = NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f") {
205 return Ok(dt);
206 }
207
208 Err(Error::Conversion(format!("Failed to parse NaiveDateTime from '{}'", value)))
209}
210
211// ============================================================================
212// NaiveDate Conversion
213// ============================================================================
214
215/// Binds a `NaiveDate` value to a SQL query based on the database driver.
216///
217/// # Arguments
218///
219/// * `query_args` - The SQLx AnyArguments to bind the value to
220/// * `value` - The NaiveDate value to bind
221/// * `driver` - The database driver being used
222///
223/// # Database-Specific Behavior
224///
225/// All drivers use standard DATE type with format "YYYY-MM-DD"
226pub fn bind_naive_date(query_args: &mut AnyArguments<'_>, value: &NaiveDate, driver: &Drivers) -> Result<(), Error> {
227 match driver {
228 Drivers::Postgres | Drivers::MySQL | Drivers::SQLite => {
229 // All databases use ISO 8601 date format: YYYY-MM-DD
230 let formatted = value.format("%Y-%m-%d").to_string();
231 let _ = query_args.add(formatted);
232 }
233 }
234 Ok(())
235}
236
237/// Parses a string into a `NaiveDate`.
238pub fn parse_naive_date(value: &str) -> Result<NaiveDate, Error> {
239 value.parse::<NaiveDate>().map_err(|e| Error::Conversion(format!("Failed to parse NaiveDate: {}", e)))
240}
241
242// ============================================================================
243// NaiveTime Conversion
244// ============================================================================
245
246/// Binds a `NaiveTime` value to a SQL query based on the database driver.
247///
248/// # Arguments
249///
250/// * `query_args` - The SQLx AnyArguments to bind the value to
251/// * `value` - The NaiveTime value to bind
252/// * `driver` - The database driver being used
253///
254/// # Database-Specific Behavior
255///
256/// All drivers use standard TIME type with format "HH:MM:SS.ffffff"
257pub fn bind_naive_time(query_args: &mut AnyArguments<'_>, value: &NaiveTime, driver: &Drivers) -> Result<(), Error> {
258 match driver {
259 Drivers::Postgres | Drivers::MySQL | Drivers::SQLite => {
260 // All databases use ISO 8601 time format: HH:MM:SS.ffffff
261 let formatted = value.format("%H:%M:%S%.6f").to_string();
262 let _ = query_args.add(formatted);
263 }
264 }
265 Ok(())
266}
267
268/// Parses a string into a `NaiveTime`.
269pub fn parse_naive_time(value: &str) -> Result<NaiveTime, Error> {
270 value.parse::<NaiveTime>().map_err(|e| Error::Conversion(format!("Failed to parse NaiveTime: {}", e)))
271}
272
273// ============================================================================
274// Generic Temporal Binding
275// ============================================================================
276
277/// Binds a temporal value to a SQL query based on its SQL type.
278///
279/// This is a convenience function that dispatches to the appropriate
280/// type-specific binding function based on the SQL type string.
281///
282/// # Arguments
283///
284/// * `query_args` - The SQLx AnyArguments to bind the value to
285/// * `value_str` - The string representation of the temporal value
286/// * `sql_type` - The SQL type of the column
287/// * `driver` - The database driver being used
288pub fn bind_temporal_value(
289 query_args: &mut AnyArguments<'_>,
290 value_str: &str,
291 sql_type: &str,
292 driver: &Drivers,
293) -> Result<(), Error> {
294 match sql_type {
295 "TIMESTAMPTZ" | "DateTime" => {
296 let value = parse_datetime_utc(value_str)?;
297 bind_datetime_utc(query_args, &value, driver)
298 }
299 "TIMESTAMP" | "NaiveDateTime" => {
300 let value = parse_naive_datetime(value_str)?;
301 bind_naive_datetime(query_args, &value, driver)
302 }
303 "DATE" | "NaiveDate" => {
304 let value = parse_naive_date(value_str)?;
305 bind_naive_date(query_args, &value, driver)
306 }
307 "TIME" | "NaiveTime" => {
308 let value = parse_naive_time(value_str)?;
309 bind_naive_time(query_args, &value, driver)
310 }
311 _ => Err(Error::Conversion(format!("Unknown temporal SQL type: {}", sql_type))),
312 }
313}
314
315// ============================================================================
316// Utility Functions
317// ============================================================================
318
319/// Returns the appropriate SQL type cast string for temporal types in PostgreSQL.
320///
321/// # Arguments
322///
323/// * `sql_type` - The SQL type identifier
324///
325/// # Returns
326///
327/// The PostgreSQL type cast string (e.g., "::TIMESTAMPTZ")
328pub fn get_postgres_type_cast(sql_type: &str) -> &'static str {
329 match sql_type {
330 "TIMESTAMPTZ" | "DateTime" => "::TIMESTAMPTZ",
331 "TIMESTAMP" | "NaiveDateTime" => "::TIMESTAMP",
332 "DATE" | "NaiveDate" => "::DATE",
333 "TIME" | "NaiveTime" => "::TIME",
334 _ => "",
335 }
336}
337
338/// Checks if a SQL type is a temporal type.
339pub fn is_temporal_type(sql_type: &str) -> bool {
340 matches!(
341 sql_type,
342 "TIMESTAMPTZ" | "DateTime" | "TIMESTAMP" | "NaiveDateTime" | "DATE" | "NaiveDate" | "TIME" | "NaiveTime"
343 )
344}
345
346// ============================================================================
347// Format Conversion Utilities
348// ============================================================================
349
350/// Converts a `DateTime<Utc>` to the format expected by a specific driver.
351///
352/// This is useful for debugging or when you need the string representation
353/// without actually binding to a query.
354pub fn format_datetime_for_driver(value: &DateTime<Utc>, driver: &Drivers) -> String {
355 match driver {
356 Drivers::Postgres | Drivers::SQLite => value.to_rfc3339(),
357 Drivers::MySQL => value.format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
358 }
359}
360
361/// Converts a `DateTime<FixedOffset>` to the format expected by a specific driver.
362pub fn format_datetime_fixed_for_driver(value: &DateTime<FixedOffset>, driver: &Drivers) -> String {
363 match driver {
364 Drivers::Postgres => value.to_rfc3339(),
365 Drivers::MySQL => {
366 // Convert to UTC for MySQL
367 let value_utc: DateTime<Utc> = value.with_timezone(&Utc);
368 value_utc.format("%Y-%m-%d %H:%M:%S%.6f").to_string()
369 }
370 Drivers::SQLite => value.to_rfc3339(),
371 }
372}
373
374/// Converts a `NaiveDateTime` to the format expected by a specific driver.
375pub fn format_naive_datetime_for_driver(value: &NaiveDateTime, driver: &Drivers) -> String {
376 match driver {
377 Drivers::Postgres | Drivers::MySQL => value.format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
378 Drivers::SQLite => value.format("%Y-%m-%d %H:%M:%S%.f").to_string(),
379 }
380}