1use std::time::Duration;
3
4use async_trait::async_trait;
5
6use crate::dialect::Dialect;
7
8#[derive(Debug, Clone)]
9pub struct Row {
10 columns: Vec<String>,
11 values: Vec<serde_json::Value>,
12}
13
14impl Row {
15 pub fn new(columns: Vec<String>, values: Vec<serde_json::Value>) -> Self {
17 debug_assert_eq!(
18 columns.len(),
19 values.len(),
20 "columns and values must have the same length"
21 );
22 Self { columns, values }
23 }
24
25 pub fn get(&self, col: &str) -> Option<&serde_json::Value> {
26 self.columns
27 .iter()
28 .position(|c| c == col)
29 .and_then(|i| self.values.get(i))
30 }
31
32 pub fn columns(&self) -> &[String] {
33 &self.columns
34 }
35
36 pub fn values(&self) -> &[serde_json::Value] {
37 &self.values
38 }
39
40 pub fn iter(&self) -> impl Iterator<Item = (&str, &serde_json::Value)> {
42 self.columns
43 .iter()
44 .map(String::as_str)
45 .zip(self.values.iter())
46 }
47}
48
49#[async_trait]
51pub trait TransactionInner: Send {
52 async fn execute(&mut self, sql: &str) -> Result<u64, RdbmsError>;
53 async fn query(&mut self, sql: &str) -> Result<Vec<Row>, RdbmsError>;
54 async fn execute_with(
55 &mut self,
56 sql: &str,
57 params: &[serde_json::Value],
58 ) -> Result<u64, RdbmsError>;
59 async fn query_with(
60 &mut self,
61 sql: &str,
62 params: &[serde_json::Value],
63 ) -> Result<Vec<Row>, RdbmsError>;
64 fn dialect(&self) -> Dialect;
65 async fn commit(&mut self) -> Result<(), RdbmsError>;
66 async fn rollback(&mut self) -> Result<(), RdbmsError>;
67}
68
69const NO_BACKING: &str = "transaction has no backing connection (created via Transaction::new)";
73
74pub struct Transaction {
75 committed: bool,
76 rolled_back: bool,
77 dialect: Dialect,
80 inner: tokio::sync::Mutex<Option<Box<dyn TransactionInner>>>,
81}
82
83impl Transaction {
84 pub fn new() -> Self {
89 Self {
90 committed: false,
91 rolled_back: false,
92 dialect: Dialect::Standard,
93 inner: tokio::sync::Mutex::new(None),
94 }
95 }
96
97 pub fn with_inner(inner: Box<dyn TransactionInner>) -> Self {
98 let dialect = inner.dialect();
99 Self {
100 committed: false,
101 rolled_back: false,
102 dialect,
103 inner: tokio::sync::Mutex::new(Some(inner)),
104 }
105 }
106
107 pub async fn commit(mut self) -> Result<(), RdbmsError> {
108 if let Some(inner) = self.inner.get_mut().as_mut() {
109 inner.commit().await?;
110 }
111 self.committed = true;
112 Ok(())
113 }
114
115 pub async fn rollback(mut self) -> Result<(), RdbmsError> {
116 if let Some(inner) = self.inner.get_mut().as_mut() {
117 inner.rollback().await?;
118 }
119 self.committed = false;
120 self.rolled_back = true;
121 Ok(())
122 }
123}
124
125impl Default for Transaction {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131#[async_trait]
132impl SqlExecutor for Transaction {
133 async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
134 let mut guard = self.inner.lock().await;
135 match guard.as_mut() {
136 Some(inner) => inner.execute(sql).await,
137 None => Err(RdbmsError::Database(NO_BACKING.into())),
138 }
139 }
140
141 async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
142 let mut guard = self.inner.lock().await;
143 match guard.as_mut() {
144 Some(inner) => inner.query(sql).await,
145 None => Err(RdbmsError::Database(NO_BACKING.into())),
146 }
147 }
148
149 async fn execute_with(
150 &self,
151 sql: &str,
152 params: &[serde_json::Value],
153 ) -> Result<u64, RdbmsError> {
154 let mut guard = self.inner.lock().await;
155 match guard.as_mut() {
156 Some(inner) => inner.execute_with(sql, params).await,
157 None => Err(RdbmsError::Database(NO_BACKING.into())),
158 }
159 }
160
161 async fn query_with(
162 &self,
163 sql: &str,
164 params: &[serde_json::Value],
165 ) -> Result<Vec<Row>, RdbmsError> {
166 let mut guard = self.inner.lock().await;
167 match guard.as_mut() {
168 Some(inner) => inner.query_with(sql, params).await,
169 None => Err(RdbmsError::Database(NO_BACKING.into())),
170 }
171 }
172
173 async fn execute_then_query(
180 &self,
181 first: &str,
182 first_params: &[serde_json::Value],
183 second: &str,
184 ) -> Result<Vec<Row>, RdbmsError> {
185 let mut guard = self.inner.lock().await;
186 match guard.as_mut() {
187 Some(inner) => {
188 inner.execute_with(first, first_params).await?;
189 inner.query(second).await
190 }
191 None => Err(RdbmsError::Database(NO_BACKING.into())),
192 }
193 }
194
195 fn dialect(&self) -> Dialect {
196 self.dialect
197 }
198}
199
200impl Drop for Transaction {
201 fn drop(&mut self) {
202 if !self.committed && !self.rolled_back {
205 crate::timeout::TRANSACTIONS_LEAKED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
206 tracing::warn!("transaction dropped without commit — rolling back");
207 }
208 }
209}
210
211#[async_trait]
212pub trait SqlExecutor: Send + Sync {
213 async fn execute(&self, sql: &str) -> Result<u64, RdbmsError>;
216 async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError>;
218 async fn execute_with(
220 &self,
221 _sql: &str,
222 _params: &[serde_json::Value],
223 ) -> Result<u64, RdbmsError> {
224 Err(RdbmsError::Database(
225 "parameterized execute not supported by this backend".into(),
226 ))
227 }
228 async fn query_with(
230 &self,
231 _sql: &str,
232 _params: &[serde_json::Value],
233 ) -> Result<Vec<Row>, RdbmsError> {
234 Err(RdbmsError::Database(
235 "parameterized query not supported by this backend".into(),
236 ))
237 }
238 async fn query_write(
242 &self,
243 sql: &str,
244 params: &[serde_json::Value],
245 ) -> Result<Vec<Row>, RdbmsError> {
246 self.query_with(sql, params).await
247 }
248 async fn execute_then_query(
259 &self,
260 _first: &str,
261 _first_params: &[serde_json::Value],
262 _second: &str,
263 ) -> Result<Vec<Row>, RdbmsError> {
264 Err(RdbmsError::Database(
265 "this backend cannot run two statements atomically on one connection".into(),
266 ))
267 }
268 fn dialect(&self) -> Dialect;
270}
271
272#[async_trait]
273pub trait RdbmsClient: SqlExecutor {
274 async fn transaction(&self) -> Result<Transaction, RdbmsError>;
275
276 async fn replica_lag(&self) -> Option<Duration> {
279 None
280 }
281
282 fn pool_size(&self) -> Option<u32> {
286 None
287 }
288}
289
290#[derive(Debug, thiserror::Error)]
291pub enum RdbmsError {
292 #[error("database error: {0}")]
293 Database(String),
294 #[error("connection error: {0}")]
295 Connection(String),
296 #[error("configuration error: {0}")]
297 Config(String),
298 #[error("timeout: {0}")]
299 Timeout(String),
300 #[error("no available replica")]
303 NoAvailableReplica,
304}
305
306#[cfg(test)]
308mod tests;