mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-09-29 22:12:04 +00:00
fix: handle zero dates in MySQL, emit as Option::None (treat as NULL)
This commit is contained in:
parent
0bd556e0ee
commit
0824723765
@ -140,7 +140,7 @@ impl Connect for MySqlConnection {
|
||||
// https://mathiasbynens.be/notes/mysql-utf8mb4
|
||||
|
||||
conn.execute(concat!(
|
||||
r#"SET sql_mode=(SELECT CONCAT(@@sql_mode, ',PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE')),"#,
|
||||
r#"SET sql_mode=(SELECT CONCAT(@@sql_mode, ',PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION')),"#,
|
||||
r#"time_zone='+00:00',"#,
|
||||
r#"NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;"#,
|
||||
)).await?;
|
||||
|
@ -5,7 +5,7 @@ use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike,
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::mysql::protocol::text::ColumnType;
|
||||
use crate::mysql::type_info::MySqlTypeInfo;
|
||||
use crate::mysql::{MySql, MySqlValueFormat, MySqlValueRef};
|
||||
@ -127,7 +127,9 @@ impl Encode<'_, MySql> for NaiveDate {
|
||||
impl<'r> Decode<'r, MySql> for NaiveDate {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => Ok(decode_date(&value.as_bytes()?[1..])),
|
||||
MySqlValueFormat::Binary => {
|
||||
decode_date(&value.as_bytes()?[1..]).ok_or_else(|| UnexpectedNullError.into())
|
||||
}
|
||||
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
@ -186,7 +188,7 @@ impl<'r> Decode<'r, MySql> for NaiveDateTime {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
let len = buf[0];
|
||||
let date = decode_date(&buf[1..]);
|
||||
let date = decode_date(&buf[1..]).ok_or(UnexpectedNullError)?;
|
||||
|
||||
let dt = if len > 4 {
|
||||
date.and_time(decode_time(len - 4, &buf[5..]))
|
||||
@ -215,9 +217,18 @@ fn encode_date(date: &NaiveDate, buf: &mut Vec<u8>) {
|
||||
buf.push(date.day() as u8);
|
||||
}
|
||||
|
||||
fn decode_date(mut buf: &[u8]) -> NaiveDate {
|
||||
let year = buf.get_u16_le();
|
||||
NaiveDate::from_ymd(year as i32, buf[0] as u32, buf[1] as u32)
|
||||
fn decode_date(mut buf: &[u8]) -> Option<NaiveDate> {
|
||||
if buf.len() == 0 {
|
||||
// MySQL specifies that if there are no bytes, this is all zeros
|
||||
None
|
||||
} else {
|
||||
let year = buf.get_u16_le();
|
||||
Some(NaiveDate::from_ymd(
|
||||
year as i32,
|
||||
buf[0] as u32,
|
||||
buf[1] as u32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_time(time: &NaiveTime, include_micros: bool, buf: &mut Vec<u8>) {
|
||||
|
@ -7,7 +7,7 @@ use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::mysql::protocol::text::ColumnType;
|
||||
use crate::mysql::type_info::MySqlTypeInfo;
|
||||
use crate::mysql::{MySql, MySqlValueFormat, MySqlValueRef};
|
||||
@ -143,7 +143,9 @@ impl Encode<'_, MySql> for Date {
|
||||
impl<'r> Decode<'r, MySql> for Date {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => decode_date(&value.as_bytes()?[1..]),
|
||||
MySqlValueFormat::Binary => {
|
||||
Ok(decode_date(&value.as_bytes()?[1..])?.ok_or(UnexpectedNullError)?)
|
||||
}
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
Date::parse(s, "%Y-%m-%d").map_err(Into::into)
|
||||
@ -195,7 +197,7 @@ impl<'r> Decode<'r, MySql> for PrimitiveDateTime {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
let len = buf[0];
|
||||
let date = decode_date(&buf[1..])?;
|
||||
let date = decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?;
|
||||
|
||||
let dt = if len > 4 {
|
||||
date.with_time(decode_time(len - 4, &buf[5..])?)
|
||||
@ -239,13 +241,19 @@ fn encode_date(date: &Date, buf: &mut Vec<u8>) {
|
||||
buf.push(date.day());
|
||||
}
|
||||
|
||||
fn decode_date(buf: &[u8]) -> Result<Date, BoxDynError> {
|
||||
fn decode_date(buf: &[u8]) -> Result<Option<Date>, BoxDynError> {
|
||||
if buf.is_empty() {
|
||||
// zero buffer means a zero date (null)
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Date::try_from_ymd(
|
||||
LittleEndian::read_u16(buf) as i32,
|
||||
buf[2] as u8,
|
||||
buf[3] as u8,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn encode_time(time: &Time, include_micros: bool, buf: &mut Vec<u8>) {
|
||||
|
@ -4,6 +4,7 @@ use std::str::from_utf8;
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::mysql::protocol::text::ColumnType;
|
||||
use crate::mysql::{MySql, MySqlTypeInfo};
|
||||
use crate::value::{Value, ValueRef};
|
||||
|
||||
@ -65,7 +66,7 @@ impl Value for MySqlValue {
|
||||
}
|
||||
|
||||
fn is_null(&self) -> bool {
|
||||
self.value.is_none()
|
||||
is_null(self.value.as_deref(), self.type_info.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +93,23 @@ impl<'r> ValueRef<'r> for MySqlValueRef<'r> {
|
||||
self.type_info.as_ref().map(Cow::Borrowed)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_null(&self) -> bool {
|
||||
self.value.is_none()
|
||||
is_null(self.value.as_deref(), self.type_info.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_null(value: Option<&[u8]>, ty: Option<&MySqlTypeInfo>) -> bool {
|
||||
if let (Some(value), Some(ty)) = (value, ty) {
|
||||
// zero dates and date times should be treated the same as NULL
|
||||
if matches!(
|
||||
ty.r#type,
|
||||
ColumnType::Date | ColumnType::Timestamp | ColumnType::Datetime
|
||||
) && value.get(0) == Some(&0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value.is_none()
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
extern crate time_ as time;
|
||||
|
||||
use sqlx::mysql::MySql;
|
||||
use sqlx::{Executor, Row};
|
||||
use sqlx_test::test_type;
|
||||
|
||||
test_type!(bool(MySql, "false" == false, "true" == true));
|
||||
@ -43,35 +44,68 @@ mod chrono {
|
||||
use super::*;
|
||||
use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
|
||||
test_type!(chrono_date<NaiveDate>(
|
||||
MySql,
|
||||
test_type!(chrono_date<NaiveDate>(MySql,
|
||||
"DATE '2001-01-05'" == NaiveDate::from_ymd(2001, 1, 5),
|
||||
"DATE '2050-11-23'" == NaiveDate::from_ymd(2050, 11, 23)
|
||||
));
|
||||
|
||||
test_type!(chrono_time_zero<NaiveTime>(
|
||||
MySql,
|
||||
test_type!(chrono_time_zero<NaiveTime>(MySql,
|
||||
"TIME '00:00:00.000000'" == NaiveTime::from_hms_micro(0, 0, 0, 0)
|
||||
));
|
||||
|
||||
test_type!(chrono_time<NaiveTime>(
|
||||
MySql,
|
||||
test_type!(chrono_time<NaiveTime>(MySql,
|
||||
"TIME '05:10:20.115100'" == NaiveTime::from_hms_micro(5, 10, 20, 115100)
|
||||
));
|
||||
|
||||
test_type!(chrono_date_time<NaiveDateTime>(
|
||||
MySql,
|
||||
test_type!(chrono_date_time<NaiveDateTime>(MySql,
|
||||
"TIMESTAMP '2019-01-02 05:10:20'" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
|
||||
));
|
||||
|
||||
test_type!(chrono_timestamp<DateTime::<Utc>>(
|
||||
MySql,
|
||||
test_type!(chrono_timestamp<DateTime::<Utc>>(MySql,
|
||||
"TIMESTAMP '2019-01-02 05:10:20.115100'"
|
||||
== DateTime::<Utc>::from_utc(
|
||||
NaiveDate::from_ymd(2019, 1, 2).and_hms_micro(5, 10, 20, 115100),
|
||||
Utc,
|
||||
)
|
||||
));
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn test_type_chrono_zero_date() -> anyhow::Result<()> {
|
||||
let mut conn = sqlx_test::new::<MySql>().await?;
|
||||
|
||||
// ensure that zero dates are turned on
|
||||
// newer MySQL has these disabled by default
|
||||
|
||||
conn.execute("SET @@sql_mode := REPLACE(@@sql_mode, 'NO_ZERO_IN_DATE', '');")
|
||||
.await?;
|
||||
|
||||
conn.execute("SET @@sql_mode := REPLACE(@@sql_mode, 'NO_ZERO_DATE', '');")
|
||||
.await?;
|
||||
|
||||
// date
|
||||
|
||||
let row = sqlx::query("SELECT DATE '0000-00-00'")
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
let val: Option<NaiveDate> = row.get(0);
|
||||
|
||||
assert_eq!(val, None);
|
||||
assert!(row.try_get::<NaiveDate, _>(0).is_err());
|
||||
|
||||
// datetime
|
||||
|
||||
let row = sqlx::query("SELECT TIMESTAMP '0000-00-00 00:00:00'")
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
let val: Option<NaiveDateTime> = row.get(0);
|
||||
|
||||
assert_eq!(val, None);
|
||||
assert!(row.try_get::<NaiveDateTime, _>(0).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
@ -110,6 +144,44 @@ mod time_tests {
|
||||
.with_time(time!(5:10:20.115100))
|
||||
.assume_utc()
|
||||
));
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn test_type_time_zero_date() -> anyhow::Result<()> {
|
||||
let mut conn = sqlx_test::new::<MySql>().await?;
|
||||
|
||||
// ensure that zero dates are turned on
|
||||
// newer MySQL has these disabled by default
|
||||
|
||||
conn.execute("SET @@sql_mode := REPLACE(@@sql_mode, 'NO_ZERO_IN_DATE', '');")
|
||||
.await?;
|
||||
|
||||
conn.execute("SET @@sql_mode := REPLACE(@@sql_mode, 'NO_ZERO_DATE', '');")
|
||||
.await?;
|
||||
|
||||
// date
|
||||
|
||||
let row = sqlx::query("SELECT DATE '0000-00-00'")
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
let val: Option<Date> = row.get(0);
|
||||
|
||||
assert_eq!(val, None);
|
||||
assert!(row.try_get::<Date, _>(0).is_err());
|
||||
|
||||
// datetime
|
||||
|
||||
let row = sqlx::query("SELECT TIMESTAMP '0000-00-00 00:00:00'")
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
let val: Option<PrimitiveDateTime> = row.get(0);
|
||||
|
||||
assert_eq!(val, None);
|
||||
assert!(row.try_get::<PrimitiveDateTime, _>(0).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bigdecimal")]
|
||||
|
Loading…
x
Reference in New Issue
Block a user