From 47f3d77e599043bc25f78765ec42189e529dc76c Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Sun, 1 Mar 2020 20:07:59 -0800 Subject: [PATCH] query_as: fully implement query_as, required a db-specific ext trait --- Cargo.lock | 36 ++++ Cargo.toml | 5 +- sqlx-core/src/connection.rs | 8 +- sqlx-core/src/database.rs | 1 + sqlx-core/src/decode.rs | 2 +- sqlx-core/src/io/buf_stream.rs | 2 +- sqlx-core/src/lib.rs | 4 + sqlx-core/src/postgres/cursor.rs | 3 +- sqlx-core/src/postgres/mod.rs | 5 + sqlx-core/src/postgres/protocol/data_row.rs | 35 ++-- sqlx-core/src/postgres/row.rs | 19 +- sqlx-core/src/postgres/stream.rs | 4 +- sqlx-core/src/postgres/types/float.rs | 2 +- sqlx-core/src/query.rs | 33 ---- sqlx-core/src/query_as.rs | 204 ++++++++++++++++++++ sqlx-core/src/row.rs | 143 ++++++++++++-- sqlx-test/Cargo.toml | 12 ++ sqlx-test/src/lib.rs | 81 ++++++++ src/lib.rs | 14 +- tests/postgres-raw.rs | 71 +++++++ tests/postgres-simple.rs | 99 ---------- tests/postgres-types.rs | 97 +++++----- tests/ui/postgres/issue_30.stderr | 2 +- 23 files changed, 631 insertions(+), 251 deletions(-) create mode 100644 sqlx-core/src/query_as.rs create mode 100644 sqlx-test/Cargo.toml create mode 100644 sqlx-test/src/lib.rs create mode 100644 tests/postgres-raw.rs delete mode 100644 tests/postgres-simple.rs diff --git a/Cargo.lock b/Cargo.lock index 1ba8164e..c231e21a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e79f6794..617e07dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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]] diff --git a/sqlx-core/src/connection.rs b/sqlx-core/src/connection.rs index 136734b6..70bbe3d8 100644 --- a/sqlx-core/src/connection.rs +++ b/sqlx-core/src/connection.rs @@ -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> { + 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?)); diff --git a/sqlx-core/src/database.rs b/sqlx-core/src/database.rs index 93b2a595..5db505f7 100644 --- a/sqlx-core/src/database.rs +++ b/sqlx-core/src/database.rs @@ -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; diff --git a/sqlx-core/src/decode.rs b/sqlx-core/src/decode.rs index e5a06391..02e286d5 100644 --- a/sqlx-core/src/decode.rs +++ b/sqlx-core/src/decode.rs @@ -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; diff --git a/sqlx-core/src/io/buf_stream.rs b/sqlx-core/src/io/buf_stream.rs index 85ca2f51..1d002fa1 100644 --- a/sqlx-core/src/io/buf_stream.rs +++ b/sqlx-core/src/io/buf_stream.rs @@ -36,7 +36,7 @@ where } #[inline] - pub fn buffer(&self) -> &[u8] { + pub fn buffer<'c>(&'c self) -> &'c [u8] { &self.rbuf[self.rbuf_rindex..] } diff --git a/sqlx-core/src/lib.rs b/sqlx-core/src/lib.rs index 697c31e1..57d385f0 100644 --- a/sqlx-core/src/lib.rs +++ b/sqlx-core/src/lib.rs @@ -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] diff --git a/sqlx-core/src/postgres/cursor.rs b/sqlx-core/src/postgres/cursor.rs index 473f67ac..9a9c8448 100644 --- a/sqlx-core/src/postgres/cursor.rs +++ b/sqlx-core/src/postgres/cursor.rs @@ -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, diff --git a/sqlx-core/src/postgres/mod.rs b/sqlx-core/src/postgres/mod.rs index c2b62d21..680b6e9d 100644 --- a/sqlx-core/src/postgres/mod.rs +++ b/sqlx-core/src/postgres/mod.rs @@ -23,3 +23,8 @@ mod types; /// An alias for [`Pool`][crate::Pool], specialized for **Postgres**. pub type PgPool = super::Pool; + +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); diff --git a/sqlx-core/src/postgres/protocol/data_row.rs b/sqlx-core/src/postgres/protocol/data_row.rs index f0bf6c65..0c9c034c 100644 --- a/sqlx-core/src/postgres/protocol/data_row.rs +++ b/sqlx-core/src/postgres/protocol/data_row.rs @@ -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>], } -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>], + // buffer: &'c [u8], + // values: &[Option>], 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 { - 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>>, + ) -> crate::Result { + // 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, + }) } } diff --git a/sqlx-core/src/postgres/row.rs b/sqlx-core/src/postgres/row.rs index a4c1f07e..fcba6c64 100644 --- a/sqlx-core/src/postgres/row.rs +++ b/sqlx-core/src/postgres/row.rs @@ -33,8 +33,7 @@ impl<'c> TryFrom>> 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, 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>> + fn get_raw<'r, I>(&'r self, index: I) -> crate::Result>> where - I: ColumnIndex<'c, Self> + 'i, + I: ColumnIndex, { 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); diff --git a/sqlx-core/src/postgres/stream.rs b/sqlx-core/src/postgres/stream.rs index 3a9af655..b1c4f111 100644 --- a/sqlx-core/src/postgres/stream.rs +++ b/sqlx-core/src/postgres/stream.rs @@ -9,12 +9,12 @@ use crate::postgres::PgError; use crate::url::Url; pub struct PgStream { - stream: BufStream, + pub(super) stream: BufStream, // 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 { diff --git a/sqlx-core/src/postgres/types/float.rs b/sqlx-core/src/postgres/types/float.rs index 5ad17eb9..3603ae66 100644 --- a/sqlx-core/src/postgres/types/float.rs +++ b/sqlx-core/src/postgres/types/float.rs @@ -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; diff --git a/sqlx-core/src/query.rs b/sqlx-core/src/query.rs index 505fc04a..23382d8c 100644 --- a/sqlx-core/src/query.rs +++ b/sqlx-core/src/query.rs @@ -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(mut self, value: T) -> Self - where - T: Type, - T: Encode, - { - self.query.arguments.add(value); - self - } - - #[doc(hidden)] - pub fn bind_all(self, arguments: DB::Arguments) -> Map<'q, DB, F, ImmutableArguments> { - 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( - sql: &str, -) -> Map fn(>::Row) -> crate::Result> -where - DB: Database, - T: Unpin + for<'c> FromRow<'c, >::Row>, -{ - query(sql).map(|row| T::from_row(row)) -} diff --git a/sqlx-core/src/query_as.rs b/sqlx-core/src/query_as.rs new file mode 100644 index 00000000..59ffd153 --- /dev/null +++ b/sqlx-core/src/query_as.rs @@ -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: ::Arguments, + database: PhantomData, + output: PhantomData, +} + +impl<'q, DB, O> QueryAs<'q, DB, O> +where + DB: Database, +{ + /// Bind a value for use with this SQL query. + #[inline] + pub fn bind(mut self, value: T) -> Self + where + T: Type, + T: Encode, + { + 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<::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(sql: &str) -> QueryAs +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> + 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>> + 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> + 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>> + 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> + 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>> + 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> + 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>> + 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) + }) + } + } + }; +} diff --git a/sqlx-core/src/row.rs b/sqlx-core/src/row.rs index b167a69b..dfb88579 100644 --- a/sqlx-core/src/row.rs +++ b/sqlx-core/src/row.rs @@ -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 where - R: Row<'c>, + DB: Database, + DB: for<'c> HasRow<'c, Database = DB>, { - fn resolve(self, row: &'c R) -> crate::Result; + fn resolve<'c>(self, row: &>::Row) -> crate::Result; } /// 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(&'c self, index: I) -> crate::Result + fn get<'r, T, I>(&'r self, index: I) -> crate::Result where T: Type, - I: ColumnIndex<'c, Self>, + I: ColumnIndex, 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<>::RawValue> where - I: ColumnIndex<'c, Self> + 'i; + I: ColumnIndex; } /// 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; } @@ -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 { + 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 { - 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 { + 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 { + impl crate::row::ColumnIndex<$DB> for &'_ str { + fn resolve<'c>( + self, + row: &<$DB as crate::database::HasRow<'c>>::Row, + ) -> crate::Result { row.columns .get(self) .ok_or_else(|| crate::Error::ColumnNotFound((*self).into())) diff --git a/sqlx-test/Cargo.toml b/sqlx-test/Cargo.toml new file mode 100644 index 00000000..eb2f567a --- /dev/null +++ b/sqlx-test/Cargo.toml @@ -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" ] } diff --git a/sqlx-test/src/lib.rs b/sqlx-test/src/lib.rs new file mode 100644 index 00000000..ea23ccf7 --- /dev/null +++ b/sqlx-test/src/lib.rs @@ -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() -> anyhow::Result +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(()) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8f2e0ddb..0fdc87c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 _; +} diff --git a/tests/postgres-raw.rs b/tests/postgres-raw.rs new file mode 100644 index 00000000..0492609a --- /dev/null +++ b/tests/postgres-raw.rs @@ -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::().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::().await?; + + let mut cursor = conn.fetch("SELECT 5"); + let row = cursor.next().await?.unwrap(); + + assert!(5i32 == row.get::(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::().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(()) +} diff --git a/tests/postgres-simple.rs b/tests/postgres-simple.rs deleted file mode 100644 index 479aac28..00000000 --- a/tests/postgres-simple.rs +++ /dev/null @@ -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::(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 { - let _ = dotenv::dotenv(); - let _ = env_logger::try_init(); - - Ok(PgConnection::connect(dotenv::var("DATABASE_URL")?).await?) -} diff --git a/tests/postgres-types.rs b/tests/postgres-types.rs index dd83fa30..47c12349 100644 --- a/tests/postgres-types.rs +++ b/tests/postgres-types.rs @@ -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 { - 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) = 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) = 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(()) +// } diff --git a/tests/ui/postgres/issue_30.stderr b/tests/ui/postgres/issue_30.stderr index 444e291d..8ed7261a 100644 --- a/tests/ui/postgres/issue_30.stderr +++ b/tests/ui/postgres/issue_30.stderr @@ -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)