Repository navigation
Name the column in DataError from the fetch methods of every cursor #1277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bc27a08
914200e
80b8f91
f81e5af
067a1ef
d555a2c
4dd1dac
8f6101f
37e9778
df71232
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -363,6 +363,51 @@ def _convert_values(self, values: Series) -> Series: | |
| return _to_timestamps(values, date=self._type == "date") | ||
|
|
||
|
|
||
| class _TimeZoneConverter: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Self-review round two, scope expansion (claims, callers, evidence). Same range as round one. Claims checked:
Result: CLEAN. Validation: |
||
| """A converter that ``pandas.read_csv()`` applies to each ``timestamp with time zone`` value. | ||
|
|
||
| With ``parse_dates`` or ``dtype`` given to ``execute()``, or with a conversion | ||
| function other than the default one, pandas applies the column's conversion | ||
| function to each value as it reads, unless ``converters`` given to ``execute()`` | ||
| replaces the conversion functions. This names the column in the | ||
| ``DataError`` of a value that fails to convert, as ``_TemporalConverter`` does. | ||
| """ | ||
|
|
||
| __slots__ = ("_converter", "_label", "error") | ||
|
|
||
| def __init__(self, converter: Callable[[str | None], Any], label: Any) -> None: | ||
| """Wrap the conversion function of a column. | ||
|
|
||
| Args: | ||
| converter: The conversion function. | ||
| label: The label of the column. | ||
| """ | ||
| self._converter = converter | ||
| self._label = label | ||
| # The DataError that the converter raised, which a whole read raises as it is. | ||
| self.error: DataError | None = None | ||
|
|
||
| def __call__(self, value: str | None) -> Any: | ||
| """Convert a value. | ||
|
|
||
| Args: | ||
| value: The text of the value. | ||
|
|
||
| Returns: | ||
| The converted value. | ||
|
|
||
| Raises: | ||
| DataError: If the value fails to convert, such as a value with an unknown | ||
| zone or a local time without one instant in its zone. The message names | ||
| the column. | ||
| """ | ||
| try: | ||
| return self._converter(value) | ||
| except DataError as e: | ||
| self.error = DataError(f"Column {self._label!r}: {e}") | ||
| raise self.error from e | ||
|
|
||
|
|
||
| def _to_timestamps(values: Series, date: bool = False) -> Series: | ||
| """Convert timestamp or date text to ``datetime64[us]``. | ||
|
|
||
|
|
@@ -1246,22 +1291,29 @@ def _get_csv_temporal_converters( | |
| temporal_converters[label] = _TemporalConverter(type_) | ||
| return temporal_converters | ||
|
|
||
| def _get_csv_converter(self, type_: str) -> Callable[[str | None], Any] | _JSONConverter: | ||
| """Get the converter for a column type of the CSV result file. | ||
| def _get_csv_converter( | ||
| self, type_: str, label: Any | ||
| ) -> Callable[[str | None], Any] | _JSONConverter: | ||
| """Get the converter for a column of the CSV result file. | ||
|
|
||
| json columns get a ``_JSONConverter``, which ``_read_json_columns_as_text()`` | ||
| replaces in the converters for ``pandas.read_csv()``, and which converts the | ||
| columns after reading. | ||
| columns after reading. ``timestamp with time zone`` columns get a | ||
| ``_TimeZoneConverter``, which names the column in a ``DataError``. | ||
|
|
||
| Args: | ||
| type_: The Athena type of the column. | ||
| label: The label of the column. | ||
|
|
||
| Returns: | ||
| The conversion function, or the ``_JSONConverter`` of a json column. | ||
| The conversion function, the ``_JSONConverter`` of a json column, or the | ||
| ``_TimeZoneConverter`` of a ``timestamp with time zone`` column. | ||
| """ | ||
| converter = self._converter.get(type_) | ||
| if type_ == "json": | ||
| return _JSONConverter(converter) | ||
| if type_ == "timestamp with time zone": | ||
| return _TimeZoneConverter(converter, label) | ||
| return converter | ||
|
|
||
| def _trunc_date(self, df: DataFrame) -> DataFrame: | ||
|
|
@@ -1317,6 +1369,8 @@ def _read_csv(self) -> TextFileReader | DataFrame: | |
| The DataFrame of the results, or a reader of their chunks. | ||
|
|
||
| Raises: | ||
| DataError: If a ``timestamp with time zone`` value that pandas converts as it | ||
| reads fails to convert, naming the column. | ||
| OperationalError: If reading the results fails. | ||
| """ | ||
| import pandas as pd | ||
|
|
@@ -1414,6 +1468,13 @@ def _read_csv(self) -> TextFileReader | DataFrame: | |
| return result | ||
|
|
||
| except Exception as e: | ||
| # converters given to execute() can be of any type. | ||
| converters = read_csv_kwargs.get("converters") | ||
| if isinstance(converters, Mapping) and any( | ||
| isinstance(converter, _TimeZoneConverter) and converter.error is e | ||
| for converter in converters.values() | ||
| ): | ||
| raise | ||
| _logger.exception(f"Failed to read {location}.") | ||
| raise OperationalError(*e.args) from e | ||
|
|
||
|
|
@@ -1519,7 +1580,7 @@ def _get_csv_read_options(self, csv_engine: str, chunksize: int | None) -> dict[ | |
| "names": names, | ||
| "dtype": {**self.dtypes, **dict.fromkeys(temporal, text_dtype)}, | ||
| "converters": { | ||
| d[0]: self._get_csv_converter(d[1]) | ||
| d[0]: self._get_csv_converter(d[1], d[0]) | ||
| for d in description | ||
| if d[1] in self._converter.mappings and d[0] not in self._csv_temporal_converters | ||
| }, | ||
|
|
@@ -1690,7 +1751,7 @@ def _key_csv_columns_by_labels( | |
| } | ||
| if "converters" not in self._kwargs: | ||
| read_csv_kwargs["converters"] = { | ||
| label: self._get_csv_converter(d[1]) | ||
| label: self._get_csv_converter(d[1], label) | ||
| for label, d in columns | ||
| if d[1] in self._converter.mappings and label not in self._csv_temporal_converters | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Independent review (relayed): Codex CLI 0.163.0 (
codex exec -s read-only, model gpt-6-astra, reasoning effort max), session01a14db5-b194-7502-b3a3-1d170d2ddc3e. Static review only: no edits, builds, tests, or network. Snapshot: detached worktree at head80b8f91d96c6c5401665f1fe49dcccb1d9ce1442, baseb8cec9dfc98fba2bcfd8470e82e1800ba02d85d0; the prompt gave the diff, intended behavior, and test policy, without the PR text or prior findings.Covered: Cursor/DictCursor, S3FS CSV and API fallback, Arrow batches, Polars eager and chunked iteration, Pandas conversion callers, async/aio delegation; hints, truncation,
dict_type, duplicate names, empty/DML results, exception boundaries, hot-path structure.Result: FINDINGS
pyathena/arrow/result_set.py:224(at 80b8f91): converting a whole column before the next changes the order in which converters see values. With two VARCHAR rows[("a", "b"), ("c", "d")]and a converterlambda v: next(counter)overiter([1, 2, 3, 4]), the base returns[(1, 2), (3, 4)]and the head[(1, 3), (2, 4)]; it can also change which failure is reported first.Non-actionable observations:
StopIteration→RuntimeErrorare preserved.dtype={}(any givendtype,parse_dates, orconverters) bypasses_TemporalConverter(pyathena/pandas/result_set.py:1233), so the error is an unprefixedOperationalError(:1418). This is outside the changed paths and is the Give PandasCursor's temporal columns one dtype per Athena type #1273 design noted on Name the column in DataError for time zone values in every cursor #1217.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repair in f81e5af: the Arrow fetch converts each row in column order again, through a per-row generator that names the column of a
DataError; the no-converter path keepslist(zip(*columns)). The tests anchor the prefix with^.Self-review of the repair (both perspectives, scope:
git range-diff b8cec9df..80b8f91d b8cec9df..f81e5afa):[(1, 2), (3, 4)]on both b8cec9d and f81e5af (script).StopIterationfrom a converter is stillRuntimeError. With no description (DML), no converter runs, as on the base. Names and converters come from the description by position, as the base's converters did.just lintpassed;pytest -n 4 tests/pyathena/test_cursor.py tests/pyathena/s3fs/test_cursor.py tests/pyathena/arrow tests/pyathena/aio/arrow tests/pyathena/polars/test_cursor.py -k "ambiguous or arrow": 146 passed.The Pandas
dtype/parse_dates/converterscase is deferred as pre-existing (#1273's design, noted on #1217). An independent follow-up of this repair follows.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Independent follow-up (relayed): Codex CLI 0.163.0 (
codex exec -s read-only), session01a14dbe-118f-7ba3-bbac-194f984d8826. Static review only. Snapshot: detached worktree atf81e5afa60d63e2180a6054830078ea93fb1c086; scope: the repair diff 80b8f91..f81e5af andgit range-diff b8cec9df..80b8f91d b8cec9df..f81e5afa.Result: CLEAN. Covered the Arrow fetch against b8cec9d (fetch and error propagation, converter order, default and DML paths, buffering) and the five anchored test assertions. The repair restores row-by-row conversion in column order; only a converter's
DataErroris wrapped, with the original as the cause;StopIterationstill becomesRuntimeError; thelist(zip(*columns))path is intact. The reviewer did not measure performance. The snapshot and the PR worktree stayed unchanged.