deltacat 2.0.0.post4
DeltaCAT is a portable Pythonic Data Lakehouse powered by Ray.
Requires Python: >=3.9

<p align="center">
  <img src="https://github.com/ray-project/deltacat/raw/2.0/media/deltacat-logo-alpha-750.png" alt="deltacat logo" style="width:55%; height:auto; text-align: center;">
</p>

DeltaCAT is a portable Multimodal Lakehouse powered by [Ray](https://github.com/ray-project/ray), [Apache Arrow](https://github.com/apache/arrow), and [Daft](https://github.com/Eventual-Inc/Daft). It lets you create ACID-compliant multimodal data lakes [that efficiently scale to exabytes of production data](https://aws.amazon.com/blogs/opensource/amazons-exabyte-scale-migration-from-apache-spark-to-ray-on-amazon-ec2/).

It provides data lake level transactions & time travel, zero-copy schema evolution, zero-copy multimodal file processing (image, audio, video, text, etc.), and transparent dataset optimization. It runs locally for rapid development or in the cloud for production workloads. It runs on any filesystem for easy setup and sharing - no external catalog services, lock managers, or key value stores required.


## Overview

DeltaCAT provides the following high-level components:
1. [**Catalog**](https://github.com/ray-project/deltacat/tree/2.0/deltacat/catalog/interface.py): Pythonic APIs to discover, read, write, and manage datasets.
2. [**Compute**](https://github.com/ray-project/deltacat/tree/2.0/deltacat/compute/): Distributed data management procedures that automatically optimize your datasets.
3. [**Storage**](https://github.com/ray-project/deltacat/tree/2.0/deltacat/storage/): A portable multimodal data lake format useable with any filesystem.
4. **Sync** (in development): Synchronize DeltaCAT datasets to data warehouses and other table formats.

DeltaCAT's **Catalog**, **Compute**, and **Storage** layers work together to bring ACID-compliant data management to any Ray application. These components automate data indexing, change management, dataset read/write optimization, schema evolution, and other common data management tasks across any set of data files readable by [Pandas](https://github.com/pandas-dev/pandas), [NumPy](https://github.com/numpy/numpy), [Polars](https://github.com/pola-rs/polars), [PyArrow](https://arrow.apache.org/docs/python/index.html), [Ray Data](https://docs.ray.io/en/latest/data/data.html), and [Daft](https://docs.daft.ai/en/stable/api/dataframe/).

<p align="center">
  <img src="https://github.com/ray-project/deltacat/raw/2.0/media/deltacat-tech-overview.png" alt="deltacat tech overview" style="width:100%; height:auto; text-align: center;">
</p>

Data consumers that prefer to stay within the ecosystem of Pythonic data management tools can use DeltaCAT's native table format to manage their data with minimal overhead. For integration with other data analytics frameworks (e.g., Apache Spark, Trino, Apache Flink), DeltaCAT's **Sync** component offers zero-copy synchronization of your tables to Apache Iceberg and other table formats.

## Getting Started
DeltaCAT applications run anywhere that Ray runs, including your local laptop, cloud computing cluster, or on-premise cluster.

DeltaCAT lets you manage **Tables** across one or more **Catalogs**. A **Table** can be thought of as a named collection of data files. A **Catalog** can be thought of as a named data lake that contains a set of **Tables**. A **Catalog** provides a root location (e.g., a local file path or S3 Bucket) to store information about all your **Tables**, and can be rooted in any [PyArrow-compatible Filesystem](https://arrow.apache.org/docs/python/filesystems.html). **Tables** can be created, read, and written using the `dc.write` and `dc.read` APIs.


### Quick Start

Install DeltaCAT with: `pip install deltacat`

Then run this script to create and read your first table:

```python
import deltacat as dc
import pandas as pd

# Initialize DeltaCAT with a default local catalog.
# Ray will be initialized automatically.
# Catalog files will be stored in .deltacat/ in the current working directory.
dc.init_local()

# Create data to write.
data = pd.DataFrame({
    "id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
    "age": [3, 7, 5]
})

# Write data to a table.
# Table creation is handled automatically.
dc.write(data, "users")

# Read the data back as a Daft DataFrame.
# Daft lazily and automatically distributes data across your Ray cluster.
daft_df = dc.read("users")  # Returns Daft DataFrame (default)
daft_df.show()  # Materialize and print the DataFrame

# Add more data and add a new column.
# Compaction and zero-copy schema evolution are handled automatically.
data = pd.DataFrame({
    "id": [4, 5, 6],
    "name": ["Tom", "Simpkin", "Delta"],
    "age": [2, 12, 4],
    "city": ["Hollywood", "Gloucester", "San Francisco"]
})
dc.write(data, "users")

# Read the full table back into a Daft DataFrame.
daft_df = dc.read("users")
# Print the names, ages, and cities (missing cities will be null).
daft_df.select("name", "age", "city").show()
```

### Core Concepts
DeltaCAT can do much more than just add data to tables and read it back again. Expand the sections below to see examples of other core DeltaCAT concepts and APIs.

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Idempotent Writes</span></summary>

If you run the quick start example repeatedly from the same working directory, you'll notice that the table it writes to just keeps growing larger. This is because DeltaCAT always **adds** table data by default. One way to prevent this perpetual table growth and make the example idempotent is to use the **REPLACE** write mode if the table already exists:

```python
import deltacat as dc
import pandas as pd

# Initialize DeltaCAT with a default local catalog.
# Ray will be initialized automatically.
# Catalog files will be stored in .deltacat/ in the current working directory.
dc.init_local()

# Create data to write.
data = pd.DataFrame({
    "id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
    "age": [3, 7, 5]
})

# Default write mode to CREATE.
# This will fail if the table already exists.
write_mode = dc.TableWriteMode.CREATE

# Change write mode to REPLACE if the table already exists.
if dc.table_exists("users"):
    write_mode = dc.TableWriteMode.REPLACE

# Write data to a fresh, empty table.
dc.write(data, "users", mode=write_mode)

# Read the data back as a Daft DataFrame.
# Daft lazily and automatically distributes data across your Ray cluster.
daft_df = dc.read("users")  # Returns Daft DataFrame (default)
daft_df.show()  # Materialize and print the DataFrame

# Explicitly add more data and add a new column.
# Compaction and schema evolution are handled automatically.
data = pd.DataFrame({
    "id": [4, 5, 6],
    "name": ["Tom", "Simpkin", "Delta"],
    "age": [2, 12, 4],
    "city": ["Hollywood", "Gloucester", "San Francisco"]
})
dc.write(data, "users", mode=dc.TableWriteMode.ADD)

# Read the full table back into a Daft DataFrame.
daft_df = dc.read("users")
# Print the names, ages, and cities (missing cities will be null).
daft_df.select("name", "age", "city").show()
# Ensure that the table length is always 6.
assert dc.dataset_length(daft_df) == 6
```

No matter how many times you run the above code, the table will always contain 6 records. Another way to achieve the same result is to use `dc.drop_table`:

```python
import deltacat as dc
from deltacat.exceptions import TableNotFoundError
import pandas as pd

# Initialize DeltaCAT with a default local catalog.
# Ray will be initialized automatically.
# Catalog files will be stored in .deltacat/ in the current working directory.
dc.init_local()

# Create data to write.
data = pd.DataFrame({
    "id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
    "age": [3, 7, 5]
})

# Drop the table if it exists.
try:
    dc.drop_table("users")
    print("Dropped 'users' table.")
except TableNotFoundError:
    print("Table 'users' not found. Creating it...")

# Write data to a new table.
dc.write(data, "users", mode=dc.TableWriteMode.CREATE)

# Read the data back as a Daft DataFrame.
# Daft lazily and automatically distributes data across your Ray cluster.
daft_df = dc.read("users")  # Returns Daft DataFrame (default)
daft_df.show()  # Materialize and print the DataFrame

# Explicitly add more data and add a new column.
# Compaction and schema evolution are handled automatically.
data = pd.DataFrame({
    "id": [4, 5, 6],
    "name": ["Tom", "Simpkin", "Delta"],
    "age": [2, 12, 4],
    "city": ["Hollywood", "Gloucester", "San Francisco"]
})
dc.write(data, "users", mode=dc.TableWriteMode.ADD)

# Read the full table back into a Daft DataFrame.
daft_df = dc.read("users")
# Print the names, ages, and cities (missing cities will be null).
daft_df.select("name", "age", "city").show()
# Ensure that the table length is always 6.
assert dc.dataset_length(daft_df) == 6
```

</details>


<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Ordered Writes</span></summary>

DeltaCAT writes are unordered by default, which means that the order of data written to the table isn't guaranteed to match the order that it is read back. While this is useful for preventing conflicts between concurrent writers, you can also use the **APPEND** write mode to preserve write order and raise explicit concurrency conflicts between parallel writers:

```python
import deltacat as dc
import pandas as pd

# Initialize DeltaCAT with a default local catalog.
# Ray will be initialized automatically.
# Catalog files will be stored in .deltacat/ in the current working directory.
dc.init_local()

# Create data to write.
data = pd.DataFrame({
    "id": [1, 2],
    "name": ["Cheshire", "Dinah"],
    "age": [3, 7]
})

# Derive a DeltaCAT schema for the data.
schema = dc.Schema.of(dc.dataset_schema(data))

# Create an empty table to hold ordered user data.
if not dc.table_exists("users_ordered"):
    dc.create_table("users_ordered", schema=schema)

# Write the first ordered delta to the table.
dc.write(data, "users_ordered", mode=dc.TableWriteMode.APPEND)

# Write the second ordered delta to the table.
data = pd.DataFrame({
    "id": [3, 4],
    "name": ["Felix", "Tom"],
    "age": [2, 12],
    "city": ["Hollywood", "Gloucester"]
})
dc.write(data, "users_ordered", mode=dc.TableWriteMode.APPEND)

# Write the third ordered delta to the table.
data = pd.DataFrame({
    "id": [5, 6],
    "name": ["Simpkin", "Delta"],
    "age": [12, 4],
    "city": ["San Francisco", "San Francisco"]
})
dc.write(data, "users_ordered", mode=dc.TableWriteMode.APPEND)

# Read the data back as a Pandas DataFrame, and ensure that the
# order of the records returned matches the order they were written.
pandas_df = dc.read("users_ordered", read_as=dc.DatasetType.PANDAS)
print(pandas_df)
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Schemaless Tables</span></summary>

Tables created automatically via `dc.write` have a schema inferred from the data written by default. However, if you create an empty table without providing a schema, it defaults to schemaless. Writes to schemaless tables are more efficient and flexible, since they simply track the location and basic metadata associated with the data files written to the table. However, if you know that a unified schema can be derived for your schemaless data, then you can you can still read it back as a structured dataset:

```python
import deltacat as dc
import pandas as pd

# Initialize DeltaCAT with a default local catalog.
# Ray will be initialized automatically.
# Catalog files will be stored in .deltacat/ in the current working directory.
dc.init_local()

# Create data to write.
data = pd.DataFrame({
    "id": [1, 2],
    "name": ["Cheshire", "Dinah"],
    "age": [3, 7]
})

# Create an empty schemaless table to hold ordered user data.
if not dc.table_exists("users_schemaless"):
    dc.create_table("users_schemaless")

# Write the first ordered delta to the table.
dc.write(data, "users_schemaless", mode=dc.TableWriteMode.APPEND)

# Write the second ordered delta to the table.
data = pd.DataFrame({
    "id": [3, 4],
    "name": ["Felix", "Tom"],
    "age": [2, 12],
    "city": ["Hollywood", "Gloucester"]
})
dc.write(data, "users_schemaless", mode=dc.TableWriteMode.APPEND)

# Read back the file manifest of the schemaless table.
# Notice that file paths, sizes, etc. are returned instead of the dataframes written.
manifest_df = dc.read("users_schemaless", read_as=dc.DatasetType.PANDAS)
print(manifest_df)

# Use from_manifest_table to convert the manifest table to a structured dataset.
structured_daft_df = dc.from_manifest_table(manifest_df)
structured_daft_df.show()
```

</details>


<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Working Across Dataset and File Types</span></summary>

DeltaCAT natively supports a variety of open dataset and file formats already integrated with Ray and Arrow. You can use `dc.read` to read tables back as a Daft DataFrame, Ray Dataset, Pandas DataFrame, PyArrow Table, Polars DataFrame, NumPy Array, or list of PyArrow ParquetFile objects:

```python
# Read directly into eagerly materialized local datasets.
# Local datasets are best for reading small tables that fit in local memory:
pandas_df = dc.read("users", read_as=dc.DatasetType.PANDAS)  # Pandas DataFrame
print("\n=== Pandas ===")
print(pandas_df)

pyarrow_table = dc.read("users", read_as=dc.DatasetType.PYARROW)  # PyArrow Table
print("\n=== PyArrow ===")
print(pyarrow_table)

polars_df = dc.read("users", read_as=dc.DatasetType.POLARS)  # Polars DataFrame
print("\n=== Polars ===")
print(polars_df)

numpy_array = dc.read("users", read_as=dc.DatasetType.NUMPY)  # NumPy Array
print("\n=== NumPy ===")
print(numpy_array)

# Or read into lazily materialized PyArrow ParquetFile objects.
# PyArrow ParquetFile objects are useful for reading larger tables that don't
# fit in local memory, but you'll need to manually handle data distribution
# and materialization:
pyarrow_pq_files = dc.read("users", read_as=dc.DatasetType.PYARROW_PARQUET)  # ParquetFile or List[ParquetFile]
print("\n=== PyArrow Parquet Unmaterialized ===")
print(pyarrow_pq_files)
print("\n=== PyArrow Parquet Materialized ===")
print(dc.to_pyarrow(pyarrow_pq_files))  # Materialize and print the ParquetFile refs

# Or read into distributed datasets for scalable data processing.
# Distributed datasets are the easiest way to read large tables that don't fit
# in either local or distributed Ray cluster memory. They automatically handle
# data distribution and materialization:
daft_df = dc.read("users", read_as=dc.DatasetType.DAFT)  # Daft DataFrame (Default)
print("\n=== Daft ===")
daft_df.show()  # Materialize and print the Daft DataFrame

ray_dataset = dc.read("users", read_as=dc.DatasetType.RAY_DATASET)  # Ray Dataset
print("\n=== Ray Data ===")
ray_dataset.show()  # Materialize and print the Ray Dataset
```

 `dc.write` can also write any of these dataset types:

```python
import pyarrow as pa

# Create a pyarrow table to write.
pyarrow_table = pa.Table.from_pydict({
    "id": [4, 5, 6],
    "name": ["Tom", "Simpkin", "Delta"],
    "age": [2, 12, 4],
    "city": ["Hollywood", "Gloucester", "San Francisco"]
})

# Write different dataset types to the default table file format (Parquet):
dc.write(pyarrow_table, "my_pyarrow_table")  # Write PyArrow Table
print("\n=== PyArrow Table ===")
dc.read("my_pyarrow_table").show()

daft_df = dc.from_pyarrow(pyarrow_table, dc.DatasetType.DAFT)
dc.write(daft_df, "my_daft_table")  # Write Daft DataFrame
print("\n=== Daft Table ===")
dc.read("my_daft_table").show()

ray_dataset = dc.from_pyarrow(pyarrow_table, dc.DatasetType.RAY_DATASET)
dc.write(ray_dataset, "my_ray_dataset")  # Write Ray Dataset
print("\n=== Ray Dataset ===")
dc.read("my_ray_dataset").show()

pandas_df = dc.from_pyarrow(pyarrow_table, dc.DatasetType.PANDAS)
dc.write(pandas_df, "my_pandas_table")  # Write Pandas DataFrame
print("\n=== Pandas Table ===")
dc.read("my_pandas_table").show()

polars_df = dc.from_pyarrow(pyarrow_table, dc.DatasetType.POLARS)
dc.write(polars_df, "my_polars_table")  # Write Polars DataFrame
print("\n=== Polars Table ===")
dc.read("my_polars_table").show()

numpy_array = dc.from_pyarrow(pyarrow_table, dc.DatasetType.NUMPY)
dc.write(numpy_array, "my_numpy_table")  # Write NumPy Array
print("\n=== NumPy Table ===")
dc.read("my_numpy_table").show()
```

DeltaCAT tables also support persisting data in heterogeneous table file formats like Avro, ORC, or Feather:

```python
data = pd.DataFrame({"id": [1], "name": ["Cheshire"], "age": [3]})

# Start by writing to a new table with a custom list of supported readers.
# Define the content types we want to write.
content_types_to_write = {
    dc.ContentType.PARQUET,
    dc.ContentType.AVRO,
    dc.ContentType.ORC,
    dc.ContentType.FEATHER,
}
# Limit supported readers to dataset types can read the above content types.
# By default, DeltaCAT will raise an error if we attempt to write data to
# a file format that can't be read by one or more dataset types.
supported_reader_types = [
    reader_type for reader_type in dc.DatasetType
    if content_types_to_write <= reader_type.readable_content_types()
]
# Write to a new table with our custom list of supported readers.
dc.write(
    data,
    "my_mixed_format_table",
    content_type=dc.ContentType.PARQUET, # Write Parquet (Default)
    table_properties={dc.TableProperty.SUPPORTED_READER_TYPES: supported_reader_types}
)

# Now write the same data to other file formats:
dc.write(data, "my_mixed_format_table", content_type=dc.ContentType.AVRO)
dc.write(data, "my_mixed_format_table", content_type=dc.ContentType.ORC)
dc.write(data, "my_mixed_format_table", content_type=dc.ContentType.FEATHER)

# Read the table back.
# All formats are automatically unified into the requested Pandas DataFrame:
pandas_df = dc.read("my_mixed_format_table", read_as=dc.DatasetType.PANDAS)
print(pandas_df)
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Live Feature Enrichment</span></summary>

DeltaCAT can update your datasets on-the-fly to keep up with a continuous stream of new insights, and support common ML use-cases like feature enrichment. Just define a table schema with one or more merge keys to start updating and deleting existing records:

```python
import deltacat as dc
import pandas as pd
import pyarrow as pa
import tempfile

# Initialize DeltaCAT with a fresh temporary catalog
dc.init_local(tempfile.mkdtemp())

# Start with minimal schema - just user_id as merge key and name
initial_schema = dc.Schema.of([
    dc.Field.of(pa.field("user_id", pa.int64()), is_merge_key=True),
    dc.Field.of(pa.field("name", pa.string())),
])

# Initial user data - just basic info
initial_users = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["Jim", "Dinah", "Bob"],
})

# Write initial data with minimal schema
dc.write(initial_users, "users", schema=initial_schema)

# Read the data back as a Pandas DataFrame
df = dc.read("users", read_as=dc.DatasetType.PANDAS)
print("=== Initial Users (Basic Info) ===")
print(df.sort_values("user_id"))

# Later, enrich with new insights: add age/job features + new users
enriched_data = pd.DataFrame({
    "user_id": [1, 3, 4, 5, 6],
    "name": ["Cheshire", "Felix", "Tom", "Simpkin", "Delta"],
    "age": [3, 2, 5, 12, 4],
    "job": ["Tour Guide", "Drifter", "Housekeeper", "Mouser", "Engineer"]
})

# DeltaCAT automatically evolves the schema and merges by user_id:
# 1. Enriches existing users (Jim -> Cheshire age=3, job="Tour Guide"; Bob -> Felix)
# 2. Adds new age/job columns with automatic schema evolution
# 3. Inserts new users (Tom, Simpkin, Delta) with full feature set
dc.write(enriched_data, "users")

# Read back to see live feature enrichment results
df = dc.read("users", read_as=dc.DatasetType.PANDAS)
print("\n=== Enriched Users (Age & Job) ===")
print(df.sort_values("user_id"))

# - Cheshire (user_id=1) name updated from Jim, gets age=3, job="Tour Guide"
# - Dinah (user_id=2) keeps original name, gets null age/job (missing features)
# - Felix (user_id=3) name updated from Bob, gets age=2, job="Drifter"
# - New users (4,5,6) added with complete feature set
# - Schema automatically evolved to include age/job columns

# Specify the users to delete.
# We only need to specify matching merge key values.
users_to_delete = pd.DataFrame({
    "user_id": [3, 5],
})

# Delete the records that match our merge keys.
dc.write(users_to_delete, "users", mode=dc.TableWriteMode.DELETE)

# Read the table back to confirm target users have been deleted.
df = dc.read("users", read_as=dc.DatasetType.PANDAS)
print("\n=== After Deletion ===")
print(df.sort_values("user_id"))

# - Felix (user_id=3) has been removed
# - Simpkin (user_id=5) has been removed
# - All other users remain unchanged
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Zero-Copy Multimodal URL Processing</span></summary>

DeltaCAT can register and process existing multimodal datasets from local or remote URLs. This enables zero-copy distributed processing of images, audio, text, and other file formats:

```python
import deltacat as dc
import pandas as pd
import pyarrow as pa
import tempfile
import ray

# Initialize DeltaCAT with a fresh temporary catalog
dc.init_local(tempfile.mkdtemp())

# Create dataset with DeltaCAT URLs pointing to existing files
urls_df = pd.DataFrame({
    "file_id": [1, 2, 3, 4, 5, 6],
    "url": [
        # URLs with common file extensions will have their content type inferred.
        "https://picsum.photos/id/237/400/300.jpg",
        "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv",
        "https://raw.githubusercontent.com/SergLam/Audio-Sample-files/master/sample.mp3",
        "https://raw.githubusercontent.com/burningtree/awesome-json/master/README.md",
        "https://raw.githubusercontent.com/microsoft/vscode/main/package.json",
        # URLs without common file extensions will be read as binary by default.
        "https://picsum.photos/200"
    ]
})

# Create empty table with merge key to efficiently add insights about each file
dc.create_table(
    "multimodal_files",
    schema=dc.Schema.of([
        dc.Field.of(pa.field("file_id", pa.int64()), is_merge_key=True),
        dc.Field.of(pa.field("url", pa.string()))
    ])
)

# Write URLs to DeltaCAT table
dc.write(urls_df, "multimodal_files")

# UDF to process each file in parallel using Ray Dataset map method
def analyze_file(row):
    file_id = row["file_id"]
    url = row["url"]

    # DeltaCAT automatically infers the right Ray Data reader for the URL
    dataset = dc.get(url)
    records = dataset.take_all()
    url_type = dc.DatastoreType.from_url(url)

    # Extract standard Ray Dataset fields for each file type
    if url_type == dc.DatastoreType.IMAGES:
        image = records[0]["image"]
        analysis = f"Image {image.shape[1]}x{image.shape[0]} pixels"
    elif url_type == dc.DatastoreType.CSV:
        analysis = f"CSV with {len(records)} rows, {len(records[0].keys())} columns"
    elif url_type == dc.DatastoreType.AUDIO:
        sample_rate = records[0]["sample_rate"]
        duration = len(records[0]["amplitude"][0]) / sample_rate
        analysis = f"Audio {duration:.1f}s, {sample_rate}Hz"
    elif url_type == dc.DatastoreType.JSON:
        analysis = f"JSON with {len(records[0].keys())} fields"
    elif url_type == dc.DatastoreType.TEXT:
        analysis = f"Text with {len(records)} records"
    else:
        analysis = f"Binary with {len(records[0]['bytes'])} bytes"

    return {"file_id": file_id, "analysis": analysis}

# Read the multimodal_files table as a Ray Dataset
ray_dataset = dc.read("multimodal_files", read_as=dc.DatasetType.RAY_DATASET)
# Download and analyze each URL in parallel using map
results_dataset = ray_dataset.map(analyze_file)

# Write results back to the multimodal_files table
dc.write(results_dataset, "multimodal_files", mode=dc.TableWriteMode.MERGE)

# Read final results and compare to initial dataset
print("\n=== Initial Dataset ===")
print(dc.to_pandas(ray_dataset))

print("\n=== Final Results with Analysis ===")
print(dc.read("multimodal_files", read_as=dc.DatasetType.PANDAS))
```

The default dataset type used by `dc.get` is a Ray Dataset but, similar to `dc.read`, `dc.get` can also read URLs into other dataset types like Daft:

```python
import deltacat as dc

# Create dataset with DeltaCAT URLs pointing to existing files
urls = [
    # URLs with common file extensions will have their content type inferred.
    "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv",
    "https://raw.githubusercontent.com/burningtree/awesome-json/master/README.md",
    # URLs without common file extensions will be read as binary by default.
    "https://picsum.photos/200"
]

# Download each URL into a Daft DataFrame serially
for url in urls:
    dataset = dc.get(url, read_as=dc.DatasetType.DAFT)
    print(f"\n=== {url} ===")
    print(dataset.show())
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Organizing Tables with Namespaces</span></summary>

In DeltaCAT, table **Namespaces** are optional but useful for organizing related tables within a catalog:

```python
import deltacat as dc
import pandas as pd
import tempfile

# Initialize DeltaCAT with a fresh temporary catalog
dc.init_local(tempfile.mkdtemp())

# Create some sample data for different business domains
user_data = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
})

product_data = pd.DataFrame({
    "product_id": [101, 102, 103],
    "name": ["Mushrooms", "Fish", "Milk"],
    "price": [12.99, 8.99, 3.99]
})

order_data = pd.DataFrame({
    "order_id": [1001, 1002, 1003],
    "user_id": [1, 2, 3],
    "product_id": [101, 102, 103],
    "quantity": [2, 1, 2]
})
# Create identity, inventory, and sales namespaces
dc.create_namespace("identity")
dc.create_namespace("inventory")
dc.create_namespace("sales")

# Write tables to different namespaces to organize them by domain
dc.write(user_data, "users", namespace="identity")
dc.write(product_data, "catalog", namespace="inventory")
dc.write(order_data, "transactions", namespace="sales")

# Read from specific namespaces
users_df = dc.read("users", namespace="identity", read_as=dc.DatasetType.PANDAS)
products_df = dc.read("catalog", namespace="inventory", read_as=dc.DatasetType.PANDAS)
orders_df = dc.read("transactions", namespace="sales", read_as=dc.DatasetType.PANDAS)

# Tables with the same name can exist in different namespaces
# Create separate marketing and finance views of users
marketing_users = pd.DataFrame({
    "user_id": [1, 2, 3],
    "segment": ["premium", "standard", "premium"],
    "acquisition_channel": ["social", "search", "referral"]
})

finance_users = pd.DataFrame({
    "user_id": [1, 2, 3],
    "lifetime_payments": [25.98, 8.99, 7.98],
    "preferred_payment_method": ["credit", "cash", "paypal"]
})

dc.create_namespace("marketing")
dc.write(marketing_users, "users", namespace="marketing")

dc.create_namespace("finance")
dc.write(finance_users, "users", namespace="finance")

# Each namespace maintains its own "users" table with different schemas
marketing_df = dc.read("users", namespace="marketing", read_as=dc.DatasetType.PANDAS)
finance_df = dc.read("users", namespace="finance", read_as=dc.DatasetType.PANDAS)

print(f"\n=== Identity Namespace Users ===")
print(users_df)
print(f"\n=== Inventory Namespace Products ===")
print(products_df)
print(f"\n=== Sales Namespace Transactions ===")
print(orders_df)
print(f"\n=== Marketing Namespace Users ===")
print(marketing_df)
print(f"\n=== Finance Namespace Users ===")
print(finance_df)
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Data Lake Level Transactions</span></summary>

DeltaCAT transactions can span multiple tables and namespaces. Since transaction history is maintained at the catalog level, every transaction operates against a consistent snapshot of every object in your data lake. Since all operations within a transaction either succeed or fail together, this simplifies keeping related datasets in sync across your entire catalog.

Consider the previous example that organized tables with namespaces. One table tracked customer orders, and another table tracked the lifetime payments of each customer. If one table was updated but not the other, then it would result in an accounting discrepancy. This edge case can be eliminated by using multi-table transactions:

```python
import deltacat as dc
import pandas as pd
import pyarrow as pa
import tempfile

# Initialize DeltaCAT with a fresh temporary catalog
dc.init_local(tempfile.mkdtemp())

# Create sample product data.
product_data = pd.DataFrame({
    "product_id": [101, 102, 103],
    "name": ["Mushrooms", "Fish", "Milk"],
    "price": [12.99, 8.99, 3.99]
})

# The product catalog can be created independently.
dc.create_namespace("inventory")
dc.write(product_data, "catalog", namespace="inventory")

print(f"\n=== Initial Product Data ===")
print(dc.read("catalog", namespace="inventory", read_as=dc.DatasetType.PANDAS))

# Create sample user and finance data.
user_data = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
})
initial_finance = pd.DataFrame({
    "user_id": [1, 2, 3],
    "preferred_payment_method": ["credit", "cash", "paypal"]
})

# Define a finance schema.
# User ID is the merge key, and lifetime payments are defaulted to 0.00.
finance_schema = dc.Schema.of([
    dc.Field.of(pa.field("user_id", pa.int64()), is_merge_key=True),
    dc.Field.of(pa.field("lifetime_payments", pa.float64()), future_default=0.00),
    dc.Field.of(pa.field("preferred_payment_method", pa.string())),
])

# Create user identities and user finance data within a single transaction.
# Since transactions are atomic, this prevents accounting discrepancies.
with dc.transaction():
    dc.create_namespace("identity")
    dc.write(user_data, "users", namespace="identity")
    dc.create_namespace("finance")
    dc.write(initial_finance, "users", namespace="finance", schema=finance_schema)

print(f"\n=== Initial User Data ===")
print(dc.read("users", namespace="identity", read_as=dc.DatasetType.PANDAS))
print(f"\n=== Initial Finance Data ===")
print(dc.read("users", namespace="finance", read_as=dc.DatasetType.PANDAS))

# Create new order data
new_orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003],
    "user_id": [1, 2, 3],
    "product_id": [101, 102, 103],
    "quantity": [2, 1, 2]
})

