diff --git a/sqlx-core/src/sqlite/connection/collation.rs b/sqlx-core/src/sqlite/connection/collation.rs index 353fa725..8bbf6f5d 100644 --- a/sqlx-core/src/sqlite/connection/collation.rs +++ b/sqlx-core/src/sqlite/connection/collation.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::ffi::CString; -use std::os::raw::{c_char, c_int, c_void}; +use std::os::raw::{c_int, c_void}; use std::slice; use std::str::from_utf8_unchecked; diff --git a/sqlx-core/src/sqlite/type_info.rs b/sqlx-core/src/sqlite/type_info.rs index 14eb9a99..508500fd 100644 --- a/sqlx-core/src/sqlite/type_info.rs +++ b/sqlx-core/src/sqlite/type_info.rs @@ -23,6 +23,8 @@ pub(crate) enum DataType { // non-standard extensions Bool, Int64, + Date, + Time, Datetime, } @@ -49,6 +51,8 @@ impl TypeInfo for SqliteTypeInfo { // non-standard extensions DataType::Bool => "BOOLEAN", + DataType::Date => "DATE", + DataType::Time => "TIME", DataType::Datetime => "DATETIME", } } @@ -81,6 +85,9 @@ impl FromStr for DataType { "int4" => DataType::Int, "int8" => DataType::Int64, "boolean" | "bool" => DataType::Bool, + + "date" => DataType::Date, + "time" => DataType::Time, "datetime" | "timestamp" => DataType::Datetime, _ if s.contains("int") => DataType::Int64, @@ -126,6 +133,8 @@ fn test_data_type_from_str() -> Result<(), BoxDynError> { assert_eq!(DataType::Bool, "BOOL".parse()?); assert_eq!(DataType::Datetime, "DATETIME".parse()?); + assert_eq!(DataType::Time, "TIME".parse()?); + assert_eq!(DataType::Date, "DATE".parse()?); Ok(()) } diff --git a/sqlx-core/src/sqlite/types/chrono.rs b/sqlx-core/src/sqlite/types/chrono.rs index a258e1c8..686c9270 100644 --- a/sqlx-core/src/sqlite/types/chrono.rs +++ b/sqlx-core/src/sqlite/types/chrono.rs @@ -1,3 +1,5 @@ +use crate::types::chrono::FixedOffset; +use crate::value::ValueRef; use crate::{ decode::Decode, encode::{Encode, IsNull}, @@ -5,30 +7,120 @@ use crate::{ sqlite::{type_info::DataType, Sqlite, SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef}, types::Type, }; -use chrono::prelude::*; +use bitflags::_core::fmt::Display; +use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Utc}; + +impl Type for DateTime { + fn type_info() -> SqliteTypeInfo { + SqliteTypeInfo(DataType::Datetime) + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} impl Type for NaiveDateTime { fn type_info() -> SqliteTypeInfo { SqliteTypeInfo(DataType::Datetime) } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + matches!( + ty.0, + DataType::Datetime | DataType::Text | DataType::Int64 | DataType::Int | DataType::Float + ) + } +} + +impl Type for NaiveDate { + fn type_info() -> SqliteTypeInfo { + SqliteTypeInfo(DataType::Date) + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + matches!(ty.0, DataType::Date | DataType::Text) + } +} + +impl Type for NaiveTime { + fn type_info() -> SqliteTypeInfo { + SqliteTypeInfo(DataType::Time) + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + matches!(ty.0, DataType::Time | DataType::Text) + } +} + +impl Encode<'_, Sqlite> for DateTime +where + Tz::Offset: Display, +{ + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + Encode::::encode(self.naive_utc().format("%F %T%.f").to_string(), buf) + } } impl Encode<'_, Sqlite> for NaiveDateTime { fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { - let text: String = self.format("%F %T%.f").to_string(); - Encode::::encode(text, buf) + Encode::::encode(self.format("%F %T%.f").to_string(), buf) } } -impl<'a> Decode<'a, Sqlite> for NaiveDateTime { - fn decode(value: SqliteValueRef<'a>) -> Result { - decode_naive_from_text(value.text()?) +impl Encode<'_, Sqlite> for NaiveDate { + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + Encode::::encode(self.format("%F").to_string(), buf) } } -fn decode_naive_from_text(text: &str) -> Result { +impl Encode<'_, Sqlite> for NaiveTime { + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + Encode::::encode(self.format("%T%.f%").to_string(), buf) + } +} + +impl<'r> Decode<'r, Sqlite> for DateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + Ok(Utc.from_utc_datetime(&decode_datetime(value)?.naive_utc())) + } +} + +impl<'r> Decode<'r, Sqlite> for DateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + Ok(Local.from_utc_datetime(&decode_datetime(value)?.naive_utc())) + } +} + +impl<'r> Decode<'r, Sqlite> for DateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + decode_datetime(value) + } +} + +fn decode_datetime(value: SqliteValueRef<'_>) -> Result, BoxDynError> { + let dt = match value.type_info().map(|ty| ty.0) { + None | Some(DataType::Text) => decode_datetime_from_text(value.text()?), + + Some(DataType::Int) | Some(DataType::Int64) => decode_datetime_from_int(value.int64()), + + Some(DataType::Float) => decode_datetime_from_float(value.double()), + + _ => None, + }; + + if let Some(dt) = dt { + Ok(dt) + } else { + Err(format!("invalid datetime: {}", value.text()?).into()) + } +} + +fn decode_datetime_from_text(value: &str) -> Option> { + println!("decode_datetime_from_text --> {}", value); + // Loop over common date time patterns, inspired by Diesel - // https://docs.diesel.rs/src/diesel/sqlite/types/date_and_time/chrono.rs.html#56-97 + // https://github.com/diesel-rs/diesel/blob/93ab183bcb06c69c0aee4a7557b6798fd52dd0d8/diesel/src/sqlite/types/date_and_time/chrono.rs#L56-L97 let sqlite_datetime_formats = &[ // Most likely format "%F %T%.f", @@ -47,52 +139,62 @@ fn decode_naive_from_text(text: &str) -> Result { ]; for format in sqlite_datetime_formats { - if let Ok(dt) = NaiveDateTime::parse_from_str(text, format) { - return Ok(dt); + if let Ok(dt) = DateTime::parse_from_str(value, format) { + return Some(dt); + } + + if let Ok(dt) = NaiveDateTime::parse_from_str(value, format) { + return Some(Utc.fix().from_utc_datetime(&dt)); } } - return Err(err_protocol!("Did not find a matching pattern").into()); + None } -impl Type for DateTime { - fn type_info() -> SqliteTypeInfo { - SqliteTypeInfo(DataType::Datetime) +fn decode_datetime_from_int(value: i64) -> Option> { + NaiveDateTime::from_timestamp_opt(value, 0).map(|dt| Utc.fix().from_utc_datetime(&dt)) +} + +fn decode_datetime_from_float(value: f64) -> Option> { + let epoch_in_julian_days = 2_440_587.5; + let seconds_in_day = 86400.0; + let timestamp = (value - epoch_in_julian_days) * seconds_in_day; + let seconds = timestamp as i64; + let nanos = (timestamp.fract() * 1E9) as u32; + + NaiveDateTime::from_timestamp_opt(seconds, nanos).map(|dt| Utc.fix().from_utc_datetime(&dt)) +} + +impl<'r> Decode<'r, Sqlite> for NaiveDateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + Ok(decode_datetime(value)?.naive_local()) } } -impl Encode<'_, Sqlite> for DateTime -where - ::Offset: std::fmt::Display, -{ - fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { - let text = self.to_rfc3339(); - Encode::::encode(text, buf) +impl<'r> Decode<'r, Sqlite> for NaiveDate { + fn decode(value: SqliteValueRef<'r>) -> Result { + Ok(NaiveDate::parse_from_str("%F", value.text()?)?) } } -impl<'a> Decode<'a, Sqlite> for DateTime { - fn decode(value: SqliteValueRef<'a>) -> Result { - let text = value.text()?; - if let Ok(dt) = DateTime::parse_from_rfc3339(text) { - Ok(dt.with_timezone(&Utc)) - } else { - let dt = decode_naive_from_text(text)?; - Ok(Utc.from_utc_datetime(&dt)) +impl<'r> Decode<'r, Sqlite> for NaiveTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + let value = value.text()?; + + // Loop over common time patterns, inspired by Diesel + // https://github.com/diesel-rs/diesel/blob/93ab183bcb06c69c0aee4a7557b6798fd52dd0d8/diesel/src/sqlite/types/date_and_time/chrono.rs#L29-L47 + let sqlite_time_formats = &[ + // Most likely format + "%T.f", // Other formats in order of appearance in docs + "%R", "%RZ", "%T%.fZ", "%R%:z", "%T%.f%:z", + ]; + + for format in sqlite_time_formats { + if let Ok(dt) = NaiveTime::parse_from_str(value, format) { + return Ok(dt); + } } - } -} -impl<'a> Decode<'a, Sqlite> for DateTime { - fn decode(value: SqliteValueRef<'a>) -> Result { - let text = value.text()?; - Ok(DateTime::parse_from_rfc3339(text)?) - } -} - -impl<'a> Decode<'a, Sqlite> for DateTime { - fn decode(value: SqliteValueRef<'a>) -> Result { - let as_utc: DateTime = Decode::::decode(value)?; - Ok(as_utc.with_timezone(&Local)) + Err(format!("invalid time: {}", value).into()) } } diff --git a/tests/sqlite/types.rs b/tests/sqlite/types.rs index 77306052..a64572ea 100644 --- a/tests/sqlite/types.rs +++ b/tests/sqlite/types.rs @@ -35,23 +35,17 @@ test_type!(bytes>(Sqlite, #[cfg(feature = "chrono")] mod chrono { use super::*; - use sqlx::types::chrono::{ - DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Utc, - }; + use sqlx::types::chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc}; test_type!(chrono_naive_date_time(Sqlite, - "'2019-01-02 05:10:20'" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20) + "datetime('2019-01-02 05:10:20')" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20) )); test_type!(chrono_date_time_utc>(Sqlite, - "'1996-12-20T00:39:57+00:00'" == Utc.ymd(1996, 12, 20).and_hms(0, 39, 57) + "datetime('1996-12-20T00:39:57+00:00')" == Utc.ymd(1996, 12, 20).and_hms(0, 39, 57) )); test_type!(chrono_date_time_fixed_offset>(Sqlite, - "'2016-11-08T03:50:23-05:00'" == FixedOffset::west(5 * 3600).ymd(2016, 11, 08).and_hms(3, 50, 23) - )); - - test_type!(chrono_date_time_local>(Sqlite, - "'2016-11-08T03:50:23+00:00'" == Local.ymd(2016, 11, 08).and_hms(3, 50, 23) + "datetime('2016-11-08T03:50:23-05:00')" == FixedOffset::west(5 * 3600).ymd(2016, 11, 08).and_hms(3, 50, 23) )); }