---
title: "Storage and Backup Management"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Storage and Backup Management}
  %\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(), "storage_backups_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)
library(fs)
```

## Introduction

Understanding how DuckLake stores and manages your data is crucial for maintaining a robust data lake. This vignette explains:

- The two-component architecture of DuckLake (catalog and storage)
- What types of files are created and how to inspect them
- Best practices for choosing storage locations
- How to implement backup and recovery strategies

## DuckLake's Two-Component Architecture

DuckLake separates data management into two distinct components:

1. **Catalog (Metadata)**: A database that stores all metadata about your tables, snapshots, transactions, and data file locations. By default this is a DuckDB file, but DuckLake also supports PostgreSQL, SQLite, or MySQL as catalog backends (see `?attach_ducklake`). The catalog is typically small but critically important.

2. **Storage (Data Files)**: A directory containing immutable Parquet files that hold your actual data. DuckLake never modifies existing files—it only creates new ones.

This separation provides several benefits:

- **Simplified consistency**: Since files are never modified, caching and replication are straightforward
- **Flexible storage options**: Store metadata locally and data in the cloud, or vice versa
- **Independent backup strategies**: Each component can be backed up differently based on your needs

## Storage Options

DuckLake works with any filesystem backend that DuckDB supports, including:

- **Local files and folders**: Fast access, ideal for single-machine workflows
- **Cloud object storage**: 
  - AWS S3 (and S3-compatible services like Cloudflare R2, MinIO)
  - Google Cloud Storage
  - Azure Blob Storage
- **Network-attached storage**: NFS, SMB, FUSE-based filesystems

### Storage Patterns

Here are how some common storage patterns may look:

```{r storage-patterns, eval=FALSE}
# Local storage - fastest, but not shared
attach_ducklake(
  ducklake_name = "local_lake",
  lake_path = "~/data/my_ducklake"
)

# PostgreSQL catalog with S3 data - multi-client, scalable
attach_ducklake(
  ducklake_name = "shared_lake",
  backend = "postgres",
  catalog_connection_string = "dbname=ducklake_catalog host=localhost",
  lake_path = "s3://my-bucket/ducklake/data"
)

# SQLite catalog - lightweight multi-client option
attach_ducklake(
  ducklake_name = "team_lake",
  backend = "sqlite",
  catalog_connection_string = "~/data/metadata.sqlite",
  lake_path = "~/data/parquet_files"
)
```

**Key considerations:**

- **Latency vs. accessibility**: Local storage is fast but not shareable; cloud storage is accessible but has higher latency
- **Scalability vs. cost**: Object stores scale easily but may charge for data transfer
- **Security**: Consider using DuckLake's encryption features for cloud storage

### Cloud Storage Credentials

Object storage needs credentials before a remote `lake_path` will work.
`create_storage_secret()` registers them with DuckDB's secrets manager:

```{r storage-secrets, eval=FALSE}
# Explicit keys, scoped to one bucket
create_storage_secret(
  "s3",
  key_id = Sys.getenv("AWS_ACCESS_KEY_ID"),
  secret = Sys.getenv("AWS_SECRET_ACCESS_KEY"),
  region = "us-east-1",
  scope = "s3://my-bucket"
)

# Or let the AWS credential chain find them (env vars, profiles,
# instance metadata) -- no keys in code
create_storage_secret("s3", provider = "credential_chain")

# Then attach as usual
attach_ducklake("shared_lake", lake_path = "s3://my-bucket/ducklake/data")
```

Secrets are in-memory by default and disappear with the session; pass
`persistent = TRUE` only if you are comfortable with DuckDB writing them,
unencrypted, under `~/.duckdb/`. GCS, Cloudflare R2, and Azure use the same
function with `type = "gcs"`, `"r2"`, or `"azure"`.

Note that `backup_ducklake()` works on local data paths only. For a lake on
object storage, use your provider's replication or sync tooling (bucket
versioning, `aws s3 sync`, and similar) for the data files, and back up the
catalog database with the tools for its backend.

## Inspecting DuckLake Files

Let's create a sample DuckLake and explore what files it generates:

```{r create-sample-lake}
# Create a temporary directory for our demo
lake_dir <- file.path(vignette_temp_dir, "storage_demo")
dir.create(lake_dir, showWarnings = FALSE, recursive = TRUE)

# Install ducklake extension
# The ducklake extension only needs installing once per machine:
# install_ducklake()

# Create and populate a DuckLake
attach_ducklake(
  ducklake_name = "demo_lake",
  lake_path = lake_dir
)

# Add some data with transactions
with_transaction(
  create_table(mtcars[1:15, ], "cars"),
  author = "Demo User",
  commit_message = "Initial load"
)

with_transaction(
  get_ducklake_table("cars") |>
    mutate(hp_per_cyl = hp / cyl) |>
    replace_table("cars"),
  author = "Demo User",
  commit_message = "Add hp_per_cyl metric"
)

