--- title: "Time Travel Queries" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Time Travel Queries} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} # Evaluate chunks only where the ducklake DuckDB extension is already # installed. The probe never downloads anything, so building this vignette # needs no network access. ducklake_available <- ducklake::ducklake_extension_available() knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = ducklake_available ) # Use a unique temp directory for this vignette to avoid conflicts during R CMD check vignette_temp_dir <- file.path(tempdir(), "time_travel_vignette") dir.create(vignette_temp_dir, showWarnings = FALSE, recursive = TRUE) knitr::opts_knit$set(root.dir = vignette_temp_dir) ``` ```{r setup, message=FALSE} library(ducklake) library(dplyr) ``` ## Introduction DuckLake's time travel capabilities provide a powerful audit trail for your data, enabling you to: - View data as it existed at any specific point in time - Query specific versions of your tables - Restore tables to previous states - Track the complete history of changes - Meet regulatory and compliance requirements This functionality is especially valuable in domains where data provenance and reproducibility are critical, such as clinical trials, financial reporting, and scientific research. ## Setting Up the Data Lake We'll start by creating a new DuckLake and loading the mtcars dataset. We'll then make several modifications to demonstrate time travel functionality. ```{r create-datalake} # Install the ducklake extension (required once per system) # The ducklake extension only needs installing once per machine: # install_ducklake() # Create or attach to a data lake attach_ducklake( ducklake_name = "time_travel_demo", lake_path = vignette_temp_dir ) # Create initial table with the mtcars dataset with_transaction( create_table(mtcars, "cars"), author = "Data Engineer", commit_message = "Initial load of mtcars dataset" ) # Verify the table was created get_ducklake_table("cars") |> select(mpg, cyl, hp, wt) |> head() ``` ## Making Changes Over Time Let's make several changes to our data to create a version history we can explore. ### Version 1: Initial data We already have our initial dataset. Let's check the current state: ```{r version1} get_ducklake_table("cars") |> summarise( n_cars = n(), avg_mpg = mean(mpg, na.rm = TRUE), avg_hp = mean(hp, na.rm = TRUE) ) ``` ### Version 2: Update fuel efficiency data Suppose we discover that fuel efficiency measurements need to be adjusted for some vehicles: ```{r version2} # Update mpg for high-performance cars (5% reduction) with_transaction( get_ducklake_table("cars") |> mutate(mpg = if_else(hp > 200, mpg * 0.95, mpg)) |> replace_table("cars"), author = "Data Analyst", commit_message = "Adjust MPG for high-performance vehicles" ) # Check the updated averages get_ducklake_table("cars") |> summarise( n_cars = n(), avg_mpg = mean(mpg, na.rm = TRUE), avg_hp = mean(hp, na.rm = TRUE) ) ``` ### Version 3: Add efficiency classification Let's add a new categorical variable to classify cars by fuel efficiency: ```{r version3} with_transaction( get_ducklake_table("cars") |> mutate( efficiency_class = case_when( mpg >= 25 ~ "High", mpg >= 20 ~ "Medium", TRUE ~ "Low" ) ) |> replace_table("cars"), author = "Data Analyst", commit_message = "Add efficiency classification" ) # View the new classification get_ducklake_table("cars") |> count(efficiency_class) |> arrange(desc(n)) ``` ### Version 4: Correct an error Suppose we realize the efficiency classification thresholds were wrong and need to be corrected: ```{r version4} with_transaction( get_ducklake_table("cars") |> mutate( efficiency_class = case_when( mpg >= 30 ~ "High", mpg >= 20 ~ "Medium", TRUE ~ "Low" ) ) |> replace_table("cars"), author = "Senior Analyst", commit_message = "Correct efficiency classification thresholds" ) # View the corrected classification get_ducklake_table("cars") |> count(efficiency_class) |> arrange(desc(n)) ``` ## Exploring Version History Now that we have a history of changes, let's explore the time travel functionality. ### List all snapshots ```{r list-snapshots} # View all available versions of the table snapshots <- list_table_snapshots("cars") snapshots ``` ### Query a specific version Let's look at version 2, before we added the efficiency classification: ```{r query-version2} # Get version 2 (after MPG adjustment, before classification) get_ducklake_table_version("cars", version = 2) |> select(mpg, cyl, hp, wt) |> head() # Notice: no efficiency_class column yet ``` Compare this with version 3, which has the classification: ```{r query-version3} # Get version 3 (with initial classification) get_ducklake_table_version("cars", version = 3) |> select(mpg, efficiency_class) |> count(efficiency_class) ``` ### Query data as of a specific timestamp We can also query data as it existed at any point in time: ```{r timestamp-query} # Get the timestamp from version 2 version2_timestamp <- snapshots |> filter(schema_version == 2) |> pull(snapshot_time) # Query data as it existed at that time # Note: Add 1 second to ensure we query AFTER the snapshot was created get_ducklake_table_asof("cars", version2_timestamp + 1) |> summarise( avg_mpg = mean(mpg, na.rm = TRUE) ) ``` ## Comparing Versions One powerful use case is comparing different versions to understand what changed: ```{r compare-versions} # Get MPG values from version 1 (original) and version 2 (after adjustment) original <- get_ducklake_table_version("cars", version = 1) |> select(mpg) |> collect() |> mutate(version = "Original") adjusted <- get_ducklake_table_version("cars", version = 2) |> select(mpg) |> collect() |> mutate(version = "Adjusted") # Combine and compare bind_rows(original, adjusted) |> group_by(version) |> summarise( avg_mpg = mean(mpg, na.rm = TRUE), min_mpg = min(mpg), max_mpg = max(mpg) ) ``` ## Restoring Previous Versions If we need to undo changes, `restore_table_version()` rolls a table back to an earlier snapshot in one call: ```{r restore-demo} # Go back to version 2 (before adding classifications) restore_table_version("cars", version = 2, author = "Senior Analyst") # Verify the restoration - efficiency_class column should be gone get_ducklake_table("cars") |> colnames() ``` You can also restore to a point in time with `restore_table_version("cars", timestamp = "2026-07-01 09:00:00")`, and pass a custom `commit_message` if the default ("Restored cars to snapshot 2") isn't descriptive enough for your audit trail. Nothing is lost in a restore: the rollback happens *forward*, as a new snapshot with its own author and commit message, so the full history — including the states after the restore point — remains available for time travel. That also means a restore is itself reversible with another `restore_table_version()` call: ```{r after-restore} list_table_snapshots("cars") ``` ## Pinning a Whole Session to a Snapshot The queries above travel one table at a time. To freeze *everything* — say, to re-run a report exactly as it stood at a submission milestone — attach the lake pinned to a snapshot: ```{r pinned-attach, eval=FALSE} attach_ducklake( "cars_milestone", lake_path = "~/data/lake", snapshot_version = 2 ) ``` Every table then reads as of snapshot 2 with no `AT (...)` clauses needed, and writes are rejected, so the milestone view can't drift. A `snapshot_time` argument does the same for a point in time. ## Use Cases for Time Travel Time travel functionality is particularly valuable for: 1. **Regulatory Compliance**: Maintain complete audit trails for datasets used in regulatory submissions (e.g., clinical trials, financial reporting) 2. **Reproducibility**: Recreate analyses exactly as they were run at specific points in time 3. **Data Recovery**: Restore accidentally modified or deleted data 4. **Change Tracking**: Understand when and how data quality issues were introduced 5. **Reporting**: Generate historical reports using data as it existed at specific time points 6. **Collaboration**: Allow team members to reference specific versions of shared datasets 7. **Debugging**: Identify when unexpected changes occurred in your data pipeline ## Metadata and Audit Information Each snapshot includes metadata about when it was created and what changes were made. The `list_table_snapshots()` function provides a complete audit trail: ```{r metadata} # Get detailed snapshot history with all metadata snapshot_history <- list_table_snapshots("cars") snapshot_history |> select(snapshot_id, snapshot_time, author, commit_message) ``` This complete audit trail ensures that you can always answer questions like: - What changes were made? - When were they made? - What version is the table at? - What was the data before this change? You can also access metadata about all tables in the DuckLake: ```{r all-metadata} # View metadata for all tables all_snapshots <- list_table_snapshots() all_snapshots |> select(snapshot_id, snapshot_time, changes) |> head(10) ``` ```{r cleanup, include=FALSE} detach_ducklake("time_travel_demo") ```