Skip to main content

elefant_tools/
postgres_client_wrapper.rs

1use crate::Result;
2use elefant_client::tokio_connection::TokioPostgresPool;
3use elefant_client::{
4    CollectBatch, ElefantClientError, FlattenTuple, FromSqlOwned, FromSqlRowOwned,
5    PostgresConnectionSettings, PostgresDataRow,
6};
7use tracing::instrument;
8
9/// A wrapper around the elefant-client connection pool, providing a convenient interface.
10pub struct PostgresClientWrapper {
11    pool: TokioPostgresPool,
12    /// The version of the postgres server, reduced by 1000. For example, version 15.0 is represented as 150.
13    version: i32,
14}
15
16impl PostgresClientWrapper {
17    /// Create a new PostgresClientWrapper.
18    ///
19    /// This will connect to the postgres server to figure out the version of the server.
20    /// If the version is less than 12, an error is returned.
21    #[instrument(skip_all)]
22    pub async fn new(settings: PostgresConnectionSettings) -> Result<Self> {
23        let pool = TokioPostgresPool::new(
24            elefant_client::tokio_connection::TokioConnectionFactory,
25            settings,
26        )
27        .await?;
28
29        let client = pool.get_client().await?;
30
31        let version_str = client
32            .get_parameter("server_version")
33            .ok_or(crate::ElefantToolsError::InvalidPostgresVersionResponse)?;
34
35        // server_version is e.g. "15.3" or "15.3 (Debian 15.3-1.pgdg120+1)"
36        let major_version: i32 = version_str
37            .split('.')
38            .next()
39            .and_then(|s| s.parse().ok())
40            .ok_or(crate::ElefantToolsError::InvalidPostgresVersionResponse)?;
41
42        if major_version < 12 {
43            return Err(crate::ElefantToolsError::UnsupportedPostgresVersion(
44                version_str.to_string(),
45            ));
46        }
47
48        let version = major_version * 10;
49
50        Ok(PostgresClientWrapper { pool, version })
51    }
52
53    /// Get the version of the postgres server
54    pub fn version(&self) -> i32 {
55        self.version
56    }
57
58    /// Get a reference to the underlying pool
59    pub fn pool(&self) -> &TokioPostgresPool {
60        &self.pool
61    }
62
63    /// Execute a query that does not return any results.
64    pub async fn execute_non_query(&self, sql: &str) -> Result {
65        let mut client = self.pool.get_client().await?;
66        client.execute_non_query_simple(sql).await.map_err(|e| {
67            crate::ElefantToolsError::PostgresErrorWithQuery {
68                source: e,
69                query: sql.to_string(),
70            }
71        })?;
72
73        Ok(())
74    }
75
76    /// Execute a query that returns results.
77    pub async fn get_results<T: FromSqlRowOwned>(&self, sql: &str) -> Result<Vec<T>> {
78        let mut client = self.pool.get_client().await?;
79        let query_result = client.query_simple(sql).await.map_err(|e| {
80            crate::ElefantToolsError::PostgresErrorWithQuery {
81                source: e,
82                query: sql.to_string(),
83            }
84        })?;
85
86        let rows = query_result.collect_to_vec::<T>().await.map_err(|e| {
87            crate::ElefantToolsError::PostgresErrorWithQuery {
88                source: e,
89                query: sql.to_string(),
90            }
91        })?;
92
93        Ok(rows)
94    }
95
96    /// Execute a query that returns a single result.
97    pub async fn get_result<T: FromSqlRowOwned>(&self, sql: &str) -> Result<T> {
98        let results = self.get_results(sql).await?;
99        if results.len() != 1 {
100            return Err(crate::ElefantToolsError::InvalidNumberOfResults {
101                actual: results.len(),
102                expected: 1,
103            });
104        }
105
106        // Safe, we have just checked the length of the vector
107        let r = results.into_iter().next().unwrap();
108
109        Ok(r)
110    }
111
112    /// Execute a query that returns a single column of results.
113    pub async fn get_single_results<T: FromSqlOwned>(&self, sql: &str) -> Result<Vec<T>> {
114        let r = self
115            .get_results::<(T,)>(sql)
116            .await?
117            .into_iter()
118            .map(|t| t.0)
119            .collect();
120
121        Ok(r)
122    }
123
124    /// Execute a query that returns a single column of a single row of results.
125    pub async fn get_single_result<T: FromSqlOwned>(&self, sql: &str) -> Result<T> {
126        let result = self.get_result::<(T,)>(sql).await?;
127        Ok(result.0)
128    }
129}
130
131/// A trait for converting a postgres char to a Rust type.
132pub(crate) trait FromPgChar: Sized {
133    fn from_pg_char(c: char) -> std::result::Result<Self, crate::ElefantToolsError>;
134}
135
136/// Provides extension methods on PostgresDataRow for working with enums that implements FromPgChar.
137pub(crate) trait RowEnumExt {
138    /// Get an enum value from a row.
139    fn try_get_enum_value<T: FromPgChar>(
140        &self,
141        idx: usize,
142    ) -> std::result::Result<T, ElefantClientError>;
143    /// Get an optional enum value from a row, aka `Option<T>`.
144    fn try_get_opt_enum_value<T: FromPgChar>(
145        &self,
146        idx: usize,
147    ) -> std::result::Result<Option<T>, ElefantClientError>;
148}
149
150impl RowEnumExt for PostgresDataRow<'_, '_> {
151    fn try_get_enum_value<T: FromPgChar>(
152        &self,
153        idx: usize,
154    ) -> std::result::Result<T, ElefantClientError> {
155        let c: char = self.get(idx)?;
156        T::from_pg_char(c).map_err(|e| ElefantClientError::PostgresError(e.to_string()))
157    }
158
159    fn try_get_opt_enum_value<T: FromPgChar>(
160        &self,
161        idx: usize,
162    ) -> std::result::Result<Option<T>, ElefantClientError> {
163        let c: Option<char> = self.get(idx)?;
164        match c {
165            Some('\0') => Ok(None),
166            Some(c) => {
167                Ok(Some(T::from_pg_char(c).map_err(|e| {
168                    ElefantClientError::PostgresError(e.to_string())
169                })?))
170            }
171            None => Ok(None),
172        }
173    }
174}
175
176/// A result type that knows its own SQL query. Implemented by each schema reader
177/// result struct so the batch builder can tie query selection to result collection.
178pub(crate) trait QueryResult: FromSqlRowOwned {
179    fn query(version: i32) -> &'static str;
180}
181
182/// Type-safe batch query builder. Each `.add::<T>()` appends a query (from the
183/// `QueryResult` trait) and its corresponding result type, ensuring the query
184/// order and collect order are always in sync.
185pub(crate) struct BatchQueryBuilder<Batch> {
186    query: String,
187    version: i32,
188    _batch: std::marker::PhantomData<Batch>,
189}
190
191impl BatchQueryBuilder<()> {
192    pub(crate) fn new(connection: &PostgresClientWrapper) -> Self {
193        Self {
194            query: String::new(),
195            version: connection.version(),
196            _batch: std::marker::PhantomData,
197        }
198    }
199}
200
201impl<Batch> BatchQueryBuilder<Batch> {
202    pub(crate) fn add<T: QueryResult>(mut self) -> BatchQueryBuilder<(Batch, Vec<T>)> {
203        self.query.push_str(T::query(self.version));
204        BatchQueryBuilder {
205            query: self.query,
206            version: self.version,
207            _batch: std::marker::PhantomData,
208        }
209    }
210}
211
212impl<Batch: CollectBatch + FlattenTuple> BatchQueryBuilder<Batch> {
213    pub(crate) async fn execute(self, connection: &PostgresClientWrapper) -> Result<Batch::Output> {
214        let mut client = connection.pool().get_client().await?;
215        let mut result = client.query_simple(&self.query).await?;
216        Ok(Batch::collect(&mut result).await?.flatten())
217    }
218}