1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! Firebird transaction

use super::connection::FbConnection;
use diesel::connection::TransactionManagerStatus;
use diesel::result::DatabaseErrorKind;
use diesel::result::Error::DatabaseError;
use diesel::QueryResult;
use diesel::{connection::*, RunQueryDsl};

/// Firebird transaction manager
pub struct FbTransactionManager {
    status: TransactionManagerStatus,
}

impl FbTransactionManager {
    pub fn new() -> Self {
        FbTransactionManager {
            status: Default::default(),
        }
    }

    fn get_transaction_state(
        conn: &mut FbConnection,
    ) -> QueryResult<&mut ValidTransactionManagerStatus> {
        match FbTransactionManager::transaction_manager_status_mut(conn) {
            TransactionManagerStatus::Valid(v) => Ok(v),
            TransactionManagerStatus::InError => {
                Err(diesel::result::Error::BrokenTransactionManager)
            }
        }
    }
}

impl Default for FbTransactionManager {
    fn default() -> Self {
        FbTransactionManager::new()
    }
}

impl TransactionManager<FbConnection> for FbTransactionManager {
    type TransactionStateData = Self;

    fn begin_transaction(conn: &mut FbConnection) -> QueryResult<()> {
        let state = Self::transaction_manager_status_mut(conn);
        let depth = state.transaction_depth()?;
        if let Some(depth) = depth {
            // Firebird does not support nested transactions, so
            // let's simulate this using save points
            diesel::sql_query(&format!("savepoint sp_diesel_{}", u32::from(depth) + 1))
                .execute(conn)?;
        } else {
            conn.raw
                .begin_transaction()
                .map_err(|e| DatabaseError(DatabaseErrorKind::Unknown, Box::new(e.to_string())))?;
        }
        if let TransactionManagerStatus::Valid(s) = Self::transaction_manager_status_mut(conn) {
            s.change_transaction_depth(TransactionDepthChange::IncreaseDepth)?;
        }
        Ok(())
    }

    fn rollback_transaction(conn: &mut FbConnection) -> QueryResult<()> {
        let transaction_state = Self::get_transaction_state(conn)?;

        let rollback_result = match transaction_state.transaction_depth().map(|d| d.get()) {
            Some(1) => conn
                .raw
                .rollback()
                .map_err(|e| DatabaseError(DatabaseErrorKind::Unknown, Box::new(e.to_string()))),
            Some(depth_gt1) => {
                diesel::sql_query(&format!("rollback to savepoint sp_diesel_{}", depth_gt1))
                    .execute(conn)
                    .map(|_| ())
            }
            None => return Err(diesel::result::Error::NotInTransaction),
        };

        match rollback_result {
            Ok(()) => {
                Self::get_transaction_state(conn)?
                    .change_transaction_depth(TransactionDepthChange::DecreaseDepth)?;
                Ok(())
            }
            Err(rollback_error) => {
                let tm_status = Self::transaction_manager_status_mut(conn);
                tm_status.set_in_error();
                Err(rollback_error)
            }
        }
    }

    fn commit_transaction(conn: &mut FbConnection) -> QueryResult<()> {
        let transaction_state = Self::get_transaction_state(conn)?;
        let transaction_depth = transaction_state.transaction_depth();
        let commit_result = match transaction_depth {
            None => return Err(diesel::result::Error::NotInTransaction),
            Some(transaction_depth) if transaction_depth.get() == 1 => conn
                .raw
                .commit()
                .map_err(|e| DatabaseError(DatabaseErrorKind::Unknown, Box::new(e.to_string()))),
            Some(_transaction_depth) => Ok(()),
        };
        match commit_result {
            Ok(()) => {
                Self::get_transaction_state(conn)?
                    .change_transaction_depth(TransactionDepthChange::DecreaseDepth)?;
                Ok(())
            }
            Err(commit_error) => {
                if let TransactionManagerStatus::Valid(ref mut s) = conn.transaction_state().status
                {
                    match s.transaction_depth().map(|p| p.get()) {
                        Some(1) => match Self::rollback_transaction(conn) {
                            Ok(()) => {}
                            Err(rollback_error) => {
                                conn.transaction_state().status.set_in_error();
                                return Err(diesel::result::Error::RollbackErrorOnCommit {
                                    rollback_error: Box::new(rollback_error),
                                    commit_error: Box::new(commit_error),
                                });
                            }
                        },
                        Some(_depth_gt1) => {
                            // There's no point in *actually* rolling back this one
                            // because we won't be able to do anything until top-level
                            // is rolled back.

                            // To make it easier on the user (that they don't have to really look
                            // at actual transaction depth and can just rely on the number of
                            // times they have called begin/commit/rollback) we don't mark the
                            // transaction manager as out of the savepoints as soon as we
                            // realize there is that issue, but instead we still decrement here:
                            s.change_transaction_depth(TransactionDepthChange::DecreaseDepth)?;
                        }
                        None => {}
                    }
                }
                Err(commit_error)
            }
        }
    }

    fn transaction_manager_status_mut(conn: &mut FbConnection) -> &mut TransactionManagerStatus {
        &mut conn.transaction_state().status
    }
}