with_transaction(
  get_ducklake_table("cars") |>
    mutate(mpg_adjusted = if_else(cyl == 4, mpg * 1.1, mpg)) |>
    replace_table("cars"),
  author = "Demo User",
  commit_message = "Add adjusted MPG for 4-cylinder cars"
)
```

### Catalog Files

The catalog is a single database file containing all metadata:

```{r view-catalog-files}
dir_tree(lake_dir)
```

The catalog files (`demo_lake.ducklake` and `.wal`) contain all metadata about tables, snapshots, and transactions.

### Storage (Data) Files

Data files are stored in Parquet format in a structured directory:

```{r view-data-files}
# Data files are organized by schema and table
main_dir <- file.path(lake_dir, "main")

dir_tree(main_dir, recurse = 2)
  
# Get details about parquet files
parquet_files <- dir_ls(main_dir, recurse = TRUE, regexp = "\\.parquet$")
for (f in parquet_files) {
  cat(sprintf("  %s (%s bytes)\n", 
              path_file(f), 
              file.size(f)))
}
```

### Understanding File Organization

Each table's data is organized by schema and table, with each transaction creating new Parquet files:

```{r examine-structure}
# List all snapshots to see the version history
snapshots <- list_table_snapshots("cars")
snapshots |>
  select(snapshot_id, author, commit_message)
```

The key insight is that **DuckLake never modifies or deletes existing Parquet files**. Each change creates new files, preserving the complete history for time travel queries.

## Backup Strategies

### Backing Up the Catalog

The catalog is the most critical component—it maps snapshots to data files. Regular backups are essential.

#### Simple File Copy

For local databases, the simplest backup is a file copy. One rule matters:
**release the file locks first**. DuckDB holds the catalog file open while a
lake is attached, and copying a live catalog produces a corrupt (or, on
Windows, unreadable) backup. Detach with `shutdown = TRUE`, copy, then
re-attach — or skip the manual steps entirely and use `backup_ducklake()`,
which does exactly this dance for you:

```{r backup-catalog-simple}
# Create backup directory
backup_dir <- file.path(lake_dir, "backups")
dir.create(backup_dir, showWarnings = FALSE)

# Release file locks before copying the catalog
detach_ducklake("demo_lake", shutdown = TRUE)

# Copy the catalog file to create a backup
file.copy(
  from = file.path(lake_dir, "demo_lake.ducklake"),
  to = file.path(backup_dir, "demo_lake.ducklake")
)

# Copy the data directory as well
dir_copy(
  path = file.path(lake_dir, "main"),
  new_path = file.path(backup_dir, "main")
)

# Verify the backup was created
dir_tree(backup_dir)

# To work with the backup, attach it. override_data_path is needed because
# the catalog remembers the original data location, which the backup no
# longer matches.
attach_ducklake(
  ducklake_name = "demo_lake",
  lake_path = backup_dir,
  override_data_path = TRUE
)

# Verify you're working with the backup
list_table_snapshots("cars")

# You can switch back to the original by detaching and reattaching
detach_ducklake("demo_lake")
attach_ducklake("demo_lake", lake_path = lake_dir)
```

**Important**: Transactions committed after a backup won't be tracked when recovering. The data will exist in the Parquet files, but the backup will point to an earlier snapshot.

**Best practices:**

- Back up after batch jobs complete
- For streaming/continuous updates, schedule periodic backups
- Consider using `{cronR}` or `{taskscheduleR}` for automated backups

### Backing Up Storage (Data Files)

Since Parquet files are immutable, backing up storage is straightforward.

#### Local Storage Backup

```{r backup-storage-local, eval=FALSE}
# Use file system tools to copy the entire data directory
backup_data_dir <- file.path(lake_dir, "backups", "main_backup")
dir_copy(
  path = file.path(lake_dir, "main"),
  new_path = backup_data_dir
)
```

#### Cloud Storage Backup

For cloud storage, use provider-specific mechanisms:

**AWS S3:**
- Cross-bucket replication (copies to a different bucket automatically)
- AWS Backup service (scheduled backups within the same bucket)
- S3 versioning (keeps previous versions of objects)

**Google Cloud Storage:**
- Cross-bucket replication
- Backup and DR service
- Object versioning with soft deletes

When using cross-bucket replication, update your data path:

```{r backup-cloud, eval=FALSE}
# Original
attach_ducklake(
  ducklake_name = "prod_lake",
  lake_path = "s3://original-bucket/data"
)

# After recovery from replicated bucket
attach_ducklake(
  ducklake_name = "prod_lake",
  lake_path = "s3://backup-bucket/data",
  override_data_path = TRUE
)
```

## Recovery Procedures

### Recovering from Catalog Backup

If your catalog is corrupted or lost:

```{r recovery-catalog, eval=FALSE}
# Restore from backup by copying the backup file
# (backup_dir here is a directory created earlier, e.g. by backup_ducklake())
file.copy(
  from = file.path(backup_dir, "demo_lake.ducklake"),
  to = file.path(lake_dir, "demo_lake.ducklake"),
  overwrite = TRUE
)