# Process new orders and update lifetime payment totals within a single transaction.
with dc.transaction():
    # Step 1: Write the new orders
    dc.create_namespace("sales")
    dc.write(new_orders, "transactions", namespace="sales")

    # Step 2: Read back transactions and products to compute actual totals
    orders_df = dc.read("transactions", namespace="sales", read_as=dc.DatasetType.PANDAS)
    products_df = dc.read("catalog", namespace="inventory", read_as=dc.DatasetType.PANDAS)

    # Step 3: Compute lifetime payment totals by joining orders with product prices
    orders_with_prices = orders_df.merge(products_df, on="product_id")
    orders_with_prices["total"] = orders_with_prices["quantity"] * orders_with_prices["price"]

    # Calculate lifetime totals per user
    finance_updates = orders_with_prices.groupby("user_id")["total"].sum().reset_index()
    finance_updates.columns = ["user_id", "lifetime_payments"]

    # Step 4: Write the computed totals
    dc.create_namespace("finance")
    dc.write(finance_updates, "users", namespace="finance", mode=dc.TableWriteMode.MERGE)

# Verify that orders and and lifetime payments are kept in sync.
print(f"\n=== New Orders Processed ===")
print(dc.read("transactions", namespace="sales", read_as=dc.DatasetType.PANDAS))
print(f"\n=== Updated Finance Records ===")
print(dc.read("users", namespace="finance", read_as=dc.DatasetType.PANDAS))
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Managing Multiple Data Lakes</span></summary>

