This cookbook provides quick recipes for common ducklake operations. Each recipe is a self-contained example you can adapt for your workflow.
For a comprehensive real-world example, see the clinical trial data lake vignette.
# PostgreSQL catalog for multi-client access
attach_ducklake(
"shared_lake",
backend = "postgres",
catalog_connection_string = "dbname=ducklake_catalog host=localhost",
lake_path = "/shared/lake/data/"
)
# SQLite catalog for lightweight local multi-client setups
attach_ducklake(
"team_lake",
backend = "sqlite",
catalog_connection_string = "metadata.sqlite",
lake_path = "data_files/"
)# First write a sample CSV (in practice, you'd have an existing file)
csv_path <- file.path(vignette_temp_dir, "sample_data.csv")
write.csv(head(iris, 20), csv_path, row.names = FALSE)
# Load the CSV into the data lake
with_transaction(
create_table(csv_path, "iris_sample"),
author = "Data Engineer",
commit_message = "Load iris sample from CSV"
)
#> Transaction started.
#> Transaction committed.If your data is already in Parquet, add_data_files()
records the files in the lake in place – no copy, no rewrite, and no
collection into R. A vector of files is registered atomically in one
snapshot. This is the fast migration path from a folder of Parquet
extracts. The target table can already exist with a compatible schema,
or create = TRUE can create it directly from the Parquet
schema. Note that the lake takes ownership of the files: later
compaction may rewrite or delete them.
# Returns a lazy dplyr tbl
cars_data <- get_ducklake_table("cars")
# Use dplyr verbs
cars_data |>
filter(cyl == 6) |>
select(mpg, cyl, hp) |>
head(3)
#> # A query: ?? x 3
#> # Database: DuckDB 1.5.1 [tgerke@Darwin 25.5.0:R 4.5.2//private/var/folders/b7/664jmq55319dcb7y4jdb39zr0000gq/T/RtmpBfCpem/ducklake/ducklake8fa6461fba10.duckdb]
#> mpg cyl hp
#> <dbl> <dbl> <dbl>
#> 1 21 6 110
#> 2 21 6 110
#> 3 21.4 6 110# Fetch all data into a data.frame
cars_df <- get_ducklake_table("cars") |> collect()
head(cars_df, 3)
#> # A tibble: 3 × 12
#> mpg cyl disp hp drat wt qsec vs am gear carb kpl
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4 8.93
#> 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4 8.93
#> 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1 9.69# See all snapshots for the cars table
list_table_snapshots("cars")
#> snapshot_id snapshot_time schema_version
#> 1 1 2026-08-27 14:25:48 1
#> 2 2 2026-08-27 14:25:48 2
#> 3 7 2026-08-27 14:25:49 7
#> 4 8 2026-08-27 14:25:49 8
#> changes
#> 1 tables_created, tables_inserted_into, main.cars, 1
#> 2 tables_created, tables_dropped, tables_inserted_into, main.cars, 1, 2
#> 3 tables_altered, 2
#> 4 tables_altered, 2
#> author commit_message commit_extra_info
#> 1 Data Engineer Initial car data load <NA>
#> 2 Data Engineer Add km/L metric to cars table <NA>
#> 3 <NA> <NA> <NA>
#> 4 <NA> <NA> <NA># Query data as it existed at snapshot 1 -- before the kpl column was added
get_ducklake_table_version("cars", version = 1) |>
select(mpg, cyl, hp) |>
head(3)
#> # A query: ?? x 3
#> # Database: DuckDB 1.5.1 [tgerke@Darwin 25.5.0:R 4.5.2//private/var/folders/b7/664jmq55319dcb7y4jdb39zr0000gq/T/RtmpBfCpem/ducklake/ducklake8fa6461fba10.duckdb]
#> mpg cyl hp
#> <dbl> <dbl> <dbl>
#> 1 21 6 110
#> 2 21 6 110
#> 3 22.8 4 93with_transaction(
get_ducklake_table("cars") |>
mutate(hp_per_cyl = hp / as.numeric(cyl)) |> # Add derived metric
replace_table("cars"),
author = "Data Engineer",
commit_message = "Add horsepower per cylinder metric"
)
#> Transaction started.
#> Stored 2 column labels as column comments.
#> Transaction committed.Note: Use replace_table() for structural changes (adding
or removing columns) and the row-level operations
(rows_update(), rows_insert(),
rows_delete()) for targeted, incremental changes. Both are
fully versioned – every committed change creates a snapshot you can
time-travel back to. See vignette("modifying-tables") for
guidance on choosing between them.
list_table_snapshots()
#> snapshot_id snapshot_time schema_version
#> 1 0 2026-08-27 14:25:47 0
#> 2 1 2026-08-27 14:25:48 1
#> 3 2 2026-08-27 14:25:48 2
#> 4 3 2026-08-27 14:25:48 3
#> 5 4 2026-08-27 14:25:48 4
#> 6 5 2026-08-27 14:25:48 5
#> 7 6 2026-08-27 14:25:49 6
#> 8 7 2026-08-27 14:25:49 7
#> 9 8 2026-08-27 14:25:49 8
#> 10 9 2026-08-27 14:25:49 9
#> 11 10 2026-08-27 14:25:50 10
#> changes
#> 1 schemas_created, main
#> 2 tables_created, tables_inserted_into, main.cars, 1
#> 3 tables_created, tables_dropped, tables_inserted_into, main.cars, 1, 2
#> 4 tables_created, tables_inserted_into, main.iris_sample, 3
#> 5 tables_created, tables_inserted_into, main.efficient_cars, 4
#> 6 views_created, main.v_efficient_cars
#> 7 views_dropped, 5
#> 8 tables_altered, 2
#> 9 tables_altered, 2
#> 10 tables_created, tables_altered, inlined_insert, main.visits, 6, 6
#> 11 tables_created, tables_dropped, tables_altered, tables_inserted_into, main.cars, 2, 7, 7
#> author commit_message commit_extra_info
#> 1 <NA> <NA> <NA>
#> 2 Data Engineer Initial car data load <NA>
#> 3 Data Engineer Add km/L metric to cars table <NA>
#> 4 Data Engineer Load iris sample from CSV <NA>
#> 5 Data Analyst Load filtered car data <NA>
#> 6 <NA> <NA> <NA>
#> 7 <NA> <NA> <NA>
#> 8 <NA> <NA> <NA>
#> 9 <NA> <NA> <NA>
#> 10 <NA> <NA> <NA>
#> 11 Data Engineer Add horsepower per cylinder metric <NA># Roll cars back to snapshot 1. The restore is recorded as a new snapshot,
# so nothing is lost -- you can still time-travel to any version.
restore_table_version(
"cars",
version = 1,
author = "Data Engineer"
)
#> Transaction started.
#> Transaction committed.
#> Table "cars" restored to snapshot 1 (recorded as a new snapshot).
list_table_snapshots("cars")
#> snapshot_id snapshot_time schema_version
#> 1 1 2026-08-27 14:25:48 1
#> 2 2 2026-08-27 14:25:48 2
#> 3 7 2026-08-27 14:25:49 7
#> 4 8 2026-08-27 14:25:49 8
#> 5 10 2026-08-27 14:25:50 10
#> 6 11 2026-08-27 14:25:50 11
#> changes
#> 1 tables_created, tables_inserted_into, main.cars, 1
#> 2 tables_created, tables_dropped, tables_inserted_into, main.cars, 1, 2
#> 3 tables_altered, 2
#> 4 tables_altered, 2
#> 5 tables_created, tables_dropped, tables_altered, tables_inserted_into, main.cars, 2, 7, 7
#> 6 tables_created, tables_dropped, tables_inserted_into, main.cars, 7, 8
#> author commit_message commit_extra_info
#> 1 Data Engineer Initial car data load <NA>
#> 2 Data Engineer Add km/L metric to cars table <NA>
#> 3 <NA> <NA> <NA>
#> 4 <NA> <NA> <NA>
#> 5 Data Engineer Add horsepower per cylinder metric <NA>
#> 6 Data Engineer Restored cars to snapshot 1 <NA>with_transaction({
# All these operations happen atomically
create_table(raw_data, "raw_table")
cleaned <- get_ducklake_table("raw_table") |>
filter(!is.na(key_field)) |>
create_table("clean_table")
get_ducklake_table("clean_table") |>
mutate(derived_field = calculate_something(x)) |>
create_table("analysis_table")
},
author = "Data Engineer",
commit_message = "Full ETL pipeline run"
)To see the SQL a read pipeline will run, use dplyr’s
show_query():
get_ducklake_table("cars") |>
filter(mpg > 25) |>
select(mpg, cyl, hp) |>
show_query()
#> <SQL>
#> SELECT mpg, cyl, hp
#> FROM cars
#> WHERE (mpg > 25.0)To preview the SQL an in-place modification would run
(before committing to it with ducklake_exec()), use
show_ducklake_query():
# Good: Filter before other operations
get_ducklake_table("cars") |>
filter(cyl == 6) |>
mutate(kpl = mpg * 0.425144) |>
head(3)
#> # A query: ?? x 12
#> # Database: DuckDB 1.5.1 [tgerke@Darwin 25.5.0:R 4.5.2//private/var/folders/b7/664jmq55319dcb7y4jdb39zr0000gq/T/RtmpBfCpem/ducklake/ducklake8fa6461fba10.duckdb]
#> mpg cyl disp hp drat wt qsec vs am gear carb kpl
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4 8.93
#> 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4 8.93
#> 3 21.4 6 258 110 3.08 3.22 19.4 1 0 3 1 9.10# Good: Select only needed columns
get_ducklake_table("cars") |>
select(mpg, cyl, hp) |>
filter(mpg > 25)
#> # A query: ?? x 3
#> # Database: DuckDB 1.5.1 [tgerke@Darwin 25.5.0:R 4.5.2//private/var/folders/b7/664jmq55319dcb7y4jdb39zr0000gq/T/RtmpBfCpem/ducklake/ducklake8fa6461fba10.duckdb]
#> mpg cyl hp
#> <dbl> <dbl> <dbl>
#> 1 32.4 4 66
#> 2 30.4 4 52
#> 3 33.9 4 65
#> 4 27.3 4 66
#> 5 26 4 91
#> 6 30.4 4 113For big tables, declaring a sort order or partition keys lets DuckLake skip whole Parquet files when a query filters on those columns:
set_ducklake_option() adjusts DuckLake’s persisted
settings at lake, schema, or table scope, and
get_ducklake_options() shows what’s set: