Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/polars.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ df = cursor.execute("SELECT * FROM many_rows").as_polars()
They apply to the reads through PyAthena's filesystem: CSV results read without the chunksize option, which use `"bytes"` and 5 MiB blocks by default, and the Parquet files of UNLOAD results read with the chunksize option when pyarrow is installed and `execute()` is given no Polars read options.
Of the latter, a file of up to 32 MiB is read at once, and a larger one is read with the given `cache_type`, or without it with the `"none"` cache, which requests only the ranges that pyarrow reads.
With `storage_options` given to `execute()`, the given options replace PyAthena's S3 settings, and `block_size` and `cache_type` do not apply.
For a CSV result read without the chunksize option, the options are passed to `fsspec.open()`, which uses PyAthena's S3FileSystem; for the other reads, they are passed to the Polars read function, which uses its native S3 access by default and fsspec with `use_pyarrow=True`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author verification and repair (finding 2): confirmed and fixed in a473e9c.

In both Polars 1.44.2 and 2.0.0, read_parquet(use_pyarrow=True) calls _read_parquet_with_pyarrow(), which opens the source with prepare_file_arg(..., storage_options=...), that is, with fsspec. The added line now says the Polars read function uses its native S3 access by default and fsspec with use_pyarrow=True. just docs lint passes.


(async-polars-cursor)=

Expand Down
17 changes: 12 additions & 5 deletions pyathena/polars/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
cast,
)

import fsspec