DeltaCAT lets you work with multiple catalogs in a single application. All catalogs registered with DeltaCAT are tracked by a Ray Actor to make them available to all workers in your Ray application.

For example, you may want to test a write against a local staging catalog before committing it to a shared production catalog:

```python
import deltacat as dc
from deltacat.exceptions import TableValidationError
import pandas as pd
import pyarrow as pa
import pyarrow.compute as pc
import tempfile
from decimal import Decimal

# Initialize catalogs with separate names and catalog roots.
dc.init(
    catalogs={
        # Use temporary directory for staging
        "staging": dc.Catalog(dc.CatalogProperties(tempfile.mkdtemp())),
        # Use S3 for prod
        "prod": dc.Catalog(dc.CatalogProperties("s3://example/deltacat"))
    }
)

# Create a PyArrow table with decimal256 data
decimal_table = pa.table({
    "item_id": [1, 2, 3],
    "price": pa.array([
        Decimal("999.99"),
        Decimal("1234.56"),
        Decimal("567.89")
    ], type=pa.decimal256(10, 2))
})

# Try to write decimal256 data to the staging table.
# DeltaCAT auto-detects that decimal256 isn't readable
# by several default supported table reader types
# (Polars, Daft, Ray Data) and raises a TableValidationError.
try:
    dc.write(decimal_table, "financial_data", catalog="staging")
    print("Decimal256 write succeeded")
except TableValidationError as e:
    print(f"\n=== Validation Error ===")
    print(e)
    print("Decimal256 may break existing data consumers in prod, trying decimal128...")

    # Cast the price column from decimal256 to decimal128
    decimal_table = decimal_table.set_column(
        decimal_table.schema.get_field_index("price"),
        "price",
        pc.cast(decimal_table["price"], pa.decimal128(10, 2))
    )

# Write the validated decimal data to staging and ensure that the write succeeds
dc.write(decimal_table, "financial_data", catalog="staging")
print(f"\n=== Successfully Staged Data ===")
print(dc.read("financial_data", catalog="staging", read_as=dc.DatasetType.PANDAS))

# Read from staging to verify
staging_data = dc.read("financial_data", catalog="staging", read_as=dc.DatasetType.PANDAS)
assert staging_data["price"].tolist() == [Decimal("999.99"), Decimal("1234.56"), Decimal("567.89")]

# Now write the validated data to production
dc.write(decimal_table, "financial_data", catalog="prod")
print(f"\n=== Production Data ===")
print(dc.read("financial_data", catalog="prod", read_as=dc.DatasetType.PANDAS))
```

