query_as: fully implement query_as, required a db-specific ext trait

This commit is contained in:
Ryan Leckey 2020-03-01 20:07:59 -08:00
parent f18ab2fecb
commit 47f3d77e59
23 changed files with 631 additions and 251 deletions

36
Cargo.lock generated
View File

@ -1061,6 +1061,26 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "paste"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"paste-impl 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "paste-impl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "paw"
version = "1.0.0"
@ -1466,8 +1486,10 @@ dependencies = [
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"paste 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"sqlx-core 0.2.6",
"sqlx-macros 0.2.5",
"sqlx-test 0.1.0",
"tokio 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"trybuild 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1580,6 +1602,18 @@ dependencies = [
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sqlx-test"
version = "0.1.0"
dependencies = [
"anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)",
"async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"sqlx 0.2.5",
"tokio 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "string"
version = "0.2.1"
@ -2173,6 +2207,8 @@ dependencies = [
"checksum openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)" = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986"
"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
"checksum paste 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046"
"checksum paste-impl 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb"
"checksum paw 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9"
"checksum paw-attributes 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0f35583365be5d148e959284f42526841917b7bfa09e2d1a7ad5dde2cf0eaa39"
"checksum paw-raw 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7f0b59668fe80c5afe998f0c0bf93322bf2cd66cafeeb80581f291716f3467f2"

View File

@ -3,6 +3,7 @@ members = [
".",
"sqlx-core",
"sqlx-macros",
"sqlx-test",
"examples/realworld-postgres",
"examples/todos-postgres",
]
@ -58,6 +59,8 @@ async-std = { version = "1.4.0", features = [ "attributes" ] }
tokio = { version = "0.2.9", features = [ "full" ] }
dotenv = "0.15.0"
trybuild = "1.0"
sqlx-test = { path = "./sqlx-test" }
paste = "0.1"
[[test]]
name = "postgres-macros"
@ -76,7 +79,7 @@ name = "postgres"
required-features = [ "postgres" ]
[[test]]
name = "postgres-simple"
name = "postgres-raw"
required-features = [ "postgres" ]
[[test]]

View File

@ -60,10 +60,10 @@ impl<'c, C> MaybeOwnedConnection<'c, C>
where
C: Connect,
{
pub(crate) fn borrow(&mut self) -> MaybeOwnedConnection<'_, C> {
pub(crate) fn borrow(&mut self) -> &'_ mut C {
match self {
MaybeOwnedConnection::Borrowed(conn) => MaybeOwnedConnection::Borrowed(&mut *conn),
MaybeOwnedConnection::Owned(ref mut conn) => MaybeOwnedConnection::Borrowed(conn),
MaybeOwnedConnection::Borrowed(conn) => &mut *conn,
MaybeOwnedConnection::Owned(ref mut conn) => conn,
}
}
}
@ -72,7 +72,7 @@ impl<'c, C> ConnectionSource<'c, C>
where
C: Connect,
{
pub(crate) async fn resolve_by_ref(&mut self) -> crate::Result<MaybeOwnedConnection<'_, C>> {
pub(crate) async fn resolve_by_ref(&mut self) -> crate::Result<&'_ mut C> {
if let ConnectionSource::Pool(pool) = self {
*self =
ConnectionSource::Connection(MaybeOwnedConnection::Owned(pool.acquire().await?));

View File

@ -3,6 +3,7 @@ use std::fmt::Display;
use crate::arguments::Arguments;
use crate::connection::{Connect, Connection};
use crate::cursor::Cursor;
use crate::query_as::QueryAs;
use crate::row::Row;
use crate::types::TypeInfo;

View File

@ -8,7 +8,7 @@ use crate::database::{Database, HasRawValue};
/// Decode a single value from the database.
pub trait Decode<'de, DB>
where
Self: Sized,
Self: Sized + 'de,
DB: HasRawValue<'de>,
{
fn decode(value: DB::RawValue) -> crate::Result<Self>;

View File

@ -36,7 +36,7 @@ where
}
#[inline]
pub fn buffer(&self) -> &[u8] {
pub fn buffer<'c>(&'c self) -> &'c [u8] {
&self.rbuf[self.rbuf_rindex..]
}

View File

@ -32,6 +32,10 @@ pub mod describe;
pub mod encode;
pub mod pool;
pub mod query;
#[macro_use]
pub mod query_as;
pub mod types;
#[macro_use]

View File

@ -183,10 +183,9 @@ async fn next<'a, 'c: 'a, 'q: 'a>(
}
Message::DataRow => {
let data = DataRow::read(&mut *conn)?;
let data = DataRow::read(conn.stream.buffer(), &mut conn.current_row_values)?;
return Ok(Some(PgRow {
connection: conn,
columns: Arc::clone(&cursor.columns),
formats: Arc::clone(&cursor.formats),
data,

View File

@ -23,3 +23,8 @@ mod types;
/// An alias for [`Pool`][crate::Pool], specialized for **Postgres**.
pub type PgPool = super::Pool<PgConnection>;
make_query_as!(PgQueryAs, Postgres, PgRow);
impl_map_row_for_row!(Postgres, PgRow);
impl_column_index_for_row!(Postgres);
impl_from_row_for_tuples!(Postgres, PgRow);

View File

@ -5,31 +5,36 @@ use byteorder::NetworkEndian;
use std::fmt::{self, Debug};
use std::ops::Range;
pub struct DataRow {
pub struct DataRow<'c> {
len: u16,
buffer: &'c [u8],
values: &'c [Option<Range<u32>>],
}
impl DataRow {
impl<'c> DataRow<'c> {
pub fn len(&self) -> usize {
self.len as usize
}
pub fn get<'a>(
pub fn get(
&self,
buffer: &'a [u8],
values: &[Option<Range<u32>>],
// buffer: &'c [u8],
// values: &[Option<Range<u32>>],
index: usize,
) -> Option<&'a [u8]> {
let range = values[index].as_ref()?;
) -> Option<&'c [u8]> {
let range = self.values[index].as_ref()?;
Some(&buffer[(range.start as usize)..(range.end as usize)])
Some(&self.buffer[(range.start as usize)..(range.end as usize)])
}
}
impl DataRow {
pub(crate) fn read(connection: &mut PgConnection) -> crate::Result<Self> {
let buffer = connection.stream.buffer();
let values = &mut connection.current_row_values;
impl<'c> DataRow<'c> {
pub(crate) fn read(
buffer: &'c [u8],
values: &'c mut Vec<Option<Range<u32>>>,
) -> crate::Result<Self> {
// let buffer = connection.stream.buffer();
// let values = &mut connection.current_row_values;
values.clear();
@ -57,6 +62,10 @@ impl DataRow {
}
}
Ok(Self { len })
Ok(Self {
len,
buffer,
values,
})
}
}

View File

@ -33,8 +33,7 @@ impl<'c> TryFrom<Option<PgValue<'c>>> for PgValue<'c> {
}
pub struct PgRow<'c> {
pub(super) connection: MaybeOwnedConnection<'c, PgConnection>,
pub(super) data: DataRow,
pub(super) data: DataRow<'c>,
pub(super) columns: Arc<HashMap<Box<str>, usize>>,
pub(super) formats: Arc<[TypeFormat]>,
}
@ -46,18 +45,14 @@ impl<'c> Row<'c> for PgRow<'c> {
self.data.len()
}
fn get_raw<'i, I>(&'c self, index: I) -> crate::Result<Option<PgValue<'c>>>
fn get_raw<'r, I>(&'r self, index: I) -> crate::Result<Option<PgValue<'c>>>
where
I: ColumnIndex<'c, Self> + 'i,
I: ColumnIndex<Self::Database>,
{
let index = index.resolve(self)?;
let buffer = self.data.get(index);
self.data
.get(
self.connection.stream.buffer(),
&self.connection.current_row_values,
index,
)
buffer
.map(|buf| match self.formats[index] {
TypeFormat::Binary => Ok(PgValue::Binary(buf)),
TypeFormat::Text => Ok(PgValue::Text(from_utf8(buf)?)),
@ -66,7 +61,3 @@ impl<'c> Row<'c> for PgRow<'c> {
.map_err(|err: Utf8Error| crate::Error::Decode(Box::new(err)))
}
}
impl_map_row_for_row!(Postgres, PgRow);
impl_column_index_for_row!(PgRow);
impl_from_row_for_row!(PgRow);

View File

@ -9,12 +9,12 @@ use crate::postgres::PgError;
use crate::url::Url;
pub struct PgStream {
stream: BufStream<MaybeTlsStream>,
pub(super) stream: BufStream<MaybeTlsStream>,
// Most recently received message
// Is referenced by our buffered stream
// Is initialized to ReadyForQuery/0 at the start
message: (Message, u32),
pub(super) message: (Message, u32),
}
impl PgStream {

View File

@ -1,7 +1,7 @@
use std::convert::TryInto;
use std::str::FromStr;
use byteorder::{ReadBytesExt, NetworkEndian};
use byteorder::{NetworkEndian, ReadBytesExt};
use crate::decode::Decode;
use crate::encode::Encode;

View File

@ -134,29 +134,6 @@ where
}
}
impl<'q, DB, F> Map<'q, DB, F>
where
DB: Database,
{
/// Bind a value for use with this SQL query.
pub fn bind<T>(mut self, value: T) -> Self
where
T: Type<DB>,
T: Encode<DB>,
{
self.query.arguments.add(value);
self
}
#[doc(hidden)]
pub fn bind_all(self, arguments: DB::Arguments) -> Map<'q, DB, F, ImmutableArguments<DB>> {
Map {
query: self.query.bind_all(arguments),
mapper: self.mapper,
}
}
}
impl<'q, DB, F, A> Map<'q, DB, F, A>
where
DB: Database,
@ -258,13 +235,3 @@ where
query: sql,
}
}
pub fn query_as<T, DB>(
sql: &str,
) -> Map<DB, for<'c> fn(<DB as HasRow<'c>>::Row) -> crate::Result<T>>
where
DB: Database,
T: Unpin + for<'c> FromRow<'c, <DB as HasRow<'c>>::Row>,
{
query(sql).map(|row| T::from_row(row))
}