from pyathena import OperationalError
from pyathena.arrow.reader import ParquetChunkReader
from pyathena.converter import Converter
Expand Down Expand Up @@ -346,10 +348,10 @@ def _read_kwargs(

@property
def _csv_storage_options(self) -> dict[str, Any]:
"""Get storage options for Polars CSV reading via fsspec.
"""Get storage options for reading a CSV result file via fsspec.

Polars read_csv uses fsspec for cloud storage access, which works
with PyAthena's registered S3FileSystem.
``_read_csv()`` opens the file with ``fsspec.open()``, which uses
PyAthena's registered S3FileSystem, and passes its contents to Polars.

Returns:
Dictionary with fsspec-compatible options for S3 access.
Expand Down Expand Up @@ -592,17 +594,22 @@ def _read_csv(self) -> pl.DataFrame:
if "schema_overrides" not in self._kwargs:
# Renamed after reading, so that Polars matches the types to the header.
kwargs.pop("new_columns", None)
# Polars 2.0 reads a path with its native object store, which does not take
# fsspec options, so the result file is read through fsspec here.
storage_options = kwargs.pop("storage_options")
source: str | bytes
if self.output_location:
source = location = self.output_location
location = self.output_location
else:
source = self._fetch_all_rows_as_csv()
if not source:
return pl.DataFrame()
location = "the GetQueryResults rows"
del kwargs["storage_options"]

try:
if self.output_location:
with fsspec.open(self.output_location, "rb", **(storage_options or {})) as file:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review round one (implementation behavior): CLEAN

Base 6ea5587dde5aae5a5066a91da1f1231803d471e0, head 0c6a0b2de52fe1c3d8d03b79f01b2b3bf52c1ab9.

Covered: _read_csv() (S3 and GetQueryResults sources, timestamp re-read, new_columns renaming, error wrapping), _csv_storage_options, the chunked CSV and Parquet paths (unchanged, already native object store), the four changed tests, and the dev-group Polars markers and lock fork.

  • _read_kwargs() always sets storage_options, so the unconditional pop() is safe; a None given to execute() opens with no options, as Polars 1.x did (storage_options or {}).
  • The S3 read is now inside the try, so S3 errors are still logged as Failed to read <location> and raised as OperationalError, as they were when Polars opened the file.
  • Polars 1.44.2 read_csv() opened a remote path with fsspec.open(file, **storage_options) (polars/io/_utils.py prepare_file_arg) and read the whole binary file, so reading the bytes here keeps the same filesystem, options, and memory profile; the timestamp re-read now reuses the bytes instead of opening the object again.
  • AioPolarsCursor and AsyncPolarsCursor use the same result set.
  • Regression coverage: test_csv_storage_options_replace_defaults fails before the change on both Polars versions (Polars 2.0 rejects anon=True; on 1.x the patched fsspec.open is not called).
  • Validation: Polars suites (AWS) 147 passed on 3.14/Polars 2.0.0 and on 3.12/Polars 1.44.2; SQLAlchemy -k polars 6 passed on 3.14.

source = file.read()
try:
df = pl.read_csv(source, **kwargs)
except pl.exceptions.ComputeError:
Expand Down
14 changes: 10 additions & 4 deletions pyathena/polars/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,18 @@ def get_athena_type(dtype: Any) -> tuple[str, int, int]:
if base_dtype == polars_type or dtype == polars_type:
return athena_info

# Handle parameterized types that didn't match above
dtype_str = str(dtype).lower()
if "list" in dtype_str:
# Nested types are matched by their outer type, not by the types they contain.
if base_dtype == pl.List:
return ("array", 0, 0)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review of the repair (both perspectives): CLEAN

Range: 6ec3e0087590eec57e94b9a0c188038e7e45bb68..a473e9cd816c3b58d2d782080b49d90b2af3458c (same base 6ea5587d).

Round-one perspective (behavior): get_athena_type() is used only by pyathena.polars.util.to_column_info() for UNLOAD/Parquet metadata (result_set.py:674). Comparing the old and new functions on Polars 1.44.2 for Array, Duration, Categorical, Null, Decimal, and List gives identical results; only Struct containing a list (array → row) and Polars 2.0 Map (string → map) change. Live UNLOAD values for map/array columns are unchanged by the description change (2.0: {'a': 1}; 1.44.2: [{'key': 'a', 'value': 1}]). getattr(pl, "Map", None) keeps the 1.39–1.x floor importable, and mypy passes against both Polars versions.

Round-two perspective (claims): the commit message, PR release notes, and docs line were checked against the live check above and the read_parquet(use_pyarrow=True) source in both versions. The tests fail before the fix (see the reply above).

Validation: Polars suites 154 passed (3.14/2.0.0) and 153 passed + 1 skipped (3.12/1.44.2); just lint, just docs lint.

if "struct" in dtype_str:
if base_dtype == pl.Struct:
return ("row", 0, 0)
# Polars 2.0 reads a Parquet map as Map; Polars 1.x reads it as a list of structs.
map_dtype = getattr(pl, "Map", None)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review (relayed): FINDINGS

Reviewer: Codex CLI 0.162.1, model gpt-6-astra, model_reasoning_effort=high, sandbox read-only; session 01a13a7f-0062-7242-b5df-82faf4563415.
Base 6ea5587dde5aae5a5066a91da1f1231803d471e0, head 6ec3e0087590eec57e94b9a0c188038e7e45bb68, detached snapshot worktree (unchanged after the review).
Constraints: review only (no edits, builds, tests, or GitHub access); the prompt omitted the PR number, description, commit messages, and prior findings. It was a static review using installed Polars 1.44.2/2.0.0 sources and public sources; nothing was executed.

Reviewer output:

Covered:

  • S3 CSV reads, filesystem registration/settings, explicit storage_options including None, reader kwargs/encoding, timestamp retries, and error wrapping.
  • GetQueryResults fallback and forwarding from all three cursor variants.
  • Chunked CSV, native Parquet, PyArrow chunking, schema conversion, and fetch paths.
  • Changed tests, comments/docstrings, documentation, dependency markers, lock, and CI Python selection.
  • Installed Polars 1.44.2/2.0.0 sources and relevant public versioned sources, including the 1.39.0 batch API.

FINDINGS

  1. P2 — Polars 2.0 UNLOAD maps receive incorrect DB-API metadata.
    pyathena/polars/util.py:108

    With unload=True, a query such as SELECT MAP(ARRAY['a'], ARRAY[1]) AS m produces a native pl.Map under Polars 2.0. get_athena_type() has no map case, so Map(String, Int32) falls through to "string" with varchar precision. Both whole-result and chunked UNLOAD paths replace their metadata using this function, affecting all three cursor variants. Polars 1.44.2 represented Arrow maps as lists; Polars 2.0 introduces native map conversion.

    Fix: Recognize the top-level Map type, guarded for Polars 1.x, and return Athena "map" metadata. Add an explicit map-description assertion: the existing complex UNLOAD test only compares whole/chunked frames and misses this error.

  2. P3 — The added documentation overstates native S3 routing.
    docs/polars.md:400

    Non-chunked UNLOAD with execute(..., use_pyarrow=True, storage_options=...) reaches Polars’ PyArrow Parquet branch. That branch uses prepare_file_arg() and fsspec for S3 paths, rather than native object-store access. Consequently, the documented instruction can lead callers to supply native storage-option keys to an fsspec path.

    Fix: Qualify native routing as the default Polars reader behavior and identify the use_pyarrow=True exception.

The CSV fix itself appears sound. The new test meaningfully checks replacement options and real parsing and would fail against the old Polars 2.0 path. The adjusted naming tests accommodate verified upstream changes, though they remove explicit partial-renaming-with-schema coverage on Polars 1.x. The dependency split matches the stated CI intent.

Static review only; no tests, builds, edits, or GitHub writes performed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author verification and repair (finding 1): confirmed and fixed in a473e9c.

Live UNLOAD check of SELECT MAP(ARRAY['a'], ARRAY[1]) AS m: Polars 1.44.2 described m as array (List(Struct(key, value))); Polars 2.0.0 described it as string (Map(String, Int32)).
get_athena_type() now matches nested types by their outer type (List → array, Struct → row, Map → map when Polars has it). That also fixes an existing defect: the old substring match described Struct({'x': List(Int32)}) as array.
Polars 1.x maps stay array, since they are indistinguishable from a list of key-value structs. The value change (dict in 2.0, list of structs in 1.x) comes from Polars and is listed in the PR's release notes.
Tests: test_get_athena_type_nested, test_get_athena_type_map (skipped on 1.x), and test_nested_type_description_unload (whole and chunked UNLOAD) failed before the fix and pass after it. The Polars suites pass: 154 on 3.14/Polars 2.0.0; 153 plus 1 skipped on 3.12/Polars 1.44.2.

Deferred, with reasons: the reviewer noted that naming every column in the new_columns + schema_overrides tests drops partial-renaming coverage on Polars 1.x. Those options pass through to Polars unchanged, so partial renaming there is Polars' own behavior, and Polars 2.0 rejects it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent follow-up review (relayed): CLEAN

Reviewer: Codex CLI 0.162.1, model gpt-6-astra, model_reasoning_effort=high, sandbox read-only; session 01a13a8d-16c7-72e0-85b1-3449e37fe5eb.
Scope: repair 6ec3e0087590eec57e94b9a0c188038e7e45bb68..a473e9cd816c3b58d2d782080b49d90b2af3458c (base 6ea5587d unchanged), detached snapshot unchanged after the review. Same constraints as above; the prompt gave the two earlier findings and the affected contracts, without the PR description or author conclusions. Static review only.

Reviewer output:

Covered surfaces:

  • Exact repair diff 6ec3e008..a473e9cd; confirmed checkout HEAD.
  • get_athena_type() → to_column_info() → UNLOAD metadata and DB API description, including chunked and non-chunked paths for all three Polars cursor variants.
  • Installed Polars 1.44.2 and 2.0.0 datatype and S3 reader sources.
  • Added tests, CLAUDE.md conventions, and changed comments/documentation.

CLEAN

Both earlier findings are resolved within the reviewed scope.

  • Polars 2.0 Map now produces map, including maps containing lists or structs. Lists containing maps remain array.
  • Another description intentionally changes: structs containing lists—or field names containing "list"—now correctly produce row instead of array. Existing scalar and decimal mappings remain unchanged. CSV descriptions and other cursor families do not call this utility.
  • The guarded getattr(pl, "Map", None) handles Polars 1.x without accessing a missing attribute. Its list-of-structs map representation remains array. Map-specific test expressions execute only inside the skipped test body.
  • The tests meaningfully distinguish the repair from the previous code through map and struct-with-list cases. Standalone helper tests, declarative datatype parameters, and class-based integration tests using indirect cursor fixtures follow repository conventions. Integration coverage includes both chunking modes.
  • The documentation now accounts for fsspec in read_parquet(use_pyarrow=True). Chunked scan paths continue using native S3 access.

Static review only; no builds, tests, or AWS calls were run. Polars 1.39 compatibility was assessed from the guarded API usage, not execution against that exact version.

if map_dtype is not None and base_dtype == map_dtype:
return ("map", 0, 0)

# Handle parameterized types that didn't match above
dtype_str = str(dtype).lower()
if "decimal" in dtype_str:
# Extract precision and scale from Decimal type if available
if hasattr(dtype, "precision") and hasattr(dtype, "scale"):
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ dev = [
"pandas>=3.0.0",
"pyarrow>=22.0.0",
"polars>=1.39.0",
# Pull requests test the newest Python with Polars 2, and the older Pythons keep
# the newest Polars 1.x release.
"polars>=2.0.0; python_version >= '3.14'",
"polars<2.0.0; python_version < '3.14'",
"Jinja2>=3.1.0",
"mypy>=0.900",
"pytest>=3.5",
Expand Down
11 changes: 9 additions & 2 deletions tests/pyathena/arrow/test_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,13 @@ def test_complex_unload_as_polars(self, arrow_cursor):
).as_polars()
assert df.height == 1
assert df.width == 16
# Polars 2.0 converts an Arrow map to Map; Polars 1.x to a list of key-value structs.
if hasattr(pl, "Map"):
map_dtype = pl.Map(pl.Int32, pl.Int32)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI failure, repair, and self-review of the repair (both perspectives): CLEAN

Test run 38570714905 at a473e9cd816c3b58d2d782080b49d90b2af3458c (Python 3.14, Polars 2.0.0): test-sqla and test-sqla-async passed; test failed only TestArrowCursor::test_complex_unload_as_polars: At index 13 diff: Map(Int32, Int32) != List(Struct({'key': Int32, 'value': Int32})). The PR went back to Draft.

Cause, checked locally: ArrowCursor.as_polars() is pl.from_arrow(self._table). For an Arrow map<int32, int32>, Polars 2.0.0 returns Map(Int32, Int32) with {1: 2, 3: 4}, and Polars 1.44.2 returns List(Struct(key, value)) with [{'key': 1, 'value': 2}, ...]. This is Polars' conversion, so the test now states each major's dtype and value explicitly (repair a473e9cd..22de45412887431ca90c180b46b243abc5ef39c7, test-only).

Behavior perspective: PyAthena code is unchanged; no other test or doc describes the map representation of as_polars(). The full test job found no other failure on Polars 2.0.
Claims perspective: added a release-note item for the as_polars() map representation under Polars 2.0.
Validation: the test passes on 3.14/Polars 2.0.0 and 3.12/Polars 1.44.2; just lint passes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent follow-up review (relayed): CLEAN

Reviewer: Codex CLI 0.162.1, model gpt-6-astra, model_reasoning_effort=high, sandbox read-only; session 01a13a9d-20f1-74e2-8465-c0e7c368b874.
Scope: a473e9cd816c3b58d2d782080b49d90b2af3458c..22de45412887431ca90c180b46b243abc5ef39c7, detached snapshot unchanged after the review; the prompt gave the CI failure and the conversion path, without the PR description or author conclusions. Static review only.

Reviewer output:

Surfaces covered:

  • Frozen diff a473e9cd..22de4541 and the full test_complex_unload_as_polars assertions.
  • CLAUDE.md test conventions.
  • Installed Polars 1.44.2 and 2.0.0 sources.
  • Arrow conversion, Polars whole/chunked UNLOAD readers, metadata mapping, and related synchronous/asynchronous tests.
  • Repository-wide searches for map-as-list-of-structs assumptions.

CLEAN

The expectations at test_cursor.py:629 correctly distinguish Polars 1.x’s list-of-structs representation from Polars 2.0’s Map(Int32, Int32) and Python dictionary. Both branches specify explicit expected types and values independently of the returned data.

The change preserves the class-based integration test, indirect fixture parametrization, and existing assertions, consistent with CLAUDE.md.

No missed unconditional Polars 1.x map assumption was found. Related metadata handling already guards Map at util.py:112, and the corresponding integration expectation handles both versions at test_cursor.py:789.

Static review only; no tests, builds, or AWS calls were run. Installed-source checks covered 1.44.2 and 2.0.0, not every supported release.

map_value = {1: 2, 3: 4}
else:
map_dtype = pl.List(pl.Struct([pl.Field("key", pl.Int32), pl.Field("value", pl.Int32)]))
map_value = [{"key": 1, "value": 2}, {"key": 3, "value": 4}]
dtypes = tuple(df.dtypes)
assert dtypes == (
pl.Boolean,
Expand All @@ -641,7 +648,7 @@ def test_complex_unload_as_polars(self, arrow_cursor):
pl.Date,
pl.Binary,
pl.List(pl.Int32),
pl.List(pl.Struct([pl.Field("key", pl.Int32), pl.Field("value", pl.Int32)])),
map_dtype,
pl.Struct([pl.Field("a", pl.Int32), pl.Field("b", pl.Int32)]),
pl.Decimal(precision=10, scale=1),
)
Expand All @@ -661,7 +668,7 @@ def test_complex_unload_as_polars(self, arrow_cursor):
"col_date": datetime(2017, 1, 2).date(),
"col_binary": b"123",
"col_array": [1, 2],
"col_map": [{"key": 1, "value": 2}, {"key": 3, "value": 4}],
"col_map": map_value,
"col_struct": {"a": 1, "b": 2},
"col_decimal": Decimal("0.1"),
}
Expand Down
30 changes: 28 additions & 2 deletions tests/pyathena/polars/test_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,29 @@ def test_description_with_chunksize_unload(self, polars_cursor, query, expected)
assert description == ([("a", "integer"), ("b", "varchar")] if expected else [])
assert polars_cursor.fetchall() == expected