</details>

<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Data Lake Sharing & Portability</span></summary>

DeltaCAT catalogs are self-contained directories on a filesystem, so you can easily share your data lake with others. A local catalog on your laptop can be compressed and sent anywhere. A cloud catalog in S3, GCS, or Azure Blog Storage can be shared via URL. The read/write permissions of your catalog are the read/write permissions of your filesystem.

For example, you can zip up your local catalog and upload it to S3 via:
```bash
# zip a local catalog
zip -r catalog.zip .deltacat/

# copy the catalog to a cloud bucket
aws s3 cp catalog.zip s3://my-bucket/catalog.zip
```

The person you shared it with can retrieve and decompress it via:
```bash
# copy the cloud catalog to local disk
aws s3 cp s3://my-bucket/catalog.zip .

# unzip the catalog to a local directory
unzip catalog.zip -d .deltacat_copy/
```

And then initialize it together with any other catalogs they're working with:
```python
import deltacat as dc

# Initialize catalogs with separate names and catalog roots.
dc.init(
    catalogs={
        "original": dc.Catalog(dc.CatalogProperties(".deltacat")),
        "copy": dc.Catalog(dc.CatalogProperties(".deltacat_copy")),
        "prod_aws": dc.Catalog(dc.CatalogProperties("s3://prod/deltacat")),
        "prod_gcp": dc.Catalog(dc.CatalogProperties("gs://prod/deltacat")),
        "prod_azure": dc.Catalog(dc.CatalogProperties("az://prod/deltacat")),
    }
)

# List all namespaces in the original catalog
namespaces = dc.list("dc://original")
print([namespace.name for namespace in namespaces])

# List all namespaces in the copy catalog
namespaces = dc.list("dc://copy")
print([namespace.name for namespace in namespaces])

# List all tables in the default namespace of the original catalog
tables = dc.list("dc://original/default")
print([table.name for table in tables])

# List all tables in the default namespace of the copy catalog
tables = dc.list("dc://copy/default")
print([table.name for table in tables])
```

`dc.copy` can also be used to copy namespaces and tables between catalogs:
```python
# Copy the "default" namespace from the original local catalog over to the "myspace" namespace in the copy catalog
dc.copy("dc://original/default", "dc://copy/default/myspace")

# By default, no tables are copied from the source namespace to the destination
tables = dc.list("dc://copy/myspace")
print(f"{len(tables)} tables in myspace.")

# Copy the "users" table from the original local catalog over to "local_users" in the prod_aws catalog
dc.copy("dc://original/default/users", "dc://prod_aws/default/local_users")

# Read the copied table back
df = dc.read("local_users", catalog="prod_aws")
df.show()

# We can also copy all tables in the default namespace using **
dc.copy("dc://original/default/**", "dc://copy/default/myspace")
tables = dc.list("dc://copy/myspace")
print(f"{len(tables)} tables in myspace.")

# Or we can copy all namespaces from the original catalog using *
dc.copy("dc://original/*", "dc://copy")
namespaces = dc.list("dc://copy")
print([namespace.name for namespace in namespaces])
```

</details>


<details>

<summary><span style="font-size: 1.25em; font-weight: bold;">Data Lake Level Time Travel</span></summary>

DeltaCAT supports time travel queries that let you read all tables in a catalog as they existed at any point in the past. Combined with catalog-level transactions, this enables consistent point-in-time views across your entire data lake.

