Skip to main content

sql_composer_postgres/
lib.rs

1//! PostgreSQL driver for sql-composer (sync and async).
2//!
3//! Provides both sync and async wrappers for composing SQL templates
4//! with bind values against PostgreSQL databases.
5//!
6//! - **Async**: [`PgClient`] wraps [`tokio_postgres::Client`] (feature `async`, enabled by default)
7//! - **Sync**: [`PgConnection`] wraps [`postgres::Client`] (feature `sync`, enabled by default)
8//!
9//! # Async Example
10//!
11//! ```ignore
12//! use sql_composer::composer::Composer;
13//! use sql_composer::driver::ComposerConnectionAsync;
14//! use sql_composer::types::{Dialect, TemplateSource};
15//! use sql_composer::bind_values;
16//! use sql_composer_postgres::{PgClient, boxed_params};
17//!
18//! let (client, connection) = tokio_postgres::connect("host=localhost", tokio_postgres::NoTls).await?;
19//! tokio::spawn(connection);
20//! let client = PgClient::from_client(client);
21//!
22//! let template = sql_composer::parser::parse_template(
23//!     "SELECT * FROM users WHERE id = :bind(user_id)",
24//!     TemplateSource::Literal("example".into()),
25//! )?;
26//! let composer = Composer::new(Dialect::Postgres);
27//! let values = bind_values!("user_id" => [Box::new(1i32) as Box<dyn tokio_postgres::types::ToSql + Sync + Send>]);
28//! let (sql, params) = client.compose(&composer, &template, values).await?;
29//! let refs = boxed_params(&params);
30//! let rows = client.query(&sql as &str, &refs).await?;
31//! ```
32//!
33//! # Sync Example
34//!
35//! ```ignore
36//! use sql_composer::composer::Composer;
37//! use sql_composer::driver::ComposerConnection;
38//! use sql_composer::types::{Dialect, TemplateSource};
39//! use sql_composer::bind_values;
40//! use sql_composer_postgres::{PgConnection, boxed_params_sync};
41//!
42//! let mut client = postgres::Client::connect("host=localhost", postgres::NoTls)?;
43//! let conn = PgConnection::from_client(client);
44//!
45//! let template = sql_composer::parser::parse_template(
46//!     "SELECT * FROM users WHERE id = :bind(user_id)",
47//!     TemplateSource::Literal("example".into()),
48//! )?;
49//! let composer = Composer::new(Dialect::Postgres);
50//! let values = bind_values!("user_id" => [Box::new(1i32) as Box<dyn postgres::types::ToSql + Sync>]);
51//! let (sql, params) = conn.compose(&composer, &template, values)?;
52//! let refs = boxed_params_sync(&params);
53//! let rows = conn.query(&sql as &str, &refs)?;
54//! ```
55
56pub use tokio_postgres;
57
58#[cfg(feature = "sync")]
59pub use postgres;
60
61use std::collections::BTreeMap;
62use std::ops::{Deref, DerefMut};
63
64use sql_composer::composer::Composer;
65use sql_composer::driver;
66use sql_composer::types::Template;
67
68/// Error type for sql-composer-postgres operations.
69#[derive(Debug, thiserror::Error)]
70pub enum Error {
71    /// An error from the sql-composer core.
72    #[error(transparent)]
73    Composer(#[from] sql_composer::Error),
74
75    /// An error from tokio-postgres (shared by both sync and async postgres crates).
76    #[error(transparent)]
77    Postgres(#[from] tokio_postgres::Error),
78}
79
80// ---------------------------------------------------------------------------
81// Async: PgClient (tokio-postgres)
82// ---------------------------------------------------------------------------
83
84/// A wrapper around [`tokio_postgres::Client`] that implements
85/// [`sql_composer::driver::ComposerConnectionAsync`].
86///
87/// Dereferences to the inner `tokio_postgres::Client`, so all native async
88/// methods are available directly.
89#[cfg(feature = "async")]
90pub struct PgClient(pub tokio_postgres::Client);
91
92#[cfg(feature = "async")]
93impl PgClient {
94    /// Wrap an existing `tokio_postgres::Client`.
95    pub fn from_client(client: tokio_postgres::Client) -> Self {
96        Self(client)
97    }
98}
99
100#[cfg(feature = "async")]
101impl Deref for PgClient {
102    type Target = tokio_postgres::Client;
103
104    fn deref(&self) -> &Self::Target {
105        &self.0
106    }
107}
108
109#[cfg(feature = "async")]
110impl DerefMut for PgClient {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116/// Helper to convert boxed async params into the reference slice
117/// that tokio-postgres query methods expect.
118#[cfg(feature = "async")]
119pub fn boxed_params(
120    params: &[Box<dyn tokio_postgres::types::ToSql + Sync + Send>],
121) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
122    params
123        .iter()
124        .map(|p| p.as_ref() as &(dyn tokio_postgres::types::ToSql + Sync))
125        .collect()
126}
127
128#[cfg(feature = "async")]
129impl driver::ComposerConnectionAsync for PgClient {
130    type Value = Box<dyn tokio_postgres::types::ToSql + Sync + Send>;
131    type Statement = String;
132    type Error = Error;
133
134    async fn compose(
135        &self,
136        composer: &Composer,
137        template: &Template,
138        mut values: BTreeMap<String, Vec<Self::Value>>,
139    ) -> Result<(String, Vec<Self::Value>), Error> {
140        let composed = composer.compose_with_values(template, &values)?;
141        let ordered = driver::resolve_values(&composed, &mut values)?;
142        Ok((composed.sql, ordered))
143    }
144}
145
146// ---------------------------------------------------------------------------
147// Sync: PgConnection (postgres)
148// ---------------------------------------------------------------------------
149
150/// A wrapper around [`postgres::Client`] that implements [`sql_composer::driver::ComposerConnection`].
151///
152/// Dereferences to the inner `postgres::Client`, so all native sync methods
153/// are available directly.
154#[cfg(feature = "sync")]
155pub struct PgConnection(pub postgres::Client);
156
157#[cfg(feature = "sync")]
158impl PgConnection {
159    /// Wrap an existing `postgres::Client`.
160    pub fn from_client(client: postgres::Client) -> Self {
161        Self(client)
162    }
163}
164
165#[cfg(feature = "sync")]
166impl Deref for PgConnection {
167    type Target = postgres::Client;
168
169    fn deref(&self) -> &Self::Target {
170        &self.0
171    }
172}
173
174#[cfg(feature = "sync")]
175impl DerefMut for PgConnection {
176    fn deref_mut(&mut self) -> &mut Self::Target {
177        &mut self.0
178    }
179}
180
181/// Helper to convert boxed sync params into the reference slice
182/// that postgres query methods expect.
183#[cfg(feature = "sync")]
184pub fn boxed_params_sync(
185    params: &[Box<dyn postgres::types::ToSql + Sync>],
186) -> Vec<&(dyn postgres::types::ToSql + Sync)> {
187    params
188        .iter()
189        .map(|p| p.as_ref() as &(dyn postgres::types::ToSql + Sync))
190        .collect()
191}
192
193#[cfg(feature = "sync")]
194impl driver::ComposerConnection for PgConnection {
195    type Value = Box<dyn postgres::types::ToSql + Sync>;
196    type Statement = String;
197    type Error = Error;
198
199    fn compose(
200        &self,
201        composer: &Composer,
202        template: &Template,
203        mut values: BTreeMap<String, Vec<Self::Value>>,
204    ) -> Result<(String, Vec<Self::Value>), Error> {
205        let composed = composer.compose_with_values(template, &values)?;
206        let ordered = driver::resolve_values(&composed, &mut values)?;
207        Ok((composed.sql, ordered))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use sql_composer::composer::Composer;
214    use sql_composer::parser::parse_template;
215    use sql_composer::types::{Dialect, TemplateSource};
216
217    #[test]
218    fn test_compose_single_bind_postgres() {
219        let input = "SELECT * FROM users WHERE id = :bind(user_id)";
220        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
221        let composer = Composer::new(Dialect::Postgres);
222        let result = composer.compose(&template).unwrap();
223        assert_eq!(result.sql, "SELECT * FROM users WHERE id = $1");
224        assert_eq!(result.bind_params, vec!["user_id"]);
225    }
226
227    #[test]
228    fn test_compose_multiple_binds_postgres() {
229        let input = "SELECT * FROM users WHERE name = :bind(name) AND active = :bind(active)";
230        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
231        let composer = Composer::new(Dialect::Postgres);
232        let result = composer.compose(&template).unwrap();
233        // Alphabetical: active=$1, name=$2
234        assert_eq!(
235            result.sql,
236            "SELECT * FROM users WHERE name = $2 AND active = $1"
237        );
238        assert_eq!(result.bind_params, vec!["active", "name"]);
239    }
240
241    #[test]
242    fn test_compose_with_values_multi_bind_postgres() {
243        let input = "SELECT * FROM users WHERE id IN (:bind(ids))";
244        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
245        let composer = Composer::new(Dialect::Postgres);
246        let values = sql_composer::bind_values!("ids" => [10, 20, 30]);
247        let result = composer.compose_with_values(&template, &values).unwrap();
248        assert_eq!(result.sql, "SELECT * FROM users WHERE id IN ($1, $2, $3)");
249        assert_eq!(result.bind_params, vec!["ids", "ids", "ids"]);
250    }
251}