@pytest.mark.parametrize(
"polars_cursor",
[
{"cursor_kwargs": {"unload": True}},
{"cursor_kwargs": {"unload": True, "chunksize": 5}},
],
indirect=["polars_cursor"],
)
def test_nested_type_description_unload(self, polars_cursor):
"""UNLOAD results describe nested Parquet columns by their outer type."""
polars_cursor.execute(
"""
SELECT
ARRAY[1, 2] AS a
,CAST(ROW(ARRAY[1]) AS ROW(x ARRAY(INTEGER))) AS r
,MAP(ARRAY['k'], ARRAY[1]) AS m
"""
)
# Polars 1.x reads a Parquet map as a list of key-value structs.
map_type = "map" if hasattr(pl, "Map") else "array"
description = [d[:2] for d in polars_cursor.description]
assert description == [("a", "array"), ("r", "row"), ("m", map_type)]

@pytest.mark.parametrize(
"polars_cursor",
[
Expand Down Expand Up @@ -922,9 +945,12 @@ def test_new_columns_renaming_first_columns(self, polars_cursor, chunksize):
assert polars_cursor.fetchall() == [("001", 2)]

def test_new_columns_with_schema_overrides(self, polars_cursor):
"""schema_overrides given with new_columns are keyed by the new names, as in Polars."""
"""schema_overrides given with new_columns are keyed by the new names, as in Polars.

Polars 2.0 rejects new_columns that do not name every column of the file.
"""
polars_cursor.execute(
"SELECT '001' AS x, 2 AS y", new_columns=["z"], schema_overrides={"z": pl.String}
"SELECT '001' AS x, 2 AS y", new_columns=["z", "w"], schema_overrides={"z": pl.String}
)
assert polars_cursor.fetchall() == [("001", 2)]

Expand Down
40 changes: 35 additions & 5 deletions tests/pyathena/polars/test_result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ def test_csv_read_kwargs_replace_defaults(self, tmp_path, reader):
"""Read arguments given to execute() replace the ones the result set chooses."""
path = tmp_path / "result.csv"
path.write_text("1;x\n2;y\n")
# Polars names headerless columns from column_1 before 2.0 and from column_0 since.
first, second = pl.read_csv(b"1;x\n", separator=";", has_header=False).columns
result_set = _chunked_result_set()
result_set._kwargs = {
"separator": ";",
"has_header": False,
"schema_overrides": {"column_1": pl.Utf8},
"schema_overrides": {first: pl.Utf8},
}
with (
patch.object(
Expand Down Expand Up @@ -152,15 +154,16 @@ def test_csv_read_kwargs_replace_defaults(self, tmp_path, reader):
):
result = getattr(result_set, reader)()
df = result if isinstance(result, pl.DataFrame) else pl.concat(list(result))
assert df.to_dict(as_series=False) == {"column_1": ["1", "2"], "column_2": ["x", "y"]}
assert df.to_dict(as_series=False) == {first: ["1", "2"], second: ["x", "y"]}

@pytest.mark.parametrize("reader", ["_read_csv", "_iter_csv_chunks"])
def test_txt_new_columns_with_schema_overrides(self, tmp_path, reader):
"""schema_overrides given with new_columns reach Polars with them for a .txt file."""
path = tmp_path / "result.txt"
path.write_text("001\tx\n")
result_set = _chunked_result_set()
result_set._kwargs = {"new_columns": ["z"], "schema_overrides": {"z": pl.Utf8}}
# Polars 2.0 rejects new_columns that do not name every column of the file.
result_set._kwargs = {"new_columns": ["z", "w"], "schema_overrides": {"z": pl.Utf8}}
with (
patch.object(
AthenaPolarsResultSet,
Expand Down Expand Up @@ -188,12 +191,11 @@ def test_txt_new_columns_with_schema_overrides(self, tmp_path, reader):
):
result = getattr(result_set, reader)()
df = result if isinstance(result, pl.DataFrame) else pl.concat(list(result))
assert df.to_dict(as_series=False) == {"z": ["001"], "b": ["x"]}
assert df.to_dict(as_series=False) == {"z": ["001"], "w": ["x"]}

@pytest.mark.parametrize(
("reader", "function"),
[
("_read_csv", "read_csv"),
("_iter_csv_chunks", "scan_csv"),
("_read_parquet", "read_parquet"),
("_iter_parquet_chunks", "scan_parquet"),
Expand Down Expand Up @@ -237,6 +239,34 @@ def test_storage_options_replace_defaults(self, reader, function):
list(result)
assert read.call_args.kwargs["storage_options"] == {"anon": True}

def test_csv_storage_options_replace_defaults(self):
"""storage_options given to execute() open a CSV result through fsspec, not Polars."""
result_set = _chunked_result_set()
result_set._kwargs = {"storage_options": {"anon": True}}
with (
patch.object(
AthenaPolarsResultSet,
"output_location",
new_callable=PropertyMock,
return_value="s3://bucket/result.csv",
),
patch.object(
AthenaPolarsResultSet, "_csv_dtypes", new_callable=PropertyMock, return_value={}
),
patch.object(
AthenaPolarsResultSet,
"_csv_storage_options",
new_callable=PropertyMock,
side_effect=AssertionError("replaced storage options were computed"),
),
patch.object(AthenaPolarsResultSet, "_is_csv_readable", return_value=True),
patch("pyathena.polars.result_set.fsspec.open") as open_,
):
open_.return_value.__enter__.return_value.read.return_value = b"a\n1\n"
df = result_set._read_csv()
open_.assert_called_once_with("s3://bucket/result.csv", "rb", anon=True)
assert df.to_dict(as_series=False) == {"a": [1]}

@pytest.mark.parametrize(
("kwargs", "expected"),
[
Expand Down
31 changes: 31 additions & 0 deletions tests/pyathena/polars/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2026 The PyAthena authors
#
# Licensed under the MIT License.
# See LICENSE or https://opensource.org/licenses/MIT.
#
# SPDX-License-Identifier: MIT

import polars as pl
import pytest

from pyathena.polars.util import get_athena_type


@pytest.mark.parametrize(
("dtype", "expected"),
[
(pl.List(pl.Int32), ("array", 0, 0)),
(pl.List(pl.Struct({"x": pl.Int32})), ("array", 0, 0)),
(pl.Struct({"x": pl.Int32}), ("row", 0, 0)),
(pl.Struct({"x": pl.List(pl.Int32)}), ("row", 0, 0)),
],
)
def test_get_athena_type_nested(dtype, expected):
assert get_athena_type(dtype) == expected


@pytest.mark.skipif(not hasattr(pl, "Map"), reason="Polars 1.x has no Map type")
def test_get_athena_type_map():
assert get_athena_type(pl.Map(pl.String, pl.Int32)) == ("map", 0, 0)
assert get_athena_type(pl.Map(pl.String, pl.List(pl.Int32))) == ("map", 0, 0)
assert get_athena_type(pl.List(pl.Map(pl.String, pl.Int32))) == ("array", 0, 0)
Loading
Loading