# Reattach to the restored database
attach_ducklake("demo_lake", lake_path = lake_dir)

# Verify recovery by listing snapshots
list_table_snapshots("cars")
```

### Recovering from Data File Loss

If data files are lost but the catalog is intact:

```{r recovery-data, eval=FALSE}
# Restore data files from backup
dir_copy(
  path = backup_data_dir,
  new_path = file.path(lake_dir, "main"),
  overwrite = TRUE
)

# DuckLake will automatically reconnect to the restored files
# since the catalog maintains the file paths
```

## Routine Maintenance

A lake that sees regular writes accumulates small Parquet files (one per
insert) and old snapshots whose files can no longer be reclaimed until the
snapshots are expired. The one-stop command is `checkpoint_ducklake()`, which
flushes inlined data, merges small files, expires old snapshots, and cleans
up unreferenced files in a single call. For finer control, each step has its
own function:

```{r routine-maintenance, eval=FALSE}
# Compact small adjacent Parquet files into larger ones
merge_adjacent_files()

# Preview a retention policy, then apply it
expire_snapshots(older_than = Sys.time() - 30 * 24 * 60 * 60, dry_run = TRUE)
expire_snapshots(older_than = Sys.time() - 30 * 24 * 60 * 60)

# Expired snapshots only *schedule* file deletion; this reclaims the storage
cleanup_old_files(cleanup_all = TRUE)

# Rewrite data files whose rows have mostly been deleted
rewrite_data_files(delete_threshold = 0.5)

# Remove untracked files from the data path -- always dry-run this one first
delete_orphaned_files(dry_run = TRUE, cleanup_all = TRUE)
```

The typical cycle is merge, then expire, then clean up: merging and expiring
both mark files as unreferenced, and `cleanup_old_files()` deletes them.
Expiring a snapshot gives up time travel to it, so choose `older_than` to
match how far back you need to audit or restore.

One task lives outside DuckLake itself: the catalog database. If you use a
PostgreSQL or SQLite catalog, occasionally run `VACUUM` there with that
database's own tooling so metadata queries stay fast. The default DuckDB-file
catalog does not need this.

## Maintenance Considerations

When planning backups, coordinate with maintenance operations:

- **Compaction** (merging adjacent files): Run before backups to ensure consistent file layout
- **Cleanup** (removing obsolete files): Run before backups to avoid backing up unnecessary files

```{r maintenance-backup-sequence, eval=FALSE}
# Recommended backup sequence

# 1. Run maintenance operations (if needed)
merge_adjacent_files()
expire_snapshots(older_than = Sys.time() - 30 * 24 * 60 * 60)
cleanup_old_files(cleanup_all = TRUE)

# 2. Ensure all transactions are committed
# (no pending work)

# 3. Release file locks before copying the catalog
detach_ducklake("demo_lake", shutdown = TRUE)

# 4. Back up catalog
dir.create(file.path(lake_dir, "backups"), showWarnings = FALSE)
file.copy(
  from = file.path(lake_dir, "demo_lake.ducklake"),
  to = file.path(lake_dir, "backups",
                 paste0("backup_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".ducklake"))
)

# 5. Back up data files
dir_copy(
  path = file.path(lake_dir, "main"),
  new_path = file.path(lake_dir, "backups", "main_latest")
)

# 6. Re-attach and continue working
attach_ducklake("demo_lake", lake_path = lake_dir)
```

## Complete Backup Example

DuckLake provides a convenient `backup_ducklake()` function for creating timestamped backups:

```{r complete-backup-function}
# Create a complete backup with timestamp
backup_dir <- backup_ducklake(
  ducklake_name = "demo_lake",
  lake_path = lake_dir,
  backup_path = file.path(lake_dir, "backups")
)

# The function returns the backup directory path
print(backup_dir)
```

The `backup_ducklake()` function:
- Creates a timestamped backup directory
- Copies the catalog database file (releasing file locks first)
- Copies the data files from every schema directory in the lake
- Returns the backup directory path for reference

## Cleanup

```{r cleanup}
# Detach the demo lake
detach_ducklake("demo_lake")

# Clean up temporary files
unlink(lake_dir, recursive = TRUE)
```

## Summary

Key takeaways for managing DuckLake storage and backups:

1. **Understand the architecture**: Catalog (metadata) and storage (data) are separate components
2. **Choose storage wisely**: Balance latency, scalability, cost, and accessibility
3. **Files are immutable**: DuckLake never modifies existing Parquet files
4. **Back up regularly**: Catalog backups are critical; back up after batch jobs
5. **Coordinate with maintenance**: Run compaction and cleanup before backups
6. **Test recovery procedures**: Ensure you can actually restore from backups

For production systems, consider:

- Automated backup scheduling (using `{cronR}` or `{taskscheduleR}`)
- Multiple backup locations (local and cloud)
- Testing recovery procedures regularly
- Monitoring backup success and storage usage
- Version control for catalog schema changes
