Files
R. Tyler Croy 49424e5897 chore: enable python tests to pass again
Signed-off-by: R. Tyler Croy <rtyler@brokenco.de>
2025-12-14 09:30:15 -08:00

215 lines
6.7 KiB
Python

from __future__ import annotations
import os
import uuid
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from arro3.core import Array, ChunkedArray, DataType, Table
from numpy.random import standard_normal
from deltalake import DeltaTable, write_deltalake
if TYPE_CHECKING:
from minio import Minio
# NOTE: make sure to run these in release mode with
# PROFILE=python-release make develop
# When profiling, use:
# PROFILE=profiling make develop
@pytest.fixture()
def sample_table() -> Table:
max_size_bytes = 128 * 1024 * 1024
ncols = 20
nrows = max_size_bytes // 20 // 8
rows = {f"x{i}": standard_normal(nrows) for i in range(ncols)}
rows["i"] = Array(range(nrows), type=DataType.int64())
return Table.from_pydict(rows)
@pytest.mark.benchmark(group="write")
def test_benchmark_write(benchmark, sample_table: Table, tmp_path: Path):
def setup() -> None:
table_path = tmp_path / str(uuid.uuid4())
table_path.mkdir()
return (table_path,), dict()
def func(table_path: str) -> None:
write_deltalake(table_path, sample_table)
benchmark.pedantic(func, setup=setup, rounds=5, warmup_rounds=3)
# TODO: figure out why this assert is failing
# dt = DeltaTable(str(tmp_path))
# table = (
# QueryBuilder()
# .register("tbl", dt)
# .execute("select * from tbl order by i asc")
# .read_all()
# )
# assert table == sample_table
@pytest.mark.benchmark(group="write")
def test_benchmark_write_minio(
benchmark, sample_table: Table, minio_container: tuple[dict, Minio]
):
import uuid
bucket_name = f"delta-bench-{uuid.uuid4()}"
storage_options, minio = minio_container
minio.make_bucket(bucket_name)
def setup() -> None:
table_path = f"s3://{bucket_name}/{uuid.uuid4()}"
return (table_path,), dict()
def func(table_path: str) -> None:
write_deltalake(table_path, sample_table, storage_options=storage_options)
benchmark.pedantic(func, setup=setup, rounds=5, warmup_rounds=3)
@pytest.mark.pyarrow
@pytest.mark.benchmark(group="read")
def test_benchmark_read(benchmark, sample_table: Table, tmp_path: Path):
import pyarrow as pa
write_deltalake(str(tmp_path), sample_table)
dt = DeltaTable(str(tmp_path))
result = benchmark(dt.to_pyarrow_table)
assert result.sort_by("i") == pa.table(sample_table)
@pytest.mark.pyarrow
@pytest.mark.benchmark(group="read")
def test_benchmark_read_pyarrow(benchmark, sample_table: Table, tmp_path: Path):
import pyarrow as pa
import pyarrow.fs as pa_fs
write_deltalake(str(tmp_path), sample_table)
dt = DeltaTable(str(tmp_path))
fs = pa_fs.SubTreeFileSystem(str(tmp_path), pa_fs.LocalFileSystem())
result = benchmark(dt.to_pyarrow_table, filesystem=fs)
assert result.sort_by("i") == pa.table(sample_table)
@pytest.mark.benchmark(group="optimize")
@pytest.mark.parametrize("max_tasks", [1, 5])
def test_benchmark_optimize(
benchmark, sample_table: Table, tmp_path: Path, max_tasks: int
):
# Create 2 partitions, each partition with 10 files.
# Each file is about 100MB, so the total size is 2GB.
files_per_part = 10
parts = ["a", "b", "c", "d", "e"]
nrows = int(sample_table.num_rows / files_per_part)
for part in parts:
tab = sample_table.slice(0, nrows)
tab = tab.append_column(
"part", ChunkedArray(Array([part] * nrows, type=DataType.utf8()))
)
for _ in range(files_per_part):
write_deltalake(tmp_path, tab, mode="append", partition_by=["part"])
dt = DeltaTable(tmp_path)
assert len(dt.file_uris()) == files_per_part * len(parts)
initial_version = dt.version()
def setup():
# Instead of recreating the table for each benchmark run, we just delete
# the optimize log file
optimize_version = initial_version + 1
try:
os.remove(
os.path.join(tmp_path, "_delta_log", f"{optimize_version:020}.json")
)
except FileNotFoundError:
pass
# Reload the table after we have altered the log
dt = DeltaTable(tmp_path)
assert dt.version() == initial_version
return (dt,), dict(max_concurrent_tasks=max_tasks)
def func(dt, max_concurrent_tasks):
return dt.optimize.compact(
max_concurrent_tasks=max_concurrent_tasks, target_size=1024 * 1024 * 1024
)
# We need to recreate the table for each benchmark run
results = benchmark.pedantic(func, setup=setup, rounds=5, warmup_rounds=3)
assert results["numFilesRemoved"] == 50
assert results["numFilesAdded"] == 5
assert results["partitionsOptimized"] == 5
@pytest.mark.benchmark(group="optimize", warmup=False)
@pytest.mark.parametrize("max_tasks", [1, 5])
def test_benchmark_optimize_minio(
benchmark, sample_table: Table, minio_container: tuple[dict, Minio], max_tasks: int
):
bucket_name = f"delta-bench-{uuid.uuid4()}"
table_path = f"s3://{bucket_name}/optimize-test"
storage_options, minio = minio_container
minio.make_bucket(bucket_name)
# Create 2 partitions, each partition with 10 files.
# Each file is about 100MB, so the total size is 2GB.
files_per_part = 10
parts = ["a", "b", "c", "d", "e"]
nrows = int(sample_table.num_rows / files_per_part)
for part in parts:
tab = sample_table.slice(0, nrows)
tab = tab.append_column(
"part", ChunkedArray(Array([part] * nrows, type=DataType.utf8()))
)
for _ in range(files_per_part):
write_deltalake(
table_path,
tab,
mode="append",
partition_by=["part"],
storage_options=storage_options,
)
dt = DeltaTable(table_path, storage_options=storage_options)
assert len(dt.file_uris()) == files_per_part * len(parts)
initial_version = dt.version()
def setup():
# Instead of recreating the table for each benchmark run, we just delete
# the optimize log file
optimize_version = initial_version + 1
try:
minio.remove_object(
bucket_name, f"optimize-test/_delta_log/{optimize_version:020}.json"
)
except Exception:
pass
dt = DeltaTable(table_path, storage_options=storage_options)
return (dt,), dict(max_concurrent_tasks=max_tasks)
def func(dt, max_concurrent_tasks):
return dt.optimize.compact(
max_concurrent_tasks=max_concurrent_tasks, target_size=1024 * 1024 * 1024
)
results = benchmark.pedantic(func, setup=setup, rounds=5, warmup_rounds=3)
assert results["numFilesRemoved"] == 50
assert results["numFilesAdded"] == 5
assert results["partitionsOptimized"] == 5