Skip to content

Commit 61ca708

Browse files
TonyMassonclaude
andauthored
Add manual transaction API with commit and automatic rollback support (#48)
## Summary - Add `Transaction` struct with explicit commit() method for manual transaction control - Implement automatic rollback via `Drop` trait when transaction is not committed - Add `begin_transaction()` method to Database for creating manual transactions - Include comprehensive unit tests for both commit and rollback scenarios ## Changes - `Database::begin_transaction()` - creates a new manual transaction - `Transaction::commit()` - explicitly commits the transaction - `Transaction` Drop implementation - automatically rolls back if not committed - Unit tests covering both commit and rollback behavior - Updated existing tests to use new collection APIs ## API Design The new Transaction API complements the existing `in_transaction()` callback approach: - **Manual transactions**: More flexible for complex logic and cross-function usage - **Callback transactions**: Simpler for basic cases with automatic cleanup ## Usage Example ```rust // Manual transaction with explicit commit let mut db = Database::open("mydb", None)?; let transaction = db.begin_transaction()?; // ... perform operations ... transaction.commit()?; // Automatic rollback via Drop { let _transaction = db.begin_transaction()?; // ... perform operations ... } // automatic rollback here ``` ## Test plan - [x] Unit tests for successful transaction commit - [x] Unit tests for automatic rollback on Drop - [x] All existing tests still pass - [x] Code formatting and linting checks pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent 53626e1 commit 61ca708

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

src/database.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,15 @@ impl Database {
353353
result
354354
}
355355

356+
/// Creates a new transaction that can be committed or rolled back manually.
357+
/// Returns a Transaction object that must be committed explicitly, otherwise
358+
/// it will be rolled back when dropped.
359+
///
360+
/// For simpler cases, consider using `in_transaction()` instead.
361+
pub fn begin_transaction(&mut self) -> Result<Transaction> {
362+
Transaction::new(self.get_ref())
363+
}
364+
356365
/// Encrypts or decrypts a database, or changes its encryption key.
357366
#[cfg(feature = "enterprise")]
358367
pub fn change_encryption_key(&mut self, encryption_key: &EncryptionKey) -> Result<()> {
@@ -594,6 +603,51 @@ impl Database {
594603
}
595604
}
596605

606+
/// A database transaction that can be committed or rolled back.
607+
/// When dropped without being committed, the transaction is automatically rolled back.
608+
pub struct Transaction {
609+
db_ref: *mut CBLDatabase,
610+
committed: bool,
611+
}
612+
613+
impl Transaction {
614+
fn new(db_ref: *mut CBLDatabase) -> Result<Self> {
615+
unsafe {
616+
let mut err = CBLError::default();
617+
if !CBLDatabase_BeginTransaction(db_ref, &mut err) {
618+
return failure(err);
619+
}
620+
}
621+
Ok(Transaction {
622+
db_ref,
623+
committed: false,
624+
})
625+
}
626+
627+
/// Commits the transaction, making all changes permanent.
628+
pub fn commit(mut self) -> Result<()> {
629+
unsafe {
630+
let mut err = CBLError::default();
631+
if !CBLDatabase_EndTransaction(self.db_ref, true, &mut err) {
632+
return failure(err);
633+
}
634+
}
635+
self.committed = true;
636+
Ok(())
637+
}
638+
}
639+
640+
impl Drop for Transaction {
641+
fn drop(&mut self) {
642+
if !self.committed {
643+
unsafe {
644+
let mut err = CBLError::default();
645+
let _ = CBLDatabase_EndTransaction(self.db_ref, false, &mut err);
646+
}
647+
}
648+
}
649+
}
650+
597651
impl Drop for Database {
598652
fn drop(&mut self) {
599653
unsafe { release(self.get_ref()) }

tests/database_tests.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ fn in_transaction() {
116116
utils::with_db(|db| {
117117
let result = db.in_transaction(|db| {
118118
let mut doc = Document::new_with_id("document");
119-
db.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
119+
let mut collection = db.default_collection_or_error().unwrap();
120+
collection
121+
.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
120122
.unwrap();
121123
Ok("document".to_string())
122124
});
@@ -126,15 +128,60 @@ fn in_transaction() {
126128

127129
let result = db.in_transaction(|db| -> Result<String> {
128130
let mut doc = Document::new_with_id("document_error");
129-
db.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
131+
let mut collection = db.default_collection_or_error().unwrap();
132+
collection
133+
.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
130134
.unwrap();
131135
Err(couchbase_lite::Error::default())
132136
});
133137

134138
assert!(result.is_err());
135139

136-
assert!(db.get_document("document").is_ok());
137-
assert!(db.get_document("document_error").is_err());
140+
let mut collection = db.default_collection_or_error().unwrap();
141+
assert!(collection.get_document("document").is_ok());
142+
assert!(collection.get_document("document_error").is_err());
143+
});
144+
}
145+
146+
#[test]
147+
fn manual_transaction_commit() {
148+
utils::with_db(|db| {
149+
let initial_count = db.count();
150+
151+
{
152+
let transaction = db.begin_transaction().unwrap();
153+
let mut doc = Document::new_with_id("manual_commit_doc");
154+
let mut collection = db.default_collection_or_error().unwrap();
155+
collection
156+
.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
157+
.unwrap();
158+
transaction.commit().unwrap();
159+
}
160+
161+
assert_eq!(db.count(), initial_count + 1);
162+
let mut collection = db.default_collection_or_error().unwrap();
163+
assert!(collection.get_document("manual_commit_doc").is_ok());
164+
});
165+
}
166+
167+
#[test]
168+
fn manual_transaction_rollback() {
169+
utils::with_db(|db| {
170+
let initial_count = db.count();
171+
172+
{
173+
let _transaction = db.begin_transaction().unwrap();
174+
let mut doc = Document::new_with_id("rollback_doc");
175+
let mut collection = db.default_collection_or_error().unwrap();
176+
collection
177+
.save_document_with_concurency_control(&mut doc, ConcurrencyControl::LastWriteWins)
178+
.unwrap();
179+
// Transaction dropped here without commit -> automatic rollback
180+
}
181+
182+
assert_eq!(db.count(), initial_count);
183+
let mut collection = db.default_collection_or_error().unwrap();
184+
assert!(collection.get_document("rollback_doc").is_err());
138185
});
139186
}
140187

0 commit comments

Comments
 (0)