--- title: "Loading Data" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Loading Data} %\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(), "loading_data_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) attach_ducklake("loading_lake", lake_path = vignette_temp_dir) ``` `create_table()` is the one entry point for data that is not in the lake yet. It accepts a data frame, a path to a file, a URL, or a lazy table, and in every case the result is a new table and a new snapshot. The recipes below start with the everyday cases and end with the migration paths: registering Parquet files that already exist, and copying whole databases in from DuckDB or Iceberg. Each load here is wrapped in `with_transaction()` so that the snapshot carries an author and a message. [Getting Started](ducklake.html) explains why. ## From a data frame ```{r data_frame} with_transaction( create_table(mtcars, "cars"), author = "Data Engineer", commit_message = "Add the Motor Trend car data" ) ``` Factor columns become character columns on the way in, because DuckLake has no `ENUM` type, and `create_table()` says so when it happens: ```{r factors} with_transaction( create_table(iris, "flowers"), author = "Data Engineer", commit_message = "Add the iris measurements" ) ``` Variable labels (the `label` attributes that haven and labelled use) are stored as column comments and come back on `collect()`. [Views, Comments, and Labels](views-comments-labels.html) shows the round trip. ## From a file A file path goes straight to DuckDB's readers, so CSV, Parquet, and JSON files all load with the same call: ```{r csv} csv_path <- file.path(vignette_temp_dir, "sample_data.csv") write.csv(head(iris, 20), csv_path, row.names = FALSE) with_transaction( create_table(csv_path, "iris_sample"), author = "Data Engineer", commit_message = "Load the iris sample from CSV" ) get_ducklake_table("iris_sample") |> head(3) ``` ## From a URL An `http://` or `https://` address works the same way. ducklake loads DuckDB's `httpfs` extension for it: ```{r url, eval=FALSE} with_transaction( create_table("https://example.com/data.csv", "remote_data"), author = "Data Engineer", commit_message = "Load the remote dataset" ) ``` ## From a dplyr pipeline A pipeline on an R data frame is collected and written like any other data frame: ```{r pipeline} with_transaction( mtcars |> filter(mpg > 20) |> create_table("efficient_cars"), author = "Data Analyst", commit_message = "Add the cars above 20 mpg" ) ``` ## Derive a table inside the database When the pipeline starts from a lake table, `create_table()` runs it as `CREATE TABLE ... AS` inside DuckDB. The rows never pass through R, and column labels follow the columns into the new table: ```{r derive} with_transaction( get_ducklake_table("cars") |> filter(cyl == 4) |> select(mpg, cyl, hp, wt) |> create_table("small_cars"), author = "Data Analyst", commit_message = "Four-cylinder subset" ) ``` This is the shape of a medallion pipeline: each layer is a query on the layer before it, written straight into the lake. ## Put tables in schemas Schemas group tables inside the lake. They suit medallion layers (`bronze`, `silver`, `gold`) or one area per study, and every function that takes a table name accepts `"schema.table"`: ```{r schemas} with_transaction({ create_schema("staging") create_table(mtcars, "staging.cars_raw") }, author = "Data Engineer", commit_message = "Stage the raw car data" ) get_ducklake_table("staging.cars_raw") |> count(cyl) list_ducklake_tables() ``` ## Register Parquet files in place If your data is already in Parquet, `add_data_files()` records the files in the lake where they are: no copy, no rewrite, and nothing collected into R. A vector of files is registered atomically in one snapshot, which makes this the fast migration path for a folder of Parquet extracts. The target table can already exist with a compatible schema, or `create = TRUE` creates it from the Parquet schema. The lake takes ownership of the files, and later compaction may rewrite or delete them. ```{r add_data_files, eval=FALSE} add_data_files( "readings", c("extracts/jan.parquet", "extracts/feb.parquet"), create = TRUE ) # See which files back a table list_ducklake_files("readings") ``` ## Migrate a DuckDB database A DuckDB database file moves into the lake with one statement: attach it read-only and copy every schema and table across. The copy lands as a single snapshot. ```{r migrate_duckdb} legacy_db <- file.path(vignette_temp_dir, "legacy.duckdb") legacy <- DBI::dbConnect(duckdb::duckdb(dbdir = legacy_db)) DBI::dbWriteTable( legacy, "sites", data.frame(site_id = 1:3, country = c("US", "DE", "JP")) ) DBI::dbDisconnect(legacy, shutdown = TRUE) conn <- get_ducklake_connection() DBI::dbExecute(conn, sprintf("ATTACH '%s' AS legacy (READ_ONLY);", legacy_db)) DBI::dbExecute(conn, "COPY FROM DATABASE legacy TO loading_lake;") DBI::dbExecute(conn, "DETACH legacy;") get_ducklake_table("sites") |> collect() ``` Types DuckLake does not support (`ENUM`, `UNION`, `VARINT`, fixed-size arrays) need converting first; the DuckLake documentation has a migration script for those cases: . ## Exchange data with Iceberg With DuckDB's iceberg extension attached to an Iceberg REST catalog, `COPY FROM DATABASE` moves tables in either direction. Copying into Iceberg needs the schemas to exist there first, and the copy adds tables rather than replacing them. ```{r iceberg, eval=FALSE} DBI::dbExecute(conn, "INSTALL iceberg; LOAD iceberg;") DBI::dbExecute(conn, " ATTACH '' AS iceberg_catalog ( TYPE iceberg, CLIENT_ID 'admin', CLIENT_SECRET 'password', ENDPOINT 'http://iceberg.example.org:8181' ); ") # Lake to Iceberg DBI::dbExecute(conn, "COPY FROM DATABASE loading_lake TO iceberg_catalog;") # Iceberg to lake: the data itself, or only the metadata, so that Iceberg # tables read as lake tables where they are DBI::dbExecute(conn, "COPY FROM DATABASE iceberg_catalog TO loading_lake;") DBI::dbExecute(conn, "CALL iceberg_to_ducklake('iceberg_catalog', 'loading_lake');") ``` ```{r cleanup} detach_ducklake("loading_lake") ```