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:

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:

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")