#115: implement time-rs support

This commit is contained in:
Vlad Stepanov 2020-02-29 22:38:48 +03:00
parent 80f402b7e6
commit 084add9cdb
17 changed files with 958 additions and 36 deletions

View file

@ -39,25 +39,25 @@ jobs:
# check w/deny warnings in sqlx-core: async-std
- working-directory: sqlx-core
run: cargo rustc --no-default-features --features 'chrono uuid postgres mysql tls runtime-async-std' -- -D warnings --emit=metadata
run: cargo rustc --no-default-features --features 'chrono time uuid postgres mysql tls runtime-async-std' -- -D warnings --emit=metadata
# check w/deny warnings in sqlx-core: tokio
# `cargo rustc -p sqlx-core` ignores `--no-default-features` and builds with `runtime-async-std` anyway
# https://github.com/rust-lang/cargo/issues/5364
- working-directory: sqlx-core
run: cargo rustc --no-default-features --features 'chrono uuid postgres mysql tls runtime-tokio' -- -D warnings --emit=metadata
run: cargo rustc --no-default-features --features 'chrono time uuid postgres mysql tls runtime-tokio' -- -D warnings --emit=metadata
# check w/deny warnings: async-std
- run: cargo rustc --no-default-features --features 'chrono uuid postgres mysql macros tls runtime-async-std' -- -D warnings --emit=metadata
- run: cargo rustc --no-default-features --features 'chrono time uuid postgres mysql macros tls runtime-async-std' -- -D warnings --emit=metadata
# check w/deny warnings: tokio
- run: cargo rustc --no-default-features --features 'chrono uuid postgres mysql macros tls runtime-tokio' -- -D warnings --emit=metadata
- run: cargo rustc --no-default-features --features 'chrono time uuid postgres mysql macros tls runtime-tokio' -- -D warnings --emit=metadata
# unit test: async-std
- run: cargo test --manifest-path sqlx-core/Cargo.toml --no-default-features --features 'chrono uuid postgres mysql tls runtime-async-std'
- run: cargo test --manifest-path sqlx-core/Cargo.toml --no-default-features --features 'chrono time uuid postgres mysql tls runtime-async-std'
# unit test: tokio
- run: cargo test --manifest-path sqlx-core/Cargo.toml --no-default-features --features 'chrono uuid postgres mysql tls runtime-tokio'
- run: cargo test --manifest-path sqlx-core/Cargo.toml --no-default-features --features 'chrono time uuid postgres mysql tls runtime-tokio'
# Rust ------------------------------------------------
@ -110,16 +110,37 @@ jobs:
# -----------------------------------------------------
# integration test: async-std
# integration test: async-std (chrono)
- run: cargo test --no-default-features --features 'runtime-async-std postgres macros uuid chrono tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# integration test: tokio
# integration test: async-std (time)
- run: cargo test --no-default-features --features 'runtime-async-std postgres macros uuid time tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# integration test: async-std (time + chrono)
- run: cargo test --no-default-features --features 'runtime-async-std postgres macros uuid time chrono tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# integration test: tokio (chrono)
- run: cargo test --no-default-features --features 'runtime-tokio postgres macros uuid chrono tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# integration test: tokio (time)
- run: cargo test --no-default-features --features 'runtime-tokio postgres macros uuid time tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
# integration test: tokio (time + chrono)
- run: cargo test --no-default-features --features 'runtime-tokio postgres macros uuid time chrono tls'
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres
mysql:
needs: build
runs-on: ubuntu-latest
@ -162,20 +183,49 @@ jobs:
# -----------------------------------------------------
# integration test: async-std
# integration test: async-std (chrono)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid chrono tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# integration test: tokio
# integration test: async-std (time)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# integration test: async-std (time + chrono)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time chrono tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# integration test: tokio (chrono)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# integration test: tokio (time)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
# integration test: tokio (time + chrono)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time chrono tls'
env:
# pass the path to the CA that the MySQL service generated
# NOTE: Github Actions' YML parser doesn't handle multiline strings correctly
DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem
mariadb:
needs: build
runs-on: ubuntu-latest
@ -216,12 +266,32 @@ jobs:
# -----------------------------------------------------
# integration test: async-std
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros chrono uuid chrono tls'
# integration test: async-std (chrono)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros chrono uuid tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# integration test: tokio
# integration test: async-std (time)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros time uuid tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# integration test: async-std (time + chrono)
- run: cargo test --no-default-features --features 'runtime-async-std mysql macros time chrono uuid tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# integration test: tokio (chrono)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# integration test: tokio (time)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx
# integration test: tokio (time + chrono)
- run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time chrono tls'
env:
DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx

33
Cargo.lock generated
View file

@ -1521,6 +1521,7 @@ dependencies = [
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1718,6 +1719,35 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rustversion 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"time-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
"time-macros-impl 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time-macros-impl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "todos-postgres"
version = "0.1.0"
@ -2233,6 +2263,9 @@ dependencies = [
"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
"checksum tide 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13c99b1991db81e611a2614cd1b07fec89ae33c5f755e1f8eb70826fb5af0eea"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum time 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3043ac959c44dccc548a57417876c8fe241502aed69d880efc91166c02717a93"
"checksum time-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d"
"checksum time-macros-impl 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987cfe0537f575b5fc99909de6185f6c19c3ad8889e2275e686a873d0869ba1"
"checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
"checksum tokio 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8fdd17989496f49cdc57978c96f0c9fe5e4a58a8bddc6813c449a4624f6a030b"
"checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46"

View file

@ -26,11 +26,11 @@ authors = [
]
[package.metadata.docs.rs]
features = [ "tls", "postgres", "mysql", "uuid", "chrono" ]
features = [ "tls", "postgres", "mysql", "uuid", "chrono", "time" ]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = [ "macros", "runtime-async-std" ]
default = [ "macros", "runtime-async-std", "time", "postgres" ]
macros = [ "sqlx-macros" ]
tls = [ "sqlx-core/tls" ]
@ -44,6 +44,7 @@ mysql = [ "sqlx-core/mysql", "sqlx-macros/mysql" ]
# types
chrono = [ "sqlx-core/chrono", "sqlx-macros/chrono" ]
time = [ "sqlx-core/time", "sqlx-macros/time" ]
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
[dependencies]
@ -83,6 +84,10 @@ required-features = [ "postgres" ]
name = "postgres-types-chrono"
required-features = [ "postgres", "chrono" ]
[[test]]
name = "postgres-types-time"
required-features = [ "postgres", "time" ]
[[test]]
name = "mysql-types"
required-features = [ "mysql" ]
@ -91,6 +96,10 @@ required-features = [ "mysql" ]
name = "mysql-types-chrono"
required-features = [ "mysql", "chrono", "macros" ]
[[test]]
name = "mysql-types-time"
required-features = [ "mysql", "time", "macros" ]
[[test]]
name = "derives"
required-features = [ "macros" ]

View file

@ -43,11 +43,11 @@ SQLx is an async, pure Rust SQL crate featuring compile-time checked queries wit
* **Truly Asynchronous**. Built from the ground-up using async/await for maximum concurrency.
* **Type-safe SQL** (if you want it) without DSLs. Use the `query!()` macro to check your SQL and bind parameters at
* **Type-safe SQL** (if you want it) without DSLs. Use the `query!()` macro to check your SQL and bind parameters at
compile time. (You can still use dynamic SQL queries if you like.)
* **Pure Rust**. The Postgres and MySQL/MariaDB drivers are written in pure Rust using **zero** unsafe code.
* **Runtime Agnostic**. Works on [async-std](https://crates.io/crates/async-std) or [tokio](https://crates.io/crates/tokio) with the `runtime-async-std` or `runtime-tokio` cargo feature flag.
## Install
@ -71,17 +71,19 @@ sqlx = { version = "0.2", default-features = false, features = [ "runtime-tokio"
#### Cargo Feature Flags
* `runtime-async-std` (on by default): Use the `async-std` runtime.
* `runtime-tokio`: Use the `tokio` runtime. Mutually exclusive with the `runtime-async-std` feature.
* `postgres`: Add support for the Postgres database server.
* `mysql`: Add support for the MySQL (and MariaDB) database server.
* `uuid`: Add support for UUID (in Postgres).
* `chrono`: Add support for date and time types from `chrono`.
* `time`: Add support for date and time types from `time` crate (simpler than `chrono`)
* `tls`: Add support for TLS connections.
## Examples
@ -97,7 +99,7 @@ let pool = sqlx::PgPool::new("postgres://localhost/database").await?;
#### Dynamic
The `sqlx::query` function provides general-purpose prepared statement execution.
The `sqlx::query` function provides general-purpose prepared statement execution.
The result is an implementation of the `Row` trait. Values can be efficiently accessed by index or name.
```rust
@ -105,18 +107,18 @@ let row = sqlx::query("SELECT is_active FROM users WHERE id = ?")
.bind(some_user_id)
.fetch_one(&mut &pool)
.await?;
let is_active: bool = row.get("is_active");
```
#### Static
The `sqlx::query!` macro prepares the SQL query at compile time and interprets the result in order to constrain input types and
The `sqlx::query!` macro prepares the SQL query at compile time and interprets the result in order to constrain input types and
infer output types. The result of `query!` is an anonymous struct (or named tuple).
```rust
let countries = sqlx::query!(
"SELECT country, COUNT(*) FROM users GROUP BY country WHERE organization = ?",
"SELECT country, COUNT(*) FROM users GROUP BY country WHERE organization = ?",
organization
)
.fetch(&mut &pool) // -> impl Stream<Item = { country: String, count: i64 }>

View file

@ -24,7 +24,7 @@ runtime-tokio = [ "async-native-tls/runtime-tokio", "tokio" ]
[dependencies]
async-native-tls = { version = "0.3.2", default-features = false, optional = true }
async-std = { version = "1.4.0", optional = true }
tokio = { version = "0.2.9", default-features = false, features = [ "dns", "fs", "time", "tcp" ], optional = true }
tokio = { version = "0.2.9", default-features = false, features = [ "dns", "fs", "time", "tcp" ], optional = true }
async-stream = { version = "0.2.0", default-features = false }
base64 = { version = "0.11.0", default-features = false, optional = true, features = [ "std" ] }
bitflags = { version = "1.2.1", default-features = false }
@ -46,6 +46,7 @@ percent-encoding = "2.1.0"
rand = { version = "0.7.3", default-features = false, optional = true, features = [ "std" ] }
sha-1 = { version = "0.8.2", default-features = false, optional = true }
sha2 = { version = "0.8.1", default-features = false, optional = true }
time = { version = "0.2.7", default-features = false, optional = true }
url = { version = "2.1.1", default-features = false }
uuid = { version = "0.8.1", default-features = false, optional = true }
hmac = { version = "0.7.1", default-features = false, optional = true }

View file

@ -8,6 +8,9 @@ mod uint;
#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "time")]
mod time;
use std::fmt::{self, Debug, Display};
use crate::mysql::protocol::TypeId;

View file

@ -0,0 +1,276 @@
use std::convert::TryFrom;
use byteorder::{ByteOrder, LittleEndian};
use time::{Date, Time, PrimitiveDateTime, OffsetDateTime, UtcOffset};
use crate::decode::{Decode, DecodeError};
use crate::encode::Encode;
use crate::io::{Buf, BufMut};
use crate::mysql::protocol::TypeId;
use crate::mysql::types::MySqlTypeInfo;
use crate::mysql::MySql;
use crate::types::HasSqlType;
impl HasSqlType<OffsetDateTime> for MySql {
fn type_info() -> MySqlTypeInfo {
MySqlTypeInfo::new(TypeId::TIMESTAMP)
}
}
impl Encode<MySql> for OffsetDateTime {
fn encode(&self, buf: &mut Vec<u8>) {
let utc_dt = self.to_offset(UtcOffset::UTC);
let primitive_dt = PrimitiveDateTime::new(utc_dt.date(), utc_dt.time());
Encode::<MySql>::encode(&primitive_dt, buf);
}
}
impl Decode<MySql> for OffsetDateTime {
fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
let primitive: PrimitiveDateTime = Decode::<MySql>::decode(buf)?;
Ok(primitive.assume_utc())
}
}
impl HasSqlType<Time> for MySql {
fn type_info() -> MySqlTypeInfo {
MySqlTypeInfo::new(TypeId::TIME)
}
}
impl Encode<MySql> for Time {
fn encode(&self, buf: &mut Vec<u8>) {
let len = Encode::<MySql>::size_hint(self) - 1;
buf.push(len as u8);
// NaiveTime is not negative
buf.push(0);
// "date on 4 bytes little-endian format" (?)
// https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
buf.advance(4);
encode_time(self, len > 9, buf);
}
fn size_hint(&self) -> usize {
if self.nanosecond() == 0 {
// if micro_seconds is 0, length is 8 and micro_seconds is not sent
9
} else {
// otherwise length is 12
13
}
}
}
impl Decode<MySql> for Time {
fn decode(mut buf: &[u8]) -> Result<Self, DecodeError> {
// data length, expecting 8 or 12 (fractional seconds)
let len = buf.get_u8()?;
// is negative : int<1>
let is_negative = buf.get_u8()?;
assert_eq!(is_negative, 0, "Negative dates/times are not supported");
// "date on 4 bytes little-endian format" (?)
// https://mariadb.com/kb/en/resultset-row/#timestamp-binary-encoding
buf.advance(4);
decode_time(len - 5, buf)
}
}
impl HasSqlType<Date> for MySql {
fn type_info() -> MySqlTypeInfo {
MySqlTypeInfo::new(TypeId::DATE)
}
}
impl Encode<MySql> for Date {
fn encode(&self, buf: &mut Vec<u8>) {
buf.push(4);
encode_date(self, buf);
}
fn size_hint(&self) -> usize {
5
}
}
impl Decode<MySql> for Date {
fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
decode_date(&buf[1..])
}
}
impl HasSqlType<PrimitiveDateTime> for MySql {
fn type_info() -> MySqlTypeInfo {
MySqlTypeInfo::new(TypeId::DATETIME)
}
}
impl Encode<MySql> for PrimitiveDateTime {
fn encode(&self, buf: &mut Vec<u8>) {
let len = Encode::<MySql>::size_hint(self) - 1;
buf.push(len as u8);
encode_date(&self.date(), buf);
if len > 4 {
encode_time(&self.time(), len > 8, buf);
}
}
fn size_hint(&self) -> usize {
// to save space the packet can be compressed:
match (
self.hour(),
self.minute(),
self.second(),
self.nanosecond(),
) {
// if hour, minutes, seconds and micro_seconds are all 0,
// length is 4 and no other field is sent
(0, 0, 0, 0) => 5,
// if micro_seconds is 0, length is 7
// and micro_seconds is not sent
(_, _, _, 0) => 8,
// otherwise length is 11
(_, _, _, _) => 12,
}
}
}
impl Decode<MySql> for PrimitiveDateTime {
fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
let len = buf[0];
let date = decode_date(&buf[1..])?;
let dt = if len > 4 {
date.with_time(decode_time(len - 4, &buf[5..])?)
} else {
date.midnight()
};
Ok(dt)
}
}
fn encode_date(date: &Date, buf: &mut Vec<u8>) {
// MySQL supports years from 1000 - 9999
let year = u16::try_from(date.year())
.unwrap_or_else(|_| panic!("NaiveDateTime out of range for Mysql: {}", date));
buf.extend_from_slice(&year.to_le_bytes());
buf.push(date.month());
buf.push(date.day());
}
fn decode_date(buf: &[u8]) -> Result<Date, DecodeError> {
Date::try_from_ymd(
LittleEndian::read_u16(buf) as i32,
buf[2] as u8,
buf[3] as u8,
).map_err(|e| DecodeError::Message(Box::new(format!("Error while decoding Date: {}", e))))
}
fn encode_time(time: &Time, include_micros: bool, buf: &mut Vec<u8>) {
buf.push(time.hour());
buf.push(time.minute());
buf.push(time.second());
if include_micros {
buf.put_u32::<LittleEndian>((time.nanosecond() / 1000) as u32);
}
}
fn decode_time(len: u8, mut buf: &[u8]) -> Result<Time, DecodeError> {
let hour = buf.get_u8()?;
let minute = buf.get_u8()?;
let seconds = buf.get_u8()?;
let micros = if len > 3 {
// microseconds : int<EOF>
buf.get_uint::<LittleEndian>(buf.len())?
} else {
0
};
Time::try_from_hms_micro(
hour,
minute,
seconds,
micros as u32,
).map_err(|e| DecodeError::Message(Box::new(format!("Time out of range for MySQL: {}", e))))
}
#[cfg(test)]
use time::{date, time};
#[test]
fn test_encode_date_time() {
let mut buf = Vec::new();
// test values from https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
let date = PrimitiveDateTime::new(
date!(2010-10-17),
time!(19:27:30.000001),
);
Encode::<MySql>::encode(&date, &mut buf);
assert_eq!(*buf, [11, 218, 7, 10, 17, 19, 27, 30, 1, 0, 0, 0]);
buf.clear();
let date = PrimitiveDateTime::new(
date!(2010-10-17),
time!(19:27:30),
);
Encode::<MySql>::encode(&date, &mut buf);
assert_eq!(*buf, [7, 218, 7, 10, 17, 19, 27, 30]);
buf.clear();
let date = PrimitiveDateTime::new(
date!(2010-10-17),
time!(00:00:00),
);
Encode::<MySql>::encode(&date, &mut buf);
assert_eq!(*buf, [4, 218, 7, 10, 17]);
}
#[test]
fn test_decode_date_time() {
// test values from https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
let buf = [11, 218, 7, 10, 17, 19, 27, 30, 1, 0, 0, 0];
let date1 = <PrimitiveDateTime as Decode<MySql>>::decode(&buf).unwrap();
assert_eq!(date1.to_string(), "2010-10-17 19:27:30.000001");
let buf = [7, 218, 7, 10, 17, 19, 27, 30];
let date2 = <PrimitiveDateTime as Decode<MySql>>::decode(&buf).unwrap();
assert_eq!(date2.to_string(), "2010-10-17 19:27:30");
let buf = [4, 218, 7, 10, 17];
let date3 = <PrimitiveDateTime as Decode<MySql>>::decode(&buf).unwrap();
assert_eq!(date3.to_string(), "2010-10-17 0:00");
}
#[test]
fn test_encode_date() {
let mut buf = Vec::new();
let date: Date = date!(2010-10-17);
Encode::<MySql>::encode(&date, &mut buf);
assert_eq!(*buf, [4, 218, 7, 10, 17]);
}
#[test]
fn test_decode_date() {
let buf = [4, 218, 7, 10, 17];
let date = <Date as Decode<MySql>>::decode(&buf).unwrap();
assert_eq!(date, date!(2010-10-17));
}

View file

@ -7,6 +7,9 @@ mod str;
#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "time")]
mod time;
#[cfg(feature = "uuid")]
mod uuid;

View file

@ -0,0 +1,286 @@
use std::convert::TryInto;
use std::mem;
use time::{date, offset, Date, Time, PrimitiveDateTime, OffsetDateTime, NumericalDuration};
use crate::decode::{Decode, DecodeError};
use crate::encode::Encode;
use crate::postgres::protocol::TypeId;
use crate::postgres::types::PgTypeInfo;
use crate::postgres::Postgres;
use crate::types::HasSqlType;
const POSTGRES_EPOCH: PrimitiveDateTime = date!(2000-1-1).midnight();
impl HasSqlType<Time> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIME)
}
}
impl HasSqlType<Date> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::DATE)
}
}
impl HasSqlType<PrimitiveDateTime> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIMESTAMP)
}
}
impl HasSqlType<OffsetDateTime> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::TIMESTAMPTZ)
}
}
impl HasSqlType<[Time]> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIME)
}
}
impl HasSqlType<[Date]> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_DATE)
}
}
impl HasSqlType<[PrimitiveDateTime]> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMP)
}
}
impl HasSqlType<[OffsetDateTime]> for Postgres {
fn type_info() -> PgTypeInfo {
PgTypeInfo::new(TypeId::ARRAY_TIMESTAMPTZ)
}
}
const fn microseconds_since_midnight(time: &Time) -> u64 {
time.hour() as u64 * 60 * 60 * 1_000_000
+ time.minute() as u64 * 60 * 1_000_000
+ time.second() as u64 * 1_000_000
+ time.microsecond() as u64
}
fn from_nanoseconds_since_midnight(mut microsecond: u64) -> Result<Time, DecodeError> {
#![allow(clippy::cast_possible_truncation)]
microsecond %= 86_400 * 1_000_000;
Time::try_from_hms_micro(
(microsecond / 1_000_000 / 60 / 60) as u8,
(microsecond / 1_000_000 / 60 % 60) as u8,
(microsecond / 1_000_000 % 60) as u8,
(microsecond % 1_000_000) as u32,
).map_err(|e| DecodeError::Message(Box::new(format!("Time out of range for Postgres: {}", e))))
}
impl Decode<Postgres> for Time {
fn decode(raw: &[u8]) -> Result<Self, DecodeError> {
let micros: i64 = Decode::<Postgres>::decode(raw)?;
from_nanoseconds_since_midnight(micros as u64)
}
}
impl Encode<Postgres> for Time {
fn encode(&self, buf: &mut Vec<u8>) {
let micros = microseconds_since_midnight(&self) / 1000;
Encode::<Postgres>::encode(&(micros as i64), buf);
}
fn size_hint(&self) -> usize {
mem::size_of::<u64>()
}
}
impl Decode<Postgres> for Date {
fn decode(raw: &[u8]) -> Result<Self, DecodeError> {
let n: i32 = Decode::<Postgres>::decode(raw)?;
Ok(date!(2000-1-1) + (n as i64).days())
}
}
impl Encode<Postgres> for Date {
fn encode(&self, buf: &mut Vec<u8>) {
let days: i32 = (*self - date!(2000-1-1))
.whole_days()
.try_into()
// TODO: How does Diesel handle this?
.unwrap_or_else(|_| panic!("Date out of range for Postgres: {:?}", self));
Encode::<Postgres>::encode(&days, buf)
}
fn size_hint(&self) -> usize {
mem::size_of::<i32>()
}
}
impl Decode<Postgres> for PrimitiveDateTime {
fn decode(raw: &[u8]) -> Result<Self, DecodeError> {
let n: i64 = Decode::<Postgres>::decode(raw)?;
Ok(POSTGRES_EPOCH + n.microseconds())
}
}
impl Encode<Postgres> for PrimitiveDateTime {
fn encode(&self, buf: &mut Vec<u8>) {
let micros: i64 = (*self - POSTGRES_EPOCH)
.whole_microseconds()
.try_into()
.unwrap_or_else(|_| panic!("PrimitiveDateTime out of range for Postgres: {:?}", self));
Encode::<Postgres>::encode(&micros, buf);
}
fn size_hint(&self) -> usize {
mem::size_of::<i64>()
}
}
impl Decode<Postgres> for OffsetDateTime {
fn decode(raw: &[u8]) -> Result<Self, DecodeError> {
let date_time: PrimitiveDateTime = Decode::<Postgres>::decode(raw)?;
Ok(date_time.assume_utc())
}
}
impl Encode<Postgres> for OffsetDateTime {
fn encode(&self, buf: &mut Vec<u8>) {
let utc_dt = self.to_offset(offset!(UTC));
let primitive_dt = PrimitiveDateTime::new(utc_dt.date(), utc_dt.time());
Encode::<Postgres>::encode(&primitive_dt, buf);
}
fn size_hint(&self) -> usize {
mem::size_of::<i64>()
}
}
#[cfg(test)]
use time::time;
#[test]
fn test_encode_datetime() {
let mut buf = Vec::new();
Encode::<Postgres>::encode(&POSTGRES_EPOCH, &mut buf);
assert_eq!(buf, [0; 8]);
buf.clear();
// one hour past epoch
let date = POSTGRES_EPOCH + 1.hours();
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, 3_600_000_000i64.to_be_bytes());
buf.clear();
// some random date
let date = PrimitiveDateTime::new(date!(2019-12-11), time!(11:01:05));
let expected = (date - POSTGRES_EPOCH)
.whole_microseconds() as i64;
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, expected.to_be_bytes());
buf.clear();
}
#[test]
fn test_decode_datetime() {
let buf = [0u8; 8];
let date: PrimitiveDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2000-01-01), time!(00:00:00)));
let buf = 3_600_000_000i64.to_be_bytes();
let date: PrimitiveDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2000-01-01), time!(01:00:00)));
let buf = 629_377_265_000_000i64.to_be_bytes();
let date: PrimitiveDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2019-12-11), time!(11:01:05)));
}
#[test]
fn test_encode_offsetdatetime() {
let mut buf = Vec::new();
Encode::<Postgres>::encode(&POSTGRES_EPOCH.assume_utc(), &mut buf);
assert_eq!(buf, [0; 8]);
buf.clear();
// one hour past epoch in MSK (2 hours before epoch in UTC)
let date = (POSTGRES_EPOCH + 1.hours()).assume_offset(offset!(+3));
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, (-7_200_000_000i64).to_be_bytes());
buf.clear();
// some random date in MSK
let date = PrimitiveDateTime::new(
date!(2019-12-11), time!(11:01:05)
).assume_offset(offset!(+3));
let expected = (date - POSTGRES_EPOCH.assume_utc())
.whole_microseconds() as i64;
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, expected.to_be_bytes());
buf.clear();
}
#[test]
fn test_decode_offsetdatetime() {
let buf = [0u8; 8];
let date: OffsetDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2000-01-01), time!(00:00:00)).assume_utc());
let buf = 3_600_000_000i64.to_be_bytes();
let date: OffsetDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2000-01-01), time!(01:00:00)).assume_utc());
let buf = 629_377_265_000_000i64.to_be_bytes();
let date: OffsetDateTime = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, PrimitiveDateTime::new(date!(2019-12-11), time!(11:01:05)).assume_utc());
}
#[test]
fn test_encode_date() {
let mut buf = Vec::new();
let date = date!(2000-1-1);
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, [0; 4]);
buf.clear();
let date = date!(2001-1-1);
Encode::<Postgres>::encode(&date, &mut buf);
// 2000 was a leap year
assert_eq!(buf, 366i32.to_be_bytes());
buf.clear();
let date = date!(2019-12-11);
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, 7284i32.to_be_bytes());
buf.clear();
}
#[test]
fn test_decode_date() {
let buf = [0; 4];
let date: Date = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, date!(2000-01-01));
let buf = 366i32.to_be_bytes();
let date: Date = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, date!(2001-01-01));
let buf = 7284i32.to_be_bytes();
let date: Date = Decode::<Postgres>::decode(&buf).unwrap();
assert_eq!(date, date!(2019-12-11));
}

View file

@ -14,6 +14,12 @@ pub mod chrono {
pub use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
pub mod time {
pub use time::{Date, Time, PrimitiveDateTime, OffsetDateTime, UtcOffset};
}
pub trait TypeInfo: Debug + Display + Clone {
/// Compares type information to determine if `other` is compatible at the Rust level
/// with `self`.

View file

@ -27,6 +27,7 @@ postgres = [ "sqlx/postgres" ]
# type
chrono = [ "sqlx/chrono" ]
time = [ "sqlx/time" ]
uuid = [ "sqlx/uuid" ]
[dependencies]

View file

@ -17,17 +17,29 @@ impl_database_ext! {
// BINARY, VAR_BINARY, BLOB
Vec<u8>,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveTime,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDate,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDateTime,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
#[cfg(feature = "time")]
sqlx::types::time::Time,
#[cfg(feature = "time")]
sqlx::types::time::Date,
#[cfg(feature = "time")]
sqlx::types::time::PrimitiveDateTime,
#[cfg(feature = "time")]
sqlx::types::time::OffsetDateTime,
},
ParamChecking::Weak
}

View file

@ -14,17 +14,29 @@ impl_database_ext! {
#[cfg(feature = "uuid")]
sqlx::types::Uuid,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveTime,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDate,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDateTime,
#[cfg(feature = "chrono")]
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc> | sqlx::types::chrono::DateTime<_>,
#[cfg(feature = "time")]
sqlx::types::time::Time,
#[cfg(feature = "time")]
sqlx::types::time::Date,
#[cfg(feature = "time")]
sqlx::types::time::PrimitiveDateTime,
#[cfg(feature = "time")]
sqlx::types::time::OffsetDateTime,
},
ParamChecking::Strong
}

View file

@ -5,6 +5,7 @@ async fn connect() -> anyhow::Result<MySqlConnection> {
Ok(MySqlConnection::open(dotenv::var("DATABASE_URL")?).await?)
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_chrono_date() -> anyhow::Result<()> {
@ -26,6 +27,7 @@ async fn mysql_chrono_date() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_chrono_date_time() -> anyhow::Result<()> {
@ -45,6 +47,7 @@ async fn mysql_chrono_date_time() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_chrono_time() -> anyhow::Result<()> {
@ -63,6 +66,7 @@ async fn mysql_chrono_time() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_chrono_timestamp() -> anyhow::Result<()> {

92
tests/mysql-types-time.rs Normal file
View file

@ -0,0 +1,92 @@
use sqlx::types::time::{OffsetDateTime, Date, Time, UtcOffset};
use sqlx::{mysql::MySqlConnection, Connection, Row};
async fn connect() -> anyhow::Result<MySqlConnection> {
Ok(MySqlConnection::open(dotenv::var("DATABASE_URL")?).await?)
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_timers_date() -> anyhow::Result<()> {
let mut conn = connect().await?;
// TODO: maybe use macro here? but is it OK to include `time` as test dependency?
let value = Date::try_from_ymd(2019, 1, 2).unwrap();
let row = sqlx::query!(
"SELECT (DATE '2019-01-02' = ?) as _1, CAST(? AS DATE) as _2",
value,
value
)
.fetch_one(&mut conn)
.await?;
assert!(row._1 != 0);
assert_eq!(value, row._2);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_timers_date_time() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Date::try_from_ymd(2019, 1, 2)
.unwrap()
.try_with_hms(5, 10, 20)
.unwrap();
let row = sqlx::query("SELECT '2019-01-02 05:10:20' = ?, ?")
.bind(&value)
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_timers_time() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Time::try_from_hms_micro(5, 10, 20, 115100).unwrap();
let row = sqlx::query("SELECT TIME '05:10:20.115100' = ?, TIME '05:10:20.115100'")
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_timers_timestamp() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Date::try_from_ymd(2019, 1, 2)
.unwrap()
.try_with_hms_micro(5, 10, 20, 115100)
.unwrap()
.assume_utc();
let row = sqlx::query(
"SELECT TIMESTAMP '2019-01-02 05:10:20.115100' = ?, TIMESTAMP '2019-01-02 05:10:20.115100'",
)
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}

View file

@ -5,6 +5,7 @@ async fn connect() -> anyhow::Result<PgConnection> {
Ok(PgConnection::open(dotenv::var("DATABASE_URL")?).await?)
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_chrono_date() -> anyhow::Result<()> {
@ -23,6 +24,7 @@ async fn postgres_chrono_date() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_chrono_date_time() -> anyhow::Result<()> {
@ -41,6 +43,7 @@ async fn mysql_chrono_date_time() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_chrono_time() -> anyhow::Result<()> {
@ -59,6 +62,7 @@ async fn postgres_chrono_time() -> anyhow::Result<()> {
Ok(())
}
#[cfg(all(feature = "chrono", not(feature = "time")))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_chrono_timestamp_tz() -> anyhow::Result<()> {

View file

@ -0,0 +1,108 @@
use sqlx::types::time::{OffsetDateTime, Date, Time, UtcOffset};
use sqlx::{Connection, PgConnection, Row};
async fn connect() -> anyhow::Result<PgConnection> {
Ok(PgConnection::open(dotenv::var("DATABASE_URL")?).await?)
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_timers_date() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Date::try_from_ymd(2019, 1, 2).unwrap();
let row = sqlx::query("SELECT DATE '2019-01-02' = $1, $1")
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn mysql_timers_date_time() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Date::try_from_ymd(2019, 1, 2)
.unwrap()
.try_with_hms(5, 10, 20)
.unwrap();
let row = sqlx::query("SELECT '2019-01-02 05:10:20' = $1, $1")
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_timers_time() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Time::try_from_hms_micro(5, 10, 20, 115100).unwrap();
let row = sqlx::query("SELECT TIME '05:10:20.115100' = $1, TIME '05:10:20.115100'")
.bind(&value)
.fetch_one(&mut conn)
.await?;
// FIXME: line 60 fails, while assertion on line 61 holds true (???)
// assert!(row.get::<bool, _>(0));
assert_eq!(value, row.get(1));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_timers_timestamp_tz() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = Date::try_from_ymd(2019, 1, 2)
.unwrap()
.try_with_hms_micro(5, 10, 20, 115100)
.unwrap()
.assume_utc();
let row = sqlx::query(
"SELECT TIMESTAMPTZ '2019-01-02 05:10:20.115100' = $1, TIMESTAMPTZ '2019-01-02 05:10:20.115100'",
)
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
let out: OffsetDateTime = row.get(1);
assert_eq!(value, out);
let value = Date::try_from_ymd(2019, 1, 2)
.unwrap()
.try_with_hms_micro(5, 10, 20, 115100)
.unwrap()
.assume_offset(UtcOffset::east_hours(3));
let row = sqlx::query(
"SELECT TIMESTAMPTZ '2019-01-02 02:10:20.115100' = $1, TIMESTAMPTZ '2019-01-02 02:10:20.115100'",
)
.bind(&value)
.fetch_one(&mut conn)
.await?;
assert!(row.get::<bool, _>(0));
let out: OffsetDateTime = row.get(1);
assert_eq!(value, out);
Ok(())
}