204
sqlx-core/src/query_as.rs Normal file
View File

@ -0,0 +1,204 @@
use core::marker::PhantomData;
use async_stream::try_stream;
use futures_core::future::BoxFuture;
use futures_core::stream::Stream;
use futures_util::future::ready;
use futures_util::future::TryFutureExt;
use crate::arguments::Arguments;
use crate::cursor::Cursor;
use crate::database::{Database, HasRow};
use crate::encode::Encode;
use crate::executor::{Execute, RefExecutor};
use crate::row::FromRow;
use crate::types::Type;
/// Raw SQL query with bind parameters, mapped to a concrete type
/// using [`FromRow`](trait.FromRow.html). Returned
/// by [`query_as`](fn.query_as.html).
pub struct QueryAs<'q, DB, O>
where
DB: Database,
{
query: &'q str,
arguments: <DB as Database>::Arguments,
database: PhantomData<DB>,
output: PhantomData<O>,
}
impl<'q, DB, O> QueryAs<'q, DB, O>
where
DB: Database,
{
/// Bind a value for use with this SQL query.
#[inline]
pub fn bind<T>(mut self, value: T) -> Self
where
T: Type<DB>,
T: Encode<DB>,
{
self.arguments.add(value);
self
}
}
impl<'q, DB, O: Send> Execute<'q, DB> for QueryAs<'q, DB, O>
where
DB: Database,
{
#[inline]
fn into_parts(self) -> (&'q str, Option<<DB as Database>::Arguments>) {
(self.query, Some(self.arguments))
}
}
/// Construct a raw SQL query that is mapped to a concrete type
/// using [`FromRow`](crate::row::FromRow).
///
/// Returns [`QueryAs`].
pub fn query_as<DB, O>(sql: &str) -> QueryAs<DB, O>
where
DB: Database,
{
QueryAs {
query: sql,
arguments: Default::default(),
database: PhantomData,
output: PhantomData,
}
}
// We need database-specific QueryAs traits to work around:
// https://github.com/rust-lang/rust/issues/62529
// If for some reason we miss that issue being resolved in a _stable_ edition of
// rust, please open up a 100 issues and shout as loud as you can to remove
// this unseemly hack.
macro_rules! make_query_as {
($name:ident, $db:ident, $row:ident) => {
pub trait $name<'q, O> {
fn fetch<'e, E>(
self,
executor: E,
) -> futures_core::stream::BoxStream<'e, crate::Result<O>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + Unpin + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e;
fn fetch_all<'e, E>(
self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<Vec<O>>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e;
fn fetch_one<'e, E>(
self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<O>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e;
fn fetch_optional<'e, E>(
self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<Option<O>>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e;
}
impl<'q, O> $name<'q, O> for crate::query_as::QueryAs<'q, $db, O> {
fn fetch<'e, E>(
mut self,
executor: E,
) -> futures_core::stream::BoxStream<'e, crate::Result<O>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + Unpin + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e,
{
use crate::cursor::Cursor;
Box::pin(async_stream::try_stream! {
let mut cursor = executor.fetch_by_ref(self);
while let Some(row) = cursor.next().await? {
let obj = O::from_row(row)?;
yield obj;
}
})
}
fn fetch_optional<'e, E>(
mut self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<Option<O>>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e,
{
use crate::cursor::Cursor;
Box::pin(async move {
let mut cursor = executor.fetch_by_ref(self);
let row = cursor.next().await?;
row.map(O::from_row).transpose()
})
}
fn fetch_one<'e, E>(
mut self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<O>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e,
{
use futures_util::TryFutureExt;
Box::pin(self.fetch_optional(executor).and_then(|row| match row {
Some(row) => futures_util::future::ready(Ok(row)),
None => futures_util::future::ready(Err(crate::Error::RowNotFound)),
}))
}
fn fetch_all<'e, E>(
mut self,
executor: E,
) -> futures_core::future::BoxFuture<'e, crate::Result<Vec<O>>>
where
E: 'e + Send + crate::executor::RefExecutor<'e, Database = $db>,
O: 'e + Send + for<'c> crate::row::FromRow<'c, $row<'c>>,
'q: 'e,
{
use crate::cursor::Cursor;
Box::pin(async move {
let mut cursor = executor.fetch_by_ref(self);
let mut out = Vec::new();
while let Some(row) = cursor.next().await? {
let obj = O::from_row(row)?;
out.push(obj);
}
Ok(out)
})
}
}
};
}

