mirror of
https://github.com/langchain-ai/delta-rs.git
synced 2026-07-18 12:25:33 -04:00
87405d48c7
# Description This test fails on local when run repeatedly. `tempfile.gettempdir()` always returns the same path and cauases test to fail on second run. Existing fixture for `tmp_path` returns unique path for each test # Related Issue(s) # Documentation NA Signed-off-by: Khalid Mammadov <khalidmammadov9@gmail.com>
1300 lines
45 KiB
Python
1300 lines
45 KiB
Python
import multiprocessing
|
|
import os
|
|
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
|
|
from datetime import date, datetime, timezone
|
|
from pathlib import Path
|
|
from threading import Barrier, Thread
|
|
from typing import Any
|
|
from unittest.mock import Mock
|
|
from urllib.parse import urlparse
|
|
|
|
import pytest
|
|
from arro3.core import Array, DataType, Table
|
|
from arro3.core import Field as ArrowField
|
|
|
|
from deltalake import DeltaTable
|
|
from deltalake._util import encode_partition_value
|
|
from deltalake.exceptions import DeltaProtocolError
|
|
from deltalake.query import QueryBuilder
|
|
from deltalake.table import ProtocolVersions
|
|
from deltalake.writer import write_deltalake
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_with_edge_timestamps():
|
|
import pyarrow.dataset as ds
|
|
from pyarrow.dataset import ParquetReadOptions
|
|
|
|
table_path = "../crates/test/tests/data/table_with_edge_timestamps"
|
|
dt = DeltaTable(table_path)
|
|
dataset = dt.to_pyarrow_dataset(
|
|
parquet_read_options=ParquetReadOptions(coerce_int96_timestamp_unit="ms")
|
|
)
|
|
assert dataset.to_table().to_pydict() == {
|
|
"BIG_DATE": [
|
|
datetime(9999, 12, 31, 0, 0, 0, tzinfo=timezone.utc),
|
|
datetime(9999, 12, 30, 0, 0, 0, tzinfo=timezone.utc),
|
|
],
|
|
"NORMAL_DATE": [
|
|
datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
datetime(2022, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
],
|
|
"SOME_VALUE": [1, 2],
|
|
}
|
|
# Can push down filters to these timestamps.
|
|
predicate = ds.field("BIG_DATE") == datetime(
|
|
9999, 12, 31, 0, 0, 0, tzinfo=timezone.utc
|
|
)
|
|
assert len(list(dataset.get_fragments(predicate))) == 1
|
|
|
|
|
|
def test_read_simple_table_to_dict():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
assert QueryBuilder().register("tbl", dt).execute(
|
|
"select * from tbl ORDER BY id"
|
|
).read_all()["id"].to_pylist() == [5, 7, 9]
|
|
|
|
|
|
def test_table_count():
|
|
table_path = "../crates/test/tests/data/COVID-19_NYT"
|
|
dt = DeltaTable(table_path)
|
|
assert dt.count() == 1111930
|
|
|
|
|
|
class _SerializableException(BaseException):
|
|
pass
|
|
|
|
|
|
def _recursively_read_simple_table(executor_class: type[Executor], depth):
|
|
try:
|
|
test_read_simple_table_to_dict()
|
|
except BaseException as e: # Ideally this would catch `pyo3_runtime.PanicException` but its seems that is not possible.
|
|
# Re-raise as something that can be serialized and therefore sent back to parent processes.
|
|
raise _SerializableException(f"Seraializatble exception: {e}") from e
|
|
|
|
if depth == 0:
|
|
return
|
|
# We use concurrent.futures.Executors instead of `threading.Thread` or `multiprocessing.Process` to that errors
|
|
# are re-raised in the parent process/thread when we call `future.result()`.
|
|
with executor_class(max_workers=1) as executor:
|
|
future = executor.submit(
|
|
_recursively_read_simple_table, executor_class, depth - 1
|
|
)
|
|
future.result()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"executor_class,multiprocessing_start_method,expect_panic",
|
|
[
|
|
(ThreadPoolExecutor, None, False),
|
|
(ProcessPoolExecutor, "forkserver", False),
|
|
(ProcessPoolExecutor, "spawn", False),
|
|
(ProcessPoolExecutor, "fork", True),
|
|
],
|
|
)
|
|
def test_read_simple_in_threads_and_processes(
|
|
executor_class, multiprocessing_start_method, expect_panic
|
|
):
|
|
if multiprocessing_start_method is not None:
|
|
multiprocessing.set_start_method(multiprocessing_start_method, force=True)
|
|
if expect_panic:
|
|
with pytest.raises(
|
|
_SerializableException,
|
|
match="The tokio runtime does not support forked processes",
|
|
):
|
|
_recursively_read_simple_table(executor_class=executor_class, depth=5)
|
|
else:
|
|
_recursively_read_simple_table(executor_class=executor_class, depth=5)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_simple_table_by_version_to_dict():
|
|
table_path = "../crates/test/tests/data/delta-0.2.0"
|
|
dt = DeltaTable(table_path, version=2)
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == {"value": [1, 2, 3]}
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_simple_table_using_options_to_dict():
|
|
table_path = "../crates/test/tests/data/delta-0.2.0"
|
|
dt = DeltaTable(table_path, version=2, storage_options={})
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == {"value": [1, 2, 3]}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["date_value", "expected_version"],
|
|
[
|
|
("2020-05-01T00:47:31-07:00", 0),
|
|
("2020-05-02T22:47:31-07:00", 1),
|
|
("2020-05-25T22:47:31-07:00", 4),
|
|
],
|
|
)
|
|
def test_load_as_version_datetime(date_value: str, expected_version):
|
|
log_dir = "../crates/test/tests/data/simple_table/_delta_log"
|
|
log_mtime_pair = [
|
|
("00000000000000000000.json", 1588398451.0),
|
|
("00000000000000000001.json", 1588484851.0),
|
|
("00000000000000000002.json", 1588571251.0),
|
|
("00000000000000000003.json", 1588657651.0),
|
|
("00000000000000000004.json", 1588744051.0),
|
|
]
|
|
for file_name, dt_epoch in log_mtime_pair:
|
|
file_path = os.path.join(log_dir, file_name)
|
|
os.utime(file_path, (dt_epoch, dt_epoch))
|
|
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
dt.load_as_version(date_value)
|
|
assert dt.version() == expected_version
|
|
dt = DeltaTable(table_path)
|
|
dt.load_as_version(datetime.fromisoformat(date_value))
|
|
assert dt.version() == expected_version
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["date_value", "expected_version", "log_mtime_pairs"],
|
|
[
|
|
("2020-05-01T00:47:31-07:00", 1, [("00000000000000000000.json", 158839841.0)]),
|
|
(
|
|
"2020-05-02T22:47:31-07:00",
|
|
2,
|
|
[
|
|
("00000000000000000000.json", 158839841.0),
|
|
("00000000000000000001.json", 1588484851.0),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
def test_load_as_version_datetime_with_logs_removed(
|
|
tmp_path,
|
|
sample_table,
|
|
date_value: str,
|
|
expected_version,
|
|
log_mtime_pairs: list[tuple[str, int]],
|
|
):
|
|
log_path = tmp_path / "_delta_log"
|
|
for i in range(6):
|
|
write_deltalake(tmp_path, data=sample_table, mode="append")
|
|
|
|
for file_name, dt_epoch in log_mtime_pairs:
|
|
file_path = log_path / file_name
|
|
os.utime(file_path, (dt_epoch, dt_epoch))
|
|
|
|
dt = DeltaTable(tmp_path, version=expected_version)
|
|
dt.create_checkpoint()
|
|
file = log_path / f"0000000000000000000{expected_version}.checkpoint.parquet"
|
|
assert file.exists()
|
|
dt.cleanup_metadata()
|
|
|
|
file = log_path / f"0000000000000000000{expected_version - 1}.json"
|
|
assert not file.exists()
|
|
dt = DeltaTable(tmp_path)
|
|
dt.load_as_version(date_value)
|
|
assert dt.version() == expected_version
|
|
dt = DeltaTable(tmp_path)
|
|
dt.load_as_version(datetime.fromisoformat(date_value))
|
|
assert dt.version() == expected_version
|
|
|
|
|
|
def test_load_as_version_datetime_bad_format():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
|
|
for bad_format in [
|
|
"2020-05-01T00:47:31",
|
|
"2020-05-01 00:47:31",
|
|
"2020-05-01T00:47:31+08",
|
|
]:
|
|
with pytest.raises(Exception, match="Failed to parse datetime string:"):
|
|
dt.load_as_version(bad_format)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_simple_table_update_incremental():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path, version=0)
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == {"id": [0, 1, 2, 3, 4]}
|
|
dt.update_incremental()
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == {"id": [5, 7, 9]}
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_simple_table_file_sizes_failure(mocker):
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
add_actions = dt.get_add_actions()
|
|
|
|
# set all sizes to -1, the idea is to break the reading, to check
|
|
# that input file sizes are actually used
|
|
add_actions_modified = {x.as_py(): -1 for x in add_actions["path"]}
|
|
mocker.patch(
|
|
"deltalake._internal.RawDeltaTable.get_add_file_sizes",
|
|
return_value=add_actions_modified,
|
|
)
|
|
|
|
with pytest.raises(OSError, match="Cannot seek past end of file."):
|
|
dt.to_pyarrow_dataset().to_table().to_pydict()
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_partitioned_table_to_dict():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
expected = {
|
|
"value": ["1", "2", "3", "6", "7", "5", "4"],
|
|
"year": ["2020", "2020", "2020", "2021", "2021", "2021", "2021"],
|
|
"month": ["1", "2", "2", "12", "12", "12", "4"],
|
|
"day": ["1", "3", "5", "20", "20", "4", "5"],
|
|
}
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == expected
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_partitioned_table_with_partitions_filters_to_dict():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
partitions = [("year", "=", "2021")]
|
|
expected = {
|
|
"value": ["6", "7", "5", "4"],
|
|
"year": ["2021", "2021", "2021", "2021"],
|
|
"month": ["12", "12", "12", "4"],
|
|
"day": ["20", "20", "4", "5"],
|
|
}
|
|
|
|
assert dt.to_pyarrow_dataset(partitions).to_table().to_pydict() == expected
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_partitioned_table_with_primitive_type_partition_filters():
|
|
table_path = "../crates/test/tests/data/partition-type-primitives"
|
|
dt = DeltaTable(table_path)
|
|
|
|
partitions_int = [("year", "=", 2020)]
|
|
result_int = dt.to_pyarrow_dataset(partitions_int).to_table().to_pydict()
|
|
assert len(result_int["id"]) == 8
|
|
assert all(year == "2020" for year in result_int["year"])
|
|
|
|
partitions_float = [("year", "=", 2021.0)]
|
|
result_float = dt.to_pyarrow_dataset(partitions_float).to_table().to_pydict()
|
|
assert len(result_float["id"]) == 8
|
|
assert all(year == "2021.0" for year in result_float["year"])
|
|
|
|
partitions_bool = [("is_active", "=", True)]
|
|
result_bool = dt.to_pyarrow_dataset(partitions_bool).to_table().to_pydict()
|
|
assert len(result_bool["id"]) == 8
|
|
assert all(is_active == "true" for is_active in result_bool["is_active"])
|
|
|
|
partitions_date = [("event_date", "=", date(2023, 1, 1))]
|
|
result_date = dt.to_pyarrow_dataset(partitions_date).to_table().to_pydict()
|
|
assert len(result_date["id"]) == 8
|
|
assert all(event_date == "2023-01-01" for event_date in result_date["event_date"])
|
|
|
|
partitions_string = [("category", "=", "A")]
|
|
result_string = dt.to_pyarrow_dataset(partitions_string).to_table().to_pydict()
|
|
assert len(result_string["id"]) == 8
|
|
assert all(category == "A" for category in result_string["category"])
|
|
|
|
partitions_bool_in = [("is_active", "in", [True, False])]
|
|
result_bool_in = dt.to_pyarrow_dataset(partitions_bool_in).to_table().to_pydict()
|
|
total_rows = len(dt.to_pyarrow_dataset().to_table().to_pydict()["id"])
|
|
assert len(result_bool_in["id"]) == total_rows
|
|
|
|
partitions_year_in = [("year", "in", [2020, 2022.0])]
|
|
result_year_in = dt.to_pyarrow_dataset(partitions_year_in).to_table().to_pydict()
|
|
assert len(result_year_in["id"]) == 8
|
|
assert all(year == "2020" for year in result_year_in["year"])
|
|
|
|
partitions_bool_true_only = [("is_active", "in", [True])]
|
|
result_bool_true_only = (
|
|
dt.to_pyarrow_dataset(partitions_bool_true_only).to_table().to_pydict()
|
|
)
|
|
assert len(result_bool_true_only["id"]) == 8
|
|
assert all(is_active == "true" for is_active in result_bool_true_only["is_active"])
|
|
|
|
with pytest.raises(ValueError, match="Could not encode partition value for type"):
|
|
partitions_invalid = [("category", "=", {"invalid": "dict"})]
|
|
dt.to_pyarrow_dataset(partitions_invalid)
|
|
|
|
with pytest.raises(ValueError, match="Could not encode partition value for type"):
|
|
partitions_invalid_list = [("category", "in", [{"invalid": "dict"}, "A"])]
|
|
dt.to_pyarrow_dataset(partitions_invalid_list)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_empty_delta_table_after_delete():
|
|
table_path = "../crates/test/tests/data/delta-0.8-empty"
|
|
dt = DeltaTable(table_path)
|
|
expected = {"column": []}
|
|
|
|
assert dt.to_pyarrow_dataset().to_table().to_pydict() == expected
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_with_column_subset():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
expected = {
|
|
"value": ["1", "2", "3", "6", "7", "5", "4"],
|
|
"day": ["1", "3", "5", "20", "20", "4", "5"],
|
|
}
|
|
assert (
|
|
dt.to_pyarrow_dataset().to_table(columns=["value", "day"]).to_pydict()
|
|
== expected
|
|
)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_as_category():
|
|
import pyarrow as pa
|
|
import pyarrow.dataset as ds
|
|
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
|
|
assert dt.schema().to_arrow().field("value").type == pa.string()
|
|
|
|
read_options = ds.ParquetReadOptions(dictionary_columns={"value"})
|
|
|
|
data = dt.to_pyarrow_dataset(parquet_read_options=read_options).to_table()
|
|
|
|
assert data.schema.field("value").type == pa.dictionary(pa.int32(), pa.string())
|
|
assert data.schema.field("day").type == pa.string()
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_with_filter():
|
|
import pyarrow.dataset as ds
|
|
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
expected = {
|
|
"value": ["6", "7", "5"],
|
|
"year": ["2021", "2021", "2021"],
|
|
"month": ["12", "12", "12"],
|
|
"day": ["20", "20", "4"],
|
|
}
|
|
filter_expr = (ds.field("year") == "2021") & (ds.field("month") == "12")
|
|
|
|
dataset = dt.to_pyarrow_dataset()
|
|
|
|
assert len(list(dataset.get_fragments(filter=filter_expr))) == 2
|
|
assert dataset.to_table(filter=filter_expr).to_pydict() == expected
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_with_stats():
|
|
import pyarrow.dataset as ds
|
|
|
|
table_path = "../crates/test/tests/data/COVID-19_NYT"
|
|
dt = DeltaTable(table_path)
|
|
dataset = dt.to_pyarrow_dataset()
|
|
|
|
filter_expr = ds.field("date") > "2021-02-20"
|
|
assert len(list(dataset.get_fragments(filter=filter_expr))) == 2
|
|
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert data.num_rows < 147181 + 47559
|
|
|
|
filter_expr = ds.field("cases") < 0
|
|
assert len(list(dataset.get_fragments(filter=filter_expr))) == 0
|
|
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert data.num_rows == 0
|
|
|
|
filter_expr = ds.field("cases").is_null()
|
|
assert len(list(dataset.get_fragments(filter=filter_expr))) == 0
|
|
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert data.num_rows == 0
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_special_partition():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-special-partition"
|
|
dt = DeltaTable(table_path)
|
|
|
|
file1 = (
|
|
r"x=A%2FA/part-00007-b350e235-2832-45df-9918-6cab4f7578f7.c000.snappy.parquet"
|
|
)
|
|
file2 = (
|
|
r"x=B%20B/part-00015-e9abbc6f-85e9-457b-be8e-e9f5b8a22890.c000.snappy.parquet"
|
|
)
|
|
|
|
def path_matcher(full_path, expected):
|
|
return full_path.endswith(expected)
|
|
|
|
files = dt.file_uris()
|
|
assert path_matcher(files[0], file1) and path_matcher(files[1], file2)
|
|
assert path_matcher(dt.file_uris([("x", "=", "A/A")])[0], file1)
|
|
assert path_matcher(dt.file_uris([("x", "=", "B B")])[0], file2)
|
|
assert dt.file_uris([("x", "=", "c")]) == []
|
|
|
|
table = dt.to_pyarrow_table()
|
|
|
|
assert set(table["x"].to_pylist()) == {"A/A", "B B"}
|
|
|
|
|
|
def test_read_partitioned_table_metadata():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
metadata = dt.metadata()
|
|
assert metadata.id == "fe5a3c11-30d4-4dd7-b115-a1c121e66a4e"
|
|
assert metadata.name is None
|
|
assert metadata.description is None
|
|
assert metadata.partition_columns == ["year", "month", "day"]
|
|
assert metadata.created_time == 1615555644515
|
|
assert metadata.configuration == {}
|
|
|
|
|
|
def test_read_partitioned_table_protocol():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
protocol = dt.protocol()
|
|
assert protocol.min_reader_version == 1
|
|
assert protocol.min_writer_version == 2
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_table_with_cdc():
|
|
table_path = "../crates/test/tests/data/simple_table_with_cdc"
|
|
dt = DeltaTable(table_path)
|
|
|
|
result = QueryBuilder().register("tbl", dt).execute("select * from tbl").read_all()
|
|
|
|
assert result["id"].to_pylist() == [0]
|
|
assert result["name"].to_pylist() == ["Mino"]
|
|
|
|
|
|
def test_history_partitioned_table_metadata():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
history = dt.history()
|
|
commit_info = history[0]
|
|
|
|
assert len(history) == 1
|
|
assert commit_info == {
|
|
"timestamp": 1615555646188,
|
|
"operation": "WRITE",
|
|
"operationParameters": {
|
|
"mode": "ErrorIfExists",
|
|
"partitionBy": '["year","month","day"]',
|
|
},
|
|
"isBlindAppend": True,
|
|
"operationMetrics": {
|
|
"numFiles": "6",
|
|
"numOutputBytes": "2477",
|
|
"numOutputRows": "7",
|
|
},
|
|
"version": 0,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("flatten", [True, False])
|
|
@pytest.mark.pyarrow
|
|
def test_add_actions_table(flatten: bool):
|
|
import pyarrow as pa
|
|
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
actions_df = dt.get_add_actions(flatten)
|
|
# RecordBatch doesn't have a sort_by method yet
|
|
actions_df = pa.table(actions_df).sort_by("path").to_batches()[0]
|
|
|
|
assert actions_df.num_rows == 6
|
|
assert actions_df["path"] == pa.array(
|
|
[
|
|
"year=2020/month=1/day=1/part-00000-8eafa330-3be9-4a39-ad78-fd13c2027c7e.c000.snappy.parquet",
|
|
"year=2020/month=2/day=3/part-00000-94d16827-f2fd-42cd-a060-f67ccc63ced9.c000.snappy.parquet",
|
|
"year=2020/month=2/day=5/part-00000-89cdd4c8-2af7-4add-8ea3-3990b2f027b5.c000.snappy.parquet",
|
|
"year=2021/month=12/day=20/part-00000-9275fdf4-3961-4184-baa0-1c8a2bb98104.c000.snappy.parquet",
|
|
"year=2021/month=12/day=4/part-00000-6dc763c0-3e8b-4d52-b19e-1f92af3fbb25.c000.snappy.parquet",
|
|
"year=2021/month=4/day=5/part-00000-c5856301-3439-4032-a6fc-22b7bc92bebb.c000.snappy.parquet",
|
|
]
|
|
)
|
|
assert actions_df["size_bytes"] == pa.array([414, 414, 414, 407, 414, 414])
|
|
assert actions_df["modification_time"] == pa.array(
|
|
[1615555646000] * 6, type=pa.int64()
|
|
)
|
|
|
|
if flatten:
|
|
partition_year = actions_df["partition.year"]
|
|
partition_month = actions_df["partition.month"]
|
|
partition_day = actions_df["partition.day"]
|
|
else:
|
|
partition_year = actions_df["partition"].field("year")
|
|
partition_month = actions_df["partition"].field("month")
|
|
partition_day = actions_df["partition"].field("day")
|
|
|
|
assert partition_year == pa.array(["2020"] * 3 + ["2021"] * 3)
|
|
assert partition_month == pa.array(["1", "2", "2", "12", "12", "4"])
|
|
assert partition_day == pa.array(["1", "3", "5", "20", "4", "5"])
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_get_add_actions_on_empty_table(tmp_path: Path):
|
|
import pyarrow as pa
|
|
|
|
data = pa.table({"value": pa.array([1, 2, 3], type=pa.int64())})
|
|
write_deltalake(tmp_path, data)
|
|
dt = DeltaTable(tmp_path)
|
|
|
|
# Sanity check to ensure table starts with files.
|
|
initial_adds = dt.get_add_actions(flatten=True)
|
|
assert initial_adds.num_rows == 1
|
|
assert len(initial_adds["path"]) == 1
|
|
|
|
dt.delete()
|
|
dt.vacuum(retention_hours=0, dry_run=False, enforce_retention_duration=False)
|
|
|
|
dt = DeltaTable(tmp_path)
|
|
add_actions = dt.get_add_actions()
|
|
assert add_actions.num_rows == 0
|
|
assert dt.get_add_actions(flatten=True).num_rows == 0
|
|
|
|
|
|
def assert_correct_files(dt: DeltaTable, partition_filters, expected_paths):
|
|
from urllib.parse import urlparse
|
|
|
|
table_path = urlparse(dt.table_uri).path
|
|
absolute_paths = [os.path.join(table_path, path) for path in expected_paths]
|
|
assert dt.file_uris(partition_filters) == absolute_paths
|
|
|
|
|
|
def test_get_files_partitioned_table():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
table_path = (
|
|
Path.cwd().parent / "crates/test/tests/data/delta-0.8.0-partitioned"
|
|
).as_posix()
|
|
|
|
partition_filters = [("day", "=", "3")]
|
|
paths = [
|
|
"year=2020/month=2/day=3/part-00000-94d16827-f2fd-42cd-a060-f67ccc63ced9.c000.snappy.parquet"
|
|
]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
# Also accepts integers
|
|
partition_filters = [("day", "=", 3)]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
partition_filters = [("day", "!=", "3")]
|
|
paths = [
|
|
"year=2020/month=1/day=1/part-00000-8eafa330-3be9-4a39-ad78-fd13c2027c7e.c000.snappy.parquet",
|
|
"year=2020/month=2/day=5/part-00000-89cdd4c8-2af7-4add-8ea3-3990b2f027b5.c000.snappy.parquet",
|
|
"year=2021/month=12/day=20/part-00000-9275fdf4-3961-4184-baa0-1c8a2bb98104.c000.snappy.parquet",
|
|
"year=2021/month=12/day=4/part-00000-6dc763c0-3e8b-4d52-b19e-1f92af3fbb25.c000.snappy.parquet",
|
|
"year=2021/month=4/day=5/part-00000-c5856301-3439-4032-a6fc-22b7bc92bebb.c000.snappy.parquet",
|
|
]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
partition_filters = [("day", "in", ["3", "20"])]
|
|
paths = [
|
|
"year=2020/month=2/day=3/part-00000-94d16827-f2fd-42cd-a060-f67ccc63ced9.c000.snappy.parquet",
|
|
"year=2021/month=12/day=20/part-00000-9275fdf4-3961-4184-baa0-1c8a2bb98104.c000.snappy.parquet",
|
|
]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
partition_filters = [("day", "not in", ["3", "20"])]
|
|
paths = [
|
|
"year=2020/month=1/day=1/part-00000-8eafa330-3be9-4a39-ad78-fd13c2027c7e.c000.snappy.parquet",
|
|
"year=2020/month=2/day=5/part-00000-89cdd4c8-2af7-4add-8ea3-3990b2f027b5.c000.snappy.parquet",
|
|
"year=2021/month=12/day=4/part-00000-6dc763c0-3e8b-4d52-b19e-1f92af3fbb25.c000.snappy.parquet",
|
|
"year=2021/month=4/day=5/part-00000-c5856301-3439-4032-a6fc-22b7bc92bebb.c000.snappy.parquet",
|
|
]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
partition_filters = [("day", "not in", ["3", "20"]), ("year", "=", "2021")]
|
|
paths = [
|
|
"year=2021/month=12/day=4/part-00000-6dc763c0-3e8b-4d52-b19e-1f92af3fbb25.c000.snappy.parquet",
|
|
"year=2021/month=4/day=5/part-00000-c5856301-3439-4032-a6fc-22b7bc92bebb.c000.snappy.parquet",
|
|
]
|
|
assert_correct_files(dt, partition_filters, paths)
|
|
|
|
partition_filters = [("invalid_operation", "=>", "3")]
|
|
with pytest.raises(Exception) as exception:
|
|
dt.file_uris(partition_filters)
|
|
assert (
|
|
str(exception.value)
|
|
== 'Invalid partition filter found: ("invalid_operation", "=>", "3").'
|
|
)
|
|
|
|
partition_filters = [("invalid_operation", "=", ["3", "20"])]
|
|
with pytest.raises(Exception) as exception:
|
|
dt.file_uris(partition_filters)
|
|
assert (
|
|
str(exception.value)
|
|
== 'Invalid partition filter found: ("invalid_operation", "=", ["3", "20"]).'
|
|
)
|
|
|
|
partition_filters = [("unknown", "=", "3")]
|
|
with pytest.raises(Exception) as exception:
|
|
dt.file_uris(partition_filters)
|
|
assert (
|
|
str(exception.value)
|
|
== "Data does not match the schema or partitions of the table: Field 'unknown' is not a root table field."
|
|
)
|
|
|
|
|
|
@pytest.mark.pandas
|
|
@pytest.mark.pyarrow
|
|
def test_delta_table_to_pandas():
|
|
import pandas as pd
|
|
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
assert dt.to_pandas().equals(pd.DataFrame({"id": [5, 7, 9]}))
|
|
|
|
|
|
@pytest.mark.pandas
|
|
@pytest.mark.pyarrow
|
|
def test_delta_table_with_filesystem():
|
|
import pandas as pd
|
|
from pyarrow.fs import LocalFileSystem, SubTreeFileSystem
|
|
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
filesystem = SubTreeFileSystem(table_path, LocalFileSystem())
|
|
assert dt.to_pandas(filesystem=filesystem).equals(pd.DataFrame({"id": [5, 7, 9]}))
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
@pytest.mark.pandas
|
|
def test_delta_table_with_filters():
|
|
import pyarrow.dataset as ds
|
|
|
|
table_path = "../crates/test/tests/data/COVID-19_NYT"
|
|
dt = DeltaTable(table_path)
|
|
dataset = dt.to_pyarrow_dataset()
|
|
|
|
filter_expr = ds.field("date") > "2021-02-20"
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert (
|
|
len(dt.to_pandas(filters=[("date", ">", "2021-02-20")]))
|
|
== len(dt.to_pandas(filters=filter_expr))
|
|
== data.num_rows
|
|
)
|
|
|
|
filter_expr = (ds.field("date") > "2021-02-20") | (
|
|
ds.field("state").isin(["Alabama", "Wyoming"])
|
|
)
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert (
|
|
len(
|
|
dt.to_pandas(
|
|
filters=[
|
|
[("date", ">", "2021-02-20")],
|
|
[("state", "in", ["Alabama", "Wyoming"])],
|
|
]
|
|
)
|
|
)
|
|
== len(dt.to_pandas(filters=filter_expr))
|
|
== data.num_rows
|
|
)
|
|
|
|
filter_expr = (ds.field("date") > "2021-02-20") & (
|
|
ds.field("state").isin(["Alabama", "Wyoming"])
|
|
)
|
|
data = dataset.to_table(filter=filter_expr)
|
|
assert (
|
|
len(
|
|
dt.to_pandas(
|
|
filters=[
|
|
("date", ">", "2021-02-20"),
|
|
("state", "in", ["Alabama", "Wyoming"]),
|
|
]
|
|
)
|
|
)
|
|
== len(dt.to_pandas(filters=filter_expr))
|
|
== data.num_rows
|
|
)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_writer_fails_on_protocol():
|
|
import pytest
|
|
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
dt.protocol = Mock(return_value=ProtocolVersions(2, 1, None, None))
|
|
with pytest.raises(DeltaProtocolError):
|
|
dt.to_pyarrow_dataset()
|
|
with pytest.raises(DeltaProtocolError):
|
|
dt.to_pyarrow_table()
|
|
with pytest.raises(DeltaProtocolError):
|
|
dt.to_pandas()
|
|
|
|
|
|
class ExcPassThroughThread(Thread):
|
|
"""Wrapper around `threading.Thread` that propagates exceptions."""
|
|
|
|
def __init__(self, target, *args):
|
|
Thread.__init__(self, target=target, *args)
|
|
self.exc = None
|
|
|
|
def run(self):
|
|
"""Method representing the thread's activity.
|
|
You may override this method in a subclass. The standard run() method
|
|
invokes the callable object passed to the object's constructor as the
|
|
target argument, if any, with sequential and keyword arguments taken
|
|
from the args and kwargs arguments, respectively.
|
|
"""
|
|
try:
|
|
Thread.run(self)
|
|
except BaseException as e:
|
|
self.exc = e
|
|
|
|
def join(self, timeout=None):
|
|
"""Wait until the thread terminates.
|
|
This blocks the calling thread until the thread whose join() method is
|
|
called terminates -- either normally or through an unhandled exception
|
|
or until the optional timeout occurs.
|
|
When the timeout argument is present and not None, it should be a
|
|
floating point number specifying a timeout for the operation in seconds
|
|
(or fractions thereof). As join() always returns None, you must call
|
|
is_alive() after join() to decide whether a timeout happened -- if the
|
|
thread is still alive, the join() call timed out.
|
|
When the timeout argument is not present or None, the operation will
|
|
block until the thread terminates.
|
|
A thread can be join()ed many times.
|
|
join() raises a RuntimeError if an attempt is made to join the current
|
|
thread as that would cause a deadlock. It is also an error to join() a
|
|
thread before it has been started and attempts to do so raises the same
|
|
exception.
|
|
"""
|
|
super().join(timeout)
|
|
if self.exc:
|
|
raise self.exc
|
|
|
|
|
|
@pytest.mark.s3
|
|
@pytest.mark.integration
|
|
@pytest.mark.timeout(timeout=5, method="thread")
|
|
def test_read_multiple_tables_from_s3(s3_localstack):
|
|
"""Should be able to create multiple cloud storage based DeltaTable instances
|
|
without blocking on async crates/test function calls.
|
|
"""
|
|
for path in ["s3://deltars/simple", "s3://deltars/simple"]:
|
|
t = DeltaTable(path)
|
|
assert t.file_uris() == [
|
|
"s3://deltars/simple/part-00000-2befed33-c358-4768-a43c-3eda0d2a499d-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00000-c1777d7d-89d9-4790-b38a-6ee7e24456b1-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00001-7891c33d-cedc-47c3-88a6-abcfb049d3b4-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00004-315835fe-fb44-4562-98f6-5e6cfa3ae45d-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00007-3a0e4727-de0d-41b6-81ef-5223cf40f025-c000.snappy.parquet",
|
|
]
|
|
|
|
|
|
@pytest.mark.s3
|
|
@pytest.mark.integration
|
|
@pytest.mark.timeout(timeout=10, method="thread")
|
|
def test_read_multiple_tables_from_s3_multi_threaded(s3_localstack):
|
|
thread_count = 10
|
|
b = Barrier(thread_count, timeout=5)
|
|
|
|
# make sure it works within multiple threads as well
|
|
def read_table():
|
|
b.wait()
|
|
t = DeltaTable("s3://deltars/simple")
|
|
assert t.file_uris() == [
|
|
"s3://deltars/simple/part-00000-2befed33-c358-4768-a43c-3eda0d2a499d-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00000-c1777d7d-89d9-4790-b38a-6ee7e24456b1-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00001-7891c33d-cedc-47c3-88a6-abcfb049d3b4-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00004-315835fe-fb44-4562-98f6-5e6cfa3ae45d-c000.snappy.parquet",
|
|
"s3://deltars/simple/part-00007-3a0e4727-de0d-41b6-81ef-5223cf40f025-c000.snappy.parquet",
|
|
]
|
|
|
|
threads = [ExcPassThroughThread(target=read_table) for _ in range(thread_count)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
|
|
def assert_num_fragments(table, predicate, count):
|
|
frags = table.to_pyarrow_dataset().get_fragments(filter=predicate)
|
|
assert len(list(frags)) == count
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_filter_nulls(tmp_path: Path):
|
|
import pyarrow as pa
|
|
import pyarrow.dataset as ds
|
|
|
|
def assert_scan_equals(table, predicate, expected):
|
|
data = table.to_pyarrow_dataset().to_table(filter=predicate).sort_by("part")
|
|
assert data == expected
|
|
|
|
# 1 all-valid part, 1 all-null part, and 1 mixed part.
|
|
data = pa.table(
|
|
{"part": ["a", "a", "b", "b", "c", "c"], "value": [1, 1, None, None, 2, None]}
|
|
)
|
|
|
|
write_deltalake(tmp_path, data, partition_by="part")
|
|
|
|
table = DeltaTable(tmp_path)
|
|
|
|
# Note: we assert number of fragments returned because that verifies
|
|
# that file skipping is working properly.
|
|
|
|
# is valid predicate
|
|
predicate = ds.field("value").is_valid()
|
|
assert_num_fragments(table, predicate, 2)
|
|
expected = pa.table({"part": ["a", "a", "c"], "value": [1, 1, 2]})
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
# is null predicate
|
|
predicate = ds.field("value").is_null()
|
|
assert_num_fragments(table, predicate, 2)
|
|
expected = pa.table(
|
|
{"part": ["b", "b", "c"], "value": pa.array([None, None, None], pa.int64())}
|
|
)
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
# inequality predicate
|
|
predicate = ds.field("value") > 1
|
|
assert_num_fragments(table, predicate, 1)
|
|
expected = pa.table({"part": ["c"], "value": pa.array([2], pa.int64())})
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
# also test nulls in partition values
|
|
data = pa.table({"part": pa.array([None], pa.string()), "value": [3]})
|
|
write_deltalake(
|
|
table,
|
|
data,
|
|
mode="append",
|
|
partition_by="part",
|
|
)
|
|
|
|
# null predicate
|
|
predicate = ds.field("part").is_null()
|
|
assert_num_fragments(table, predicate, 1)
|
|
expected = pa.table({"part": pa.array([None], pa.string()), "value": [3]})
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
# valid predicate
|
|
predicate = ds.field("part").is_valid()
|
|
assert_num_fragments(table, predicate, 3)
|
|
expected = pa.table(
|
|
{"part": ["a", "a", "b", "b", "c", "c"], "value": [1, 1, None, None, 2, None]}
|
|
)
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
# inequality predicate
|
|
predicate = ds.field("part") < "c"
|
|
assert_num_fragments(table, predicate, 2)
|
|
expected = pa.table({"part": ["a", "a", "b", "b"], "value": [1, 1, None, None]})
|
|
assert_scan_equals(table, predicate, expected)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_issue_1653_filter_bool_partition(tmp_path: Path):
|
|
import pyarrow as pa
|
|
|
|
ta = pa.Table.from_pydict(
|
|
{
|
|
"bool_col": [True, False, True, False],
|
|
"int_col": [0, 1, 2, 3],
|
|
"str_col": ["a", "b", "c", "d"],
|
|
}
|
|
)
|
|
write_deltalake(
|
|
tmp_path, ta, partition_by=["bool_col", "int_col"], mode="overwrite"
|
|
)
|
|
dt = DeltaTable(tmp_path)
|
|
|
|
assert (
|
|
dt.to_pyarrow_table(
|
|
filters=[
|
|
("int_col", "=", 0),
|
|
("bool_col", "=", True),
|
|
]
|
|
).num_rows
|
|
== 1
|
|
)
|
|
assert (
|
|
len(
|
|
dt.file_uris(
|
|
partition_filters=[
|
|
("int_col", "=", 0),
|
|
("bool_col", "=", "true"),
|
|
]
|
|
)
|
|
)
|
|
== 1
|
|
)
|
|
assert (
|
|
len(
|
|
dt.file_uris(
|
|
partition_filters=[
|
|
("int_col", "=", 0),
|
|
("bool_col", "=", True),
|
|
]
|
|
)
|
|
)
|
|
== 1
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_value, expected",
|
|
[
|
|
(True, "true"),
|
|
(False, "false"),
|
|
(1, "1"),
|
|
(1.5, "1.5"),
|
|
("string", "string"),
|
|
(date(2023, 10, 17), "2023-10-17"),
|
|
(datetime(2023, 10, 17, 12, 34, 56), "2023-10-17 12:34:56"),
|
|
(b"bytes", "bytes"),
|
|
([True, False], ["true", "false"]),
|
|
([1, 2], ["1", "2"]),
|
|
([1.5, 2.5], ["1.5", "2.5"]),
|
|
(["a", "b"], ["a", "b"]),
|
|
([date(2023, 10, 17), date(2023, 10, 18)], ["2023-10-17", "2023-10-18"]),
|
|
(
|
|
[datetime(2023, 10, 17, 12, 34, 56), datetime(2023, 10, 18, 12, 34, 56)],
|
|
["2023-10-17 12:34:56", "2023-10-18 12:34:56"],
|
|
),
|
|
([b"bytes", b"testbytes"], ["bytes", "testbytes"]),
|
|
],
|
|
)
|
|
def test_encode_partition_value(input_value: Any, expected: str) -> None:
|
|
if isinstance(input_value, list):
|
|
assert [encode_partition_value(val) for val in input_value] == expected
|
|
else:
|
|
assert encode_partition_value(input_value) == expected
|
|
|
|
|
|
def test_partitions_partitioned_table():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
expected = [
|
|
{"year": "2020", "month": "2", "day": "5"},
|
|
{"year": "2021", "month": "12", "day": "4"},
|
|
{"year": "2020", "month": "2", "day": "3"},
|
|
{"year": "2021", "month": "4", "day": "5"},
|
|
{"year": "2020", "month": "1", "day": "1"},
|
|
{"year": "2021", "month": "12", "day": "20"},
|
|
]
|
|
actual = dt.partitions()
|
|
for partition in expected:
|
|
assert partition in actual
|
|
|
|
|
|
def test_partitions_filtering_partitioned_table():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
expected = [
|
|
{"day": "5", "month": "4", "year": "2021"},
|
|
{"day": "20", "month": "12", "year": "2021"},
|
|
{"day": "4", "month": "12", "year": "2021"},
|
|
]
|
|
|
|
partition_filters = [("year", ">=", "2021")]
|
|
actual = dt.partitions(partition_filters=partition_filters)
|
|
assert len(expected) == len(actual)
|
|
for partition in expected:
|
|
partition in actual
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_partitions_date_partitioned_table(tmp_path: Path):
|
|
import pyarrow as pa
|
|
|
|
table_path = tmp_path / "date_partition_table"
|
|
date_partitions = [
|
|
date(2024, 8, 1),
|
|
date(2024, 8, 2),
|
|
date(2024, 8, 3),
|
|
date(2024, 8, 4),
|
|
]
|
|
sample_data_pyarrow = pa.table(
|
|
{
|
|
"date_field": pa.array(date_partitions, pa.date32()),
|
|
"numeric_data": pa.array([1, 2, 3, 4], pa.int64()),
|
|
}
|
|
)
|
|
write_deltalake(
|
|
table_path, sample_data_pyarrow, mode="overwrite", partition_by=["date_field"]
|
|
)
|
|
|
|
delta_table = DeltaTable(table_path)
|
|
expected = [
|
|
{"date_field": "2024-08-01"},
|
|
{"date_field": "2024-08-02"},
|
|
{"date_field": "2024-08-03"},
|
|
{"date_field": "2024-08-04"},
|
|
]
|
|
actual = sorted(delta_table.partitions(), key=lambda x: x["date_field"])
|
|
assert expected == actual
|
|
|
|
|
|
def test_partitions_special_partitioned_table():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-special-partition"
|
|
dt = DeltaTable(table_path)
|
|
|
|
expected = [{"x": "A/A"}, {"x": "B B"}]
|
|
actual = dt.partitions()
|
|
for partition in expected:
|
|
partition in actual
|
|
|
|
|
|
def test_partitions_unpartitioned_table():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
assert len(dt.partitions()) == 0
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires upstream fix in delta-kernel")
|
|
def test_read_table_last_checkpoint_not_updated():
|
|
dt = DeltaTable("../crates/test/tests/data/table_failed_last_checkpoint_update")
|
|
|
|
assert dt.version() == 3
|
|
|
|
|
|
def test_is_deltatable_valid_path():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
assert DeltaTable.is_deltatable(table_path)
|
|
|
|
|
|
def test_is_deltatable_empty_path(tmp_path: Path):
|
|
not_delta_path = tmp_path / "not_delta_table"
|
|
# Ensure path exists
|
|
not_delta_path.mkdir()
|
|
assert not DeltaTable.is_deltatable(str(not_delta_path))
|
|
|
|
|
|
def test_is_deltatable_invalid_path(tmp_path: Path):
|
|
not_existing_path = tmp_path / "not_existing_path"
|
|
assert not DeltaTable.is_deltatable(str(not_existing_path))
|
|
|
|
|
|
def test_is_deltatable_does_not_create_path(tmp_path: Path):
|
|
not_existing_path = tmp_path / "not_existing_path"
|
|
assert not DeltaTable.is_deltatable(str(not_existing_path))
|
|
assert not not_existing_path.exists()
|
|
|
|
|
|
def test_is_deltatable_with_storage_opts():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
storage_options = {
|
|
"AWS_ACCESS_KEY_ID": "THE_AWS_ACCESS_KEY_ID",
|
|
"AWS_SECRET_ACCESS_KEY": "THE_AWS_SECRET_ACCESS_KEY",
|
|
"AWS_ALLOW_HTTP": "true",
|
|
"AWS_S3_ALLOW_UNSAFE_RENAME": "true",
|
|
"AWS_S3_LOCKING_PROVIDER": "dynamodb",
|
|
"DELTA_DYNAMO_TABLE_NAME": "custom_table_name",
|
|
}
|
|
assert DeltaTable.is_deltatable(table_path, storage_options=storage_options)
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
@pytest.mark.xfail(
|
|
reason="Issue: columns are encoded as dictionary due to TableProvider"
|
|
)
|
|
def test_read_query_builder():
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-partitioned"
|
|
dt = DeltaTable(table_path)
|
|
|
|
expected = Table(
|
|
{
|
|
"value": Array(
|
|
["4", "5", "6", "7"],
|
|
ArrowField("value", type=DataType.string(), nullable=True),
|
|
),
|
|
"year": Array(
|
|
["2021", "2021", "2021", "2021"],
|
|
ArrowField("year", type=DataType.string(), nullable=True),
|
|
),
|
|
"month": Array(
|
|
["4", "12", "12", "12"],
|
|
ArrowField("month", type=DataType.string(), nullable=True),
|
|
),
|
|
"day": Array(
|
|
["5", "4", "20", "20"],
|
|
ArrowField("day", type=DataType.string(), nullable=True),
|
|
),
|
|
}
|
|
)
|
|
|
|
qb = QueryBuilder().register("tbl", dt)
|
|
query = "SELECT * FROM tbl WHERE year >= 2021 ORDER BY value"
|
|
|
|
actual = qb.execute(query).read_all()
|
|
assert expected == actual
|
|
|
|
|
|
@pytest.mark.pyarrow
|
|
def test_read_query_builder_join_multiple_tables(tmp_path):
|
|
table_path = "../crates/test/tests/data/delta-0.8.0-date"
|
|
dt1 = DeltaTable(table_path)
|
|
|
|
write_deltalake(
|
|
tmp_path,
|
|
data=Table(
|
|
{
|
|
"date": Array(
|
|
["2021-01-01", "2021-01-02", "2021-01-03", "2021-12-31"],
|
|
ArrowField("date", type=DataType.string(), nullable=True),
|
|
),
|
|
"value": Array(
|
|
["a", "b", "c", "d"],
|
|
ArrowField("value", type=DataType.string(), nullable=True),
|
|
),
|
|
}
|
|
),
|
|
)
|
|
dt2 = DeltaTable(tmp_path)
|
|
|
|
expected = Table(
|
|
{
|
|
"date": Array(
|
|
["2021-01-01", "2021-01-02", "2021-01-03"],
|
|
ArrowField("date", type=DataType.string_view(), nullable=True),
|
|
),
|
|
"dayOfYear": Array(
|
|
[1, 2, 3],
|
|
ArrowField("dayOfYear", type=DataType.int32(), nullable=True),
|
|
),
|
|
"value": Array(
|
|
["a", "b", "c"],
|
|
ArrowField("value", type=DataType.string_view(), nullable=True),
|
|
),
|
|
}
|
|
)
|
|
actual = (
|
|
QueryBuilder()
|
|
.register("tbl1", dt1)
|
|
.register("tbl2", dt2)
|
|
.execute(
|
|
"""
|
|
SELECT tbl2.date, tbl1.dayOfYear, tbl2.value
|
|
FROM tbl1
|
|
INNER JOIN tbl2 ON tbl1.date = tbl2.date
|
|
ORDER BY tbl1.date
|
|
"""
|
|
)
|
|
.read_all()
|
|
)
|
|
assert expected == actual
|
|
|
|
|
|
def test_deletion_vectors_api_smoke():
|
|
table_path = "../crates/test/tests/data/table-with-dv-small"
|
|
dt = DeltaTable(table_path)
|
|
expected_selection_vector = [
|
|
[False, True, True, True, True, True, True, True, True, False]
|
|
]
|
|
expected_suffix = (
|
|
"part-00000-fae5310a-a37d-4e51-827b-c3d5516560ca-c000.snappy.parquet"
|
|
)
|
|
expected_filepath = next(
|
|
Path(path).resolve().as_uri()
|
|
for path in dt.file_uris()
|
|
if path.endswith(expected_suffix)
|
|
)
|
|
assert expected_filepath.endswith(expected_suffix)
|
|
|
|
vectors = dt.deletion_vectors()
|
|
assert vectors.schema.names == ["filepath", "selection_vector"]
|
|
|
|
table = vectors.read_all()
|
|
assert table.num_rows == 1
|
|
assert table["filepath"].to_pylist() == [expected_filepath]
|
|
assert table["selection_vector"].to_pylist() == expected_selection_vector
|
|
|
|
table_second = dt.deletion_vectors().read_all()
|
|
assert table_second["filepath"].to_pylist() == [expected_filepath]
|
|
assert table_second["selection_vector"].to_pylist() == expected_selection_vector
|
|
|
|
|
|
def test_deletion_vectors_empty_table():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path)
|
|
|
|
vectors = dt.deletion_vectors()
|
|
assert vectors.schema.names == ["filepath", "selection_vector"]
|
|
assert vectors.read_all().num_rows == 0
|
|
|
|
|
|
def test_deletion_vectors_without_files_raises():
|
|
table_path = "../crates/test/tests/data/simple_table"
|
|
dt = DeltaTable(table_path, without_files=True)
|
|
|
|
with pytest.raises(Exception, match="without files"):
|
|
dt.deletion_vectors()
|
|
|
|
|
|
def test_read_deletion_vectors():
|
|
table_path = "../crates/test/tests/data/table-with-dv-small"
|
|
dt = DeltaTable(table_path)
|
|
assert QueryBuilder().register("tbl", dt).execute("select * from tbl").read_all()[
|
|
"value"
|
|
].to_pylist() == [1, 2, 3, 4, 5, 6, 7, 8]
|
|
|
|
|
|
def test_deletion_vectors_table_with_deletion_logs():
|
|
table_path = "../crates/test/tests/data/table_with_deletion_logs"
|
|
dt = DeltaTable(table_path)
|
|
|
|
vectors = dt.deletion_vectors().read_all()
|
|
assert vectors.num_rows > 0
|
|
|
|
add_actions = dt.get_add_actions(flatten=True)
|
|
table_root = Path(table_path).resolve()
|
|
add_paths = add_actions["path"].to_pylist()
|
|
add_num_records = add_actions["num_records"].to_pylist()
|
|
num_records_by_file_path: dict[str, int] = {}
|
|
for add_path, num_records in zip(add_paths, add_num_records, strict=True):
|
|
file_path = (table_root / add_path).resolve().as_posix()
|
|
assert file_path not in num_records_by_file_path
|
|
num_records_by_file_path[file_path] = num_records
|
|
|
|
found_deleted_row = False
|
|
known_file_suffix = (
|
|
"part-00000-cb251d5e-b665-437a-a9a7-fbfc5137c77d.c000.snappy.parquet"
|
|
)
|
|
known_file_mask = None
|
|
known_file_num_records = None
|
|
|
|
for filepath, mask in zip(
|
|
vectors["filepath"].to_pylist(),
|
|
vectors["selection_vector"].to_pylist(),
|
|
strict=True,
|
|
):
|
|
file_path = Path(urlparse(filepath).path).as_posix()
|
|
assert file_path in num_records_by_file_path
|
|
|
|
num_records = num_records_by_file_path[file_path]
|
|
filename = Path(file_path).name
|
|
assert len(mask) == num_records
|
|
|
|
if False in mask:
|
|
found_deleted_row = True
|
|
if filename == known_file_suffix:
|
|
known_file_mask = mask
|
|
known_file_num_records = num_records
|
|
|
|
assert found_deleted_row
|
|
assert known_file_mask is not None
|
|
assert known_file_num_records is not None
|
|
assert len(known_file_mask) == known_file_num_records
|
|
assert False in known_file_mask
|
|
assert known_file_mask[-1] is True
|
|
|
|
|
|
@pytest.mark.pandas
|
|
def test_nested_runtimes(tmp_path):
|
|
import pandas as pd
|
|
|
|
csv_path = tmp_path / "csv_data"
|
|
pd.DataFrame({"id": [1, 2, 3], "name": ["a", "b", "c"]}).to_csv(
|
|
csv_path, index=False
|
|
)
|
|
|
|
con = QueryBuilder()
|
|
con.execute(f"CREATE EXTERNAL TABLE raw_csv STORED AS CSV LOCATION '{csv_path}'")
|
|
df = con.execute("SELECT * FROM raw_csv")
|
|
write_deltalake(tmp_path / "delta", df, mode="overwrite")
|