---
title: "Working with Transactions"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Working with Transactions}
  %\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(), "transactions_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)

# Setup for examples
# The ducklake extension only needs installing once per machine:
# install_ducklake()
attach_ducklake("transactions_lake", lake_path = vignette_temp_dir)
```

## Introduction

Transactions are essential for maintaining data integrity when making multiple related changes to your data lake. 
DuckLake provides full ACID (Atomicity, Consistency, Isolation, Durability) transaction support, ensuring that either all operations succeed or none do.

DuckLake offers two approaches for working with transactions:

1. **`with_transaction()` (Recommended)**: A modern, R-idiomatic approach that automatically handles errors and rollbacks
2. **`begin_transaction()` / `commit_transaction()` / `rollback_transaction()`**: Manual transaction control for advanced use cases

This vignette demonstrates both approaches and explains when to use each one.

## Setup: Loading Initial Data

We'll use the `mtcars` dataset throughout this vignette to demonstrate transaction workflows.

```{r load-data}
# Load initial data
with_transaction(
  create_table(mtcars, "cars"),
  author = "Tutorial",
  commit_message = "Initial load of mtcars dataset"
)

# View the data
get_ducklake_table("cars") |>
  select(mpg, cyl, hp, wt) |>
  head()
```

## Approach 1: with_transaction() (Recommended)

The `with_transaction()` function provides automatic error handling and cleanup, similar to the `withr::with_*()` pattern used throughout the R ecosystem. This is the **recommended approach** for most use cases.

### Why use with_transaction()?

- **Automatic rollback on error**: If any operation fails, all changes are automatically rolled back
- **Cleaner code**: No need to manually call `begin_transaction()` and `commit_transaction()`
- **Built-in metadata support**: Easily add author and commit messages
- **Safer**: Prevents accidentally leaving transactions open
- **R-idiomatic**: Follows familiar patterns from packages like `withr`

### Single Operation with Metadata

```{r with-transaction-single}
# Add a new column with automatic metadata tracking
with_transaction(
  get_ducklake_table("cars") |>
    mutate(kpl = mpg * 0.425144) |>
    replace_table("cars"),
  author = "Data Team",
  commit_message = "Add kilometers per liter column"
)

# Verify the change
get_ducklake_table("cars") |>
  select(mpg, kpl) |>
  head()
```

### Multiple Operations in a Single Transaction

You can group multiple operations together by wrapping them in curly braces:

```{r with-transaction-multiple}
# Multiple related changes in one atomic transaction
with_transaction({
  # Add efficiency rating
  get_ducklake_table("cars") |>
    mutate(
      efficiency = case_when(
        mpg >= 25 ~ "high",
        mpg >= 20 ~ "medium",
        TRUE ~ "low"
      )
    ) |>
    replace_table("cars")
  
  # Create a summary table
  get_ducklake_table("cars") |>
    group_by(cyl) |>
    summarize(
      avg_mpg = mean(mpg, na.rm = TRUE),
      avg_hp = mean(hp, na.rm = TRUE),
      count = n()
    ) |>
    create_table("cars_summary")
}, author = "Data Team", commit_message = "Add efficiency ratings and summary table")

# View results
get_ducklake_table("cars") |>
  select(mpg, cyl, efficiency) |>
  head()

get_ducklake_table("cars_summary") |>
  collect()
```

### Automatic Rollback on Error

One of the key benefits of `with_transaction()` is automatic error handling:

```{r with-transaction-error}
# This transaction will fail and automatically rollback
tryCatch(
  with_transaction({
    # This will succeed
    get_ducklake_table("cars") |>
      mutate(test_column = "temporary") |>
      replace_table("cars")
    
    # This will fail
    stop("Simulated error - something went wrong!")
  }, author = "Data Team", commit_message = "This will be rolled back"),
  error = function(e) {
    message("Transaction automatically rolled back: ", e$message)
  }
)

# Verify that test_column was NOT added (transaction was rolled back)
get_ducklake_table("cars") |>
  colnames()

# View all versioned changes
list_table_snapshots("cars")
```

## Approach 2: Manual Transaction Control

For advanced use cases where you need explicit control over transaction boundaries, DuckLake provides manual transaction functions.

### When to use manual transactions?

- **Interactive workflows**: When you want to inspect data between operations before committing
- **Conditional commits**: When commit logic depends on runtime conditions
- **Long-running transactions**: When you need fine-grained control over transaction lifecycle
- **Legacy code**: When migrating from other transaction systems

### Basic Manual Transaction Workflow

```{r manual-basic}
# Start a transaction
begin_transaction()

# Make changes
get_ducklake_table("cars") |>
  mutate(weight_kg = wt * 453.592) |>
  replace_table("cars")

