Skip to main content

tusker_query/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![deny(
4    nonstandard_style,
5    rust_2018_idioms,
6    rustdoc::broken_intra_doc_links,
7    rustdoc::private_intra_doc_links
8)]
9#![forbid(non_ascii_idents, unsafe_code)]
10#![warn(
11    deprecated_in_future,
12    missing_copy_implementations,
13    missing_debug_implementations,
14    missing_docs,
15    unreachable_pub,
16    unused_import_braces,
17    unused_labels,
18    unused_lifetimes,
19    unused_qualifications,
20    unused_results
21)]
22#![allow(clippy::uninlined_format_args)]
23
24use tokio_postgres::{types::ToSql, Error, Row, Statement};
25
26pub use tusker_query_derive::Query;
27
28/// Marker traits and PostgreSQL type markers used by checked query validation.
29pub mod types;
30
31#[doc(hidden)]
32pub mod __private {
33    use super::{Error, Row, Statement, ToSql};
34
35    pub mod sealed {
36        pub trait Sealed {}
37
38        impl Sealed for tokio_postgres::Client {}
39        impl Sealed for tokio_postgres::Transaction<'_> {}
40
41        #[cfg(feature = "deadpool")]
42        impl Sealed for deadpool_postgres::Client {}
43        #[cfg(feature = "deadpool")]
44        impl Sealed for deadpool_postgres::Transaction<'_> {}
45    }
46
47    #[doc(hidden)]
48    pub trait QueryClient: sealed::Sealed {
49        fn prepare_query(
50            &self,
51            query: &str,
52        ) -> impl core::future::Future<Output = Result<Statement, Error>> + Send;
53
54        fn query_prepared<'a>(
55            &'a self,
56            statement: &'a Statement,
57            params: &'a [&'a (dyn ToSql + Sync)],
58        ) -> impl core::future::Future<Output = Result<Vec<Row>, Error>> + Send + 'a;
59
60        fn query_one_prepared<'a>(
61            &'a self,
62            statement: &'a Statement,
63            params: &'a [&'a (dyn ToSql + Sync)],
64        ) -> impl core::future::Future<Output = Result<Row, Error>> + Send + 'a;
65    }
66
67    impl QueryClient for tokio_postgres::Client {
68        async fn prepare_query(&self, query: &str) -> Result<Statement, Error> {
69            self.prepare(query).await
70        }
71
72        async fn query_prepared<'a>(
73            &'a self,
74            statement: &'a Statement,
75            params: &'a [&'a (dyn ToSql + Sync)],
76        ) -> Result<Vec<Row>, Error> {
77            self.query(statement, params).await
78        }
79
80        async fn query_one_prepared<'a>(
81            &'a self,
82            statement: &'a Statement,
83            params: &'a [&'a (dyn ToSql + Sync)],
84        ) -> Result<Row, Error> {
85            self.query_one(statement, params).await
86        }
87    }
88
89    impl QueryClient for tokio_postgres::Transaction<'_> {
90        async fn prepare_query(&self, query: &str) -> Result<Statement, Error> {
91            self.prepare(query).await
92        }
93
94        async fn query_prepared<'a>(
95            &'a self,
96            statement: &'a Statement,
97            params: &'a [&'a (dyn ToSql + Sync)],
98        ) -> Result<Vec<Row>, Error> {
99            self.query(statement, params).await
100        }
101
102        async fn query_one_prepared<'a>(
103            &'a self,
104            statement: &'a Statement,
105            params: &'a [&'a (dyn ToSql + Sync)],
106        ) -> Result<Row, Error> {
107            self.query_one(statement, params).await
108        }
109    }
110
111    #[cfg(feature = "deadpool")]
112    impl QueryClient for deadpool_postgres::Client {
113        async fn prepare_query(&self, query: &str) -> Result<Statement, Error> {
114            deadpool_postgres::GenericClient::prepare_cached(self, query).await
115        }
116
117        async fn query_prepared<'a>(
118            &'a self,
119            statement: &'a Statement,
120            params: &'a [&'a (dyn ToSql + Sync)],
121        ) -> Result<Vec<Row>, Error> {
122            deadpool_postgres::GenericClient::query(self, statement, params).await
123        }
124
125        async fn query_one_prepared<'a>(
126            &'a self,
127            statement: &'a Statement,
128            params: &'a [&'a (dyn ToSql + Sync)],
129        ) -> Result<Row, Error> {
130            deadpool_postgres::GenericClient::query_one(self, statement, params).await
131        }
132    }
133
134    #[cfg(feature = "deadpool")]
135    impl QueryClient for deadpool_postgres::Transaction<'_> {
136        async fn prepare_query(&self, query: &str) -> Result<Statement, Error> {
137            deadpool_postgres::GenericClient::prepare_cached(self, query).await
138        }
139
140        async fn query_prepared<'a>(
141            &'a self,
142            statement: &'a Statement,
143            params: &'a [&'a (dyn ToSql + Sync)],
144        ) -> Result<Vec<Row>, Error> {
145            deadpool_postgres::GenericClient::query(self, statement, params).await
146        }
147
148        async fn query_one_prepared<'a>(
149            &'a self,
150            statement: &'a Statement,
151            params: &'a [&'a (dyn ToSql + Sync)],
152        ) -> Result<Row, Error> {
153            deadpool_postgres::GenericClient::query_one(self, statement, params).await
154        }
155    }
156
157    pub trait RowFieldCount<const N: usize> {}
158
159    pub trait RowFieldType<const I: usize> {
160        type Ty;
161    }
162}
163
164/// A typed SQL query that can be executed through `tokio-postgres`.
165pub trait Query: Sized {
166    /// SQL text loaded from the query file.
167    const SQL: &'static str;
168    /// Row type returned by the query.
169    type Row: FromRow;
170    /// Query bind parameters in positional order.
171    fn as_params(&self) -> Box<[&(dyn ToSql + Sync)]>;
172}
173
174/// Converts a `tokio-postgres` row into a strongly typed Rust value.
175pub trait FromRow {
176    /// Builds `Self` from a single database row.
177    fn from_row(row: Row) -> Self;
178}
179
180pub use tusker_query_derive::FromRow;
181
182impl FromRow for () {
183    fn from_row(_: Row) -> Self {}
184}
185
186impl __private::RowFieldCount<0> for () {}
187
188/// Executes a query that must return exactly one row.
189pub async fn query_one<Q: Query, C>(client: &C, query: Q) -> Result<Q::Row, Error>
190where
191    C: __private::QueryClient + ?Sized,
192{
193    let stmt = client.prepare_query(Q::SQL).await?;
194    Ok(Q::Row::from_row(
195        client.query_one_prepared(&stmt, &query.as_params()).await?,
196    ))
197}
198
199/// Executes a query and collects all returned rows.
200pub async fn query<Q: Query, C>(client: &C, query: Q) -> Result<Vec<Q::Row>, Error>
201where
202    C: __private::QueryClient + ?Sized,
203{
204    let stmt = client.prepare_query(Q::SQL).await?;
205    let rows = client.query_prepared(&stmt, &query.as_params()).await?;
206    Ok(rows.into_iter().map(Q::Row::from_row).collect())
207}
208
209#[cfg(all(test, feature = "deadpool"))]
210mod tests {
211    use super::__private::QueryClient;
212
213    fn assert_query_client<T: QueryClient>() {}
214
215    #[test]
216    fn deadpool_client_implements_query_client() {
217        assert_query_client::<deadpool_postgres::Client>();
218    }
219
220    #[test]
221    fn deadpool_transaction_implements_query_client() {
222        fn assert_transaction<'a>()
223        where
224            deadpool_postgres::Transaction<'a>: QueryClient,
225        {
226        }
227
228        let _ = assert_transaction;
229    }
230}
231
232#[cfg(test)]
233mod doc_tests {
234    #[test]
235    fn readme_does_not_reference_public_query_client_trait() {
236        assert!(!include_str!("../README.md").contains("tusker_query::QueryClient"));
237    }
238}