Implementing Transaction Handling with a Simple Database
This challenge focuses on implementing a robust transaction handling mechanism for a simplified database system in Python. Transactions are crucial for maintaining data integrity, ensuring that a series of operations either all succeed or all fail together, preventing partial updates and inconsistent states.
Problem Description
You are tasked with building a Python class that simulates a basic key-value store database with transaction capabilities. This means you'll need to implement the ability to:
- Start a transaction: Begin a new sequence of operations that can be committed or rolled back.
- Set a key-value pair: Store or update a value associated with a key. This operation should only be visible within the current transaction until it's committed.
- Get a value by key: Retrieve the value associated with a key. This should respect the current transaction's state.
- Commit a transaction: Make all changes made within the current transaction permanent in the database.
- Rollback a transaction: Discard all changes made within the current transaction.
Key Requirements:
- Isolation: Operations within one transaction should not affect other concurrent transactions (though in this simplified model, we only deal with one active transaction at a time).
- Atomicity: A transaction is an "all-or-nothing" proposition. If it commits, all changes are applied; if it rolls back, no changes are applied.
- Durability: Once committed, changes should be persistent. (For this exercise, "persistent" means permanently stored within the
Databaseobject instance).
Expected Behavior:
- When a transaction starts, a new "scope" for data modifications is created.
setoperations within a transaction should update a temporary storage.getoperations should first check the temporary storage of the current transaction. If the key is not found there, it should then check the main database. If the key was deleted in the current transaction,getshould returnNone.commitshould merge the temporary transaction data into the main database.rollbackshould discard the temporary transaction data.- If
getis called without an active transaction, it should directly query the main database.
Edge Cases:
- Setting a key that doesn't exist.
- Setting a key that already exists.
- Getting a key that doesn't exist.
- Setting a key and then immediately getting it within the same transaction.
- Setting a key, then deleting it (implicitly via rollback or explicitly if a delete operation were added), then getting it.
- Committing an empty transaction.
- Rolling back an empty transaction.
- Attempting to perform operations (set, get) when no transaction is active (depending on design, this could be an error or default to direct database operations).
Examples
Example 1: Basic Set, Get, Commit
db = Database()
db.set("a", 1)
db.begin_transaction()
db.set("a", 2)
print(db.get("a")) # Should print 2
db.commit()
print(db.get("a")) # Should print 2
Output:
2
2
Explanation:
Initially, a is not in the database. db.set("a", 1) adds it to the main database. db.begin_transaction() starts a transaction. db.set("a", 2) updates a in the transaction's temporary storage. db.get("a") first checks the transaction's temporary storage, finds a: 2, and returns 2. db.commit() merges the transaction's changes into the main database, so a in the main database is now 2. The final db.get("a") reads from the now-updated main database, returning 2.
Example 2: Set, Rollback, Get
db = Database()
db.set("a", 1)
db.begin_transaction()
db.set("a", 2)
db.rollback()
print(db.get("a")) # Should print 1
Output:
1
Explanation:
a is set to 1 in the main database. A transaction begins. a is set to 2 in the transaction's temporary storage. db.rollback() discards the transaction's temporary changes. The main database remains unchanged. db.get("a") reads from the main database, returning the original value 1.
Example 3: Transactional Get and Non-Transactional Get
db = Database()
db.set("x", 10)
db.begin_transaction()
print(db.get("x")) # Should print 10 (reads from main db as transaction is "read-only" for non-modified keys)
db.set("y", 20)
print(db.get("y")) # Should print 20 (reads from transaction's temp storage)
db.commit()
print(db.get("x")) # Should print 10 (reads from main db)
print(db.get("y")) # Should print 20 (reads from main db)
Output:
10
20
10
20
Explanation:
x is 10 in the main DB. A transaction starts. db.get("x") first checks transaction storage (miss), then main DB (hit, 10). db.set("y", 20) adds y: 20 to transaction storage. db.get("y") checks transaction storage (hit, 20). db.commit() merges y: 20 into the main DB. Subsequent get calls read from the updated main DB.
Example 4: Key Not Found in Transaction
db = Database()
db.begin_transaction()
print(db.get("nonexistent_key")) # Should print None
db.rollback()
Output:
None
Explanation:
No transaction storage exists for nonexistent_key, and it's not in the main database. get returns None. Rolling back an empty transaction has no effect.
Constraints
- The database will store Python primitive types (integers, strings, booleans,
None). - There will be at most one active transaction at any given time.
- Your implementation should be efficient for typical use cases, meaning
setandgetoperations within a transaction should ideally be O(1) on average. - The
Databaseclass should be thread-safe if multiple threads were to interact with it simultaneously (though this is a conceptual constraint for a robust design; you don't need to implement explicit locking for this challenge unless specified).
Notes
- Consider how you will manage the state of the main database and the state of the current transaction's modifications. A common approach is to use two dictionaries: one for the persistent data and one for the uncommitted changes in the active transaction.
- Think about how
getshould behave. It needs to be aware of whether a transaction is active and, if so, check the transaction's modifications before falling back to the main database. - What should happen if
commitorrollbackis called when no transaction is active? You can define this behavior (e.g., raise an error, do nothing). - Consider the case where a key is set multiple times within a single transaction. The latest value should be the one that is eventually committed or rolled back.
- The problem implies that keys are immutable once set within the main database until they are modified within a transaction.