View File

@ -1,14 +1,15 @@
//! Contains the Row and FromRow traits.
use crate::database::{Database, HasRawValue};
use crate::database::{Database, HasRawValue, HasRow};
use crate::decode::Decode;
use crate::types::Type;
pub trait ColumnIndex<'c, R: ?Sized>
pub trait ColumnIndex<DB>
where
R: Row<'c>,
DB: Database,
DB: for<'c> HasRow<'c, Database = DB>,
{
fn resolve(self, row: &'c R) -> crate::Result<usize>;
fn resolve<'c>(self, row: &<DB as HasRow<'c>>::Row) -> crate::Result<usize>;
}
/// Represents a single row of the result set.
@ -23,28 +24,28 @@ pub trait Row<'c>: Unpin + Send {
/// Returns the number of values in the row.
fn len(&self) -> usize;
fn get<T, I>(&'c self, index: I) -> crate::Result<T>
fn get<'r, T, I>(&'r self, index: I) -> crate::Result<T>
where
T: Type<Self::Database>,
I: ColumnIndex<'c, Self>,
I: ColumnIndex<Self::Database>,
T: Decode<'c, Self::Database>,
{
Ok(Decode::decode(self.get_raw(index)?)?)
}
fn get_raw<'i, I>(
&'c self,
fn get_raw<'r, I>(
&self,
index: I,
) -> crate::Result<<Self::Database as HasRawValue<'c>>::RawValue>
where
I: ColumnIndex<'c, Self> + 'i;
I: ColumnIndex<Self::Database>;
}
/// A **record** that can be built from a row returned from by the database.
pub trait FromRow<'a, R>
pub trait FromRow<'c, R>
where
Self: Sized,
R: Row<'a>,
R: Row<'c>,
{
fn from_row(row: R) -> crate::Result<Self>;
}
@ -52,6 +53,101 @@ where
// Macros to help unify the internal implementations as a good chunk
// is very similar
#[allow(unused_macros)]
macro_rules! impl_from_row_for_tuple {
($db:ident, $r:ident; $( ($idx:tt) -> $T:ident );+;) => {
impl<'c, $($T,)+> crate::row::FromRow<'c, $r<'c>> for ($($T,)+)
where
$($T: crate::types::Type<$db>,)+
$($T: crate::decode::Decode<'c, $db>,)+
{
#[inline]
fn from_row(row: $r<'c>) -> crate::Result<Self> {
use crate::row::Row;
Ok(($(row.get($idx as usize)?,)+))
}
}
};
}
#[allow(unused_macros)]
macro_rules! impl_from_row_for_tuples {
($db:ident, $r:ident) => {
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
(4) -> T5;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
(4) -> T5;
(5) -> T6;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
(4) -> T5;
(5) -> T6;
(6) -> T7;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
(4) -> T5;
(5) -> T6;
(6) -> T7;
(7) -> T8;
);
impl_from_row_for_tuple!($db, $r;
(0) -> T1;
(1) -> T2;
(2) -> T3;
(3) -> T4;
(4) -> T5;
(5) -> T6;
(6) -> T7;
(7) -> T8;
(8) -> T9;
);
};
}
#[allow(unused_macros)]
macro_rules! impl_map_row_for_row {
($DB:ident, $R:ident) => {
@ -70,22 +166,27 @@ macro_rules! impl_map_row_for_row {
#[allow(unused_macros)]
macro_rules! impl_column_index_for_row {
($R:ident) => {
impl<'c> crate::row::ColumnIndex<'c, $R<'c>> for usize {
fn resolve(self, row: &'c $R<'c>) -> crate::Result<usize> {
if self >= row.len() {
return Err(crate::Error::ColumnIndexOutOfBounds {
len: row.len(),
index: self,
});
($DB:ident) => {
impl crate::row::ColumnIndex<$DB> for usize {
fn resolve<'c>(
self,
row: &<$DB as crate::database::HasRow<'c>>::Row,
) -> crate::Result<usize> {
let len = crate::row::Row::len(row);
if self >= len {
return Err(crate::Error::ColumnIndexOutOfBounds { len, index: self });
}
Ok(self)
}
}
impl<'c> crate::row::ColumnIndex<'c, $R<'c>> for &'_ str {
fn resolve(self, row: &'c $R<'c>) -> crate::Result<usize> {
impl crate::row::ColumnIndex<$DB> for &'_ str {
fn resolve<'c>(
self,
row: &<$DB as crate::database::HasRow<'c>>::Row,
) -> crate::Result<usize> {
row.columns
.get(self)
.ok_or_else(|| crate::Error::ColumnNotFound((*self).into()))

12
sqlx-test/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "sqlx-test"
version = "0.1.0"
edition = "2018"
[dependencies]
sqlx = { default-features = false, path = ".." }
env_logger = "0.7.1"
dotenv = "0.15.0"
anyhow = "1.0.26"
async-std = { version = "1.4.0", features = [ "attributes" ] }
tokio = { version = "0.2.9", features = [ "full" ] }

81
sqlx-test/src/lib.rs Normal file
View File

@ -0,0 +1,81 @@
use sqlx::{Connect, Database};
fn setup_if_needed() {
let _ = dotenv::dotenv();
let _ = env_logger::try_init();
}
// Make a new connection
// Ensure [dotenv] and [env_logger] have been setup
pub async fn new<DB>() -> anyhow::Result<DB::Connection>
where
DB: Database,
{
setup_if_needed();
Ok(DB::Connection::connect(dotenv::var("DATABASE_URL")?).await?)
}
// Test type encoding and decoding
#[macro_export]
macro_rules! test_type {
($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),+));
}
}
// 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),+)) => {
paste::item! {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn [< test_unprepared_type_ $name >] () -> anyhow::Result<()> {
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.get::<$ty, _>("_1")?;
assert!($value == rec);
)+
Ok(())
}
}
}
}
// Test type encoding and decoding for the prepared query API
#[macro_export]
macro_rules! test_prepared_type {
($name:ident($db:ident, $ty:ty, $($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::*;
let mut conn = sqlx_test::new::<$db>().await?;
$(
let query = format!("SELECT {} = $1, $1 as _1", $text);
let rec: (bool, $ty) = sqlx::query_as(&query)
.bind($value)
.fetch_one(&mut conn)
.await?;
assert!(rec.0);
assert!($value == rec.1);
)+
Ok(())
}
}
}
}

View File

@ -8,18 +8,17 @@ compile_error!("one of 'runtime-async-std' or 'runtime-tokio' features must be e
compile_error!("only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled");
// Modules
pub use sqlx_core::{arguments, describe, error, pool, query, row, types};
pub use sqlx_core::{arguments, describe, error, pool, row, types};
// Types
pub use sqlx_core::{
query::Query, Connect, Connection, Cursor, Error, Execute, Executor, FromRow, Pool, Result, Row,
Connect, Connection, Cursor, Error, Execute, Executor, FromRow, Pool, Result, Row,
Transaction
};
pub use sqlx_core::database::{Database, HasCursor, HasRawValue, HasRow};
// Functions
pub use query::{query, query_as};
pub use sqlx_core::query::{self, query, Query};
pub use sqlx_core::query_as::{query_as, QueryAs};
#[cfg(feature = "mysql")]
#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
@ -58,3 +57,8 @@ pub mod decode {
#[cfg(feature = "macros")]
pub use sqlx_macros::Decode;
}
pub mod prelude {
#[cfg(feature = "postgres")]
pub use super::postgres::PgQueryAs as _;
}

71
tests/postgres-raw.rs Normal file
View File

@ -0,0 +1,71 @@
//! 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.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.get::<&str, _>("_1")?);
let row = cursor.next().await?.unwrap();
let id: i64 = row.get("id")?;
let text: &str = row.get("text")?;
assert_eq!(1_i64, id);
assert_eq!("this is a test", text);
Ok(())
}

View File

@ -1,99 +0,0 @@
use sqlx::{Connect, Executor, Cursor, Row, PgConnection};
use sqlx::postgres::PgRow;
#[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 = connect().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 test_select_1() -> anyhow::Result<()> {
let mut conn = connect().await?;
let mut cursor = conn.fetch("SELECT 5");
let row = cursor.next().await?.unwrap();
assert!(5i32 == row.get::<i32, _>(0)?);
Ok(())
}
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn test_multi_create_insert() -> anyhow::Result<()> {
let mut conn = connect().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';
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.get::<&str, _>(0)?);
let row = cursor.next().await?.unwrap();
let id: i64 = row.get(0)?;
let text: &str = row.get(1)?;
assert!(1_i64 == id);
assert!("this is a test" == text);
Ok(())
}
macro_rules! test {
($name:ident: $ty:ty: $($text:literal == $value:expr),+) => {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn $name () -> anyhow::Result<()> {
let mut conn = connect().await?;
$(
let rec: $ty = sqlx::query(&format!("SELECT $1 as _1"))
.bind($value)
.map(|row: PgRow| row.get(0))
.fetch_one(&mut conn)
.await?;
assert!($value == rec);
)+
Ok(())
}
}
}
test!(postgres_simple_bool: bool: "false::boolean" == false, "true::boolean" == true);
test!(postgres_simple_smallint: i16: "821::smallint" == 821_i16);
test!(postgres_simple_int: i32: "94101::int" == 94101_i32);
test!(postgres_simple_bigint: i64: "9358295312::bigint" == 9358295312_i64);
test!(postgres_simple_real: f32: "9419.122::real" == 9419.122_f32);
test!(postgres_simple_double: f64: "939399419.1225182::double precision" == 939399419.1225182_f64);
test!(postgres_simple_text: String: "'this is foo'" == "this is foo", "''" == "");
async fn connect() -> anyhow::Result<PgConnection> {
let _ = dotenv::dotenv();
let _ = env_logger::try_init();
Ok(PgConnection::connect(dotenv::var("DATABASE_URL")?).await?)
}

View File

@ -1,59 +1,50 @@
use sqlx::postgres::PgRow;
use sqlx::{Connect, PgConnection, Row};
use sqlx::Postgres;
use sqlx_test::test_type;
async fn connect() -> anyhow::Result<PgConnection> {
Ok(PgConnection::connect(dotenv::var("DATABASE_URL")?).await?)
}
test_type!(bool(
Postgres,
bool,
"false::boolean" == false,
"true::boolean" == true
));
macro_rules! test {
($name:ident: $ty:ty: $($text:literal == $value:expr),+) => {
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn $name () -> anyhow::Result<()> {
let mut conn = connect().await?;
test_type!(i16(Postgres, i16, "821::smallint" == 821_i16));
test_type!(i32(Postgres, i32, "94101::int" == 94101_i32));
test_type!(i64(Postgres, i64, "9358295312::bigint" == 9358295312_i64));
$(
let rec: (bool, $ty) = sqlx::query(&format!("SELECT {} = $1, $1 as _1", $text))
.bind($value)
.map(|row: PgRow| Ok((row.get(0)?, row.get(1)?)))
.fetch_one(&mut conn)
.await?;
test_type!(f32(Postgres, f32, "9419.122::real" == 9419.122_f32));
test_type!(f64(
Postgres,
f64,
"939399419.1225182::double precision" == 939399419.1225182_f64
));
assert!(rec.0);
assert!($value == rec.1);
)+
test_type!(string(
Postgres,
String,
"'this is foo'" == "this is foo",
"''" == ""
));
Ok(())
}
}
}
// TODO: BYTEA
// TODO: UUID
// TODO: CHRONO
test!(postgres_bool: bool: "false::boolean" == false, "true::boolean" == true);
test!(postgres_smallint: i16: "821::smallint" == 821_i16);
test!(postgres_int: i32: "94101::int" == 94101_i32);
test!(postgres_bigint: i64: "9358295312::bigint" == 9358295312_i64);
test!(postgres_real: f32: "9419.122::real" == 9419.122_f32);
test!(postgres_double: f64: "939399419.1225182::double precision" == 939399419.1225182_f64);
test!(postgres_text: String: "'this is foo'" == "this is foo", "''" == "");
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
async fn postgres_bytes() -> anyhow::Result<()> {
let mut conn = connect().await?;
let value = b"Hello, World";
let rec: (bool, Vec<u8>) = sqlx::query("SELECT E'\\\\x48656c6c6f2c20576f726c64' = $1, $1")
.bind(&value[..])
.map(|row: PgRow| Ok((row.get(0)?, row.get(1)?)))
.fetch_one(&mut conn)
.await?;
assert!(rec.0);
assert_eq!(&value[..], &*rec.1);
Ok(())
}
// #[cfg_attr(feature = "runtime-async-std", async_std::test)]
// #[cfg_attr(feature = "runtime-tokio", tokio::test)]
// async fn postgres_bytes() -> anyhow::Result<()> {
// let mut conn = connect().await?;
//
// let value = b"Hello, World";
//
// let rec: (bool, Vec<u8>) = sqlx::query("SELECT E'\\\\x48656c6c6f2c20576f726c64' = $1, $1")
// .bind(&value[..])
// .map(|row: PgRow| Ok((row.get(0)?, row.get(1)?)))
// .fetch_one(&mut conn)
// .await?;
//
// assert!(rec.0);
// assert_eq!(&value[..], &*rec.1);
//
// Ok(())
// }

View File

@ -4,4 +4,4 @@ error: "\'1" is not a valid Rust identifier
2 | let query = sqlx::query!("select 1 as \"'1\"");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)