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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use ejdb_sys;

use super::Collection;
use Result;

impl<'db> Collection<'db> {
    /// Starts a transaction, returning a guard object for it.
    ///
    /// This method can be used to start transactions over an EJDB collection. Transactions
    /// are controlled with their guard objects, which rely on the RAII pattern to abort or
    /// commit transactions when appropriate.
    ///
    /// # Failures
    ///
    /// Returns an error if the corresponding EJDB operation can't be completed successfully.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use ejdb::Database;
    /// let db = Database::open("/path/to/db").unwrap();
    /// let coll = db.collection("some_collection").unwrap();
    /// let tx = coll.begin_transaction().unwrap();
    /// // transaction is now active until `tx` goes out of scope or otherwise consumed
    /// ```
    #[inline]
    pub fn begin_transaction(&self) -> Result<Transaction> {
        Transaction::new(self)
    }

    /// Checks whether there is an active transaction on this collection.
    ///
    /// # Failures
    ///
    /// Returns an error if the corresponding EJDB operation can't be completed successfully.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use ejdb::Database;
    /// let db = Database::open("/path/to/db").unwrap();
    /// let coll = db.collection("some_collection").unwrap();
    /// let tx = coll.begin_transaction().unwrap();
    /// assert!(coll.transaction_active().unwrap());
    /// ```
    pub fn transaction_active(&self) -> Result<bool> {
        let mut result = false;
        if unsafe { ejdb_sys::ejdbtranstatus(self.coll, &mut result) } {
            Ok(result)
        } else {
            self.db.last_error("error getting transaction status")
        }
    }
}

/// Represents an active transaction.
///
/// This structure is a transaction guard for an EJDB collection. It employs the RAII pattern:
/// a value of this structure is returned when a transaction is started and when this value
/// is dropped, the transaction is committed or aborted.
///
/// By default when a transaction goes out of scope, it is aborted. This is done because
/// of the way how errors are handled in Rust: if you interleave working with an EJDB collection
/// with other, potentially failing operations, then it is possible for an error to cause
/// an early return, dropping the transaction in progress. In this case aborting the transaction
/// is usually the most natural option.
///
/// However, it is possible to change the default behavior with `set_commit()/set_abort()` methods,
/// and it is also possible to commit or abort the transaction with `commit()`/`abort()` methods.
/// `finish()` method is essentially equivalent to dropping the transaction guard, except that
/// it returns a value which may contain an error (for regular drops any errors are ignored).
/// `will_commit()`/`will_abort()` methods return `true` if their respective action will be taken
/// upon finishing.
///
/// In EJDB transactions can only span one collection, therefore transactions created from a
/// collection has a direct lifetime dependency on it.
///
/// See `Collection::begin_transaction()` documentation for examples.
pub struct Transaction<'coll, 'db: 'coll> {
    coll: &'coll Collection<'db>,
    commit: bool,
    finished: bool,
}

impl<'coll, 'db> Drop for Transaction<'coll, 'db> {
    fn drop(&mut self) {
        let _ = self.finish_mut(); // ignore the result
    }
}

impl<'coll, 'db> Transaction<'coll, 'db> {
    fn new(coll: &'coll Collection<'db>) -> Result<Transaction<'coll, 'db>> {
        if unsafe { ejdb_sys::ejdbtranbegin(coll.coll) } {
            coll.db.last_error("error opening transaction")
        } else {
            Ok(Transaction {
                coll: coll,
                commit: false,
                finished: false,
            })
        }
    }

    /// Checks whether this transaction will be committed upon drop.
    ///
    /// Returns `true` if this transaction will be committed when dropped or when `finish()`
    /// method is called.
    #[inline]
    pub fn will_commit(&self) -> bool {
        self.commit
    }

    /// Checks whether this transaction will be aborted upon drop.
    ///
    /// Returns `true` if this transaction will be aborted when dropped or when `finish()`
    /// method is called.
    #[inline]
    pub fn will_abort(&self) -> bool {
        !self.commit
    }

    /// Makes this transaction commit when dropped.
    #[inline]
    pub fn set_commit(&mut self) {
        self.commit = true;
    }

    /// Makes this transaction abort when dropped.
    #[inline]
    pub fn set_abort(&mut self) {
        self.commit = false;
    }

    /// Aborts or commits the transaction depending on the finish mode.
    ///
    /// The mode can be changed with `set_commit()` and `set_abort()` methods.
    #[inline]
    pub fn finish(mut self) -> Result<()> {
        self.finish_mut()
    }

    /// Attempts to commit this transaction.
    #[inline]
    pub fn commit(mut self) -> Result<()> {
        self.commit_mut()
    }

    /// Attempts to abort this transaction.
    #[inline]
    pub fn abort(mut self) -> Result<()> {
        self.abort_mut()
    }

    fn finish_mut(&mut self) -> Result<()> {
        if self.finished {
            Ok(())
        } else {
            if self.commit {
                self.commit_mut()
            } else {
                self.abort_mut()
            }
        }
    }

    fn commit_mut(&mut self) -> Result<()> {
        self.finished = true;
        if unsafe { ejdb_sys::ejdbtrancommit(self.coll.coll) } {
            Ok(())
        } else {
            self.coll.db.last_error("error commiting transaction")
        }
    }

    fn abort_mut(&mut self) -> Result<()> {
        self.finished = true;
        if unsafe { ejdb_sys::ejdbtranabort(self.coll.coll) } {
            Ok(())
        } else {
            self.coll.db.last_error("error aborting transaction")
        }
    }
}