|
| 1 | +# Copyright 2026 The PyAthena authors |
| 2 | +# |
| 3 | +# Licensed under the MIT License. |
| 4 | +# See LICENSE or https://opensource.org/licenses/MIT. |
| 5 | +# |
| 6 | +# SPDX-License-Identifier: MIT |
| 7 | + |
| 8 | +from unittest.mock import PropertyMock, patch |
| 9 | + |
| 10 | +import polars as pl |
| 11 | +import pytest |
| 12 | + |
| 13 | +from pyathena.error import OperationalError |
| 14 | +from pyathena.polars.result_set import AthenaPolarsResultSet |
| 15 | + |
| 16 | +_ROWS_BEFORE_FAILURE = 300_000 |
| 17 | + |
| 18 | + |
| 19 | +def _chunked_result_set() -> AthenaPolarsResultSet: |
| 20 | + """Create a chunked result set without calling Athena. |
| 21 | +
|
| 22 | + Returns: |
| 23 | + An AthenaPolarsResultSet with only the attributes used by the chunk readers. |
| 24 | + """ |
| 25 | + result_set = AthenaPolarsResultSet.__new__(AthenaPolarsResultSet) # bypass __init__ |
| 26 | + result_set._chunksize = 10_000 |
| 27 | + result_set._kwargs = {} |
| 28 | + return result_set |
| 29 | + |
| 30 | + |
| 31 | +class TestAthenaPolarsResultSet: |
| 32 | + def test_iter_csv_chunks_raises_when_read_fails_partway(self, tmp_path): |
| 33 | + """A CSV read that fails partway through the data raises instead of ending early.""" |
| 34 | + path = tmp_path / "result.csv" |
| 35 | + path.write_text( |
| 36 | + "a\n" + "".join(f"{i}\n" for i in range(_ROWS_BEFORE_FAILURE)) + "not-a-number\n" |
| 37 | + ) |
| 38 | + result_set = _chunked_result_set() |
| 39 | + with ( |
| 40 | + patch.object( |
| 41 | + AthenaPolarsResultSet, |
| 42 | + "output_location", |
| 43 | + new_callable=PropertyMock, |
| 44 | + return_value=str(path), |
| 45 | + ), |
| 46 | + patch.object( |
| 47 | + AthenaPolarsResultSet, |
| 48 | + "dtypes", |
| 49 | + new_callable=PropertyMock, |
| 50 | + return_value={"a": pl.Int64}, |
| 51 | + ), |
| 52 | + patch.object( |
| 53 | + AthenaPolarsResultSet, |
| 54 | + "_parquet_storage_options", |
| 55 | + new_callable=PropertyMock, |
| 56 | + return_value={}, |
| 57 | + ), |
| 58 | + patch.object(AthenaPolarsResultSet, "_is_csv_readable", return_value=True), |
| 59 | + pytest.raises(OperationalError, match="not-a-number"), |
| 60 | + ): |
| 61 | + list(result_set._iter_csv_chunks()) |
| 62 | + |
| 63 | + def test_iter_parquet_chunks_raises_when_read_fails_partway(self, tmp_path): |
| 64 | + """A Parquet read that fails partway through the data raises instead of ending early.""" |
| 65 | + pl.DataFrame({"a": range(_ROWS_BEFORE_FAILURE)}).write_parquet( |
| 66 | + tmp_path / "0.parquet", row_group_size=10_000 |
| 67 | + ) |
| 68 | + valid = (tmp_path / "0.parquet").read_bytes() |
| 69 | + # Keep the footer magic but corrupt the metadata so the second file fails to read. |
| 70 | + (tmp_path / "1.parquet").write_bytes(valid[: len(valid) // 2] + b"\x00" * 64 + valid[-8:]) |
| 71 | + result_set = _chunked_result_set() |
| 72 | + result_set._unload_location = f"{tmp_path}/" |
| 73 | + with ( |
| 74 | + patch.object( |
| 75 | + AthenaPolarsResultSet, |
| 76 | + "_parquet_storage_options", |
| 77 | + new_callable=PropertyMock, |
| 78 | + return_value={}, |
| 79 | + ), |
| 80 | + patch.object(AthenaPolarsResultSet, "_prepare_parquet_location", return_value=True), |
| 81 | + pytest.raises(OperationalError), |
| 82 | + ): |
| 83 | + list(result_set._iter_parquet_chunks()) |
0 commit comments