Skip to main content

qail_pg/driver/
transaction.rs

1//! Transaction control methods for PostgreSQL connection.
2
3use super::{PgConnection, PgError, PgResult};
4
5/// Quote a SQL identifier (for savepoint names).
6/// Wraps in double-quotes and escapes embedded double-quotes.
7fn quote_savepoint_name(name: &str) -> PgResult<String> {
8    if name.is_empty() {
9        return Err(PgError::Query("savepoint name is empty".to_string()));
10    }
11    if name.contains('\0') {
12        return Err(PgError::Query(
13            "savepoint name contains NUL byte".to_string(),
14        ));
15    }
16    Ok(format!("\"{}\"", name.replace('"', "\"\"")))
17}
18
19impl PgConnection {
20    /// Begin a new transaction.
21    /// After calling this, all queries run within the transaction
22    /// until `commit()` or `rollback()` is called.
23    pub async fn begin_transaction(&mut self) -> PgResult<()> {
24        self.execute_simple("BEGIN").await
25    }
26
27    /// Commit the current transaction.
28    /// Makes all changes since `begin_transaction()` permanent.
29    pub async fn commit(&mut self) -> PgResult<()> {
30        self.execute_simple("COMMIT").await
31    }
32
33    /// Rollback the current transaction.
34    /// Discards all changes since `begin_transaction()`.
35    pub async fn rollback(&mut self) -> PgResult<()> {
36        self.execute_simple("ROLLBACK").await
37    }
38
39    /// Create a named savepoint within the current transaction.
40    /// Savepoints allow partial rollback within a transaction.
41    /// Use `rollback_to()` to return to this savepoint.
42    pub async fn savepoint(&mut self, name: &str) -> PgResult<()> {
43        self.execute_simple(&format!("SAVEPOINT {}", quote_savepoint_name(name)?))
44            .await
45    }
46
47    /// Rollback to a previously created savepoint.
48    /// Discards all changes since the named savepoint was created,
49    /// but keeps the transaction open.
50    pub async fn rollback_to(&mut self, name: &str) -> PgResult<()> {
51        self.execute_simple(&format!(
52            "ROLLBACK TO SAVEPOINT {}",
53            quote_savepoint_name(name)?
54        ))
55        .await
56    }
57
58    /// Release a savepoint (free resources, if no longer needed).
59    pub async fn release_savepoint(&mut self, name: &str) -> PgResult<()> {
60        self.execute_simple(&format!(
61            "RELEASE SAVEPOINT {}",
62            quote_savepoint_name(name)?
63        ))
64        .await
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::quote_savepoint_name;
71
72    #[test]
73    fn quote_savepoint_name_escapes_quotes() {
74        assert_eq!(quote_savepoint_name("sp\"1").unwrap(), "\"sp\"\"1\"");
75    }
76
77    #[test]
78    fn quote_savepoint_name_rejects_empty_or_nul() {
79        assert!(quote_savepoint_name("").is_err());
80        assert!(quote_savepoint_name("sp\0shadow").is_err());
81    }
82}