duckplyr

repository·main·Indexed 18 days ago

https://github.com/tidyverse/duckplyr

A high-performance, drop-in replacement for dplyr that uses DuckDB to accelerate data manipulation. It enables the analysis of larger-than-memory datasets directly from disk or the web via remote Parquet files and provides a lazy evaluation model that builds query plans before materializing results into standard tibbles.

Tokens
4.6K
Snippets
18
Records
21
Agent score
63%

What's inside duckplyr

  1. How the code generation pipeline works

    main

    The code generation pipeline (scripts 00 through 07) automates the creation of duckplyr methods by introspecting dplyr's S3 methods for data.frame.

    • Extraction: 01-dplyr-methods.R extracts the source code of data.frame methods from dplyr and saves them as text files in dplyr-methods/ to serve as fallbacks.
    • Implementation: 02-duckplyr_df-methods.R generates R/<verb>.R files. These files contain the <verb>.duckplyr_df S3 method, which attempts a DuckDB execution via rel_try() and falls back to the embedded dplyr implementation on failure.
    • Testing:
      • 03-tests.R ensures duckplyr_df results are equivalent to plain dplyr results.
      • 04-dplyr-tests.R adapts existing dplyr tests for duckplyr.
      • 05-duckdb-tests.R generates tests that validate the DuckDB relational API plans.
    • Registration: 07-overwrite.R generates methods_overwrite_impl() to register duckplyr methods as the primary data.frame methods via vctrs::s3_register(), making the transition transparent to users.
  2. How duckplyr execution and computation works

    main

    duckplyr uses a lazy evaluation model. When you write dplyr code (like filter, mutate, or summarize), the results are not computed immediately. Instead, duckplyr builds a query plan.

    Computation is triggered only when you attempt to access the data, such as:

    • Querying a specific column (e.g., out$column_name)
    • Printing the object
    • Using functions that require materialized data

    Key Behaviors

    • Result Type: The output is a standard tibble (tbl_df, tbl, data.frame), making it fully compatible with the rest of the tidyverse.
    • Ordering: Unlike standard dplyr, results are not guaranteed to be ordered by default (see ?config for details). However, once the data is materialized, the results are stable.
    • Automatic Fallback: If a specific computation or operation is not supported by DuckDB, duckplyr will automatically fall back to using dplyr to ensure the code still runs correctly.
    # Example of lazy evaluation and computation trigger
    out <-
      flights_df() |>
      filter(!is.na(arr_delay), !is.na(dep_delay)) |
      mutate(inflight_delay = arr_delay - dep_delay) |
      summarize(
        .by = c(year, month),
        mean_inflight_delay = mean(inflight_delay),
        median_inflight_delay = median(inflight_delay),
      ) |
      filter(month <= 6)
    
    # Nothing has been computed yet.
    
    # Querying a column triggers the computation:
    out$month
    #> [1] 1 2 3 4 5 6
    
    # Printing the object also materializes the results:
    print(out)
  3. Run TPC-H benchmarks

    main

    The TPC-H benchmarking suite allows you to compare duckplyr performance against dplyr.

    Step-by-step Pipeline

    1. Setup: Generate and serialize TPC-H data.
    2. Load: Load query sets.
    3. Verify: Ensure correctness of the data/queries.
    4. Benchmark: Run benchmarks for both duckplyr and dplyr, then compare the results.

    Full Pipeline

    You can run the entire process (setup, load, and comparison) in a single command using the master script.

    # One-time setup: generate and serialize TPC-H data
    source("tools/30-tpch-export.R", echo = TRUE)
    source("tools/31-tpch-load-qs.R", echo = TRUE)
    
    # Verify correctness
    source("tools/33-tpch-compare.R", echo = TRUE)
    
    # Benchmark duckplyr vs dplyr
    source("tools/34-tpch-bench.R", echo = TRUE)
    source("tools/35-tpch-bench-dplyr.R", echo = TRUE)
    source("tools/36-tpch-bench-compare.R", echo = TRUE)
    
    # Or run the full pipeline in one go:
    source("tools/50-load-compare-bench.R", echo = TRUE)
  4. Regenerate code and tests after dplyr changes

    main

    When the dplyr package is updated, you must run the full sync process to regenerate method implementations, tests, and the overwrite/restore machinery.

    Note: It is recommended to pull the latest dplyr into .sync/dplyr-main first. While scripts 01-dplyr-methods.R and 04-dplyr-tests.R perform an automatic git pull, manual synchronization ensures consistency.

    After running the sync, use git diff to review the generated changes and address any new test failures.

    # Pull latest dplyr into .sync/dplyr-main first
    # (01-dplyr-methods.R and 04-dplyr-tests.R do a git pull automatically)
    
    source("tools/99-sync.R", echo = TRUE)
  5. Build a rigged dplyr for reverse-dependency (revdep) checks

    main

    To verify that duckplyr's behavior is compatible with the broader R ecosystem, you can build a 'rigged' version of dplyr. This process injects duckplyr methods into the dplyr fork as if they were native data.frame methods.

    Prerequisite: Ensure that .sync/dplyr-revdep is on the f-revdep-duckplyr branch and is in a clean state before running the script.

    # Ensure .sync/dplyr-revdep is on branch f-revdep-duckplyr and clean
    source("tools/90-patch-dplyr.R", echo = TRUE)
  6. Analyze larger-than-memory data using remote Parquet files

    main

    You can query remote Parquet files directly from R without downloading them first by using the httpfs DuckDB extension. This allows for efficient, lazy querying of massive datasets over the internet.

    To use this capability:

    1. Install and load the httpfs extension using db_exec().
    2. Use read_parquet_duckdb() to create a duckplyr data frame from a vector of URLs.
    3. Perform transformations (like mutate, summarize, filter) lazily.

    Important: Memory Protection Unlike local data frames, duckplyr prevents automatic materialization of remote data if the result is expected to be too large. This protects your system memory. If a query (like nrow()) would result in a large dataset, you must explicitly call collect() or as_tibble() to materialize the results.

    # 1. Setup extensions
    db_exec("INSTALL httpfs")
    db_exec("LOAD httpfs")
    
    # 2. Load remote files
    urls <- c("https://example.com/data_1.parquet", "https://example.com/data_2.parquet")
    flights <- read_parquet_duckdb(urls)
    
    # 3. Query lazily
    # This will error if the result is too large to protect memory
    nrow(flights)
    
    # 4. Explicitly materialize when needed
    flights_materialized <- flights |> collect()
  7. Implement a new verb in duckplyr

    main

    To add a new verb to the package, follow these steps:

    1. Handle manual adjustments: If the verb requires manual code changes that the generator cannot produce, create a patch file at patch/<verb>.patch. The generator in 02-duckplyr_df-methods.R automatically applies these patches and preserves them during subsequent regenerations.
    2. Regenerate the codebase: Run the sync script to update all implementations and tests:
      source("tools/99-sync.R", echo = TRUE)
    3. **Add implementation:** Add the relational implementation inside the `rel_try()` block within the corresponding file `R/<verb>.R`.
    
  8. Regenerate code-generated files with 99-sync.R

    main

    The 99-sync.R script is the master orchestration script. Run this whenever you make changes to dplyr or duckplyr that require regenerating the code-generated files. It executes the following pipeline in order:

    1. 80-unsupported.R (Unsupported verbs documentation)
    2. 01-dplyr-methods.R (Extract dplyr source)
    3. 02-duckplyr_df-methods.R (Generate duckplyr S3 methods)
    4. 03-tests.R (Generate equivalence tests)
    5. 04-dplyr-tests.R (Copy and adapt dplyr tests)
    6. 05-duckdb-tests.R (Generate relational API tests)
    7. 06-patch-duckdb.R (Transform tests for duckdb-r)
    8. 07-overwrite.R (Generate registration/restore code)
    9. 37-tpch-peel.R (TPC-H peeling)
    10. 39-tpch-peel-oo.R (TPC-H peeling with order preservation)
    source("tools/99-sync.R", echo = TRUE)
  9. Run the GitHub activity analysis scripts

    main

    The analysis scripts are designed to be executed from the project root in a specific succession. Each script runs in a fresh R session. Note that interrupting a script will result in partial data that is not automatically recomputed upon restarting the script.

    To run the main analysis script, use:

    R -q -f gh-analysis/60-gh.R

    The scripts require the dataset to be located in the data subdirectory of the gh-analysis directory.

  10. Set up developer tool prerequisites

    main

    Before using the development scripts in tools/, you must set up helper repositories for dplyr and duckdb-r.

    1. Clone dplyr forks: Inside a .sync directory, clone the main dplyr repository and a specific branch for reverse-dependency testing.
    2. Clone duckdb-r: Clone the duckdb-r repository into a directory adjacent to your duckplyr checkout (specifically at ../../duckdb/duckdb-r).

    Refer to .sync/README.md for more details.

    # Setup dplyr forks
    cd .sync
    git clone git@github.com:krlmlr/dplyr.git dplyr-main
    git clone git@github.com:krlmlr/dplyr.git -b f-revdep-duckplyr dplyr-revdep --reference dplyr-main
    
    # Setup duckdb-r in a cousin directory
    mkdir -p ../../duckdb
    git -C ../../duckdb clone git@github.com:duckdb/duckdb-r.git
  11. Run the full TPC-H benchmark pipeline

    main

    The 50-tpch-bench-compare.R script is a convenience tool that runs the entire benchmarking workflow in sequence:

    1. Export data (30)
    2. Load data (31)
    3. Run queries (32)
    4. Compare correctness (33)
    5. Benchmark duckplyr (34)
    6. Benchmark dplyr (35)
    7. Generate comparison plots (36)
    source("tools/50-tpch-bench-compare.R", echo = TRUE)
  12. Provision a clean test environment using OrbStack

    main

    On macOS with OrbStack installed, you can provision a fresh Ubuntu VM. This environment will automatically clone the repository and execute the full code-generation pipeline, providing a clean state for testing.

    # On macOS with OrbStack installed:
    bash tools/orbstack.sh