1mod migrate;
11mod options;
12
13pub use migrate::{MigrateError, MigrateReport, Migration};
14pub use options::{ConnectOptions, TlsPolicy};
15
16use persistant_relational::{Dialect, RelError, RelationalBackend, RelationalTx, Row, Value};
17use sqlx::postgres::{PgArguments, PgPoolOptions, PgRow};
18use sqlx::query::Query;
19use sqlx::{Column, Postgres, Row as SqlxRow, TypeInfo, ValueRef};
20
21pub(crate) fn classify(e: sqlx::Error) -> RelError {
23 match &e {
24 sqlx::Error::Database(db) => {
25 let constraint = db.constraint().map(str::to_string);
26 match db.code().as_deref() {
27 Some("23505") => RelError::UniqueViolation { constraint },
28 Some("23503") => RelError::ForeignKeyViolation { constraint },
29 Some("23514") | Some("23502") => RelError::CheckViolation { constraint },
30 Some("40001") => RelError::Serialization,
31 Some("40P01") => RelError::Deadlock,
32 Some(c) if c.starts_with("08") || c.starts_with("57P") => {
33 RelError::Unavailable(e.to_string())
34 }
35 _ => RelError::Backend(e.to_string()),
36 }
37 }
38 sqlx::Error::Io(_) | sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed => {
39 RelError::Unavailable(e.to_string())
40 }
41 _ => RelError::Backend(e.to_string()),
42 }
43}
44
45fn bind<'q>(
46 mut q: Query<'q, Postgres, PgArguments>,
47 params: &'q [Value],
48) -> Result<Query<'q, Postgres, PgArguments>, RelError> {
49 for p in params {
50 q = match p {
51 Value::Null => q.bind(Option::<String>::None),
54 Value::Bool(b) => q.bind(*b),
55 Value::Int(n) => q.bind(*n),
56 Value::Float(f) => q.bind(*f),
57 Value::Text(s) => q.bind(s.as_str()),
58 Value::Bytes(b) => q.bind(b.as_slice()),
59 other => {
60 return Err(RelError::Unsupported(format!(
61 "parameter {other:?} has no PostgreSQL binding in this driver"
62 )));
63 }
64 };
65 }
66 Ok(q)
67}
68
69fn decode(row: &PgRow) -> Result<Row, RelError> {
70 let mut values = Vec::with_capacity(row.columns().len());
71 for (i, col) in row.columns().iter().enumerate() {
72 let raw = row.try_get_raw(i).map_err(classify)?;
73 if raw.is_null() {
74 values.push(Value::Null);
75 continue;
76 }
77 let name = col.type_info().name().to_string();
78 let v = match name.as_str() {
79 "BOOL" => Value::Bool(row.try_get::<bool, _>(i).map_err(classify)?),
80 "INT2" => Value::Int(i64::from(row.try_get::<i16, _>(i).map_err(classify)?)),
81 "INT4" => Value::Int(i64::from(row.try_get::<i32, _>(i).map_err(classify)?)),
82 "INT8" => Value::Int(row.try_get::<i64, _>(i).map_err(classify)?),
83 "FLOAT4" => Value::Float(f64::from(row.try_get::<f32, _>(i).map_err(classify)?)),
84 "FLOAT8" => Value::Float(row.try_get::<f64, _>(i).map_err(classify)?),
85 "TEXT" | "VARCHAR" | "NAME" | "BPCHAR" => {
86 Value::Text(row.try_get::<String, _>(i).map_err(classify)?)
87 }
88 "BYTEA" => Value::Bytes(row.try_get::<Vec<u8>, _>(i).map_err(classify)?),
89 other => {
90 return Err(RelError::Unsupported(format!(
91 "column {:?} has type {other}, which Value cannot carry; use PgBackend::pool",
92 col.name()
93 )));
94 }
95 };
96 values.push(v);
97 }
98 Ok(Row::new(values))
99}
100
101#[derive(Debug, Clone)]
103pub struct PgBackend {
104 pool: sqlx::PgPool,
105 version: String,
106}
107
108impl PgBackend {
109 pub async fn connect(url: &str) -> Result<Self, RelError> {
113 let pool = PgPoolOptions::new()
114 .max_connections(16)
115 .connect(url)
116 .await
117 .map_err(classify)?;
118 Self::from_pool(pool).await
119 }
120
121 pub async fn connect_secret(
124 secret: &persistant_relational::ConnectionSecret,
125 ) -> Result<Self, RelError> {
126 let url = secret.resolve()?;
127 Self::connect(url.expose()).await
128 }
129
130 pub async fn from_pool(pool: sqlx::PgPool) -> Result<Self, RelError> {
132 let full: String = sqlx::query_scalar("SHOW server_version")
133 .fetch_one(&pool)
134 .await
135 .map_err(classify)?;
136 let version = full.split_whitespace().next().unwrap_or("").to_string();
138 Ok(PgBackend { pool, version })
139 }
140
141 pub fn pool(&self) -> &sqlx::PgPool {
143 &self.pool
144 }
145}
146
147impl RelationalBackend for PgBackend {
148 type Tx<'a> = PgTx<'a>;
149
150 fn backend(&self) -> &str {
151 "postgres"
152 }
153
154 fn version(&self) -> &str {
155 &self.version
156 }
157
158 fn dialect(&self) -> Dialect {
159 Dialect::Postgres
160 }
161
162 async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
163 let q = bind(sqlx::query(sqlx::AssertSqlSafe(sql.to_string())), params)?;
164 q.execute(&self.pool)
165 .await
166 .map(|r| r.rows_affected())
167 .map_err(classify)
168 }
169
170 async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
171 let q = bind(sqlx::query(sqlx::AssertSqlSafe(sql.to_string())), params)?;
172 let rows = q.fetch_all(&self.pool).await.map_err(classify)?;
173 rows.iter().map(decode).collect()
174 }
175
176 async fn begin(&self) -> Result<PgTx<'_>, RelError> {
177 Ok(PgTx {
178 tx: self.pool.begin().await.map_err(classify)?,
179 })
180 }
181}
182
183#[derive(Debug)]
185pub struct PgTx<'a> {
186 tx: sqlx::Transaction<'a, Postgres>,
187}
188
189impl RelationalTx for PgTx<'_> {
190 async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
191 let q = bind(sqlx::query(sqlx::AssertSqlSafe(sql.to_string())), params)?;
192 q.execute(&mut *self.tx)
193 .await
194 .map(|r| r.rows_affected())
195 .map_err(classify)
196 }
197
198 async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
199 let q = bind(sqlx::query(sqlx::AssertSqlSafe(sql.to_string())), params)?;
200 let rows = q.fetch_all(&mut *self.tx).await.map_err(classify)?;
201 rows.iter().map(decode).collect()
202 }
203
204 async fn commit(self) -> Result<(), RelError> {
205 self.tx.commit().await.map_err(classify)
206 }
207
208 async fn rollback(self) -> Result<(), RelError> {
209 self.tx.rollback().await.map_err(classify)
210 }
211}