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
10 changes: 9 additions & 1 deletion docs/pandas.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,18 @@ cursor.execute("SELECT * FROM typed_table",
Common performance options:

- `engine`: CSV parsing engine ('auto', 'c', 'python', 'pyarrow'); 'auto' uses the C engine
- `low_memory`: Parse the file in internal chunks to reduce memory use (C engine only; pandas default `True`)
- `low_memory`: Parse the file in internal chunks to reduce memory use (C engine only)
- `dtype`: Explicit column data types
- `parse_dates`: Columns to parse as dates

When the C engine's converter mapping includes PyAthena's JSON converter, `low_memory` defaults to `False`.
This avoids pandas' `DtypeWarning` when JSON numbers or booleans and NULLs occur in different internal parser blocks.
The setting applies to the entire CSV read, including other columns whose dtypes pandas infers.

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 two, bounded rebase/documentation follow-up: CLEAN. Reviewed the same literal old/new patch-series comparison as round one; current base/head are 200762088e7b0bac45054f22e32a16aa0ce95dbb / 4596cb242cea8a23214f6587c58797ac196fe06a. Checked the revised claims against final converter-map detection and pandas' read-wide low_memory behavior. The docs now state that converter-map membership triggers the default, including unusual parsing overrides that retain an unused JSON converter, and that other inferred columns also receive whole-read dtype inference. Memory cost, preserved requested chunk sizes, and explicit-option precedence remain accurately described. The adjacent upstream DDL/PyArrow explanation is preserved. No new API, positional argument, AWS request, or retry change. Prior measured performance remains explicitly limited to the old head's unchanged local conversion/parser code; the 401-test old-head result is not represented as current-head execution. No findings or deferred claims.

Parsing each result or chunk at once can use more memory; `chunksize` still limits the rows read per chunk.
An explicit `low_memory=True` or `low_memory=False` passed to `execute()` takes precedence.

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 two (claims, compatibility, operations): CLEAN. Full diff: d98976731b29eed7649c3095990e9b6f4fbc6d14..c2e602283264b38b5384009838c9f812f6e8dae5. Audited the PR body, changed comments/docstrings, documentation, and commit claims independently of round one's behavior checklist. Checked pandas 3.0.6 CParserWrapper: low_memory=False uses read(nrows), while True uses read_low_memory(nrows) and mixed-dtype concatenation. TextFileReader.get_chunk(size) still passes nrows=size. Real-parser tests verify both overrides, 400,005-row first chunks, exact JSON integers/booleans with NULL, and inferred int64 without NULL. Traced unchanged sync/Async/aio keyword forwarding and absence of added AWS calls/retries. The added loop inspects converter metadata only; it adds no per-value allocation or column-value scan in PyAthena. Benchmark numbers match the recorded three-run medians and show higher peak RSS for the numeric-only whole read; no general memory bound or S3 performance claim is made. Corrected the PR's validation sequence to distinguish offline CI before Ready from AWS CI after Ready. Live AWS results remain pending, separate from this source-level CLEAN result. No source repairs or deferred findings; review HEAD/worktree unchanged.

With `low_memory=True`, the warning can return.
Without PyAthena's JSON converter, the C engine keeps pandas' default `low_memory=True`.

With `engine="pyarrow"`, PandasCursor uses the PyArrow engine only when the result is not a tab-separated `.txt` file, pyarrow is installed, no chunksize is set (explicitly or by `auto_optimize_chunksize`), `quoting` is the default, the result has no columns that need a converter (`boolean`, `decimal`, `varbinary`, `json`, `time with time zone`, and `timestamp with time zone` with the default converter), and the result file is at least `AthenaPandasResultSet.PYARROW_MIN_FILE_SIZE_BYTES` bytes.
Otherwise, it falls back to the C engine.
With `engine="pyarrow"`, tab-separated `.txt` results from DDL statements such as `SHOW TABLES`, `SHOW COLUMNS`, and `DESCRIBE` use the C engine to preserve leading zeros, exponent notation, and padding in string values.
Expand Down
6 changes: 6 additions & 0 deletions pyathena/pandas/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,12 @@ def _read_csv(self) -> TextFileReader | DataFrame:
# After _configure_binary_csv_read(), which checks for the header row.
self._read_csv_header_as_labels(read_csv_kwargs, csv_engine)
self._csv_converters = read_csv_kwargs.get("converters") or {}
if csv_engine == "c" and any(
isinstance(converter, _JSONConverter)
for converter in self._csv_converters.values()
):
# A NULL placeholder can give internal parser blocks different dtypes.
read_csv_kwargs.setdefault("low_memory", False)

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. Full diff: d98976731b29eed7649c3095990e9b6f4fbc6d14..c2e602283264b38b5384009838c9f812f6e8dae5. Covered all three changed files and traced converter construction, selected/renamed column handling, explicit converter and low_memory overrides, actual engine selection including PyArrow fallback, whole and chunked reads, NULL restoration in columns/indexes, and the sync/Async/aio callers. The new option is applied after final converter resolution, and setdefault preserves explicit options. Validation: required lint and docs checks passed; all 67 offline result-set tests passed using the real parser; removing only the new default in the test process causes the original DtypeWarning failure. No repairs or deferred source findings. Live AWS validation is still pending and is not implied by this source-level CLEAN result. HEAD and tracked worktree are unchanged.

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.

Relayed independent review: CLEAN, static source review only. Reviewer: Claude Code claude-opus-5-5, profile max, effort high; first-party claude.ai authentication verified with subscriptionType=max, no API/provider override or fallback model. Session: d06619e5-2d05-4f07-9ab8-208985fe194f; returned modelUsage confirms claude-opus-5-5/firstParty. Full diff: d98976731b29eed7649c3095990e9b6f4fbc6d14..c2e602283264b38b5384009838c9f812f6e8dae5. Restricted to Read/Glob/Grep in an exported tracked-source snapshot; no builds, tests, code execution, edits, GitHub/commit/PR context, local memory, external tools, or delegation. Covered all changed source/tests/docs, engine and option precedence, selected/renamed columns, custom converters, S3/GetQueryResults paths, sync/Async/aio callers, chunk reads, value/dtype/NULL/index restoration, and regression failure coverage. No actionable findings. Reviewer noted the intentional memory tradeoff, the CSV-wide effect on inferred dtypes, and that unusual parsing overrides can leave an unused JSON converter in the map; these are implications of choosing low_memory=False, not suppressed warnings. The author will clarify the read-wide effect in docs when resolving the concurrent upstream rebase. All 287 snapshot files and the PR worktree remained unchanged. This review applies to the stated head; the updated upstream revision will receive a follow-up before Ready.

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.

Updated to head 4596cb242cea8a23214f6587c58797ac196fe06a on base 200762088e7b0bac45054f22e32a16aa0ce95dbb, preserving upstream's DDL/PyArrow fix and clarifying converter-map membership and the CSV-wide inferred-dtype effect. Both self-review perspectives and the Claude Opus 5.5 Max/high independent follow-up are CLEAN for the bounded range-diff. The code and regression tests are unchanged. Current-head validation: 115 offline tests, 17 focused AWS tests, and both auto/c runs of the issue's 400,010-row Athena query passed. The earlier 401-test full pandas/aio run remains recorded against its original head.

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, bounded rebase/documentation follow-up: CLEAN. Previous full range: d98976731b29eed7649c3095990e9b6f4fbc6d14..c2e602283264b38b5384009838c9f812f6e8dae5; current range: 200762088e7b0bac45054f22e32a16aa0ce95dbb..4596cb242cea8a23214f6587c58797ac196fe06a. Verified old objects and reviewed the literal range-diff. The implementation and 22 added regression cases are unchanged; only the docs were clarified and reconciled with upstream. Traced upstream's new .txt PyArrow-to-C fallback into final converter resolution: DDL string-only results receive no JSON-specific low_memory option. Upstream filesystem changes concern copying and annotations; the CSV open/read path is unchanged. Lint and current docs checks passed after rebase; all 115 offline result-set tests passed, including the upstream DDL cases. The full pandas/aio suite passed 401 tests on the old head; current-head focused live tests are running and are not yet reported as passed. No source findings or repairs.

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.

Relayed independent rebase/documentation follow-up: CLEAN, static source review only. Reviewer: Claude Code claude-opus-5-5, verified first-party max subscription/profile, effort high, no provider/API override or fallback. Session: 2f27307c-3e04-4ecf-bddb-96ddd1369ca5; returned modelUsage confirms claude-opus-5-5/firstParty. Compared literal git range-diff d98976731b29eed7649c3095990e9b6f4fbc6d14..c2e602283264b38b5384009838c9f812f6e8dae5 200762088e7b0bac45054f22e32a16aa0ce95dbb..4596cb242cea8a23214f6587c58797ac196fe06a, the current diff, and directly affected upstream pandas changes. Confirmed unchanged code/tests, accurate converter-map/CSV-wide documentation, preserved chunk limits and explicit overrides, and compatibility with upstream's .txt-to-C engine fallback and its tests. No actionable findings. Read/Glob/Grep only in an exported source snapshot, with no executed validation, writes, PR/commit discussion, memory, or external tools. All 289 snapshot files and the PR worktree remained unchanged. Separate author-run current-head validation: 115 offline result-set tests and 17 focused real-AWS tests passed; the issue's 400,010-row Athena reproduction passed on auto and c with ten None values and DtypeWarning treated as an error. This runtime evidence is the author's validation, not the reviewer's.

if data is not None:
# The rows are in memory, with nothing to open with storage options.
read_csv_kwargs.pop("storage_options", None)
Expand Down
135 changes: 135 additions & 0 deletions tests/pyathena/pandas/test_result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,141 @@ def _pyarrow_read_csv_kwargs(types, tab_separated=False, **kwargs):
return result_set._get_csv_read_options("pyarrow", None)


def _read_csv_result(data, types, engine="c", chunksize=None, **kwargs):
"""Read an in-memory CSV through the result set and the real pandas parser."""
result_set = AthenaPandasResultSet.__new__(AthenaPandasResultSet)
result_set._converter = DefaultPandasTypeConverter()
result_set._keep_default_na = False
result_set._na_values = ("",)
result_set._quoting = 1
result_set._engine = engine
result_set._chunksize = chunksize
result_set._auto_optimize_chunksize = False
result_set._kwargs = kwargs
result_set._time_columns = []
result_set._csv_stream = None
result_set._fs = MagicMock()
result_set._fs.open.return_value = io.BytesIO(data.encode())
description = [(name, type_, None, None, 0, 0, "UNKNOWN") for name, type_ in types.items()]
with (
patch.object(
AthenaPandasResultSet,
"description",
new_callable=PropertyMock,
return_value=description,
),
patch.object(
AthenaPandasResultSet,
"output_location",
new_callable=PropertyMock,
return_value="s3://bucket/result.csv",
),
patch.object(
AthenaPandasResultSet,
"substatement_type",
new_callable=PropertyMock,
return_value=None,
),
patch.object(AthenaPandasResultSet, "_get_content_length", return_value=len(data)),
patch("pandas.read_csv", wraps=pd.read_csv) as read_csv,
patch(
"pyathena.pandas.result_set._read_csv_with_pyarrow",
wraps=_read_csv_with_pyarrow,
) as read_pyarrow,
):
result = result_set._read_csv()
options = read_pyarrow.call_args.args[1] if read_pyarrow.called else read_csv.call_args.kwargs
if isinstance(result, pd.DataFrame):
return result_set._finish_csv_frame(result), options
return PandasDataFrameIterator(
result, result_set._finish_csv_frame, result_set._csv_stream
), options


@pytest.mark.filterwarnings("error::pandas.errors.DtypeWarning")
@pytest.mark.parametrize(
("json_value", "expected"), [("9007199254740993", 9007199254740993), ("true", True)]
)
@pytest.mark.parametrize(
("engine", "kwargs", "access"),
[
pytest.param("c", {}, "whole", id="c"),
pytest.param("auto", {}, "whole", id="auto"),
pytest.param("pyarrow", {}, "whole", id="pyarrow-fallback"),
pytest.param("c", {"chunksize": 400005}, "iteration", id="iteration"),
pytest.param("c", {"chunksize": 400010}, "get_chunk", id="get-chunk"),
pytest.param("c", {"index_col": "j"}, "whole", id="index"),
pytest.param("c", {"usecols": ["j"]}, "whole", id="usecols"),
],
)
def test_read_csv_json_null_without_dtype_warning(json_value, expected, engine, kwargs, access):
"""JSON NULLs in a later parser block keep exact values without a warning."""
data = "n,j\n" + f"1,{json_value}\n" * 400000 + "2,\n" * 10
result, options = _read_csv_result(data, {"n": "integer", "j": "json"}, engine=engine, **kwargs)
if access != "whole":
with result:
first = result.get_chunk(400005) if access == "get_chunk" else next(result)
assert len(first) == 400005
df = pd.concat([first, *result])
else:
df = result
values = df.index if kwargs.get("index_col") == "j" else df["j"]
assert options["engine"] == "c"
assert options["low_memory"] is False
assert len(df) == 400010
assert values.dtype == object
assert type(values[0]) is type(expected)
assert (values[:400000] == expected).all()
assert values[-10:].tolist() == [None] * 10


def test_read_csv_json_explicit_low_memory_true():
"""An explicit low_memory=True keeps pandas' internal block parsing and warning."""
data = "n,j\n" + "1,9007199254740993\n" * 400000 + "2,\n" * 10
with pytest.warns(pd.errors.DtypeWarning, match="have mixed types"):
df, options = _read_csv_result(data, {"n": "integer", "j": "json"}, low_memory=True)
assert options["low_memory"] is True
assert df["j"].iloc[0] == 9007199254740993
assert df["j"].iloc[-10:].tolist() == [None] * 10


@pytest.mark.filterwarnings("error::pandas.errors.DtypeWarning")
@pytest.mark.parametrize("low_memory", [None, True, False])
def test_read_csv_json_without_null(low_memory):
"""JSON without NULL keeps its inferred dtype with default or explicit low_memory."""
kwargs = {} if low_memory is None else {"low_memory": low_memory}
df, options = _read_csv_result("n,j\n1,2\n3,4\n", {"n": "integer", "j": "json"}, **kwargs)
assert options["low_memory"] is (False if low_memory is None else low_memory)
assert df["j"].dtype == "int64"
assert df["j"].tolist() == [2, 4]


@pytest.mark.parametrize(
("types", "engine", "kwargs"),
[
pytest.param({"n": "integer", "j": "integer"}, "c", {}, id="no-json"),
pytest.param({"n": "integer", "j": "json"}, "c", {"usecols": ["n"]}, id="json-excluded"),
pytest.param(
{"n": "integer", "j": "json"}, "c", {"converters": {}}, id="custom-converters"
),
pytest.param(
{"n": "integer", "j": "json"},
"c",
{"converters": {"j": int}},
id="custom-json-converter",
),
pytest.param({"n": "integer", "j": "json"}, "python", {}, id="python"),
pytest.param({"n": "integer", "j": "integer"}, "pyarrow", {}, id="pyarrow"),
],
)
def test_read_csv_without_json_c_converter_keeps_low_memory_default(types, engine, kwargs):
"""Other columns, converters, and engines do not get a low_memory option."""
df, options = _read_csv_result("n,j\n" + "1,2\n" * 30, types, engine=engine, **kwargs)
assert options["engine"] == engine
assert "low_memory" not in options
assert len(df) == 30


@pytest.mark.filterwarnings("ignore:Could not infer format")
@pytest.mark.parametrize("infer_string", [True, False])
@pytest.mark.parametrize(
Expand Down
Loading