```python
import deltacat as dc
import pandas as pd
import tempfile
import time

# Initialize DeltaCAT with a fresh temporary catalog
dc.init_local(tempfile.mkdtemp())

# Create initial state with existing users, products, and orders
initial_users = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["Cheshire", "Dinah", "Felix"],
})

initial_products = pd.DataFrame({
    "product_id": [101, 102, 103],
    "name": ["Mushrooms", "Fish", "Milk"],
    "price": [12.99, 8.99, 3.99]
})

initial_orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003],
    "user_id": [1, 2, 3],
    "product_id": [101, 102, 103],
    "quantity": [2, 1, 2]
})

initial_finance = pd.DataFrame({
    "user_id": [1, 2, 3],
    "lifetime_payments": [25.98, 8.99, 7.98],
})

# Write initial state atomically with a commit message
with dc.transaction(commit_message="Initial data load: users, products, orders, and finance"):
    dc.write(initial_users, "users", namespace="identity", auto_create_namespace=True)
    dc.write(initial_products, "catalog", namespace="inventory", auto_create_namespace=True)
    dc.write(initial_orders, "transactions", namespace="sales", auto_create_namespace=True)
    dc.write(initial_finance, "users", namespace="finance", auto_create_namespace=True)

# Sleep briefly to ensure transaction timestamp separation
time.sleep(0.1)

# Later, add new orders for existing and new users
new_orders = pd.DataFrame({
    "order_id": [1004, 1005, 1006, 1007, 1008],
    "user_id": [1, 2, 1, 4, 5],
    "product_id": [101, 102, 101, 104, 105],
    "quantity": [1, 2, 3, 5, 1]
})

new_users = pd.DataFrame({
    "user_id": [4, 5],
    "name": ["Tom", "Simpkin"],
})

new_products = pd.DataFrame({
    "product_id": [1