# Commit the changes with metadata
commit_transaction(
  author = "Data Team",
  commit_message = "Add weight in kg"
)

# Verify changes
get_ducklake_table("cars") |>
  filter(cyl == 4) |>
  select(wt, weight_kg) |>
  head()
```

### Manual Rollback

Sometimes you may want to inspect data before deciding whether to commit:

```{r manual-rollback}
# Start a transaction
begin_transaction()

# Make a test change
get_ducklake_table("cars") |>
  mutate(test_flag = TRUE) |>
  replace_table("cars")

# Check the result
test_result <- get_ducklake_table("cars") |>
  select(mpg, test_flag) |>
  head() |>
  collect()

print(test_result)

# Decide to rollback
rollback_transaction()

# Verify the change was NOT applied
"test_flag" %in% colnames(get_ducklake_table("cars"))

# View all versioned changes
list_table_snapshots("cars")
```

### Snapshot Metadata: At Commit Time vs After the Fact

DuckLake supports two ways to attach metadata (author, commit message, and optional extra info) to a snapshot.

**At commit time (recommended)**: Pass `author`, `commit_message`, and/or `commit_extra_info` to `commit_transaction()` or `with_transaction()`. This uses the DuckLake v1.0 `set_commit_message()` API to record metadata as part of the transaction itself, before the commit is finalized.

```{r metadata-at-commit}
# Metadata set at commit time (preferred approach)
begin_transaction()

get_ducklake_table("cars") |>
  mutate(hp_per_liter = hp / (cyl * 0.5)) |>
  replace_table("cars")

commit_transaction(
  author = "Performance Team",
  commit_message = "Add horsepower per liter metric",
  commit_extra_info = '{"ticket": "DATA-123"}'
)

get_ducklake_table("cars") |>
  select(hp, cyl, hp_per_liter) |>
  head()
```

**After the fact**: Use `set_snapshot_metadata()` to retroactively update the metadata on the most recent snapshot. This directly updates the `ducklake_snapshot_changes` metadata table.

```{r metadata-after-fact}
# Retrospectively update metadata on the last snapshot
set_snapshot_metadata(
  ducklake_name = "transactions_lake",
  author = "Performance Team (reviewed)",
  commit_message = "Add horsepower per liter metric (approved)"
)
```

Both approaches result in the same metadata fields being populated — the choice is about workflow. Use at-commit-time metadata for automated pipelines where metadata is known upfront. Use after-the-fact metadata for interactive workflows where you want to annotate a snapshot after reviewing the results.

## Viewing Transaction History

Regardless of which approach you use, all transactions are tracked with complete metadata:

```{r view-history}
# View recent transaction history
list_table_snapshots("cars") |>
  select(snapshot_id, snapshot_time, author, commit_message) |>
  tail(5)
```

## Comparison: with_transaction() vs Manual Control

| Feature | `with_transaction()` | Manual (`begin/commit/rollback`) |
|---------|---------------------|----------------------------------|
| **Ease of use** | ✅ Simple, one function | ❌ Requires multiple function calls |
| **Error handling** | ✅ Automatic rollback | ❌ Must handle manually |
| **Metadata** | ✅ Inline with transaction | ✅ Inline via parameter, or retroactive via `set_snapshot_metadata()` |
| **Safety** | ✅ Can't forget to commit | ❌ Risk of open transactions |
| **Use case** | Most production workflows | Interactive/conditional workflows |
| **Code clarity** | ✅ Clear transaction scope | ⚠ Scope can be unclear |

## Best Practices

1. **Default to `with_transaction()`**: Use it for all standard workflows
2. **Always add metadata**: Include `author` and `commit_message` for audit trails
3. **Keep transactions focused**: Group related changes, but avoid overly long transactions
4. **Handle errors gracefully**: When using manual transactions, always use `tryCatch()` to ensure rollback
5. **Test rollback behavior**: Verify that your error handling works correctly

## Key Concepts Summary

- **`with_transaction(expr, author, commit_message, commit_extra_info)`**: Modern, automatic transaction handling (recommended)
- **`begin_transaction()`**: Start a manual transaction
- **`commit_transaction(author, commit_message, commit_extra_info)`**: Apply changes from a manual transaction, with optional metadata set at commit time via the DuckLake v1.0 API
- **`rollback_transaction()`**: Discard changes from a manual transaction
- **`set_snapshot_metadata()`**: Retroactively update metadata on the most recent snapshot

Transactions ensure data integrity and provide complete audit trails for all changes in your DuckLake.

```{r cleanup, include=FALSE}
detach_ducklake("transactions_lake")
```
