feat: introduce docker-compose based testing for running locally against many database combinations

This commit is contained in:
Ryan Leckey 2020-05-30 14:51:57 -07:00
parent afd831b0d3
commit e5b6047009
No known key found for this signature in database
GPG Key ID: BBDFC5595030E7D3
56 changed files with 2192 additions and 2326 deletions

4
.gitignore vendored
View File

@ -9,6 +9,4 @@ target/
# Environment
.env
tests/fixtures/*.sqlite-shm
tests/fixtures/*.sqlite-wal
!tests/.env

1
Cargo.lock generated
View File

@ -2042,6 +2042,7 @@ dependencies = [
"serde_json",
"sqlx-core",
"sqlx-macros",
"sqlx-rt",
"sqlx-test",
"time 0.2.16",
"tokio 0.2.21",

View File

@ -78,67 +78,65 @@ async-std = { version = "1.5.0", features = [ "attributes" ] }
tokio = { version = "0.2.13", features = [ "full" ] }
dotenv = "0.15.0"
trybuild = "1.0.24"
sqlx-rt = { path = "./sqlx-rt" }
sqlx-test = { path = "./sqlx-test" }
paste = "0.1.7"
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0.48"
[[test]]
name = "postgres-macros"
required-features = [ "postgres", "macros" ]
[[test]]
name = "mysql-macros"
required-features = [ "mysql", "macros" ]
#
# SQLite
#
[[test]]
name = "sqlite"
path = "tests/sqlite/sqlite.rs"
required-features = [ "sqlite" ]
[[test]]
name = "sqlite-macros"
required-features = [ "sqlite", "macros" ]
[[test]]
name = "sqlite-raw"
required-features = [ "sqlite" ]
[[test]]
name = "sqlite-derives"
required-features = [ "sqlite", "macros" ]
[[test]]
name = "sqlite-types"
path = "tests/sqlite/types.rs"
required-features = [ "sqlite" ]
[[test]]
name = "sqlite-describe"
path = "tests/sqlite/describe.rs"
required-features = [ "sqlite" ]
#
# MySQL
#
[[test]]
name = "mysql"
path = "tests/mysql/mysql.rs"
required-features = [ "mysql" ]
[[test]]
name = "mysql-raw"
name = "mysql-types"
path = "tests/mysql/types.rs"
required-features = [ "mysql" ]
[[test]]
name = "mysql-derives"
required-features = [ "mysql", "macros" ]
name = "mysql-describe"
path = "tests/mysql/describe.rs"
required-features = [ "mysql" ]
#
# PostgreSQL
#
[[test]]
name = "postgres"
required-features = [ "postgres" ]
[[test]]
name = "postgres-raw"
path = "tests/postgres/postgres.rs"
required-features = [ "postgres" ]
[[test]]
name = "postgres-types"
path = "tests/postgres/types.rs"
required-features = [ "postgres" ]
[[test]]
name = "postgres-derives"
required-features = [ "postgres", "macros" ]
[[test]]
name = "mysql-types"
required-features = [ "mysql" ]
name = "postgres-describe"
path = "tests/postgres/describe.rs"
required-features = [ "postgres" ]

View File

@ -117,14 +117,6 @@ fn test_encodes_string_lenenc() {
assert_eq!(&buf[..], b"\x0Drandom_string");
}
#[test]
fn test_encodes_string_null() {
let mut buf = Vec::with_capacity(1024);
buf.put_str_nul("random_string");
assert_eq!(&buf[..], b"random_string\0");
}
#[test]
fn test_encodes_byte_lenenc() {
let mut buf = Vec::with_capacity(1024);

View File

@ -75,8 +75,9 @@ impl FromStr for MySqlSslMode {
/// # use sqlx_core::connection::Connect;
/// # use sqlx_core::mysql::{MySqlConnectOptions, MySqlConnection, MySqlSslMode};
/// #
/// # #[sqlx_rt::main]
/// # async fn main() -> Result<(), Error> {
/// # fn main() {
/// # #[cfg(feature = "runtime-async-std")]
/// # sqlx_rt::async_std::task::block_on(async move {
/// // URI connection string
/// let conn = MySqlConnection::connect("mysql://root:password@localhost/db").await?;
///
@ -87,7 +88,7 @@ impl FromStr for MySqlSslMode {
/// .password("password")
/// .database("db")
/// ).await?;
/// # Ok(())
/// # }).unwrap();
/// # }
/// ```
#[derive(Debug, Clone)]

View File

@ -160,8 +160,8 @@ fn assert_pool_traits() {
fn assert_send_sync<T: Send + Sync>() {}
fn assert_clone<T: Clone>() {}
fn assert_pool<C: Connect>() {
assert_send_sync::<Pool<C>>();
assert_clone::<Pool<C>>();
fn assert_pool<DB: Database>() {
assert_send_sync::<Pool<DB>>();
assert_clone::<Pool<DB>>();
}
}

View File

@ -29,9 +29,9 @@ impl Decode<'_> for Notification {
fn test_decode_notification_response() {
const NOTIFICATION_RESPONSE: &[u8] = b"\x34\x20\x10\x02TEST-CHANNEL\0THIS IS A TEST\0";
let message = Notification::read(NOTIFICATION_RESPONSE).unwrap();
let message = Notification::decode(Bytes::from(NOTIFICATION_RESPONSE)).unwrap();
assert_eq!(message.process_id, 0x34201002);
assert_eq!(&*message.channel, b"TEST-CHANNEL"[..]);
assert_eq!(&*message.payload, b"THIS IS A TEST"[..]);
assert_eq!(&*message.channel, &b"TEST-CHANNEL"[..]);
assert_eq!(&*message.payload, &b"THIS IS A TEST"[..]);
}

View File

@ -88,8 +88,9 @@ impl FromStr for PgSslMode {
/// # use sqlx_core::connection::Connect;
/// # use sqlx_core::postgres::{PgConnectOptions, PgConnection, PgSslMode};
/// #
/// # #[sqlx_rt::main]
/// # async fn main() -> Result<(), Error> {
/// # fn main() {
/// # #[cfg(feature = "runtime-async-std")]
/// # sqlx_rt::async_std::task::block_on(async move {
/// // URI connection string
/// let conn = PgConnection::connect("postgres://localhost/mydb").await?;
///
@ -101,7 +102,7 @@ impl FromStr for PgSslMode {
/// .password("secret-password")
/// .ssl_mode(PgSslMode::Require)
/// ).await?;
/// # Ok(())
/// # }).unwrap();
/// # }
/// ```
#[derive(Debug, Clone)]

View File

@ -308,7 +308,7 @@ where
{
Query {
database: PhantomData,
arguments: Default::default(),
arguments: Some(Default::default()),
query: sql,
}
}

View File

@ -1,22 +1,31 @@
use std::ptr::null_mut;
use std::sync::atomic::{spin_loop_hint, AtomicI32, AtomicPtr, Ordering};
use std::sync::Arc;
use std::thread::{self, park, spawn, JoinHandle};
use either::Either;
use libsqlite3_sys::{sqlite3_step, sqlite3_stmt, SQLITE_DONE, SQLITE_ROW};
use sqlx_rt::yield_now;
use libsqlite3_sys::{sqlite3_step, SQLITE_DONE, SQLITE_ROW};
use crate::error::Error;
use crate::sqlite::statement::StatementHandle;
#[cfg(not(feature = "runtime-tokio"))]
use {
libsqlite3_sys::sqlite3_stmt,
sqlx_rt::yield_now,
std::ptr::null_mut,
std::sync::atomic::{spin_loop_hint, AtomicI32, AtomicPtr, Ordering},
std::sync::Arc,
std::thread::{self, park, spawn, JoinHandle},
};
// For async-std and actix, the worker maintains a dedicated thread for each SQLite connection
// All invocations of [sqlite3_step] are run on this thread
// For tokio, the worker is a thin wrapper around an invocation to [block_in_place]
#[cfg(not(feature = "runtime-tokio"))]
const STATE_CLOSE: i32 = -1;
#[cfg(not(feature = "runtime-tokio"))]
const STATE_READY: i32 = 0;
#[cfg(not(feature = "runtime-tokio"))]
const STATE_INITIAL: i32 = 1;
#[cfg(not(feature = "runtime-tokio"))]
@ -156,7 +165,7 @@ impl StatementWorker {
pub(crate) async fn step(&self, statement: &StatementHandle) -> Result<Either<u64, ()>, Error> {
let statement = *statement;
let status = sqlx_rt::blocking!({ unsafe { sqlite3_step(statement.0.as_ptr()) } });
let status = sqlx_rt::blocking!(unsafe { sqlite3_step(statement.0.as_ptr()) });
match status {
// a row was found

View File

@ -79,3 +79,51 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream {
Err(e) => e.to_compile_error().into(),
}
}
#[doc(hidden)]
#[proc_macro_attribute]
pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::ItemFn);
let ret = &input.sig.output;
let name = &input.sig.ident;
let body = &input.block;
let attrs = &input.attrs;
let result = if cfg!(feature = "runtime-tokio") {
quote! {
#[test]
#(#attrs)*
fn #name() #ret {
sqlx_rt::tokio::runtime::Builder::new()
.threaded_scheduler()
.enable_io()
.enable_time()
.build()
.unwrap()
.block_on(async { #body })
}
}
} else if cfg!(feature = "runtime-async-std") {
quote! {
#[test]
#(#attrs)*
fn #name() #ret {
sqlx_rt::async_std::task::block_on(async { #body })
}
}
} else if cfg!(feature = "runtime-actix") {
quote! {
#[test]
#(#attrs)*
fn #name() #ret {
sqlx_rt::actix_rt::System::new("sqlx-test")
.block_on(async { #body })
}
}
} else {
panic!("one of 'runtime-actix', 'runtime-async-std' or 'runtime-tokio' features must be enabled");
};
result.into()
}

View File

@ -28,7 +28,7 @@ pub use native_tls;
))]
pub use tokio::{
self, fs, io::AsyncRead, io::AsyncReadExt, io::AsyncWrite, io::AsyncWriteExt, net::TcpStream,
task::yield_now, time::delay_for as sleep, time::timeout,
task::spawn, task::yield_now, time::delay_for as sleep, time::timeout,
};
#[cfg(all(

View File

@ -1,4 +1,5 @@
use sqlx::{Connect, Database};
use sqlx::{database::Database, Connect};
use std::env;
fn setup_if_needed() {
let _ = dotenv::dotenv();
@ -13,42 +14,81 @@ where
{
setup_if_needed();
Ok(DB::Connection::connect(dotenv::var("DATABASE_URL")?).await?)
Ok(DB::Connection::connect(&env::var("DATABASE_URL")?).await?)
}
// Test type encoding and decoding
#[macro_export]
macro_rules! test_type {
($name:ident($db:ident, $ty:ty, $sql:literal, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_prepared_type!($name($db, $ty, $sql, $($text == $value),+));
$crate::test_unprepared_type!($name($db, $ty, $($text == $value),+));
($name:ident<$ty:ty>($db:ident, $sql:literal, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::__test_prepared_type!($name<$ty>($db, $sql, $($text == $value),+));
$crate::test_unprepared_type!($name<$ty>($db, $($text == $value),+));
};
($name:ident($db:ident, $ty:ty, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_prepared_type!($name($db, $ty, $($text == $value),+));
$crate::test_unprepared_type!($name($db, $ty, $($text == $value),+));
($name:ident<$ty:ty>($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
paste::item! {
$crate::__test_prepared_type!($name<$ty>($db, $crate::[< $db _query_for_test_prepared_type >]!(), $($text == $value),+));
$crate::test_unprepared_type!($name<$ty>($db, $($text == $value),+));
}
};
($name:ident($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_type!($name<$name>($db, $($text == $value),+));
};
}
// Test type decoding only
#[macro_export]
macro_rules! test_decode_type {
($name:ident<$ty:ty>($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::__test_prepared_decode_type!($name<$ty>($db, $($text == $value),+));
$crate::test_unprepared_type!($name<$ty>($db, $($text == $value),+));
};
($name:ident($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_decode_type!($name<$name>($db, $($text == $value),+));
};
}
// Test type encoding and decoding
#[macro_export]
macro_rules! test_prepared_type {
($name:ident<$ty:ty>($db:ident, $sql:literal, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::__test_prepared_type!($name<$ty>($db, $sql, $($text == $value),+));
};
($name:ident<$ty:ty>($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
paste::item! {
$crate::__test_prepared_type!($name<$ty>($db, $crate::[< $db _query_for_test_prepared_type >]!(), $($text == $value),+));
}
};
($name:ident($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::__test_prepared_type!($name<$name>($db, $($text == $value),+));
};
}
// Test type decoding for the simple (unprepared) query API
#[macro_export]
macro_rules! test_unprepared_type {
($name:ident($db:ident, $ty:ty, $($text:literal == $value:expr),+ $(,)?)) => {
($name:ident<$ty:ty>($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
paste::item! {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn [< test_unprepared_type_ $name >] () -> anyhow::Result<()> {
use sqlx::prelude::*;
use futures::TryStreamExt;
let mut conn = sqlx_test::new::<$db>().await?;
$(
let query = format!("SELECT {} as _1", $text);
let mut cursor = conn.fetch(&*query);
let row = cursor.next().await?.unwrap();
let rec = row.try_get::<$ty, _>("_1")?;
let query = format!("SELECT {}", $text);
let mut s = conn.fetch(&*query);
let row = s.try_next().await?.unwrap();
let rec = row.try_get::<$ty, _>(0)?;
assert!($value == rec);
drop(s);
)+
Ok(())
@ -57,95 +97,76 @@ macro_rules! test_unprepared_type {
}
}
// TODO: This macro is cursed. Needs a good re-factor.
// Test type encoding and decoding for the prepared query API
// Test type decoding only for the prepared query API
#[macro_export]
macro_rules! test_prepared_type {
($name:ident($db:ident, $ty:ty, $sql:literal, $($text:literal == $value:expr),+ $(,)?)) => {
macro_rules! __test_prepared_decode_type {
($name:ident<$ty:ty>($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
paste::item! {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn [< test_prepared_type_ $name >] () -> anyhow::Result<()> {
use sqlx::prelude::*;
#[sqlx_macros::test]
async fn [< test_prepared_decode_type_ $name >] () -> anyhow::Result<()> {
use sqlx::Row;
let mut conn = sqlx_test::new::<$db>().await?;
$(
let query = format!($sql, $text);
let query = format!("SELECT {}", $text);
let rec: (bool, Option<String>, $ty, $ty) = sqlx::query_as(&query)
.bind($value)
.bind($value)
.bind($value)
let row = sqlx::query(&query)
.fetch_one(&mut conn)
.await?;
assert!(rec.0,
"[1] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
let rec: $ty = row.try_get(0)?;
assert_eq!($value, rec.2,
"[2] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
assert_eq!($value, rec.3,
"[3] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
assert!($value == rec);
)+
Ok(())
}
}
};
}
($name:ident($db:ident, $ty:ty, $($text:literal == $value:expr),+ $(,)?)) => {
// Test type encoding and decoding for the prepared query API
#[macro_export]
macro_rules! __test_prepared_type {
($name:ident<$ty:ty>($db:ident, $sql:expr, $($text:literal == $value:expr),+ $(,)?)) => {
paste::item! {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn [< test_prepared_type_ $name >] () -> anyhow::Result<()> {
use sqlx::prelude::*;
use sqlx::Row;
let mut conn = sqlx_test::new::<$db>().await?;
$(
let query = format!($crate::[< $db _query_for_test_prepared_type >]!(), $text);
let query = format!($sql, $text);
let rec: (bool, Option<String>, $ty, $ty) = sqlx::query_as(&query)
.bind($value)
let row = sqlx::query(&query)
.bind($value)
.bind($value)
.fetch_one(&mut conn)
.await?;
assert!(rec.0,
let matches: bool = row.try_get(0)?;
let returned: $ty = row.try_get(1)?;
let round_trip: $ty = row.try_get(2)?;
assert!(matches,
"[1] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
$value, returned, round_trip);
assert_eq!($value, rec.2,
assert_eq!($value, returned,
"[2] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
$value, returned, round_trip);
assert_eq!($value, rec.3,
assert_eq!($value, round_trip,
"[3] DB value mismatch; given value: {:?}\n\
as received: {:?}\n\
as returned: {:?}\n\
round-trip: {:?}",
$value, rec.1, rec.2, rec.3);
$value, returned, round_trip);
)+
Ok(())
@ -157,20 +178,20 @@ macro_rules! test_prepared_type {
#[macro_export]
macro_rules! MySql_query_for_test_prepared_type {
() => {
"SELECT {0} <=> ?, '<UNKNOWN>' as _1, ? as _2, ? as _3"
"SELECT {0} <=> ?, {0}, ?"
};
}
#[macro_export]
macro_rules! Sqlite_query_for_test_prepared_type {
() => {
"SELECT {0} is ?, cast(? as text) as _1, {0} as _2, ? as _3"
"SELECT {0} is ?, {0}, ?"
};
}
#[macro_export]
macro_rules! Postgres_query_for_test_prepared_type {
() => {
"SELECT {0} is not distinct from $1, $2::text as _1, {0} as _2, $3 as _3"
"SELECT {0} is not distinct from $1, {0}, $2"
};
}

1
tests/.dockerignore Normal file
View File

@ -0,0 +1 @@
*

2
tests/.env Normal file
View File

@ -0,0 +1,2 @@
# environment values for docker-compose
COMPOSE_PROJECT_NAME=sqlx

4
tests/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM rust:stretch
RUN rustup toolchain install nightly-2020-05-30 && \
rustup default nightly-2020-05-30

21
tests/certs/ca.crt Normal file
View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgIUDlkBz/x3GgypHOBLD9grAx2DQT0wDQYJKoZIhvcNAQEL
BQAwSjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoM
C0xhdW5jaEJhZGdlMRAwDgYDVQQDDAdzcWx4LWNhMB4XDTIwMDUyMjA2NDEzNFoX
DTQ3MTAwNzA2NDEzNFowSjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju
aWExFDASBgNVBAoMC0xhdW5jaEJhZGdlMRAwDgYDVQQDDAdzcWx4LWNhMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwiG50Cfhp/3jRRF2Zk+bWsFe52S4
s+xfGX6NoQwWZRUWqUuiYNXDXPX82lMXcdoTV8U/QgKAG7P19zwPN2q+pNPdd6S1
t1SSJP5ozaQRHFxwVGwj99Nu7La/00Qo0P4VI5kldISHz0fFA6BBT7XGSKwRQj0N
gk7Gc2Si5EG3WTaAkz5A+jsbBBzJ4CrqK/O2jHRJDExeptxKu4tY5RkX1lI9HAvm
AUMPtlguEdldWJo5qOOpytEEyhnDRbiat4hNV68UzsYB5AUucRXgatgENCoADQf/
qEqFU0RjFml72H4RHxY2iPGEF+GPxVm/u2wnJHOSQ/fUO1x+kcGEXJ5Z+wIDAQAB
o1MwUTAdBgNVHQ4EFgQUeHc1DDZ2/SJwIjFk4QkuiyXJiyQwHwYDVR0jBBgwFoAU
eHc1DDZ2/SJwIjFk4QkuiyXJiyQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOCAQEAO5lYFNaFwYjSkSzO19xery699gdzAFKrDxVrKg1Pybatiz6WXMRp
O/5wo61wXg3qMFhj5WdKj0A/nbkGnLoFgW99nw1V7vDqEba5FDfautZNnImIpNkg
WPXN7NRlVBDBKKg+Vx0DFBo9ucAa11gwT+eSdLf4GCwQW8NahmloOorDbg0/CAPw
65duNS260+LsHXqb9jlxB2KXCi6GhenwyIglGtT9g3vh5ntIH53fr1P1aP+zup6b
Atry/KLvi+qLWtgWQwPGDlsft7ieBU0mpX+avs9lKmtwkxaFOfG5aHylxjbC1mKa
v497AO7TATCTsHhCaWgkIg4GAT61fUK9iA==
-----END CERTIFICATE-----

1
tests/certs/ca.srl Normal file
View File

@ -0,0 +1 @@
1FCE7310D7E3C29BA5ED14EEF264E66C25727029

19
tests/certs/server.crt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDGDCCAgACFB/OcxDX48Kbpe0U7vJk5mwlcnApMA0GCSqGSIb3DQEBCwUAMEox
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtMYXVu
Y2hCYWRnZTEQMA4GA1UEAwwHc3FseC1jYTAeFw0yMDA1MjIwNjQyNDJaFw00NzEw
MDcwNjQyNDJaMEcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQw
EgYDVQQKDAtMYXVuY2hCYWRnZTENMAsGA1UEAwwEc3FseDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAM6rGt+k8fcURVGOIVpYcSpHYVJqvWaYEp+SGmvQ
KdMGjE1AHHwd6iT692G3hrZ4hHzvEmrWX4Cp7o9GEF1v912qVWxjUAuufJUFt8Jb
Xa6h0n/PDbgIqU5RQPoOnvlkLrE3fC+BYpBCy+TuEezrTINIOnKVUplYJWYJ78/u
nrensQOj/IeOF1kSeeF049nG7dnqEA+5qTMLAFTVacMIcGyDZX7AqDTsNpwy5QMq
fld1RRPOoNLTYcHq9q3a8XeVXONmxMGh9oatnsIIxGuw22exfvhJzNKgx/+5x6UI
2R+MRBdqz7UWksN37X/lhG8fm5a1zdvmaGC/JSfkaQAMM78CAwEAATANBgkqhkiG
9w0BAQsFAAOCAQEAsz7ty4bK9ml1YwRrWNcSAk0kmGufThhAYhMOu+7rfsV6V5x5
Eh+TvmBoLNFQ/i7LZdmVGTdp90co+FgLHtJ0j/HQguNou02VZ5/5GCSDsJWGxR+6
nZj9M8yP+thYapd7ndkWmjDBioTRKUVeQN8c3L8u6G+ElUJYImebaK2GxivPUCC0
ReHYTACYTf9GdgYYplYdNK+KiKpMLCKvB9f5sXI0Rk8pDXrQxoK34FacBttGdHek
Dbiq26LCszdwbUJhamvne8XFpNqAAh5WMA+b6bKH1OQpr+VNdkAsDZwuAj/MQqJE
vP7IzDLrnjy0fjeI0nW0J8/ch0+1BayJsz8aoQ==
-----END CERTIFICATE-----

174
tests/docker-compose.yml Normal file
View File

@ -0,0 +1,174 @@
version: "3.8"
services:
sqlx:
build: "."
volumes:
- "../:/home/rust/src"
- "$HOME/.cargo/registry:/usr/local/cargo/registry"
working_dir: "/home/rust/src"
environment:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
#
# MySQL 5.6.x, 5.7.x, 8.x
# https://www.mysql.com/support/supportedplatforms/database.html
#
mysql_8:
image: mysql:8.0
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mysql_5_7:
image: mysql:5.7
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mysql_5_6:
image: mysql:5.6
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
#
# MariaDB 10.5, 10.4, 10.3, 10.2, 10.1
# https://mariadb.org/about/#maintenance-policy
#
mariadb_10_5:
image: mariadb:10.5
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mariadb_10_4:
image: mariadb:10.4
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mariadb_10_3:
image: mariadb:10.3
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mariadb_10_2:
image: mariadb:10.2
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
mariadb_10_1:
image: mariadb:10.1
volumes:
- "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: sqlx
#
# PostgreSQL 12.x, 10.x, 9.6.x, 9.5.x
# https://www.postgresql.org/support/versioning/
#
postgres_13:
build:
context: .
dockerfile: postgres/Dockerfile
args:
VERSION: 13-beta1
environment:
POSTGRES_DB: sqlx
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
volumes:
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
command: >
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
postgres_12:
build:
context: .
dockerfile: postgres/Dockerfile
args:
VERSION: 12.3
environment:
POSTGRES_DB: sqlx
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
volumes:
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
command: >
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
postgres_10:
build:
context: .
dockerfile: postgres/Dockerfile
args:
VERSION: 10.13
environment:
POSTGRES_DB: sqlx
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
command: >
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
postgres_9_6:
build:
context: .
dockerfile: postgres/Dockerfile
args:
VERSION: 9.6
environment:
POSTGRES_DB: sqlx
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
command: >
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
postgres_9_5:
build:
context: .
dockerfile: postgres/Dockerfile
args:
VERSION: 9.5
environment:
POSTGRES_DB: sqlx
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: password
volumes:
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
command: >
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key

View File

@ -1,7 +0,0 @@
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
name VARCHAR(255) NOT NULL,
is_active BOOLEAN,
score DOUBLE
);

View File

@ -1,14 +0,0 @@
CREATE TABLE accounts (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
name TEXT NOT NULL,
is_active BOOLEAN,
score DOUBLE PRECISION
);
-- https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-DECLARING
CREATE TYPE inventory_item AS (
name TEXT,
supplier_id INT,
price BIGINT
);

28
tests/keys/ca.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCIbnQJ+Gn/eNF
EXZmT5tawV7nZLiz7F8Zfo2hDBZlFRapS6Jg1cNc9fzaUxdx2hNXxT9CAoAbs/X3
PA83ar6k0913pLW3VJIk/mjNpBEcXHBUbCP3027str/TRCjQ/hUjmSV0hIfPR8UD
oEFPtcZIrBFCPQ2CTsZzZKLkQbdZNoCTPkD6OxsEHMngKuor87aMdEkMTF6m3Eq7
i1jlGRfWUj0cC+YBQw+2WC4R2V1Ymjmo46nK0QTKGcNFuJq3iE1XrxTOxgHkBS5x
FeBq2AQ0KgANB/+oSoVTRGMWaXvYfhEfFjaI8YQX4Y/FWb+7bCckc5JD99Q7XH6R
wYRcnln7AgMBAAECggEALAMNZ13DURzES8Jbv3JI3Fh+taMmJNRv8w23+k0NPrl7
O8KD+8Q62HaEbtLru8ofHIUBhGugs6cnGngpUv0GX8QQr7FN6VRpFa4AAK8zmeRz
KxChTuxGd7Au6SzIvCj+jeWIklQBnkK9LcdFR1cErzEjcIr65xII99xW+bzUXosR
k+aPOSUQXpvkzVgh03uxYBFS3MdF3o+YsLUIQMce4FGwCZJp2vuJfIo8Lfa0s4wd
fae6J6lq2dSoNG9q6rHYFJ3C+IjHxO+Ak/kEvxpSam8oz8+9YsuJ8idjweoHRo+P
mCIZVeAHHdtBM2T4xhvV51HOa89fB8SHVhtww/3OYQKBgQDma5IEbGOtOAi+muam
4by5npDA/3mwXwHADaO5GA7g0BA6XSMjNBQ58fMd/MoJZiUMUFyxzVpXGpLbNNTr
V+w9ol7YoVh6RHpdkpo96MlAwkpxJr06csHX0NDrw4WHvQuBGch4Mm9UnBZaRZhb
VE+s8Piw3cQocd3Kvy5vEflhkwKBgQDXrtrxv1rdsWbt7JYMtm0TIBWVzDV/5qcJ
n4FmkSAjHdGHUwsGCuBCWQ7d+YLtoeCR1XThds2TnNu+zJAHRyFWaUxh+XeWNFnk
R2KAW0WzRS/CMXBYV5k0oXnginFjtigO8MVlbsdunY7IfgeMh4/HXt6cYiOiA/Io
SO4Bw0cG+QKBgQCcWP9iOv3DipL8anT5ZZC+TqagHnm+wCia3WZrcLSfvO5V+96c
w3i5/L9faKjaidG3skvDZbjYA7MERKv46Nbm12cODSTRCegR2CkKuwrcAzmp34Rk
xXtcaldosmnHufG1bv5E+MvsGGFebXy888+AZJ4KvN+eJe095k2mlgamOwKBgC6f
rtapbeQUkFKIXRtcaBHFUsUyArKmUp+C6n8YiiDtNjkRm4Vv3nCZPdyALmxeHOSJ
hx2iB+iL9Pi20b+xAaTjWE6plc9Te8ccI0/p6xRItX0+ILIIJac57NW8N6y9WMV6
CMHXg6cGyjHPBKS9PTh06pxVnqxMPBG3SjP5WaZJAoGBAOA1zE2z6xJCO8cEvo3d
C3FB9NWVqortQtYCn6Y/dt35noLEhe5gfr94BFwhZ2paJZ4dKQXsreaVzMMG6dEP
BjsOPQUHItIOrph2tuWvjWdC51jqgKOGTCCuuX7VJHlhxtaAnrS4RW84yVHugsj0
c3xRxbj6IWgUQfXU6NhEocXv
-----END PRIVATE KEY-----

28
tests/keys/server.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOqxrfpPH3FEVR
jiFaWHEqR2FSar1mmBKfkhpr0CnTBoxNQBx8Heok+vdht4a2eIR87xJq1l+Aqe6P
RhBdb/ddqlVsY1ALrnyVBbfCW12uodJ/zw24CKlOUUD6Dp75ZC6xN3wvgWKQQsvk
7hHs60yDSDpylVKZWCVmCe/P7p63p7EDo/yHjhdZEnnhdOPZxu3Z6hAPuakzCwBU
1WnDCHBsg2V+wKg07DacMuUDKn5XdUUTzqDS02HB6vat2vF3lVzjZsTBofaGrZ7C
CMRrsNtnsX74SczSoMf/ucelCNkfjEQXas+1FpLDd+1/5YRvH5uWtc3b5mhgvyUn
5GkADDO/AgMBAAECggEBAKEqxlZKJ3frkvM6x7/Q4M97wuWm3/U1+q/+HCSfvT4Y
CSnlKVGRcptMK8dEfAWojolvVwmCDBAmdSe+F90GB/4/a0aPXEj/1Z/RSbCD19br
02Bgc+5kssOkketvo4IRImiJQIs7d0XREXiCP/BmvtBSb2IUGMoE94VPknixOY02
Dbcxy7s/r7lqqWwaJNT1vDR/7l5kwsZnWfMRleaQPSGSh3ShVANEDCFb2bK0/s6w
DwshCn/Hh8ThVXK9JcchszIyvIIWWIdaaaHf4iN6G/4a0rqTaGAAo6Si9mZO1WK1
emqnM/KQuhCI/Upwfcu/P5lfhscpc4vIpILUsEV6WKECgYEA8F/mwCIpz3NLe++z
dXCjByhwgpPyaZHJ2P+Av2q9jiIOVhFxheQ+7zuBYc7dOmLDmLoQGaOSQABWkzdm
hjn6NhJoJVY01Np5dSwIFZuC/PQaevidRVCWI1yLxU20ezN6xT2JeDJCuINjyYGG
oropg9KENUFbmNwRUqvVlzCCYLUCgYEA3BpLb9LASpOtiDLvVD+IWxdbcbjVFmuG
/1RPq+1PQNoJdjdEocf9r7K4jMTJuLULmy8MDyox94SqJ+91jvGDDoNisGLRCbic
gUg3HuC1J5oVwlqRC5F21tx9W0ZK85VWkvJQ6MiCZy1eqlaABSEcQ8Mn/4PhX2P/
0/1sofQw7yMCgYEA0/t/QAng3XZMJ2xo0zUWUQW00zMuITPU0m3hWO4FZQdbpUOU
3gNADTJpE5yfNRJMdLAB6tp5679gmkvNOqp+opjxB5xS0zQo0NCYAJY4mmObxr7h
03MSNPU0vjec5tmrd66hQULx3E7i/Z4g4flTC1HoDh8pbFEHZeTsZHz/PdECgYA/
p/MtUhx+9Rr5CxIgoYdEIQs3ZqdqJosSiUXJiYakUOrvn6hfycFa8Stiuv9ERkgn
B4JLWH6/AUVc62pqfvrSVblTHiEq2JOa6FHYwlBiNbQZU6wjVlyyY2512WyP6h7x
vNcdm+/q+zontYCs+xh7mJOW2INz3S3+F4s1g7QrVQKBgE0rBT29byusUQCV/AZM
hFIyJIEYO+R3n8b8ocfsMTvAUVJzXe+KAHQIvgjiAX1e4jo0xjgR4FNJyd8olhYR
BSIZyWZTk5KuFjWztt0SCIAdXLiFBbTKsLR044jvjBSIx8XDfXv3kYx6YcbxbnsK
noIkTIopteVr069/pyL6PiUx
-----END PRIVATE KEY-----

View File

@ -1,70 +0,0 @@
use sqlx::MySql;
use sqlx_test::test_type;
use std::fmt::Debug;
// Transparent types are rust-side wrappers over DB types
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(transparent)]
struct Transparent(i32);
// "Weak" enums map to an integer type indicated by #[repr]
#[derive(PartialEq, Copy, Clone, Debug, sqlx::Type)]
#[repr(i32)]
enum Weak {
One = 0,
Two = 2,
Three = 4,
}
// "Strong" enums can map to TEXT or a custom enum
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "lowercase")]
enum ColorLower {
Red,
Green,
Blue,
}
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "snake_case")]
enum ColorSnake {
RedGreen,
BlueBlack,
}
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "uppercase")]
enum ColorUpper {
Red,
Green,
Blue,
}
test_type!(transparent(
MySql,
Transparent,
"0" == Transparent(0),
"23523" == Transparent(23523)
));
test_type!(weak_enum(
MySql,
Weak,
"0" == Weak::One,
"2" == Weak::Two,
"4" == Weak::Three
));
test_type!(strong_color_lower_enum(
MySql,
ColorLower,
"'green'" == ColorLower::Green
));
test_type!(strong_color_snake_enum(
MySql,
ColorSnake,
"'red_green'" == ColorSnake::RedGreen
));
test_type!(strong_color_upper_enum(
MySql,
ColorUpper,
"'GREEN'" == ColorUpper::Green
));

View File

@ -1,99 +0,0 @@
use sqlx::MySql;
use sqlx_test::new;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select_from_cte() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let account =
sqlx::query!("select * from (select (1) as id, 'Herp Derpinson' as name, cast(null as char) email) accounts")
.fetch_one(&mut conn)
.await?;
assert_eq!(account.id, 1);
assert_eq!(account.name, "Herp Derpinson");
// MySQL can tell us the nullability of expressions, ain't that cool
assert_eq!(account.email, None);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select_from_cte_bind() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let account = sqlx::query!(
"select * from (select (1) as id, 'Herp Derpinson' as name) accounts where id = ?",
1i32
)
.fetch_one(&mut conn)
.await?;
println!("{:?}", account);
println!("{}: {}", account.id, account.name);
Ok(())
}
#[derive(Debug)]
struct RawAccount {
r#type: i32,
name: Option<String>,
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_query_as_raw() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let account = sqlx::query_as!(
RawAccount,
"SELECT * from (select 1 as type, cast(null as char) as name) accounts"
)
.fetch_one(&mut conn)
.await?;
assert_eq!(account.name, None);
assert_eq!(account.r#type, 1);
println!("{:?}", account);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_query_as_bool() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
struct Article {
id: i32,
deleted: bool,
}
let article = sqlx::query_as_unchecked!(
Article,
"select * from (select 51 as id, true as deleted) articles"
)
.fetch_one(&mut conn)
.await?;
assert_eq!(51, article.id);
assert_eq!(true, article.deleted);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_query_bytes() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let rec = sqlx::query!("SELECT X'01AF' as _1")
.fetch_one(&mut conn)
.await?;
assert_eq!(rec._1, &[0x01_u8, 0xAF_u8]);
Ok(())
}

View File

@ -1,56 +0,0 @@
//! Tests for the raw (unprepared) query API for MySql.
use sqlx::{Cursor, Executor, MySql, Row};
use sqlx_test::new;
/// Test a simple select expression. This should return the row.
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_select_expression() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let mut cursor = conn.fetch("SELECT 5");
let row = cursor.next().await?.unwrap();
assert!(5i32 == row.try_get::<i32, _>(0)?);
Ok(())
}
/// Test that we can interleave reads and writes to the database
/// in one simple query. Using the `Cursor` API we should be
/// able to fetch from both queries in sequence.
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_multi_read_write() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let mut cursor = conn.fetch(
"
CREATE TEMPORARY TABLE messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO messages (text) VALUES ('this is a test');
SELECT id, text FROM messages;
",
);
let row = cursor.next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = cursor.next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(1_i64, id);
assert_eq!("this is a test", text);
Ok(())
}

View File

@ -1,234 +0,0 @@
use futures::TryStreamExt;
use sqlx::{mysql::MySqlQueryAs, Connection, Executor, MySql, MySqlPool};
use sqlx_test::new;
use std::time::Duration;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_connects() -> anyhow::Result<()> {
Ok(new::<MySql>().await?.ping().await?)
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_drops_results_in_affected_rows() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
// ~1800 rows should be iterated and dropped
let affected = conn.execute("select * from mysql.time_zone").await?;
// In MySQL, rows being returned isn't enough to flag it as an _affected_ row
assert_eq!(0, affected);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_executes() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY)
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES (?)")
.bind(index)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query_as("SELECT id FROM users")
.fetch(&mut conn)
.try_fold(0_i32, |acc, (x,): (i32,)| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 55);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_selects_null() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let (val,): (Option<i32>,) = sqlx::query_as("SELECT NULL").fetch_one(&mut conn).await?;
assert!(val.is_none());
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_describe() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE describe_test (
id int primary key auto_increment,
name text not null,
hash blob
)
"#,
)
.await?;
let describe = conn
.describe("select nt.*, false from describe_test nt")
.await?;
assert_eq!(describe.result_columns[0].non_null, Some(true));
assert_eq!(
describe.result_columns[0]
.type_info
.as_ref()
.unwrap()
.to_string(),
"INT"
);
assert_eq!(describe.result_columns[1].non_null, Some(true));
assert_eq!(
describe.result_columns[1]
.type_info
.as_ref()
.unwrap()
.to_string(),
"TEXT"
);
assert_eq!(describe.result_columns[2].non_null, Some(false));
assert_eq!(
describe.result_columns[2]
.type_info
.as_ref()
.unwrap()
.to_string(),
"BLOB"
);
assert_eq!(describe.result_columns[3].non_null, Some(true));
let bool_ty_name = describe.result_columns[3]
.type_info
.as_ref()
.unwrap()
.to_string();
// MySQL 5.7, 8 and MariaDB 10.1 return BIG_INT, MariaDB 10.4 returns INT (optimization?)
assert!(
["BIGINT", "INT"].contains(&bool_ty_name.as_str()),
"type name returned: {}",
bool_ty_name
);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn pool_immediately_fails_with_db_error() -> anyhow::Result<()> {
// Malform the database url by changing the password
let url = dotenv::var("DATABASE_URL")?.replace("password", "not-the-password");
let pool = MySqlPool::new(&url).await?;
let res = pool.acquire().await;
match res {
Err(sqlx::Error::Database(err)) if (*err).message().contains("Access denied") => {
// Access was properly denied
}
Err(e) => panic!("unexpected error: {:?}", e),
Ok(_) => panic!("unexpected ok"),
}
Ok(())
}
// run with `cargo test --features mysql -- --ignored --nocapture pool_smoke_test`
#[ignore]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn pool_smoke_test() -> anyhow::Result<()> {
#[cfg(feature = "runtime-tokio")]
use tokio::{task::spawn, time::delay_for as sleep, time::timeout};
#[cfg(feature = "runtime-async-std")]
use async_std::{future::timeout, task::sleep, task::spawn};
eprintln!("starting pool");
let pool = MySqlPool::builder()
.connect_timeout(Duration::from_secs(5))
.min_size(5)
.max_size(10)
.build(&dotenv::var("DATABASE_URL")?)
.await?;
// spin up more tasks than connections available, and ensure we don't deadlock
for i in 0..20 {
let pool = pool.clone();
spawn(async move {
loop {
if let Err(e) = sqlx::query("select 1 + 1").execute(&mut &pool).await {
eprintln!("pool task {} dying due to {}", i, e);
break;
}
}
});
}
for _ in 0..5 {
let pool = pool.clone();
spawn(async move {
while !pool.is_closed() {
// drop acquire() futures in a hot loop
// https://github.com/launchbadge/sqlx/issues/83
drop(pool.acquire());
}
});
}
eprintln!("sleeping for 30 seconds");
sleep(Duration::from_secs(30)).await;
assert_eq!(pool.size(), 10);
eprintln!("closing pool");
timeout(Duration::from_secs(30), pool.close()).await?;
eprintln!("pool closed successfully");
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_fetch_one_and_ping() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let (_id,): (i32,) = sqlx::query_as("SELECT 1 as id")
.fetch_one(&mut conn)
.await?;
conn.ping().await?;
let (_id,): (i32,) = sqlx::query_as("SELECT 1 as id")
.fetch_one(&mut conn)
.await?;
Ok(())
}

39
tests/mysql/describe.rs Normal file
View File

@ -0,0 +1,39 @@
use futures::TryStreamExt;
use sqlx::mysql::{MySql, MySqlRow};
use sqlx::{Connection, Executor, Row};
use sqlx_core::describe::Column;
use sqlx_test::new;
fn type_names(columns: &[Column<MySql>]) -> Vec<String> {
columns
.iter()
.filter_map(|col| Some(col.type_info.as_ref()?.to_string()))
.collect()
}
#[sqlx_macros::test]
async fn it_describes_simple() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let d = conn.describe("SELECT * FROM tweet").await?;
let columns = d.columns;
assert_eq!(columns[0].name, "id");
assert_eq!(columns[1].name, "created_at");
assert_eq!(columns[2].name, "text");
assert_eq!(columns[3].name, "owner_id");
assert_eq!(columns[0].not_null, Some(true));
assert_eq!(columns[1].not_null, Some(true));
assert_eq!(columns[2].not_null, Some(true));
assert_eq!(columns[3].not_null, Some(false));
let column_type_names = type_names(&columns);
assert_eq!(column_type_names[0], "BIGINT");
assert_eq!(column_type_names[1], "TIMESTAMP");
assert_eq!(column_type_names[2], "TEXT");
assert_eq!(column_type_names[3], "BIGINT");
Ok(())
}

144
tests/mysql/mysql.rs Normal file
View File

@ -0,0 +1,144 @@
use futures::TryStreamExt;
use sqlx::mysql::{MySql, MySqlRow};
use sqlx::{Connection, Executor, Row};
use sqlx_test::new;
#[sqlx_macros::test]
async fn it_connects() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
conn.ping().await?;
conn.close().await?;
Ok(())
}
#[sqlx_macros::test]
async fn it_maths() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let value = sqlx::query("select 1 + CAST(? AS SIGNED)")
.bind(5_i32)
.try_map(|row: MySqlRow| row.try_get::<i32, _>(0))
.fetch_one(&mut conn)
.await?;
assert_eq!(6i32, value);
Ok(())
}
#[sqlx_macros::test]
async fn it_executes() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY);
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES (?)")
.bind(index)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query("SELECT id FROM users")
.try_map(|row: MySqlRow| row.try_get::<i32, _>(0))
.fetch(&mut conn)
.try_fold(0_i32, |acc, x| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 55);
Ok(())
}
#[sqlx_macros::test]
async fn it_drops_results_in_affected_rows() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
// ~1800 rows should be iterated and dropped
let affected = conn
.execute("select * from mysql.time_zone limit 1575")
.await?;
// In MySQL, rows being returned isn't enough to flag it as an _affected_ row
assert_eq!(0, affected);
Ok(())
}
#[sqlx_macros::test]
async fn it_selects_null() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let (val,): (Option<i32>,) = sqlx::query_as("SELECT NULL").fetch_one(&mut conn).await?;
assert!(val.is_none());
let val: Option<i32> = conn.fetch_one("SELECT NULL").await?.try_get(0)?;
assert!(val.is_none());
Ok(())
}
#[sqlx_macros::test]
async fn it_can_fetch_one_and_ping() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let (_id,): (i32,) = sqlx::query_as("SELECT 1 as id")
.fetch_one(&mut conn)
.await?;
conn.ping().await?;
let (_id,): (i32,) = sqlx::query_as("SELECT 1 as id")
.fetch_one(&mut conn)
.await?;
Ok(())
}
/// Test that we can interleave reads and writes to the database in one simple query.
#[sqlx_macros::test]
async fn it_interleaves_reads_and_writes() -> anyhow::Result<()> {
let mut conn = new::<MySql>().await?;
let mut s = conn.fetch(
"
CREATE TEMPORARY TABLE messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO messages (text) VALUES ('this is a test');
SELECT id, text FROM messages;
",
);
let row = s.try_next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = s.try_next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(1_i64, id);
assert_eq!("this is a test", text);
Ok(())
}

8
tests/mysql/setup.sql Normal file
View File

@ -0,0 +1,8 @@
-- https://github.com/prisma/database-schema-examples/tree/master/postgres/basic-twitter#basic-twitter
CREATE TABLE tweet
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
text TEXT NOT NULL,
owner_id BIGINT
);

View File

@ -1,49 +1,35 @@
extern crate time_ as time;
use sqlx::MySql;
use sqlx::mysql::MySql;
use sqlx_test::test_type;
test_type!(null(
MySql,
Option<i16>,
"NULL" == None::<i16>
));
test_type!(bool(MySql, "false" == false, "true" == true));
test_type!(bool(MySql, bool, "false" == false, "true" == true));
test_type!(u8(MySql, "CAST(253 AS UNSIGNED)" == 253_u8));
test_type!(i8(MySql, "5" == 5_i8, "0" == 0_i8));
test_type!(u8(MySql, u8, "CAST(253 AS UNSIGNED)" == 253_u8));
test_type!(i8(MySql, i8, "5" == 5_i8, "0" == 0_i8));
test_type!(u16(MySql, "CAST(21415 AS UNSIGNED)" == 21415_u16));
test_type!(i16(MySql, "21415" == 21415_i16));
test_type!(u16(MySql, u16, "CAST(21415 AS UNSIGNED)" == 21415_u16));
test_type!(i16(MySql, i16, "21415" == 21415_i16));
test_type!(u32(MySql, "CAST(2141512 AS UNSIGNED)" == 2141512_u32));
test_type!(i32(MySql, "2141512" == 2141512_i32));
test_type!(u32(MySql, u32, "CAST(2141512 AS UNSIGNED)" == 2141512_u32));
test_type!(i32(MySql, i32, "2141512" == 2141512_i32));
test_type!(u64(MySql, "CAST(2141512 AS UNSIGNED)" == 2141512_u64));
test_type!(i64(MySql, "2141512" == 2141512_i64));
test_type!(u64(MySql, u64, "CAST(2141512 AS UNSIGNED)" == 2141512_u64));
test_type!(i64(MySql, i64, "2141512" == 2141512_i64));
test_type!(double(MySql, f64, "3.14159265E0" == 3.14159265f64));
test_type!(f64(MySql, "3.14159265e0" == 3.14159265_f64));
// NOTE: This behavior can be very surprising. MySQL implicitly widens FLOAT bind parameters
// to DOUBLE. This results in the weirdness you see below. MySQL generally recommends to stay
// away from FLOATs.
test_type!(float(
MySql,
f32,
"3.1410000324249268e0" == 3.141f32 as f64 as f32
));
test_type!(f32(MySql, "3.1410000324249268e0" == 3.141f32 as f64 as f32));
test_type!(string(
MySql,
String,
test_type!(string<String>(MySql,
"'helloworld'" == "helloworld",
"''" == ""
));
test_type!(bytes(
MySql,
Vec<u8>,
test_type!(bytes<Vec<u8>>(MySql,
"X'DEADBEEF'"
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
"X''"
@ -57,28 +43,24 @@ mod chrono {
use super::*;
use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
test_type!(chrono_date(
test_type!(chrono_date<NaiveDate>(
MySql,
NaiveDate,
"DATE '2001-01-05'" == NaiveDate::from_ymd(2001, 1, 5),
"DATE '2050-11-23'" == NaiveDate::from_ymd(2050, 11, 23)
));
test_type!(chrono_time(
test_type!(chrono_time<NaiveTime>(
MySql,
NaiveTime,
"TIME '05:10:20.115100'" == NaiveTime::from_hms_micro(5, 10, 20, 115100)
));
test_type!(chrono_date_time(
test_type!(chrono_date_time<NaiveDateTime>(
MySql,
NaiveDateTime,
"TIMESTAMP '2019-01-02 05:10:20'" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
));
test_type!(chrono_timestamp(
test_type!(chrono_timestamp<DateTime::<Utc>>(
MySql,
DateTime::<Utc>,
"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),
@ -93,30 +75,26 @@ mod time_tests {
use sqlx::types::time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use time::{date, time};
test_type!(time_date(
test_type!(time_date<Date>(
MySql,
Date,
"DATE '2001-01-05'" == date!(2001 - 1 - 5),
"DATE '2050-11-23'" == date!(2050 - 11 - 23)
));
test_type!(time_time(
test_type!(time_time<Time>(
MySql,
Time,
"TIME '05:10:20.115100'" == time!(5:10:20.115100)
));
test_type!(time_date_time(
test_type!(time_date_time<PrimitiveDateTime>(
MySql,
PrimitiveDateTime,
"TIMESTAMP '2019-01-02 05:10:20'" == date!(2019 - 1 - 2).with_time(time!(5:10:20)),
"TIMESTAMP '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2).with_time(time!(5:10:20.115100))
));
test_type!(time_timestamp(
test_type!(time_timestamp<OffsetDateTime>(
MySql,
OffsetDateTime,
"TIMESTAMP '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2)
.with_time(time!(5:10:20.115100))
@ -125,9 +103,8 @@ mod time_tests {
}
#[cfg(feature = "bigdecimal")]
test_type!(decimal(
test_type!(decimal<sqlx::types::BigDecimal>(
MySql,
sqlx::types::BigDecimal,
"CAST(0 as DECIMAL(0, 0))" == "0".parse::<sqlx::types::BigDecimal>().unwrap(),
"CAST(1 AS DECIMAL(1, 0))" == "1".parse::<sqlx::types::BigDecimal>().unwrap(),
"CAST(10000 AS DECIMAL(5, 0))" == "10000".parse::<sqlx::types::BigDecimal>().unwrap(),
@ -144,10 +121,9 @@ mod json_tests {
use sqlx::types::Json;
use sqlx_test::test_type;
test_type!(json(
test_type!(json<JsonValue>(
MySql,
JsonValue,
"SELECT CAST({0} AS JSON) <=> CAST(? AS JSON), '<UNKNOWN>' as _1, ? as _2, ? as _3",
"SELECT CAST({0} AS JSON) <=> CAST(? AS JSON), CAST({0} AS JSON) as _2, ? as _3",
"'\"Hello, World\"'" == json!("Hello, World"),
"'\"😎\"'" == json!("😎"),
"'\"🙋‍♀️\"'" == json!("🙋‍♀️"),
@ -160,10 +136,9 @@ mod json_tests {
age: u32,
}
test_type!(json_struct(
test_type!(json_struct<Json<Friend>>(
MySql,
Json<Friend>,
"SELECT CAST({0} AS JSON) <=> CAST(? AS JSON), '<UNKNOWN>' as _1, ? as _2, ? as _3",
"SELECT CAST({0} AS JSON) <=> CAST(? AS JSON), CAST({0} AS JSON) as _2, ? as _3",
"\'{\"name\": \"Joe\", \"age\":33}\'" == Json(Friend { name: "Joe".to_string(), age: 33 })
));
}

View File

@ -1,71 +0,0 @@
//! Tests for the raw (unprepared) query API for Postgres.
use sqlx::{Cursor, Executor, Postgres, Row};
use sqlx_test::new;
/// Tests the edge case of executing a completely empty query string.
///
/// This gets flagged as an `EmptyQueryResponse` in Postgres. We currently
/// catch this and just return no rows.
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_empty_query() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let affected = conn.execute("").await?;
assert_eq!(affected, 0);
Ok(())
}
/// Test a simple select expression. This should return the row.
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_select_expression() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let mut cursor = conn.fetch("SELECT 5");
let row = cursor.next().await?.unwrap();
assert!(5i32 == row.try_get::<i32, _>(0)?);
Ok(())
}
/// Test that we can interleave reads and writes to the database
/// in one simple query. Using the `Cursor` API we should be
/// able to fetch from both queries in sequence.
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_multi_read_write() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let mut cursor = conn.fetch(
"
CREATE TABLE IF NOT EXISTS _sqlx_test_postgres_5112 (
id BIGSERIAL PRIMARY KEY,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO _sqlx_test_postgres_5112 (text) VALUES ('this is a test');
SELECT id, text FROM _sqlx_test_postgres_5112;
",
);
let row = cursor.next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = cursor.next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(1_i64, id);
assert_eq!("this is a test", text);
Ok(())
}

View File

@ -1,713 +0,0 @@
extern crate time_ as time;
use sqlx::decode::Decode;
use sqlx::encode::Encode;
use sqlx::postgres::types::raw::{PgNumeric, PgNumericSign, PgRecordDecoder, PgRecordEncoder};
use sqlx::postgres::{PgQueryAs, PgRawBuffer, PgTypeInfo, PgValue};
use sqlx::{Cursor, Executor, Postgres, Row, Type};
use sqlx_test::{new, test_prepared_type, test_type};
// TODO: With support for concatenation of sql literals in query! macros this should be updated
macro_rules! array_macro_test {
($name:ident($type:ty, $($sql:literal == $value:expr),+ $(,)?)) => {
paste::item! {
#[allow(unused_imports)]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn [< test_array_type_ $name >] () -> anyhow::Result<()> {
use sqlx::prelude::*;
let mut conn = sqlx_test::new::<Postgres>().await?;
$(
let v: &[$type] = $value;
let res = sqlx::query!($sql, v).fetch_one(&mut conn).await?;
// these must be unwrapped in query! because postgres infers expressions
// to be potentially NULL at all times even if it's impossible to be NULL
assert_eq!(res.value.unwrap(), v);
assert_eq!(res.out.unwrap(), v);
)+
Ok(())
}
}
};
}
test_type!(null(
Postgres,
Option<i16>,
"NULL::int2" == None::<i16>
));
test_type!(bool(
Postgres,
bool,
"false::boolean" == false,
"true::boolean" == true
));
array_macro_test!(bool(
bool,
"select '{true,false,true}'::boolean[] as value, $1::boolean[] as out" == &[true, false, true]
));
test_type!(i8(Postgres, i8, "120::\"char\"" == 120_i8));
test_type!(i16(Postgres, i16, "821::smallint" == 821_i16));
test_type!(i32(
Postgres,
i32,
"94101::int" == 94101_i32,
"-5101::int" == -5101_i32
));
array_macro_test!(i32(
i32,
"select '{1,3,-5}'::int[] as value, $1::int[] as out" == &[1, 3, -5]
));
test_type!(u32(Postgres, u32, "94101::oid" == 94101_u32));
test_type!(i64(Postgres, i64, "9358295312::bigint" == 9358295312_i64));
test_type!(f32(Postgres, f32, "9419.122::real" == 9419.122_f32));
test_type!(f64(
Postgres,
f64,
"939399419.1225182::double precision" == 939399419.1225182_f64
));
array_macro_test!(f64(
f64,
"select '{939399419.1225182,-12.0}'::double precision[] as value, $1::double precision[] as out"
== &[939399419.1225182_f64, -12.0]
));
test_type!(string(
Postgres,
String,
"'this is foo'" == "this is foo",
"''" == ""
));
test_type!(bytea(
Postgres,
Vec<u8>,
"E'\\\\xDEADBEEF'::bytea"
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
"E'\\\\x'::bytea"
== Vec::<u8>::new(),
"E'\\\\x0000000052'::bytea"
== vec![0_u8, 0, 0, 0, 0x52]
));
// PgNumeric only works on the wire protocol
test_prepared_type!(numeric(
Postgres,
PgNumeric,
"0::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 0,
scale: 0,
digits: vec![]
},
"(-0)::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 0,
scale: 0,
digits: vec![]
},
"1::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 0,
scale: 0,
digits: vec![1]
},
"1234::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 0,
scale: 0,
digits: vec![1234]
},
"10000::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 1,
scale: 0,
digits: vec![1]
},
"0.1::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: -1,
scale: 1,
digits: vec![1000]
},
"0.01234::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: -1,
scale: 5,
digits: vec![123, 4000]
},
"12.34::numeric"
== PgNumeric::Number {
sign: PgNumericSign::Positive,
weight: 0,
scale: 2,
digits: vec![12, 3400]
},
"'NaN'::numeric" == PgNumeric::NotANumber,
));
#[cfg(feature = "bigdecimal")]
test_type!(decimal(
Postgres,
sqlx::types::BigDecimal,
// https://github.com/launchbadge/sqlx/issues/283
"0::numeric" == "0".parse::<sqlx::types::BigDecimal>().unwrap(),
"1::numeric" == "1".parse::<sqlx::types::BigDecimal>().unwrap(),
"10000::numeric" == "10000".parse::<sqlx::types::BigDecimal>().unwrap(),
"0.1::numeric" == "0.1".parse::<sqlx::types::BigDecimal>().unwrap(),
"0.01234::numeric" == "0.01234".parse::<sqlx::types::BigDecimal>().unwrap(),
"12.34::numeric" == "12.34".parse::<sqlx::types::BigDecimal>().unwrap(),
"12345.6789::numeric" == "12345.6789".parse::<sqlx::types::BigDecimal>().unwrap(),
));
// TODO: This is a minimal example that reproduces a typechecking error with
// arrays of BigDecimal in macros.
//
// The error is:
// error: unsupported type _NUMERIC for param #1
//
// The implementation for bigdecimal is of the same form as all the other types.
// My (oeb25) hypothesis is that it is due to some overlap with PgNumeric, but I've been
// conclude any results.
// I have left the implementation in its ill form. It should not interfere with any of the other
// types, but it just doesn't compile if you try to use arrays of bigdecimal in query macros.
// #[cfg(feature = "bigdecimal")]
// #[test]
// fn minimal_decimal_macro_repro() {
// use sqlx::prelude::*;
// let v: &[sqlx::types::BigDecimal] = &[];
// sqlx::query!("select $1::numeric[] as out", v);
// }
// array_macro_test!(decimal(
// sqlx::types::BigDecimal,
// "select '{12345.6789}'::numeric[] as value, $1::numeric[] as out"
// == &["12345.6789".parse::<sqlx::types::BigDecimal>().unwrap()]
// ));
#[cfg(feature = "uuid")]
test_type!(uuid(
Postgres,
sqlx::types::Uuid,
"'b731678f-636f-4135-bc6f-19440c13bd19'::uuid"
== sqlx::types::Uuid::parse_str("b731678f-636f-4135-bc6f-19440c13bd19").unwrap(),
"'00000000-0000-0000-0000-000000000000'::uuid"
== sqlx::types::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()
));
#[cfg(feature = "uuid")]
array_macro_test!(uuid(sqlx::types::Uuid, "select '{b731678f-636f-4135-bc6f-19440c13bd19,00000000-0000-0000-0000-000000000000}'::uuid[] as value, $1::uuid[] as out"
== &[
sqlx::types::Uuid::parse_str("b731678f-636f-4135-bc6f-19440c13bd19").unwrap(),
sqlx::types::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()
]));
#[cfg(feature = "ipnetwork")]
test_type!(ipnetwork(
Postgres,
sqlx::types::ipnetwork::IpNetwork,
"'127.0.0.1'::inet"
== "127.0.0.1"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'8.8.8.8/24'::inet"
== "8.8.8.8/24"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'::ffff:1.2.3.0'::inet"
== "::ffff:1.2.3.0"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'2001:4f8:3:ba::/64'::inet"
== "2001:4f8:3:ba::/64"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'192.168'::cidr"
== "192.168.0.0/24"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'::ffff:1.2.3.0/120'::cidr"
== "::ffff:1.2.3.0/120"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
));
#[cfg(feature = "ipnetwork")]
array_macro_test!(ipnetwork(
sqlx::types::ipnetwork::IpNetwork,
"select '{127.0.0.1,8.8.8.8/24}'::inet[] as value, $1::inet[] as out"
== &[
"127.0.0.1"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"8.8.8.8/24"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap()
]
));
#[cfg(feature = "chrono")]
mod chrono {
use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use super::*;
test_type!(chrono_date(
Postgres,
NaiveDate,
"DATE '2001-01-05'" == NaiveDate::from_ymd(2001, 1, 5),
"DATE '2050-11-23'" == NaiveDate::from_ymd(2050, 11, 23)
));
test_type!(chrono_time(
Postgres,
NaiveTime,
"TIME '05:10:20.115100'" == NaiveTime::from_hms_micro(5, 10, 20, 115100)
));
test_type!(chrono_date_time(
Postgres,
NaiveDateTime,
"'2019-01-02 05:10:20'::timestamp" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
));
array_macro_test!(chrono_date_time(
NaiveDateTime,
"select '{2019-01-02 05:10:20}'::timestamp[] as value, $1::timestamp[] as out"
== &[NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)]
));
test_type!(chrono_date_time_tz(
Postgres,
DateTime::<Utc>,
"TIMESTAMPTZ '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,
)
));
// TODO: Can't seem to get this to work
// array_macro_test!(chrono_date_time_tz(
// DateTime::<Utc>,
// "select ARRAY[TIMESTAMPTZ '2019-01-02 05:10:20.115100'] as value, $1::TIMESTAMPTZ as out"
// == &[DateTime::<Utc>::from_utc(
// NaiveDate::from_ymd(2019, 1, 2).and_hms_micro(5, 10, 20, 115100),
// Utc,
// )]
// ));
}
#[cfg(feature = "time")]
mod time_tests {
use super::*;
use sqlx::types::time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use time::{date, time};
test_type!(time_date(
Postgres,
Date,
"DATE '2001-01-05'" == date!(2001 - 1 - 5),
"DATE '2050-11-23'" == date!(2050 - 11 - 23)
));
test_type!(time_time(
Postgres,
Time,
"TIME '05:10:20.115100'" == time!(5:10:20.115100)
));
test_type!(time_date_time(
Postgres,
PrimitiveDateTime,
"TIMESTAMP '2019-01-02 05:10:20'" == date!(2019 - 1 - 2).with_time(time!(5:10:20)),
"TIMESTAMP '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2).with_time(time!(5:10:20.115100))
));
test_type!(time_timestamp(
Postgres,
OffsetDateTime,
"TIMESTAMPTZ '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2)
.with_time(time!(5:10:20.115100))
.assume_utc()
));
}
// This is trying to break my complete lack of understanding of null bitmaps for array/record
// decoding. The docs in pg are either wrong or I'm reading the wrong docs.
test_type!(lots_of_nulls_vec(Postgres, Vec<Option<bool>>,
"ARRAY[NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, true]::bool[]" == {
vec![None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(true)]
},
));
test_type!(bool_vec(Postgres, Vec<bool>,
"ARRAY[true, true, false, true]::bool[]" == vec![true, true, false, true],
));
test_type!(bool_opt_vec(Postgres, Vec<Option<bool>>,
"ARRAY[NULL, true, NULL, false]::bool[]" == vec![None, Some(true), None, Some(false)],
));
test_type!(f32_vec(Postgres, Vec<f32>,
"ARRAY[0.0, 1.0, 3.14, 1.234, -0.002, 100000.0]::real[]" == vec![0.0_f32, 1.0, 3.14, 1.234, -0.002, 100000.0],
));
test_type!(f64_vec(Postgres, Vec<f64>,
"ARRAY[0.0, 1.0, 3.14, 1.234, -0.002, 100000.0]::double precision[]" == vec![0.0_f64, 1.0, 3.14, 1.234, -0.002, 100000.0],
));
test_type!(i16_vec(Postgres, Vec<i16>,
"ARRAY[1, 152, -12412]::smallint[]" == vec![1_i16, 152, -12412],
"ARRAY[]::smallint[]" == Vec::<i16>::new(),
"ARRAY[0]::smallint[]" == vec![0_i16]
));
test_type!(string_vec(Postgres, Vec<String>,
"ARRAY['', '\"']::text[]"
== vec!["".to_string(), "\"".to_string()],
"ARRAY['Hello, World', '', 'Goodbye']::text[]"
== vec!["Hello, World".to_string(), "".to_string(), "Goodbye".to_string()],
));
//
// These require some annoyingly different tests as anonymous records cannot be read from the
// database. If someone enterprising comes along and wants to try and just the macro to handle
// this, that would be super awesome.
//
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_prepared_anonymous_record() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
// Tuple of no elements is not possible
// Tuple of 1 element requires a concrete type
// Tuple with a NULL requires a concrete type
// Tuple of 2 elements
let rec: ((bool, i32),) = sqlx::query_as("SELECT (true, 23512)")
.fetch_one(&mut conn)
.await?;
assert_eq!((rec.0).0, true);
assert_eq!((rec.0).1, 23512);
// Tuple with an empty string
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'')")
.fetch_one(&mut conn)
.await?;
assert_eq!((rec.0).1, "");
// Tuple with a string with an interior comma
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'Hello, World!')")
.fetch_one(&mut conn)
.await?;
assert_eq!((rec.0).1, "Hello, World!");
// Tuple with a string with an emoji
let rec: ((bool, String),) = sqlx::query_as("SELECT (true,'Hello, 🐕!')")
.fetch_one(&mut conn)
.await?;
assert_eq!((rec.0).1, "Hello, 🐕!");
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_unprepared_anonymous_record() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
// Tuple of no elements is not possible
// Tuple of 1 element requires a concrete type
// Tuple with a NULL requires a concrete type
// Tuple of 2 elements
let mut cursor = conn.fetch("SELECT (true, 23512)");
let row = cursor.next().await?.unwrap();
let rec: (bool, i32) = row.get(0);
assert_eq!(rec.0, true);
assert_eq!(rec.1, 23512);
// Tuple with an empty string
let mut cursor = conn.fetch("SELECT (true, '')");
let row = cursor.next().await?.unwrap();
let rec: (bool, String) = row.get(0);
assert_eq!(rec.1, "");
// Tuple with a string with an interior comma
let mut cursor = conn.fetch("SELECT (true, 'Hello, World!')");
let row = cursor.next().await?.unwrap();
let rec: (bool, String) = row.get(0);
assert_eq!(rec.1, "Hello, World!");
// Tuple with a string with an emoji
let mut cursor = conn.fetch("SELECT (true, 'Hello, 🐕!')");
let row = cursor.next().await?.unwrap();
let rec: (bool, String) = row.get(0);
assert_eq!(rec.1, "Hello, 🐕!");
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_unprepared_anonymous_record_arrays() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
// record of arrays
let mut cursor = conn.fetch("SELECT (ARRAY['', '\"']::text[], false)");
let row = cursor.next().await?.unwrap();
let rec: (Vec<String>, bool) = row.get(0);
assert_eq!(rec, (vec!["".to_string(), "\"".to_string()], false));
// array of records
let mut cursor = conn.fetch("SELECT ARRAY[('','\"'), (NULL,'')]::record[]");
let row = cursor.next().await?.unwrap();
let rec: Vec<(Option<String>, String)> = row.get(0);
assert_eq!(
rec,
vec![
(Some(String::from("")), String::from("\"")),
(None, String::from(""))
]
);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_prepared_anonymous_record_arrays() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
// record of arrays
let rec: ((Vec<String>, bool),) = sqlx::query_as("SELECT (ARRAY['', '\"']::text[], false)")
.fetch_one(&mut conn)
.await?;
assert_eq!(rec.0, (vec!["".to_string(), "\"".to_string()], false));
// array of records
let rec: (Vec<(Option<String>, String)>,) =
sqlx::query_as("SELECT ARRAY[('','\"'), (NULL,'')]::record[]")
.fetch_one(&mut conn)
.await?;
assert_eq!(
rec.0,
vec![
(Some(String::from("")), String::from("\"")),
(None, String::from(""))
]
);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_prepared_structs() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
//
// Setup custom types if needed
//
conn.execute(
r#"
DO $$ BEGIN
CREATE TYPE _sqlx_record_empty AS ();
CREATE TYPE _sqlx_record_1 AS (_1 int8);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
"#,
)
.await?;
//
// Record of no elements
//
struct RecordEmpty {}
impl Type<Postgres> for RecordEmpty {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("_sqlx_record_empty")
}
}
impl Encode<Postgres> for RecordEmpty {
fn encode(&self, buf: &mut PgRawBuffer) {
PgRecordEncoder::new(buf).finish();
}
}
impl<'de> Decode<'de, Postgres> for RecordEmpty {
fn decode(_value: PgValue<'de>) -> sqlx::Result<Self> {
Ok(RecordEmpty {})
}
}
let _: (RecordEmpty, RecordEmpty) = sqlx::query_as("SELECT '()'::_sqlx_record_empty, $1")
.bind(RecordEmpty {})
.fetch_one(&mut conn)
.await?;
//
// Record of one element
//
#[derive(Debug, PartialEq)]
struct Record1 {
_1: i64,
}
impl Type<Postgres> for Record1 {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("_sqlx_record_1")
}
}
impl Encode<Postgres> for Record1 {
fn encode(&self, buf: &mut PgRawBuffer) {
PgRecordEncoder::new(buf).encode(self._1).finish();
}
}
impl<'de> Decode<'de, Postgres> for Record1 {
fn decode(value: PgValue<'de>) -> sqlx::Result<Self> {
let mut decoder = PgRecordDecoder::new(value)?;
let _1 = decoder.decode()?;
Ok(Record1 { _1 })
}
}
let rec: (Record1, Record1) = sqlx::query_as("SELECT '(324235)'::_sqlx_record_1, $1")
.bind(Record1 { _1: 324235 })
.fetch_one(&mut conn)
.await?;
assert_eq!(rec.0, rec.1);
Ok(())
}
//
// JSON
//
#[cfg(feature = "json")]
mod json {
use super::*;
use serde_json::value::RawValue;
use serde_json::{json, Value as JsonValue};
use sqlx::postgres::PgRow;
use sqlx::types::Json;
use sqlx::Row;
// When testing JSON, coerce to JSONB for `=` comparison as `JSON = JSON` is not
// supported in PostgreSQL
test_type!(json(
Postgres,
JsonValue,
"SELECT {0}::jsonb is not distinct from $1::jsonb, $2::text as _1, {0} as _2, $3 as _3",
"'\"Hello, World\"'::json" == json!("Hello, World"),
"'\"😎\"'::json" == json!("😎"),
"'\"🙋‍♀️\"'::json" == json!("🙋‍♀️"),
"'[\"Hello\", \"World!\"]'::json" == json!(["Hello", "World!"])
));
test_type!(jsonb(
Postgres,
JsonValue,
"'\"Hello, World\"'::jsonb" == json!("Hello, World"),
"'\"😎\"'::jsonb" == json!("😎"),
"'\"🙋‍♀️\"'::jsonb" == json!("🙋‍♀️"),
"'[\"Hello\", \"World!\"]'::jsonb" == json!(["Hello", "World!"])
));
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq)]
struct Friend {
name: String,
age: u32,
}
test_type!(jsonb_struct(Postgres, Json<Friend>,
"'{\"name\":\"Joe\",\"age\":33}'::jsonb" == Json(Friend { name: "Joe".to_string(), age: 33 })
));
test_type!(json_struct(
Postgres,
Json<Friend>,
"SELECT {0}::jsonb is not distinct from $1::jsonb, $2::text as _1, {0} as _2, $3 as _3",
"'{\"name\":\"Joe\",\"age\":33}'::json" == Json(Friend { name: "Joe".to_string(), age: 33 })
));
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_prepared_jsonb_raw_value() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let mut cursor = sqlx::query("SELECT '{\"hello\": \"world\"}'::jsonb").fetch(&mut conn);
let row: PgRow = cursor.next().await?.unwrap();
let value: &RawValue = row.get::<&RawValue, usize>(0_usize);
assert_eq!(value.get(), "{\"hello\": \"world\"}");
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_json_value_in_macro() -> anyhow::Result<()> {
let mut conn = sqlx_test::new::<Postgres>().await?;
let v: serde_json::Value = json!({
"name": "Joe".to_string(),
"age": 33
});
let res = sqlx::query!(
"SELECT '{\"name\":\"Joe\",\"age\":33}'::jsonb as _1, $1::jsonb as _2",
v,
)
.fetch_one(&mut conn)
.await?;
assert_eq!(Some(v), res._1);
assert_eq!(res._1, res._2);
Ok(())
}
}

View File

@ -1,385 +0,0 @@
use futures::TryStreamExt;
use sqlx::postgres::{PgPool, PgQueryAs, PgRow};
use sqlx::{Connection, Cursor, Executor, Postgres, Row};
use sqlx_test::new;
use std::time::Duration;
// TODO: As soon as I tried to deserialize a json value in a function, inferance for this test stopped working. I am at a loss as to how to resolve this.
#[cfg(not(feature = "json"))]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_connects() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let value = sqlx::query("select 1 + 1")
.try_map(|row: PgRow| row.try_get::<i32, _>(0))
.fetch_one(&mut conn)
.await?;
assert_eq!(2i32, value);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_executes() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY);
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES ($1)")
.bind(index)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query("SELECT id FROM users")
.try_map(|row: PgRow| row.try_get::<i32, _>(0))
.fetch(&mut conn)
.try_fold(0_i32, |acc, x| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 55);
Ok(())
}
// https://github.com/launchbadge/sqlx/issues/104
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_can_return_interleaved_nulls_issue_104() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let tuple = sqlx::query("SELECT NULL, 10::INT, NULL, 20::INT, NULL, 40::INT, NULL, 80::INT")
.try_map(|row: PgRow| {
Ok((
row.get::<Option<i32>, _>(0),
row.get::<Option<i32>, _>(1),
row.get::<Option<i32>, _>(2),
row.get::<Option<i32>, _>(3),
row.get::<Option<i32>, _>(4),
row.get::<Option<i32>, _>(5),
row.get::<Option<i32>, _>(6),
row.get::<Option<i32>, _>(7),
))
})
.fetch_one(&mut conn)
.await?;
assert_eq!(tuple.0, None);
assert_eq!(tuple.1, Some(10));
assert_eq!(tuple.2, None);
assert_eq!(tuple.3, Some(20));
assert_eq!(tuple.4, None);
assert_eq!(tuple.5, Some(40));
assert_eq!(tuple.6, None);
assert_eq!(tuple.7, Some(80));
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_can_work_with_transactions() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_1922 (id INTEGER PRIMARY KEY)")
.await?;
conn.execute("TRUNCATE _sqlx_users_1922").await?;
// begin .. rollback
let mut tx = conn.begin().await?;
sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
.bind(10_i32)
.execute(&mut tx)
.await?;
conn = tx.rollback().await?;
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
.fetch_one(&mut conn)
.await?;
assert_eq!(count, 0);
// begin .. commit
let mut tx = conn.begin().await?;
sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
.bind(10_i32)
.execute(&mut tx)
.await?;
conn = tx.commit().await?;
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
.fetch_one(&mut conn)
.await?;
assert_eq!(count, 1);
// begin .. (drop)
{
let mut tx = conn.begin().await?;
sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
.bind(20_i32)
.execute(&mut tx)
.await?;
}
conn = new::<Postgres>().await?;
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
.fetch_one(&mut conn)
.await?;
assert_eq!(count, 1);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_2523 (id INTEGER PRIMARY KEY)")
.await?;
conn.execute("TRUNCATE _sqlx_users_2523").await?;
// begin
let mut tx = conn.begin().await?;
// insert a user
sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)")
.bind(50_i32)
.execute(&mut tx)
.await?;
// begin once more
let mut tx = tx.begin().await?;
// insert another user
sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)")
.bind(10_i32)
.execute(&mut tx)
.await?;
// never mind, rollback
let mut tx = tx.rollback().await?;
// did we really?
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523")
.fetch_one(&mut tx)
.await?;
assert_eq!(count, 1);
// actually, commit
let mut conn = tx.commit().await?;
// did we really?
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523")
.fetch_one(&mut conn)
.await?;
assert_eq!(count, 1);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_can_rollback_nested_transactions() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_512412 (id INTEGER PRIMARY KEY)")
.await?;
conn.execute("TRUNCATE _sqlx_users_512412").await?;
// begin
let mut tx = conn.begin().await?;
// insert a user
sqlx::query("INSERT INTO _sqlx_users_512412 (id) VALUES ($1)")
.bind(50_i32)
.execute(&mut tx)
.await?;
// begin once more
let mut tx = tx.begin().await?;
// insert another user
sqlx::query("INSERT INTO _sqlx_users_512412 (id) VALUES ($1)")
.bind(10_i32)
.execute(&mut tx)
.await?;
// stop the phone, drop the entire transaction
tx.close().await?;
// did we really?
let mut conn = new::<Postgres>().await?;
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_512412")
.fetch_one(&mut conn)
.await?;
assert_eq!(count, 0);
Ok(())
}
// run with `cargo test --features postgres -- --ignored --nocapture pool_smoke_test`
#[ignore]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn pool_smoke_test() -> anyhow::Result<()> {
#[cfg(feature = "runtime-tokio")]
use tokio::{task::spawn, time::delay_for as sleep, time::timeout};
#[cfg(feature = "runtime-async-std")]
use async_std::{future::timeout, task::sleep, task::spawn};
eprintln!("starting pool");
let pool = PgPool::builder()
.connect_timeout(Duration::from_secs(5))
.min_size(5)
.max_size(10)
.build(&dotenv::var("DATABASE_URL")?)
.await?;
// spin up more tasks than connections available, and ensure we don't deadlock
for i in 0..20 {
let pool = pool.clone();
spawn(async move {
loop {
if let Err(e) = sqlx::query("select 1 + 1").execute(&pool).await {
eprintln!("pool task {} dying due to {}", i, e);
break;
}
}
});
}
for _ in 0..5 {
let pool = pool.clone();
spawn(async move {
while !pool.is_closed() {
// drop acquire() futures in a hot loop
// https://github.com/launchbadge/sqlx/issues/83
drop(pool.acquire());
}
});
}
eprintln!("sleeping for 30 seconds");
sleep(Duration::from_secs(30)).await;
assert_eq!(pool.size(), 10);
eprintln!("closing pool");
timeout(Duration::from_secs(30), pool.close()).await?;
eprintln!("pool closed successfully");
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_invalid_query() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
conn.execute("definitely not a correct query")
.await
.unwrap_err();
let mut cursor = conn.fetch("select 1");
let row = cursor.next().await?.unwrap();
assert_eq!(row.get::<i32, _>(0), 1i32);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_describe() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let _ = conn
.execute(
r#"
CREATE TEMP TABLE describe_test (
id SERIAL primary key,
name text not null,
hash bytea
)
"#,
)
.await?;
let describe = conn
.describe("select nt.*, false from describe_test nt")
.await?;
assert_eq!(describe.result_columns[0].non_null, Some(true));
assert_eq!(
describe.result_columns[0]
.type_info
.as_ref()
.unwrap()
.to_string(),
"INT4"
);
assert_eq!(describe.result_columns[1].non_null, Some(true));
assert_eq!(
describe.result_columns[1]
.type_info
.as_ref()
.unwrap()
.to_string(),
"TEXT"
);
assert_eq!(describe.result_columns[2].non_null, Some(false));
assert_eq!(
describe.result_columns[2]
.type_info
.as_ref()
.unwrap()
.to_string(),
"BYTEA"
);
assert_eq!(describe.result_columns[3].non_null, None);
assert_eq!(
describe.result_columns[3]
.type_info
.as_ref()
.unwrap()
.to_string(),
"BOOL"
);
Ok(())
}

10
tests/postgres/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
ARG VERSION
FROM postgres:${VERSION}-alpine
# Copy SSL certificate (and key)
COPY certs/server.crt /var/lib/postgresql/server.crt
COPY keys/server.key /var/lib/postgresql/server.key
# Fix permissions
RUN chown 70:70 /var/lib/postgresql/server.crt /var/lib/postgresql/server.key
RUN chmod 0600 /var/lib/postgresql/server.crt /var/lib/postgresql/server.key

View File

@ -96,8 +96,7 @@ test_type!(strong_enum(
"'four'::text" == Strong::Three
));
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_enum_type() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -231,8 +230,7 @@ SELECT $1 = 'RED'::color_upper, $1
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_record_type() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -275,8 +273,7 @@ END $$;
}
#[cfg(feature = "macros")]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_from_row() -> anyhow::Result<()> {
// Needed for PgQueryAs
use sqlx::prelude::*;
@ -322,8 +319,7 @@ async fn test_from_row() -> anyhow::Result<()> {
}
#[cfg(feature = "macros")]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_from_row_with_keyword() -> anyhow::Result<()> {
use sqlx::prelude::*;
@ -355,8 +351,7 @@ async fn test_from_row_with_keyword() -> anyhow::Result<()> {
}
#[cfg(feature = "macros")]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_from_row_with_rename() -> anyhow::Result<()> {
use sqlx::prelude::*;

114
tests/postgres/describe.rs Normal file
View File

@ -0,0 +1,114 @@
use sqlx::{postgres::Postgres, Executor};
use sqlx_core::describe::Column;
use sqlx_test::new;
fn type_names(columns: &[Column<Postgres>]) -> Vec<String> {
columns
.iter()
.filter_map(|col| Some(col.type_info.as_ref()?.to_string()))
.collect()
}
#[sqlx_macros::test]
async fn it_describes_simple() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let d = conn.describe("SELECT * FROM tweet").await?;
let columns = d.columns;
assert_eq!(columns[0].name, "id");
assert_eq!(columns[1].name, "created_at");
assert_eq!(columns[2].name, "text");
assert_eq!(columns[3].name, "owner_id");
assert_eq!(columns[0].not_null, Some(true));
assert_eq!(columns[1].not_null, Some(true));
assert_eq!(columns[2].not_null, Some(true));
assert_eq!(columns[3].not_null, Some(false));
let column_type_names = type_names(&columns);
assert_eq!(column_type_names[0], "int8");
assert_eq!(column_type_names[1], "timestamptz");
assert_eq!(column_type_names[2], "text");
assert_eq!(column_type_names[3], "int8");
Ok(())
}
#[sqlx_macros::test]
async fn it_describes_expression() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let d = conn.describe("SELECT 1::int8 + 10").await?;
let columns = d.columns;
// ?column? will cause the macro to emit an error ad ask the user to explicitly name the type
assert_eq!(columns[0].name, "?column?");
// postgres cannot infer nullability from an expression
// this will cause the macro to emit `Option<_>`
assert_eq!(columns[0].not_null, None);
let column_type_names = type_names(&columns);
assert_eq!(column_type_names[0], "int8");
Ok(())
}
#[sqlx_macros::test]
async fn it_describes_enum() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let d = conn.describe("SELECT 'open'::status as _1").await?;
let columns = d.columns;
assert_eq!(columns[0].name, "_1");
assert_eq!(columns[0].not_null, None);
let ty = columns[0].type_info.as_ref().unwrap();
assert_eq!(ty.to_string(), "status");
assert_eq!(
format!("{:?}", ty.__kind()),
r#"Enum(["new", "open", "closed"])"#
);
Ok(())
}
#[sqlx_macros::test]
async fn it_describes_record() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let d = conn.describe("SELECT (true, 10::int2)").await?;
let columns = d.columns;
let ty = columns[0].type_info.as_ref().unwrap();
assert_eq!(ty.to_string(), "record");
Ok(())
}
#[sqlx_macros::test]
async fn it_describes_composite() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let d = conn
.describe("SELECT ROW('name',10,500)::inventory_item")
.await?;
let columns = d.columns;
let ty = columns[0].type_info.as_ref().unwrap();
assert_eq!(ty.to_string(), "inventory_item");
assert_eq!(
format!("{:?}", ty.__kind()),
r#"Composite([("name", PgTypeInfo(Text)), ("supplier_id", PgTypeInfo(Int4)), ("price", PgTypeInfo(Int8))])"#
);
Ok(())
}

View File

@ -3,8 +3,7 @@ use sqlx_test::new;
use futures::TryStreamExt;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_query() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -20,8 +19,7 @@ async fn test_query() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_no_result() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -32,8 +30,7 @@ async fn test_no_result() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_text_var_char_char_n() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -64,8 +61,7 @@ async fn test_text_var_char_char_n() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn _file() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -84,8 +80,7 @@ struct Account {
name: Option<String>,
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_query_as() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -111,8 +106,7 @@ struct RawAccount {
name: Option<String>,
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_query_as_raw() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -131,8 +125,7 @@ async fn test_query_as_raw() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_query_file_as() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -145,8 +138,7 @@ async fn test_query_file_as() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn query_by_string() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -172,8 +164,7 @@ async fn query_by_string() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_nullable_err() -> anyhow::Result<()> {
#[derive(Debug)]
struct Account {
@ -200,8 +191,7 @@ async fn test_nullable_err() -> anyhow::Result<()> {
panic!("expected `UnexpectedNullError`, got {}", err)
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_many_args() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -221,8 +211,7 @@ async fn test_many_args() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn test_array_from_slice() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
@ -247,8 +236,7 @@ async fn test_array_from_slice() -> anyhow::Result<()> {
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[sqlx_macros::test]
async fn fetch_is_usable_issue_224() -> anyhow::Result<()> {
// ensures that the stream returned by `query::Map::fetch()` is usable with `TryStreamExt`
let mut conn = new::<Postgres>().await?;

443
tests/postgres/postgres.rs Normal file
View File

@ -0,0 +1,443 @@
use futures::TryStreamExt;
use sqlx::postgres::PgRow;
use sqlx::{postgres::Postgres, Executor, Row};
use sqlx_test::new;
#[sqlx_macros::test]
async fn it_connects() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let value = sqlx::query("select 1 + 1")
.try_map(|row: PgRow| row.try_get::<i32, _>(0))
.fetch_one(&mut conn)
.await?;
assert_eq!(2i32, value);
Ok(())
}
#[sqlx_macros::test]
async fn it_maths() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let value = sqlx::query("select 1 + $1::int")
.bind(5_i32)
.try_map(|row: PgRow| row.try_get::<i32, _>(0))
.fetch_one(&mut conn)
.await?;
assert_eq!(6i32, value);
Ok(())
}
#[sqlx_macros::test]
async fn it_executes() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY);
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES ($1)")
.bind(index)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query("SELECT id FROM users")
.try_map(|row: PgRow| row.try_get::<i32, _>(0))
.fetch(&mut conn)
.try_fold(0_i32, |acc, x| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 55);
Ok(())
}
// https://github.com/launchbadge/sqlx/issues/104
#[sqlx_macros::test]
async fn it_can_return_interleaved_nulls_issue_104() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let tuple = sqlx::query("SELECT NULL, 10::INT, NULL, 20::INT, NULL, 40::INT, NULL, 80::INT")
.try_map(|row: PgRow| {
Ok((
row.get::<Option<i32>, _>(0),
row.get::<Option<i32>, _>(1),
row.get::<Option<i32>, _>(2),
row.get::<Option<i32>, _>(3),
row.get::<Option<i32>, _>(4),
row.get::<Option<i32>, _>(5),
row.get::<Option<i32>, _>(6),
row.get::<Option<i32>, _>(7),
))
})
.fetch_one(&mut conn)
.await?;
assert_eq!(tuple.0, None);
assert_eq!(tuple.1, Some(10));
assert_eq!(tuple.2, None);
assert_eq!(tuple.3, Some(20));
assert_eq!(tuple.4, None);
assert_eq!(tuple.5, Some(40));
assert_eq!(tuple.6, None);
assert_eq!(tuple.7, Some(80));
Ok(())
}
#[sqlx_macros::test]
async fn it_can_query_scalar() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let scalar: i32 = sqlx::query_scalar("SELECT 42").fetch_one(&mut conn).await?;
assert_eq!(scalar, 42);
let scalar: Option<i32> = sqlx::query_scalar("SELECT 42").fetch_one(&mut conn).await?;
assert_eq!(scalar, Some(42));
let scalar: Option<i32> = sqlx::query_scalar("SELECT NULL")
.fetch_one(&mut conn)
.await?;
assert_eq!(scalar, None);
let scalar: Option<i64> = sqlx::query_scalar("SELECT 42::bigint")
.fetch_optional(&mut conn)
.await?;
assert_eq!(scalar, Some(42));
let scalar: Option<i16> = sqlx::query_scalar("").fetch_optional(&mut conn).await?;
assert_eq!(scalar, None);
Ok(())
}
#[sqlx_macros::test]
/// This is seperate from `it_can_query_scalar` because while implementing it I ran into a
/// bug which that prevented `Vec<i32>` from compiling but allowed Vec<Option<i32>>.
async fn it_can_query_all_scalar() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let scalar: Vec<i32> = sqlx::query_scalar("SELECT $1")
.bind(42)
.fetch_all(&mut conn)
.await?;
assert_eq!(scalar, vec![42]);
let scalar: Vec<Option<i32>> = sqlx::query_scalar("SELECT $1 UNION ALL SELECT NULL")
.bind(42)
.fetch_all(&mut conn)
.await?;
assert_eq!(scalar, vec![Some(42), None]);
Ok(())
}
// #[cfg_attr(feature = "runtime-async-std", async_std::test)]
// #[cfg_attr(feature = "runtime-tokio", tokio::test)]
// async fn it_can_work_with_transactions() -> anyhow::Result<()> {
// let mut conn = new::<Postgres>().await?;
//
// conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_1922 (id INTEGER PRIMARY KEY)")
// .await?;
//
// conn.execute("TRUNCATE _sqlx_users_1922").await?;
//
// // begin .. rollback
//
// let mut tx = conn.begin().await?;
//
// sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
// .bind(10_i32)
// .execute(&mut tx)
// .await?;
//
// conn = tx.rollback().await?;
//
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
// .fetch_one(&mut conn)
// .await?;
//
// assert_eq!(count, 0);
//
// // begin .. commit
//
// let mut tx = conn.begin().await?;
//
// sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
// .bind(10_i32)
// .execute(&mut tx)
// .await?;
//
// conn = tx.commit().await?;
//
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
// .fetch_one(&mut conn)
// .await?;
//
// assert_eq!(count, 1);
//
// // begin .. (drop)
//
// {
// let mut tx = conn.begin().await?;
//
// sqlx::query("INSERT INTO _sqlx_users_1922 (id) VALUES ($1)")
// .bind(20_i32)
// .execute(&mut tx)
// .await?;
// }
//
// conn = new::<Postgres>().await?;
//
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_1922")
// .fetch_one(&mut conn)
// .await?;
//
// assert_eq!(count, 1);
//
// Ok(())
// }
//
// #[cfg_attr(feature = "runtime-async-std", async_std::test)]
// #[cfg_attr(feature = "runtime-tokio", tokio::test)]
// async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> {
// let mut conn = new::<Postgres>().await?;
//
// conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_2523 (id INTEGER PRIMARY KEY)")
// .await?;
//
// conn.execute("TRUNCATE _sqlx_users_2523").await?;
//
// // begin
// let mut tx = conn.begin().await?;
//
// // insert a user
// sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)")
// .bind(50_i32)
// .execute(&mut tx)
// .await?;
//
// // begin once more
// let mut tx = tx.begin().await?;
//
// // insert another user
// sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)")
// .bind(10_i32)
// .execute(&mut tx)
// .await?;
//
// // never mind, rollback
// let mut tx = tx.rollback().await?;
//
// // did we really?
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523")
// .fetch_one(&mut tx)
// .await?;
//
// assert_eq!(count, 1);
//
// // actually, commit
// let mut conn = tx.commit().await?;
//
// // did we really?
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523")
// .fetch_one(&mut conn)
// .await?;
//
// assert_eq!(count, 1);
//
// Ok(())
// }
//
// #[cfg_attr(feature = "runtime-async-std", async_std::test)]
// #[cfg_attr(feature = "runtime-tokio", tokio::test)]
// async fn it_can_rollback_nested_transactions() -> anyhow::Result<()> {
// let mut conn = new::<Postgres>().await?;
//
// conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_512412 (id INTEGER PRIMARY KEY)")
// .await?;
//
// conn.execute("TRUNCATE _sqlx_users_512412").await?;
//
// // begin
// let mut tx = conn.begin().await?;
//
// // insert a user
// sqlx::query("INSERT INTO _sqlx_users_512412 (id) VALUES ($1)")
// .bind(50_i32)
// .execute(&mut tx)
// .await?;
//
// // begin once more
// let mut tx = tx.begin().await?;
//
// // insert another user
// sqlx::query("INSERT INTO _sqlx_users_512412 (id) VALUES ($1)")
// .bind(10_i32)
// .execute(&mut tx)
// .await?;
//
// // stop the phone, drop the entire transaction
// tx.close().await?;
//
// // did we really?
// let mut conn = new::<Postgres>().await?;
// let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_512412")
// .fetch_one(&mut conn)
// .await?;
//
// assert_eq!(count, 0);
//
// Ok(())
// }
//
// // run with `cargo test --features postgres -- --ignored --nocapture pool_smoke_test`
// #[ignore]
// #[cfg_attr(feature = "runtime-async-std", async_std::test)]
// #[cfg_attr(feature = "runtime-tokio", tokio::test)]
// async fn pool_smoke_test() -> anyhow::Result<()> {
// #[cfg(feature = "runtime-tokio")]
// use tokio::{task::spawn, time::delay_for as sleep, time::timeout};
//
// #[cfg(feature = "runtime-async-std")]
// use async_std::{future::timeout, task::sleep, task::spawn};
//
// eprintln!("starting pool");
//
// let pool = PgPool::builder()
// .connect_timeout(Duration::from_secs(5))
// .min_size(5)
// .max_size(10)
// .build(&dotenv::var("DATABASE_URL")?)
// .await?;
//
// // spin up more tasks than connections available, and ensure we don't deadlock
// for i in 0..20 {
// let pool = pool.clone();
// spawn(async move {
// loop {
// if let Err(e) = sqlx::query("select 1 + 1").execute(&pool).await {
// eprintln!("pool task {} dying due to {}", i, e);
// break;
// }
// }
// });
// }
//
// for _ in 0..5 {
// let pool = pool.clone();
// spawn(async move {
// while !pool.is_closed() {
// // drop acquire() futures in a hot loop
// // https://github.com/launchbadge/sqlx/issues/83
// drop(pool.acquire());
// }
// });
// }
//
// eprintln!("sleeping for 30 seconds");
//
// sleep(Duration::from_secs(30)).await;
//
// assert_eq!(pool.size(), 10);
//
// eprintln!("closing pool");
//
// timeout(Duration::from_secs(30), pool.close()).await?;
//
// eprintln!("pool closed successfully");
//
// Ok(())
// }
#[sqlx_macros::test]
async fn test_invalid_query() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
conn.execute("definitely not a correct query")
.await
.unwrap_err();
let mut s = conn.fetch("select 1");
let row = s.try_next().await?.unwrap();
assert_eq!(row.get::<i32, _>(0), 1i32);
Ok(())
}
/// Tests the edge case of executing a completely empty query string.
///
/// This gets flagged as an `EmptyQueryResponse` in Postgres. We
/// catch this and just return no rows.
#[sqlx_macros::test]
async fn test_empty_query() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let affected = conn.execute("").await?;
assert_eq!(affected, 0);
Ok(())
}
/// Test a simple select expression. This should return the row.
#[sqlx_macros::test]
async fn test_select_expression() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let mut s = conn.fetch("SELECT 5");
let row = s.try_next().await?.unwrap();
assert!(5i32 == row.try_get::<i32, _>(0)?);
Ok(())
}
/// Test that we can interleave reads and writes to the database
/// in one simple query. Using the `Cursor` API we should be
/// able to fetch from both queries in sequence.
#[sqlx_macros::test]
async fn test_multi_read_write() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
let mut s = conn.fetch(
"
CREATE TABLE IF NOT EXISTS _sqlx_test_postgres_5112 (
id BIGSERIAL PRIMARY KEY,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO _sqlx_test_postgres_5112 (text) VALUES ('this is a test');
SELECT id, text FROM _sqlx_test_postgres_5112;
",
);
let row = s.try_next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = s.try_next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(1_i64, id);
assert_eq!("this is a test", text);
Ok(())
}

19
tests/postgres/setup.sql Normal file
View File

@ -0,0 +1,19 @@
-- https://www.postgresql.org/docs/current/sql-createtype.html
CREATE TYPE status AS ENUM ('new', 'open', 'closed');
-- https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-DECLARING
CREATE TYPE inventory_item AS
(
name TEXT,
supplier_id INT,
price BIGINT
);
-- https://github.com/prisma/database-schema-examples/tree/master/postgres/basic-twitter#basic-twitter
CREATE TABLE tweet
(
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
text TEXT NOT NULL,
owner_id BIGINT
);

335
tests/postgres/types.rs Normal file
View File

@ -0,0 +1,335 @@
extern crate time_ as time;
use sqlx::postgres::Postgres;
use sqlx_test::{test_decode_type, test_prepared_type, test_type};
test_type!(null<Option<i16>>(Postgres,
"NULL::int2" == None::<i16>
));
test_type!(null_vec<Vec<Option<i16>>>(Postgres,
"array[10,NULL,50]::int2[]" == vec![Some(10_i16), None, Some(50)],
));
test_type!(bool<bool>(Postgres,
"false::boolean" == false,
"true::boolean" == true
));
test_type!(bool_vec<Vec<bool>>(Postgres,
"array[true,false,true]::bool[]" == vec![true, false, true],
));
test_type!(byte_vec<Vec<u8>>(Postgres,
"E'\\\\xDEADBEEF'::bytea"
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
"E'\\\\x'::bytea"
== Vec::<u8>::new(),
"E'\\\\x0000000052'::bytea"
== vec![0_u8, 0, 0, 0, 0x52]
));
// BYTEA cannot be decoded by-reference from a simple query as postgres sends it as hex
test_prepared_type!(byte_slice<&[u8]>(Postgres,
"E'\\\\xDEADBEEF'::bytea"
== &[0xDE_u8, 0xAD, 0xBE, 0xEF][..],
"E'\\\\x0000000052'::bytea"
== &[0_u8, 0, 0, 0, 0x52][..]
));
test_type!(str<&str>(Postgres,
"'this is foo'" == "this is foo",
"''" == "",
"'identifier'::name" == "identifier",
"'five'::char(4)" == "five",
"'more text'::varchar" == "more text",
));
test_type!(string<String>(Postgres,
"'this is foo'" == format!("this is foo"),
));
test_type!(string_vec<Vec<String>>(Postgres,
"array['one','two','three']::text[]"
== vec!["one","two","three"],
"array['', '\"']::text[]"
== vec!["", "\""],
"array['Hello, World', '', 'Goodbye']::text[]"
== vec!["Hello, World", "", "Goodbye"]
));
test_type!(i8(
Postgres,
"0::\"char\"" == 0_i8,
"120::\"char\"" == 120_i8,
));
test_type!(u32(Postgres, "325235::oid" == 325235_u32,));
test_type!(i16(
Postgres,
"-2144::smallint" == -2144_i16,
"821::smallint" == 821_i16,
));
test_type!(i32(
Postgres,
"94101::int" == 94101_i32,
"-5101::int" == -5101_i32
));
test_type!(i32_vec<Vec<i32>>(Postgres,
"'{5,10,50,100}'::int[]" == vec![5_i32, 10, 50, 100],
"'{1050}'::int[]" == vec![1050_i32],
"'{}'::int[]" == Vec::<i32>::new(),
"'{1,3,-5}'::int[]" == vec![1_i32, 3, -5]
));
test_type!(i64(Postgres, "9358295312::bigint" == 9358295312_i64));
test_type!(f32(Postgres, "9419.122::real" == 9419.122_f32));
test_type!(f64(
Postgres,
"939399419.1225182::double precision" == 939399419.1225182_f64
));
test_type!(f64_vec<Vec<f64>>(Postgres,
"'{939399419.1225182,-12.0}'::float8[]" == vec![939399419.1225182_f64, -12.0]
));
test_decode_type!(bool_tuple<(bool,)>(Postgres, "row(true)" == (true,)));
test_decode_type!(num_tuple<(i32, i64, f64,)>(Postgres, "row(10,515::int8,3.124::float8)" == (10,515,3.124)));
test_decode_type!(empty_tuple<()>(Postgres, "row()" == ()));
test_decode_type!(string_tuple<(String, String, String)>(Postgres,
"row('one','two','three')"
== ("one".to_string(), "two".to_string(), "three".to_string()),
"row('', '\"', '\"\"\"\"\"\"')"
== ("".to_string(), "\"".to_string(), "\"\"\"\"\"\"".to_string()),
"row('Hello, World', '', 'Goodbye')"
== ("Hello, World".to_string(), "".to_string(), "Goodbye".to_string())
));
#[cfg(feature = "uuid")]
test_type!(uuid<sqlx::types::Uuid>(Postgres,
"'b731678f-636f-4135-bc6f-19440c13bd19'::uuid"
== sqlx::types::Uuid::parse_str("b731678f-636f-4135-bc6f-19440c13bd19").unwrap(),
"'00000000-0000-0000-0000-000000000000'::uuid"
== sqlx::types::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()
));
#[cfg(feature = "uuid")]
test_type!(uuid_vec<Vec<sqlx::types::Uuid>>(Postgres,
"'{b731678f-636f-4135-bc6f-19440c13bd19,00000000-0000-0000-0000-000000000000}'::uuid[]"
== vec![
sqlx::types::Uuid::parse_str("b731678f-636f-4135-bc6f-19440c13bd19").unwrap(),
sqlx::types::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()
]
));
#[cfg(feature = "ipnetwork")]
test_type!(ipnetwork<sqlx::types::ipnetwork::IpNetwork>(Postgres,
"'127.0.0.1'::inet"
== "127.0.0.1"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'8.8.8.8/24'::inet"
== "8.8.8.8/24"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'::ffff:1.2.3.0'::inet"
== "::ffff:1.2.3.0"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'2001:4f8:3:ba::/64'::inet"
== "2001:4f8:3:ba::/64"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'192.168'::cidr"
== "192.168.0.0/24"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
"'::ffff:1.2.3.0/120'::cidr"
== "::ffff:1.2.3.0/120"
.parse::<sqlx::types::ipnetwork::IpNetwork>()
.unwrap(),
));
#[cfg(feature = "ipnetwork")]
test_type!(ipnetwork_vec<Vec<sqlx::types::ipnetwork::IpNetwork>>(Postgres,
"'{127.0.0.1,8.8.8.8/24}'::inet[]"
== vec![
"127.0.0.1".parse::<sqlx::types::ipnetwork::IpNetwork>().unwrap(),
"8.8.8.8/24".parse::<sqlx::types::ipnetwork::IpNetwork>().unwrap()
]
));
#[cfg(feature = "chrono")]
mod chrono {
use super::*;
use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
test_type!(chrono_date<NaiveDate>(Postgres,
"DATE '2001-01-05'" == NaiveDate::from_ymd(2001, 1, 5),
"DATE '2050-11-23'" == NaiveDate::from_ymd(2050, 11, 23)
));
test_type!(chrono_time<NaiveTime>(Postgres,
"TIME '05:10:20.115100'" == NaiveTime::from_hms_micro(5, 10, 20, 115100)
));
test_type!(chrono_date_time<NaiveDateTime>(Postgres,
"'2019-01-02 05:10:20'::timestamp" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
));
test_type!(chrono_date_time_vec<Vec<NaiveDateTime>>(Postgres,
"array['2019-01-02 05:10:20']::timestamp[]"
== vec![NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)]
));
test_type!(chrono_date_time_tz<DateTime::<Utc>>(Postgres,
"TIMESTAMPTZ '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,
)
));
test_type!(chrono_date_time_tz_vec<Vec<DateTime::<Utc>>>(Postgres,
"array['2019-01-02 05:10:20.115100']::timestamptz[]"
== vec![
DateTime::<Utc>::from_utc(
NaiveDate::from_ymd(2019, 1, 2).and_hms_micro(5, 10, 20, 115100),
Utc,
)
]
));
}
#[cfg(feature = "time")]
mod time_tests {
use super::*;
use sqlx::types::time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use time::{date, time};
test_type!(time_date<Date>(
Postgres,
"DATE '2001-01-05'" == date!(2001 - 1 - 5),
"DATE '2050-11-23'" == date!(2050 - 11 - 23)
));
test_type!(time_time<Time>(
Postgres,
"TIME '05:10:20.115100'" == time!(5:10:20.115100)
));
test_type!(time_date_time<PrimitiveDateTime>(
Postgres,
"TIMESTAMP '2019-01-02 05:10:20'" == date!(2019 - 1 - 2).with_time(time!(5:10:20)),
"TIMESTAMP '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2).with_time(time!(5:10:20.115100))
));
test_type!(time_timestamp<OffsetDateTime>(
Postgres,
"TIMESTAMPTZ '2019-01-02 05:10:20.115100'"
== date!(2019 - 1 - 2)
.with_time(time!(5:10:20.115100))
.assume_utc()
));
}
#[cfg(feature = "json")]
mod json {
use super::*;
use serde_json::value::RawValue as JsonRawValue;
use serde_json::{json, Value as JsonValue};
use sqlx::postgres::PgRow;
use sqlx::types::Json;
use sqlx::{Executor, Row};
use sqlx_test::new;
// When testing JSON, coerce to JSONB for `=` comparison as `JSON = JSON` is not
// supported in PostgreSQL
test_type!(json<JsonValue>(
Postgres,
"SELECT {0}::jsonb is not distinct from $1::jsonb, {0} as _2, $2 as _3",
"'\"Hello, World\"'::json" == json!("Hello, World"),
"'\"😎\"'::json" == json!("😎"),
"'\"🙋‍♀️\"'::json" == json!("🙋‍♀️"),
"'[\"Hello\", \"World!\"]'::json" == json!(["Hello", "World!"])
));
test_type!(jsonb<JsonValue>(
Postgres,
"'\"Hello, World\"'::jsonb" == json!("Hello, World"),
"'\"😎\"'::jsonb" == json!("😎"),
"'\"🙋‍♀️\"'::jsonb" == json!("🙋‍♀️"),
"'[\"Hello\", \"World!\"]'::jsonb" == json!(["Hello", "World!"])
));
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq)]
struct Friend {
name: String,
age: u32,
}
test_type!(json_struct<Json<Friend>>(Postgres,
"'{\"name\":\"Joe\",\"age\":33}'::jsonb" == Json(Friend { name: "Joe".to_string(), age: 33 })
));
test_type!(json_struct_vec<Vec<Json<Friend>>>(Postgres,
"array['{\"name\":\"Joe\",\"age\":33}','{\"name\":\"Bob\",\"age\":22}']::jsonb[]"
== vec![
Json(Friend { name: "Joe".to_string(), age: 33 }),
Json(Friend { name: "Bob".to_string(), age: 22 }),
]
));
#[sqlx_macros::test]
async fn test_json_raw_value() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
// unprepared, text API
let row: PgRow = conn
.fetch_one("SELECT '{\"hello\": \"world\"}'::jsonb")
.await?;
let value: &JsonRawValue = row.try_get(0)?;
assert_eq!(value.get(), "{\"hello\": \"world\"}");
// prepared, binary API
let row: PgRow = conn
.fetch_one(sqlx::query("SELECT '{\"hello\": \"world\"}'::jsonb"))
.await?;
let value: &JsonRawValue = row.try_get(0)?;
assert_eq!(value.get(), "{\"hello\": \"world\"}");
Ok(())
}
}
#[cfg(feature = "bigdecimal")]
test_type!(decimal<sqlx::types::BigDecimal>(Postgres,
// https://github.com/launchbadge/sqlx/issues/283
"0::numeric" == "0".parse::<sqlx::types::BigDecimal>().unwrap(),
"1::numeric" == "1".parse::<sqlx::types::BigDecimal>().unwrap(),
"10000::numeric" == "10000".parse::<sqlx::types::BigDecimal>().unwrap(),
"0.1::numeric" == "0.1".parse::<sqlx::types::BigDecimal>().unwrap(),
"0.01234::numeric" == "0.01234".parse::<sqlx::types::BigDecimal>().unwrap(),
"12.34::numeric" == "12.34".parse::<sqlx::types::BigDecimal>().unwrap(),
"12345.6789::numeric" == "12345.6789".parse::<sqlx::types::BigDecimal>().unwrap(),
));

View File

@ -1,70 +0,0 @@
use sqlx::Sqlite;
use sqlx_test::test_type;
use std::fmt::Debug;
// Transparent types are rust-side wrappers over DB types
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(transparent)]
struct Transparent(i32);
// "Weak" enums map to an integer type indicated by #[repr]
#[derive(PartialEq, Copy, Clone, Debug, sqlx::Type)]
#[repr(i32)]
enum Weak {
One = 0,
Two = 2,
Three = 4,
}
// "Strong" enums can map to TEXT or a custom enum
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "lowercase")]
enum ColorLower {
Red,
Green,
Blue,
}
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "snake_case")]
enum ColorSnake {
RedGreen,
BlueBlack,
}
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(rename_all = "uppercase")]
enum ColorUpper {
Red,
Green,
Blue,
}
test_type!(transparent(
Sqlite,
Transparent,
"0" == Transparent(0),
"23523" == Transparent(23523)
));
test_type!(weak_enum(
Sqlite,
Weak,
"0" == Weak::One,
"2" == Weak::Two,
"4" == Weak::Three
));
test_type!(strong_color_lower_enum(
Sqlite,
ColorLower,
"'green'" == ColorLower::Green
));
test_type!(strong_color_snake_enum(
Sqlite,
ColorSnake,
"'red_green'" == ColorSnake::RedGreen
));
test_type!(strong_color_upper_enum(
Sqlite,
ColorUpper,
"'GREEN'" == ColorUpper::Green
));

View File

@ -1,77 +0,0 @@
use sqlx::Sqlite;
use sqlx_test::new;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let account = sqlx::query!("select id, name, is_active from accounts where id = 1")
.fetch_one(&mut conn)
.await?;
assert_eq!(1, account.id);
assert_eq!("Herp Derpinson", account.name);
assert_eq!(account.is_active, None);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select_bind() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let account = sqlx::query!(
"select id, name, is_active from accounts where id = ?",
1i32
)
.fetch_one(&mut conn)
.await?;
assert_eq!(1, account.id);
assert_eq!("Herp Derpinson", account.name);
assert_eq!(account.is_active, None);
Ok(())
}
#[derive(Debug)]
struct RawAccount {
id: i32,
name: String,
is_active: Option<bool>,
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_query_as_raw() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let account = sqlx::query_as!(RawAccount, "SELECT id, name, is_active from accounts")
.fetch_one(&mut conn)
.await?;
assert_eq!(account.id, 1);
assert_eq!(account.name, "Herp Derpinson");
assert_eq!(account.is_active, None);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn macro_select_from_view() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let account = sqlx::query!("SELECT id, name, is_active from accounts_view")
.fetch_one(&mut conn)
.await?;
// SQLite tells us the true origin of these columns even through the view
assert_eq!(account.id, 1);
assert_eq!(account.name, "Herp Derpinson");
assert_eq!(account.is_active, None);
Ok(())
}

View File

@ -1,52 +0,0 @@
//! Tests for the raw (unprepared) query API for Sqlite.
use sqlx::{Cursor, Executor, Row, Sqlite};
use sqlx_test::new;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_select_expression() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let mut cursor = conn.fetch("SELECT 5");
let row = cursor.next().await?.unwrap();
assert!(5i32 == row.try_get::<i32, _>(0)?);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_multi_read_write() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let mut cursor = conn.fetch(
"
CREATE TABLE IF NOT EXISTS _sqlx_test (
id INT PRIMARY KEY,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO _sqlx_test (text) VALUES ('this is a test');
SELECT id, text FROM _sqlx_test;
",
);
let row = cursor.next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = cursor.next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(0, id);
assert_eq!("this is a test", text);
Ok(())
}

View File

@ -1,46 +0,0 @@
use sqlx::Sqlite;
use sqlx_test::test_type;
test_type!(null(
Sqlite,
Option<i32>,
"NULL" == None::<i32>
));
test_type!(bool(Sqlite, bool, "FALSE" == false, "TRUE" == true));
test_type!(i32(Sqlite, i32, "94101" == 94101_i32));
test_type!(i64(Sqlite, i64, "9358295312" == 9358295312_i64));
// NOTE: This behavior can be surprising. Floating-point parameters are widening to double which can
// result in strange rounding.
test_type!(f32(
Sqlite,
f32,
"3.1410000324249268" == 3.141f32 as f64 as f32
));
test_type!(f64(
Sqlite,
f64,
"939399419.1225182" == 939399419.1225182_f64
));
test_type!(string(
Sqlite,
String,
"'this is foo'" == "this is foo",
"''" == ""
));
test_type!(bytes(
Sqlite,
Vec<u8>,
"X'DEADBEEF'"
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
"X''"
== Vec::<u8>::new(),
"X'0000000052'"
== vec![0_u8, 0, 0, 0, 0x52]
));

View File

@ -1,212 +0,0 @@
use futures::TryStreamExt;
use sqlx::{sqlite::SqliteQueryAs, Connect, Connection, Executor, Sqlite, SqliteConnection};
use sqlx_test::new;
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_connects() -> anyhow::Result<()> {
Ok(new::<Sqlite>().await?.ping().await?)
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_fails_to_connect() -> anyhow::Result<()> {
// empty connection string
assert!(SqliteConnection::connect("").await.is_err());
assert!(
SqliteConnection::connect("sqlite:///please_do_not_run_sqlx_tests_as_root")
.await
.is_err()
);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_fails_to_parse() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let res = conn.execute("SEELCT 1").await;
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert_eq!("near \"SEELCT\": syntax error", err);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_handles_empty_queries() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let affected = conn.execute("").await?;
assert_eq!(affected, 0);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_executes() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY)
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES (?)")
.bind(index)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query_as("SELECT id FROM users")
.fetch(&mut conn)
.try_fold(0_i32, |acc, (x,): (i32,)| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 55);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_can_execute_multiple_statements() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let affected = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY, other INTEGER);
INSERT INTO users DEFAULT VALUES;
"#,
)
.await?;
assert_eq!(affected, 1);
for index in 2..5_i32 {
let (id, other): (i32, i32) = sqlx::query_as(
r#"
INSERT INTO users (other) VALUES (?);
SELECT id, other FROM users WHERE id = last_insert_rowid();
"#,
)
.bind(index)
.fetch_one(&mut conn)
.await?;
assert_eq!(id, index);
assert_eq!(other, index);
}
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn it_describes() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE describe_test (
_1 int primary key,
_2 text not null,
_3 blob,
_4 boolean,
_5 float,
_6 varchar(255),
_7 double,
_8 bigint
)
"#,
)
.await?;
let describe = conn
.describe("select nt.*, false from describe_test nt")
.await?;
assert_eq!(
describe.result_columns[0]
.type_info
.as_ref()
.unwrap()
.to_string(),
"INTEGER"
);
assert_eq!(
describe.result_columns[1]
.type_info
.as_ref()
.unwrap()
.to_string(),
"TEXT"
);
assert_eq!(
describe.result_columns[2]
.type_info
.as_ref()
.unwrap()
.to_string(),
"BLOB"
);
assert_eq!(
describe.result_columns[3]
.type_info
.as_ref()
.unwrap()
.to_string(),
"BOOLEAN"
);
assert_eq!(
describe.result_columns[4]
.type_info
.as_ref()
.unwrap()
.to_string(),
"DOUBLE"
);
assert_eq!(
describe.result_columns[5]
.type_info
.as_ref()
.unwrap()
.to_string(),
"TEXT"
);
assert_eq!(
describe.result_columns[6]
.type_info
.as_ref()
.unwrap()
.to_string(),
"DOUBLE"
);
assert_eq!(
describe.result_columns[7]
.type_info
.as_ref()
.unwrap()
.to_string(),
"INTEGER"
);
// Expressions can not be described
assert!(describe.result_columns[8].type_info.is_none());
Ok(())
}

54
tests/sqlite/describe.rs Normal file
View File

@ -0,0 +1,54 @@
use sqlx::{sqlite::Sqlite, Executor};
use sqlx_core::describe::Column;
use sqlx_test::new;
fn type_names(columns: &[Column<Sqlite>]) -> Vec<String> {
columns
.iter()
.filter_map(|col| Some(col.type_info.as_ref()?.to_string()))
.collect()
}
#[sqlx_macros::test]
async fn it_describes_simple() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let d = conn.describe("SELECT * FROM tweet").await?;
let columns = d.columns;
assert_eq!(columns[0].name, "id");
assert_eq!(columns[1].name, "text");
assert_eq!(columns[2].name, "is_sent");
assert_eq!(columns[3].name, "owner_id");
assert_eq!(columns[0].not_null, Some(true));
assert_eq!(columns[1].not_null, Some(true));
assert_eq!(columns[2].not_null, Some(true));
assert_eq!(columns[3].not_null, Some(false)); // owner_id
let column_type_names = type_names(&columns);
assert_eq!(column_type_names[0], "BIGINT");
assert_eq!(column_type_names[1], "TEXT");
assert_eq!(column_type_names[2], "BOOLEAN");
assert_eq!(column_type_names[3], "BIGINT");
Ok(())
}
#[sqlx_macros::test]
async fn it_describes_expression() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let d = conn.describe("SELECT 1 + 10").await?;
let columns = d.columns;
assert_eq!(columns[0].name, "1 + 10");
assert_eq!(columns[0].not_null, None);
// SQLite cannot infer types for expressions
assert_eq!(columns[0].type_info, None);
Ok(())
}

8
tests/sqlite/setup.sql Normal file
View File

@ -0,0 +1,8 @@
-- https://github.com/prisma/database-schema-examples/tree/master/postgres/basic-twitter#basic-twitter
CREATE TABLE tweet
(
id BIGINT NOT NULL PRIMARY KEY,
text TEXT NOT NULL,
is_sent BOOLEAN NOT NULL DEFAULT TRUE,
owner_id BIGINT
);

253
tests/sqlite/sqlite.rs Normal file
View File

@ -0,0 +1,253 @@
use futures::TryStreamExt;
use sqlx::{query, sqlite::Sqlite, Connect, Connection, Executor, Row, SqliteConnection};
use sqlx_test::new;
#[sqlx_macros::test]
async fn it_connects() -> anyhow::Result<()> {
Ok(new::<Sqlite>().await?.ping().await?)
}
#[sqlx_macros::test]
async fn it_fetches_and_inflates_row() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
// process rows, one-at-a-time
// this reuses the memory of the row
{
let expected = [15, 39, 51];
let mut i = 0;
let mut s = conn.fetch("SELECT 15 UNION SELECT 51 UNION SELECT 39");
while let Some(row) = s.try_next().await? {
let v1 = row.get::<i32, _>(0);
assert_eq!(expected[i], v1);
i += 1;
}
}
// same query, but fetch all rows at once
// this triggers the internal inflation
let rows = conn
.fetch_all("SELECT 15 UNION SELECT 51 UNION SELECT 39")
.await?;
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].get::<i32, _>(0), 15);
assert_eq!(rows[1].get::<i32, _>(0), 39);
assert_eq!(rows[2].get::<i32, _>(0), 51);
// same query but fetch the first row a few times from a non-persistent query
// these rows should be immediately inflated
let row1 = conn
.fetch_one("SELECT 15 UNION SELECT 51 UNION SELECT 39")
.await?;
assert_eq!(row1.get::<i32, _>(0), 15);
let row2 = conn
.fetch_one("SELECT 15 UNION SELECT 51 UNION SELECT 39")
.await?;
assert_eq!(row1.get::<i32, _>(0), 15);
assert_eq!(row2.get::<i32, _>(0), 15);
// same query (again) but make it persistent
// and fetch the first row a few times
let row1 = conn
.fetch_one(query("SELECT 15 UNION SELECT 51 UNION SELECT 39"))
.await?;
assert_eq!(row1.get::<i32, _>(0), 15);
let row2 = conn
.fetch_one(query("SELECT 15 UNION SELECT 51 UNION SELECT 39"))
.await?;
assert_eq!(row1.get::<i32, _>(0), 15);
assert_eq!(row2.get::<i32, _>(0), 15);
Ok(())
}
#[sqlx_macros::test]
async fn it_fetches_in_loop() -> anyhow::Result<()> {
// this is trying to check for any data races
// there were a few that triggered *sometimes* while building out StatementWorker
for _ in 0..1000_usize {
let mut conn = new::<Sqlite>().await?;
let v: Vec<(i32,)> = sqlx::query_as("SELECT 1").fetch_all(&mut conn).await?;
assert_eq!(v[0].0, 1);
}
Ok(())
}
#[sqlx_macros::test]
async fn it_opens_in_memory() -> anyhow::Result<()> {
// If the filename is ":memory:", then a private, temporary in-memory database
// is created for the connection.
let _ = SqliteConnection::connect(":memory:").await?;
Ok(())
}
#[sqlx_macros::test]
async fn it_opens_temp_on_disk() -> anyhow::Result<()> {
// If the filename is an empty string, then a private, temporary on-disk database will
// be created.
let _ = SqliteConnection::connect("").await?;
Ok(())
}
#[sqlx_macros::test]
async fn it_fails_to_parse() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let res = conn.execute("SEELCT 1").await;
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert_eq!(
"error returned from database: near \"SEELCT\": syntax error",
err
);
Ok(())
}
#[sqlx_macros::test]
async fn it_handles_empty_queries() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let affected = conn.execute("").await?;
assert_eq!(affected, 0);
Ok(())
}
#[sqlx_macros::test]
fn it_binds_parameters() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let v: i32 = sqlx::query_scalar("SELECT ?")
.bind(10_i32)
.fetch_one(&mut conn)
.await?;
assert_eq!(v, 10);
let v: (i32, i32) = sqlx::query_as("SELECT ?1, ?")
.bind(10_i32)
.fetch_one(&mut conn)
.await?;
assert_eq!(v.0, 10);
assert_eq!(v.1, 10);
Ok(())
}
#[sqlx_macros::test]
async fn it_executes_queries() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let _ = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY)
"#,
)
.await?;
for index in 1..=10_i32 {
let cnt = sqlx::query("INSERT INTO users (id) VALUES (?)")
.bind(index * 2)
.execute(&mut conn)
.await?;
assert_eq!(cnt, 1);
}
let sum: i32 = sqlx::query_as("SELECT id FROM users")
.fetch(&mut conn)
.try_fold(0_i32, |acc, (x,): (i32,)| async move { Ok(acc + x) })
.await?;
assert_eq!(sum, 110);
Ok(())
}
#[sqlx_macros::test]
async fn it_can_execute_multiple_statements() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let affected = conn
.execute(
r#"
CREATE TEMPORARY TABLE users (id INTEGER PRIMARY KEY, other INTEGER);
INSERT INTO users DEFAULT VALUES;
"#,
)
.await?;
assert_eq!(affected, 1);
for index in 2..5_i32 {
let (id, other): (i32, i32) = sqlx::query_as(
r#"
INSERT INTO users (other) VALUES (?);
SELECT id, other FROM users WHERE id = last_insert_rowid();
"#,
)
.bind(index)
.fetch_one(&mut conn)
.await?;
assert_eq!(id, index);
assert_eq!(other, index);
}
Ok(())
}
#[sqlx_macros::test]
async fn it_interleaves_reads_and_writes() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;
let mut cursor = conn.fetch(
"
CREATE TABLE IF NOT EXISTS _sqlx_test (
id INT PRIMARY KEY,
text TEXT NOT NULL
);
SELECT 'Hello World' as _1;
INSERT INTO _sqlx_test (text) VALUES ('this is a test');
SELECT id, text FROM _sqlx_test;
",
);
let row = cursor.try_next().await?.unwrap();
assert!("Hello World" == row.try_get::<&str, _>("_1")?);
let row = cursor.try_next().await?.unwrap();
let id: i64 = row.try_get("id")?;
let text: &str = row.try_get("text")?;
assert_eq!(0, id);
assert_eq!("this is a test", text);
Ok(())
}

33
tests/sqlite/types.rs Normal file
View File

@ -0,0 +1,33 @@
use sqlx::sqlite::Sqlite;
use sqlx_test::test_type;
test_type!(null<Option<i32>>(Sqlite,
"NULL" == None::<i32>
));
test_type!(bool(Sqlite, "FALSE" == false, "TRUE" == true));
test_type!(i32(Sqlite, "94101" == 94101_i32));
test_type!(i64(Sqlite, "9358295312" == 9358295312_i64));
// NOTE: This behavior can be surprising. Floating-point parameters are widening to double which can
// result in strange rounding.
test_type!(f32(Sqlite, "3.1410000324249268" == 3.141f32 as f64 as f32));
test_type!(f64(Sqlite, "939399419.1225182" == 939399419.1225182_f64));
test_type!(str<String>(Sqlite,
"'this is foo'" == "this is foo",
"cast(x'7468697320006973206E756C2D636F6E7461696E696E67' as text)" == "this \0is nul-containing",
"''" == ""
));
test_type!(bytes<Vec<u8>>(Sqlite,
"X'DEADBEEF'"
== vec![0xDE_u8, 0xAD, 0xBE, 0xEF],
"X''"
== Vec::<u8>::new(),
"X'0000000052'"
== vec![0_u8, 0, 0, 0, 0x52]
));

View File

@ -1,2 +0,0 @@
select * from (select (1) as id, 'Herp Derpinson' as name) accounts
where id = ?

View File

@ -1 +0,0 @@
SELECT * from (VALUES (1, 'Herp Derpinson')) accounts(id, name)

View File

@ -1,6 +1,7 @@
use std::path::Path;
#[test]
#[ignore]
fn ui_tests() {
let t = trybuild::TestCases::new();

209
tests/x.py Executable file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env python
import subprocess
import os
import sys
import time
import argparse
from glob import glob
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target")
parser.add_argument("-l", "--list-targets", action="store_true")
parser.add_argument("--test")
argv, unknown = parser.parse_known_args()
def start(service):
res = subprocess.run(
["docker-compose", "up", "-d", service],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=os.path.dirname(__file__),
)
if res.returncode != 0:
print(res.stderr, file=sys.stderr)
if b"done" in res.stderr:
time.sleep(30)
def run(command, comment=None, env=None, service=None, tag=None, args=None):
if argv.list_targets:
if tag:
print(f"{tag}")
return
if argv.target and tag != argv.target:
return
if comment is not None:
print(f"\x1b[2m # {comment}\x1b[0m")
environ = []
if env is not None:
for name, value in env.items():
print(f"\x1b[93m $ {name}={value}\x1b[0m")
environ.append(f"-e{name}={value}")
if service is not None:
start(service)
command_args = []
if argv.test:
command_args.extend(["--test", argv.test])
if unknown:
command_args.extend(["--", *unknown])
if args is not None:
command_args.extend(args)
print(f"\x1b[93m $ {command} {' '.join(command_args)}\x1b[0m")
res = subprocess.run(
[
"docker-compose",
"run",
"--user",
f"{os.getuid()}:{os.getgid()}",
"--rm",
*environ,
"sqlx",
*command.split(" "),
*command_args
],
cwd=os.path.dirname(__file__),
)
if res.returncode != 0:
sys.exit(res.returncode)
# before we start, we clean previous profile data
# keeping these around can cause weird errors
for path in glob(os.path.join(os.path.dirname(__file__), "../target/**/*.gc*"), recursive=True):
os.remove(path)
#
# check
#
run("cargo c", comment="check with a default set of features", tag="check")
run(
"cargo c --no-default-features --features runtime-async-std,all-databases,all-types",
comment="check with async-std",
tag="check_async_std"
)
run(
"cargo c --no-default-features --features runtime-tokio,all-databases,all-types",
comment="check with tokio",
tag="check_tokio"
)
run(
"cargo c --no-default-features --features runtime-actix,all-databases,all-types",
comment="check with actix",
tag="check_actix"
)
#
# unit test
#
run(
"cargo test --manifest-path sqlx-core/Cargo.toml --features mysql,postgres,sqlite,all-types",
comment="unit test core",
tag="unit"
)
run(
"cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml --features mysql,postgres,sqlite,all-types,runtime-tokio",
comment="unit test core",
tag="unit_tokio"
)
#
# integration tests
#
for runtime in ["async-std", "tokio", "actix"]:
#
# sqlite
#
run(
f"cargo test --no-default-features --features all-types,sqlite,runtime-{runtime}",
comment=f"test sqlite",
env={"DATABASE_URL": f"sqlite://tests/sqlite/sqlite.db"},
tag=f"sqlite" if runtime == "async-std" else f"sqlite_{runtime}",
# FIXME: The SQLite driver does not currently support concurrent access to the same database
args=["--test-threads=1"],
)
#
# postgres
#
for version in ["12", "10", "9.6", "9.5"]:
v = version.replace(".", "_")
run(
f"cargo test --no-default-features --features all-types,postgres,runtime-{runtime}",
comment=f"test postgres {version}",
env={"DATABASE_URL": f"postgres://postgres:password@postgres_{v}/sqlx"},
service=f"postgres_{v}",
tag=f"postgres_{v}" if runtime == "async-std" else f"postgres_{v}_{runtime}",
)
#
# postgres ssl
#
for version in ["12", "10", "9.6", "9.5"]:
v = version.replace(".", "_")
run(
f"cargo test --no-default-features --features all-types,postgres,runtime-{runtime}",
comment=f"test postgres {version} ssl",
env={
"DATABASE_URL": f"postgres://postgres:password@postgres_{v}/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt"
},
service=f"postgres_{v}",
tag=f"postgres_{v}_ssl" if runtime == "async-std" else f"postgres_{v}_ssl_{runtime}",
)
#
# mysql
#
for version in ["8", "5.7", "5.6"]:
v = version.replace(".", "_")
run(
f"cargo test --no-default-features --features all-types,mysql,runtime-{runtime}",
comment=f"test mysql {version}",
env={"DATABASE_URL": f"mysql://root:password@mysql_{v}/sqlx"},
service=f"mysql_{v}",
tag=f"mysql_{v}" if runtime == "async-std" else f"mysql_{v}_{runtime}",
)
#
# mariadb
#
for version in ["10_5", "10_4", "10_3", "10_2", "10_1"]:
v = version.replace(".", "_")
run(
f"cargo test --no-default-features --features all-types,mysql,runtime-{runtime}",
comment=f"test mariadb {version}",
env={"DATABASE_URL": f"mysql://root:password@mariadb_{v}/sqlx"},
service=f"mariadb_{v}",
tag=f"mariadb_{v}" if runtime == "async-std" else f"mariadb_{v}_{runtime}",
)
# TODO: Use [grcov] if available
# ~/.cargo/bin/grcov tests/.cache/target/debug -s sqlx-core/ -t html --llvm --branch -o ./target/debug/coverage