Snapshots, Time Travel, and Isolation 🕰️
dbzero provides a powerful snapshot mechanism that gives you a perfectly isolated, read-only view of your applications's state at a specific moment in time. This feature is the foundation for safe, concurrent data processing and enables "time travel," allowing you to query the state of your data as it existed in any past transaction.
Creating and Using Snapshots
You can create a snapshot of the current state at any time by calling dbzero.snapshot(). The best practice is to use it as a context manager, which ensures that the snapshot's resources are automatically released.
Once you have a snapshot, you can use its fetch() and find() methods to query data. This view is completely isolated; any changes made to the live objects after the snapshot is taken will not be visible through the snapshot.
# Create some initial data
obj = MemoTestSingleton(123)
dbzero.tags(obj).add("my-tag")
dbzero.commit()
# Create a snapshot of the current state
with dbzero.snapshot() as snap:
# Modify the live object
obj.value = 999
dbzero.tags(obj).add("another-tag")
# Querying the snapshot still returns the old, isolated data
assert snap.fetch(MemoTestSingleton).value == 123
assert len(list(snap.find("my-tag"))) == 1
assert len(list(snap.find("another-tag"))) == 0
# The snapshot is automatically closed outside the 'with' blockSnapshots are Immutable: Snapshots are strictly read-only. Attempting to modify an object fetched from a snapshot will raise an exception.
Time Travel: Accessing Past States
You can travel back in time by creating a snapshot of a specific historical transaction. Simply pass the desired state_num to dbzero.snapshot(). This allows you to inspect or analyze data exactly as it was at that point, regardless of any subsequent modifications or deletions.
# Create an object and commit, capturing the state number
obj = MemoTestClass(100)
dbzero.tags(obj).add("some-tag")
dbzero.commit()
state_1 = dbzero.get_state_num(finalized=True)
# Make more changes in a new transaction
obj.value = 200
dbzero.tags(obj).remove("some-tag")
dbzero.commit()
# Create a snapshot of the first state using its number
with dbzero.snapshot(state_1) as past_snap:
# Fetch the object as it existed in the past
past_obj = past_snap.fetch(dbzero.uuid(obj))
# The object has its old value and tags
assert past_obj.value == 100
assert "some-tag" in dbzero.tags(past_obj)Change Data Capture: Comparing Snapshots
One of the most powerful applications of snapshots is comparing two points in time to identify exactly what has changed. This is essential for building event-driven services, data replication pipelines, or auditing systems.
By providing two snapshots to specialized select functions, you can efficiently get a "diff" of the changes.
dbzero.select_new(query, snap_before, snap_after): Returns items that were created.dbzero.select_deleted(query, snap_before, snap_after): Returns items that were deleted.dbzero.select_modified(query, snap_before, snap_after): Returns items that were modified.
# STEP 1: Create initial state and first snapshot
obj_A = MemoTestClass(1)
obj_B = MemoTestClass(2)
dbzero.tags(obj_A, obj_B).add("tracked")
dbzero.commit()
snap_1 = dbzero.snapshot()
# STEP 2: Make changes and create a second snapshot
obj_B.value = 200 # Modify obj_B
dbzero.tags(obj_A).remove("tracked") # Delete obj_A from the query's view
obj_C = MemoTestClass(3) # Create obj_C
dbzero.tags(obj_C).add("tracked")
dbzero.commit()
snap_2 = dbzero.snapshot()
# STEP 3: Compare the snapshots to find the delta
with snap_1, snap_2:
query = dbzero.find("tracked")
# Find what was created between snap_1 and snap_2
new_items = list(dbzero.select_new(query, snap_1, snap_2))
assert new_items[0].value == 3
# Find what was deleted
deleted_items = list(dbzero.select_deleted(query, snap_1, snap_2))
assert deleted_items[0].value == 1
# Find what was modified
# This returns tuples of (old_version, new_version)
modified_items = list(dbzero.select_modified(query, snap_1, snap_2))
old_ver, new_ver = modified_items[0]
assert old_ver.value == 2
assert new_ver.value == 200