Transactions
ACID transactions with MVCC concurrency control
Overview
jasonisnthappy provides full ACID (Atomicity, Consistency, Isolation, Durability) transaction support with multi-version concurrency control (MVCC). This means:
- Atomicity: All operations in a transaction succeed or fail together
- Consistency: Transactions maintain database integrity constraints
- Isolation: Concurrent transactions don't interfere with each other
- Durability: Committed transactions are permanently saved
All single collection operations are automatically wrapped in transactions. For operations spanning multiple collections or requiring explicit control, use manual transactions.
Manual Transactions
Begin a transaction, perform operations, and explicitly commit or rollback:
use jasonisnthappy::Database;
use serde_json::json;
let db = Database::open("myapp.db")?;
// Begin transaction
let mut tx = db.begin()?;
let mut users = tx.collection("users")?;
// Perform operations
let id1 = users.insert(json!({"name": "Alice"}))?;
let id2 = users.insert(json!({"name": "Bob"}))?;
// Commit all changes
tx.commit()?;
// Or rollback if something went wrong
// tx.rollback()?;
from jasonisnthappy import Database
db = Database.open("./myapp.db")
# Begin transaction
tx = db.begin_transaction()
try:
# Perform operations
id1 = tx.insert("users", {"name": "Alice"})
id2 = tx.insert("users", {"name": "Bob"})
# Commit all changes
tx.commit()
except Exception:
# Rollback on error
tx.rollback()
raise
finally:
db.close()
import { Database } from 'jasonisnthappy';
const db = Database.open('./myapp.db');
// Begin transaction
const tx = db.beginTransaction();
try {
// Perform operations
const id1 = tx.insert('users', { name: 'Alice' });
const id2 = tx.insert('users', { name: 'Bob' });
// Commit all changes
tx.commit();
} catch (err) {
// Rollback on error
tx.rollback();
throw err;
} finally {
db.close();
}
import jasonisnthappy "github.com/jasonisnthappy/jasonisnthappy-rs/bindings/go"
db, err := jasonisnthappy.Open("./myapp.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Begin transaction
tx, err := db.BeginTransaction()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // Rollback if not committed
// Perform operations
id1, err := tx.Insert("users", map[string]interface{}{
"name": "Alice",
})
if err != nil {
log.Fatal(err)
}
id2, err := tx.Insert("users", map[string]interface{}{
"name": "Bob",
})
if err != nil {
log.Fatal(err)
}
// Commit all changes
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
Using Context Managers / Defer
Automatically handle commit/rollback with language idioms:
// Rust doesn't have built-in context managers
// Use explicit commit/rollback or the automatic retry function
# Automatically commits on success, rolls back on exception
with db.begin_transaction() as tx:
tx.insert("users", {"name": "Alice"})
tx.insert("users", {"name": "Bob"})
# Commit happens automatically here
// JavaScript doesn't have built-in context managers
// Use try/finally for cleanup
tx, err := db.BeginTransaction()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // Safe to call even after commit
// Perform operations
id, err := tx.Insert("users", map[string]interface{}{"name": "Alice"})
if err != nil {
return err
}
// Explicit commit
return tx.Commit()
Automatic Retry with Conflict Resolution
The database can automatically retry transactions that encounter conflicts. This is the recommended approach for most use cases:
use jasonisnthappy::Database;
use serde_json::json;
let db = Database::open("myapp.db")?;
// Automatically retries on conflict
let result = db.run_transaction(|tx| {
let mut users = tx.collection("users")?;
let id = users.insert(json!({"name": "Alice"}))?;
Ok(id)
})?;
println!("Inserted: {}", result);
# Python bindings handle retries automatically
# Just use begin_transaction and commit
// JavaScript bindings handle retries automatically
// Just use beginTransaction and commit
// Automatically retries on conflict
result, err := db.RunTransaction(func(tx *jasonisnthappy.Transaction) (string, error) {
id, err := tx.Insert("users", map[string]interface{}{
"name": "Alice",
})
if err != nil {
return "", err
}
return id, nil
})
fmt.Printf("Inserted: %s\n", result)
Transaction Operations
All collection operations are available within transactions:
CRUD Operations
let mut tx = db.begin()?;
let mut users = tx.collection("users")?;
// Insert
let id = users.insert(json!({"name": "Alice", "age": 30}))?;
// Find
let user = users.find_by_id(&id)?;
// Update
users.update_by_id(&id, json!({"age": 31}))?;
// Delete
users.delete_by_id(&id)?;
tx.commit()?;
tx = db.begin_transaction()
# Insert
doc_id = tx.insert("users", {"name": "Alice", "age": 30})
# Find
user = tx.find_by_id("users", doc_id)
# Update
tx.update_by_id("users", doc_id, {"age": 31})
# Delete
tx.delete_by_id("users", doc_id)
tx.commit()
const tx = db.beginTransaction();
// Insert
const id = tx.insert('users', { name: 'Alice', age: 30 });
// Find
const user = tx.findById('users', id);
// Update
tx.updateById('users', id, { age: 31 });
// Delete
tx.deleteById('users', id);
tx.commit();
tx, err := db.BeginTransaction()
defer tx.Rollback()
// Insert
id, err := tx.Insert("users", map[string]interface{}{
"name": "Alice",
"age": 30,
})
// Find
user, err := tx.FindByID("users", id)
// Update
err = tx.UpdateByID("users", id, map[string]interface{}{"age": 31})
// Delete
err = tx.DeleteByID("users", id)
tx.Commit()
Collection Management
Create, drop, and rename collections within transactions:
let mut tx = db.begin()?;
// Create collection
tx.create_collection("posts")?;
// Rename collection
tx.rename_collection("old_users", "users")?;
// Drop collection
tx.drop_collection("temp_data")?;
tx.commit()?;
tx = db.begin_transaction()
# Create collection
tx.create_collection("posts")
# Rename collection
tx.rename_collection("old_users", "users")
# Drop collection
tx.drop_collection("temp_data")
tx.commit()
const tx = db.beginTransaction();
// Create collection
tx.createCollection('posts');
// Rename collection
tx.renameCollection('old_users', 'users');
// Drop collection
tx.dropCollection('temp_data');
tx.commit();
tx, err := db.BeginTransaction()
defer tx.Rollback()
// Create collection
err = tx.CreateCollection("posts")
// Rename collection
err = tx.RenameCollection("old_users", "users")
// Drop collection
err = tx.DropCollection("temp_data")
tx.Commit()
Isolation & MVCC
jasonisnthappy uses Multi-Version Concurrency Control (MVCC) to provide snapshot isolation. This means:
- Readers never block writers
- Writers never block readers
- Each transaction sees a consistent snapshot of the database
- Conflicts are detected at commit time
Handling Transaction Conflicts
When two transactions modify the same data, one will succeed and the other will get a conflict error:
use jasonisnthappy::Error;
match users.update_by_id(&id, json!({"age": 31})) {
Ok(_) => println!("Updated successfully"),
Err(Error::TransactionConflict) => {
println!("Conflict - retry the operation");
}
Err(e) => eprintln!("Error: {}", e),
}
try:
tx.update_by_id("users", doc_id, {"age": 31})
tx.commit()
except RuntimeError as e:
if "conflict" in str(e).lower():
print("Conflict - retry the operation")
tx.rollback()
else:
raise
try {
tx.updateById('users', id, { age: 31 });
tx.commit();
} catch (err) {
if (err.message.includes('conflict')) {
console.log('Conflict - retry the operation');
tx.rollback();
} else {
throw err;
}
}
err := tx.UpdateByID("users", id, map[string]interface{}{"age": 31})
if err != nil {
if strings.Contains(err.Error(), "conflict") {
fmt.Println("Conflict - retry the operation")
tx.Rollback()
} else {
return err
}
}
Complete Examples
Bank Transfer (Atomicity)
Transfer funds between accounts atomically - both updates succeed or both fail:
fn transfer_funds(
db: &Database,
from_id: &str,
to_id: &str,
amount: f64
) -> Result<(), Error> {
db.run_transaction(|tx| {
let mut accounts = tx.collection("accounts")?;
let from = accounts.find_by_id(from_id)?;
let to = accounts.find_by_id(to_id)?;
let from_balance = from["balance"].as_f64().unwrap();
if from_balance < amount {
return Err(Error::Custom("Insufficient funds".into()));
}
accounts.update_by_id(from_id, json!({
"balance": from_balance - amount
}))?;
let to_balance = to["balance"].as_f64().unwrap();
accounts.update_by_id(to_id, json!({
"balance": to_balance + amount
}))?;
Ok(())
})
}
def transfer_funds(db, from_id, to_id, amount):
with db.begin_transaction() as tx:
from_acc = tx.find_by_id("accounts", from_id)
to_acc = tx.find_by_id("accounts", to_id)
if from_acc["balance"] < amount:
raise ValueError("Insufficient funds")
tx.update_by_id("accounts", from_id, {
"balance": from_acc["balance"] - amount
})
tx.update_by_id("accounts", to_id, {
"balance": to_acc["balance"] + amount
})
# Automatically commits
function transferFunds(db, fromId, toId, amount) {
const tx = db.beginTransaction();
try {
const fromAcc = tx.findById('accounts', fromId);
const toAcc = tx.findById('accounts', toId);
if (fromAcc.balance < amount) {
throw new Error('Insufficient funds');
}
tx.updateById('accounts', fromId, {
balance: fromAcc.balance - amount
});
tx.updateById('accounts', toId, {
balance: toAcc.balance + amount
});
tx.commit();
} catch (err) {
tx.rollback();
throw err;
}
}
func transferFunds(db *jasonisnthappy.Database, fromID, toID string, amount float64) error {
return db.RunTransaction(func(tx *jasonisnthappy.Transaction) error {
fromAcc, err := tx.FindByID("accounts", fromID)
if err != nil {
return err
}
toAcc, err := tx.FindByID("accounts", toID)
if err != nil {
return err
}
fromBalance := fromAcc["balance"].(float64)
if fromBalance < amount {
return fmt.Errorf("insufficient funds")
}
err = tx.UpdateByID("accounts", fromID, map[string]interface{}{
"balance": fromBalance - amount,
})
if err != nil {
return err
}
toBalance := toAcc["balance"].(float64)
return tx.UpdateByID("accounts", toID, map[string]interface{}{
"balance": toBalance + amount,
})
})
}
Bulk Insert with Transaction
Insert many documents efficiently within a single transaction:
let mut tx = db.begin()?;
let mut items = tx.collection("items")?;
for i in 0..1000 {
items.insert(json!({
"seq": i,
"value": i * 2
}))?;
}
tx.commit()?;
println!("Inserted 1000 items");
with db.begin_transaction() as tx:
for i in range(1000):
tx.insert("items", {
"seq": i,
"value": i * 2
})
print("Inserted 1000 items")
const tx = db.beginTransaction();
try {
for (let i = 0; i < 1000; i++) {
tx.insert('items', {
seq: i,
value: i * 2
});
}
tx.commit();
console.log('Inserted 1000 items');
} catch (err) {
tx.rollback();
throw err;
}
tx, err := db.BeginTransaction()
if err != nil {
return err
}
defer tx.Rollback()
for i := 0; i < 1000; i++ {
_, err := tx.Insert("items", map[string]interface{}{
"seq": i,
"value": i * 2,
})
if err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
fmt.Println("Inserted 1000 items")