Skip to content

BREAKING: Reject unknown cursor keyword arguments - #1102

Merged
laughingman7743 merged 16 commits into
masterfrom
fix/1093-reject-unknown-cursor-keywords
Oct 10, 2026
Merged

laughingman7743 merged 16 commits into
masterfrom
fix/1093-reject-unknown-cursor-keywords

Conversation

@laughingman7743

@laughingman7743 laughingman7743 commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

WHAT

Cursor constructors and execute() raise TypeError for unknown keyword arguments instead of ignoring them.

  • BaseCursor.__init__ no longer accepts **kwargs, so an unknown constructor name, given directly or through the connection's cursor_kwargs, raises TypeError. on_start_query_execution and on_poll remain accepted by every cursor.
  • execute() of the default, dict, Arrow, S3FS, and Spark cursors (sync, Future-based, and native asyncio) checks its extra keyword names against the result-set options it supports before changing cursor state or starting a query or calculation.
  • The pandas and Polars cursors are unchanged: they pass the execute() keyword arguments they do not use themselves to the reader (pandas.read_csv(), polars.read_csv(), ...). The reader rejects an unknown name when it reads the query results, raised as OperationalError; the name is not checked when there are no results to read.
  • The SQLAlchemy dialect opens its API cursor with Connection._internal_cursor(), which applies only the cursor_kwargs that Cursor accepts, leaving out options of the default cursor class such as unload and chunksize of the pandas dialect, and arraysize (a DataFrame cursor allows values above Cursor's 1000 limit, which made reflection raise ProgrammingError).
  • docs/usage.md documents the behavior in the existing PyAthena 4.0 keyword-argument section.

WHY

Closes #1093.
Typos and unsupported options were silently dropped.
Breaking change for the 4.0.0 release note: unknown cursor constructor and execute() keywords now raise TypeError, and connection.cursor(OtherCursor) raises if the connection's cursor_kwargs contain options that OtherCursor does not accept.
A cursor subclass that passes its own settings from cursor_kwargs on to the base constructor now raises TypeError; for example, dbt-athena's legacy PyAthena connection manager (connections_legacy.py, currently pinned to pyathena<3.35) passes debug_query_state and num_iceberg_retries this way.

An earlier revision of this PR also checked pandas/Polars reader option names against the installed readers' signatures before running the query.
That duplicated the readers' own check and needed signature inspection with a cache, UNLOAD statement classification, Polars deprecated-alias handling, and a second check for the chunked Polars CSV fallback, so it was removed.

TEST

Tested commit: dd296bf5d231c5025f2f428384eec44a35ae876b.

  • just format, just lint, just docs lint: passed.
  • uv run --env-file .env pytest --noconftest -p no:rerunfailures -q tests/pyathena/test_options.py tests/pyathena/test_connection.py tests/pyathena/aio/test_connection.py tests/pyathena/pandas/test_result_set.py tests/pyathena/polars/test_result_set.py tests/pyathena/sqlalchemy/test_base.py::TestAthenaDialect: 435 passed.
  • New offline tests cover unknown constructor names (direct and from cursor_kwargs) and unknown execute() names with preserved cursor state for every cursor family, supported result-set options, shared callbacks, and the internal API cursor for sync and async connections.
  • Ready-triggered Test run 37960627229 at this commit, Python 3.14: PyAthena 3740 passed, 12 skipped; SQLAlchemy 589 passed, 759 skipped; SQLAlchemy async 589 passed, 759 skipped.
  • Reviews: self-review rounds one and two and a Codex independent review with a follow-up are recorded inline; all findings were repaired or deferred with reasons. Follow-ups: PolarsCursor fails with the deprecated dtypes reader option #1111 (Polars dtypes alias), executemany() does not check keyword names for an empty parameter list #1112 (executemany() with an empty parameter list).

🤖 Generated with Claude Code

Comment thread pyathena/_kwargs.py Outdated
"""
for name in kwargs:
if name not in allowed:
raise TypeError(f"{method}() got an unexpected keyword argument '{name}'")

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 — CLEAN (implementation and regression coverage).

Base: 23a54e1
Head: 09f7fb3

Covered the complete 28-file change inventory: terminal constructor binding and forwarding through each cursor MRO; all sync, Future, and native asyncio execute entry points; validation before state reset, query/calculation start, and executor submission; Arrow/S3FS result-set overrides; pandas CSV/Parquet and Polars eager/scan reader names; SQLAlchemy API-cursor defaults and async adaptation; documentation and regression tests.

Verified the existing connection-default precedence and custom constructor extension coverage. The regression tests exercise all 21 cursor classes, assert unchanged state and no AWS requests on rejection, cover valid reader names and invalid reader modes, and keep unknown reflection defaults visible. No actionable implementation findings remain.

Validation: just format; just lint; 355 offline tests; Markdown lint; multi-version and working-tree Sphinx builds passed. This is a static self-review with the stated offline evidence; AWS validation is pending and is not inferred from CLEAN.

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 repair follow-up — CLEAN.

Base: 23a54e1
Head: 0241fc2
Previous reviewed head: 09f7fb3

Checked both old objects and the bounded patch-series comparison, then traced all 11 repaired files into their callers and tests. The initial CLEAN record missed the independently identified reader-selection and test-factory regressions.

The six pandas/Polars execute methods now resolve the statement locally and validate its effective reader before resetting state, executing a query, or submitting work. Non-SELECT statements under unload=True retain CSV options. Polars accepts both possible CSV readers before result metadata is available and checks the actual reader at read time; renamed reader arguments remain accepted. Existing SQLAlchemy test doubles now implement the new private contract without weakening reflection, missing-table, cache, or error assertions. The live view-definition baseline uses a separate API connection. Spark tests seed actual calculation state, and internal-cursor factory failures now enter guarded cleanup.

Validation: just format, just lint, 428 offline tests, Markdown lint, and working-tree Sphinx build passed. No actionable repair findings remain. AWS-suite validation and independent follow-up remain pending; this CLEAN record does not claim either.

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 second repair follow-up — CLEAN.

Base: 23a54e1
Head: 7496dcd
Previous reviewed head: 0241fc2

Verified the old commit objects and range-diff correspondence: the two prior entries are unchanged, followed by a runtime repair and a documentation clarification. Traced all 10 affected files and callers.

CSV dtypes replaces inferred schema_overrides without suppressing Polars deprecation handling; eager and scan readers preserve leading-zero strings, renamed columns, and timestamp fallback. Scan-specific option validation now runs before generator creation, so result-set creation fails deterministically rather than consuming a one-shot error. Preparation failures reset sync/native-async cursor state as before, while keyword rejection preserves it; Future-based cursors retain their no-reset contract. The fallback classifier is guarded for non-string SQL, and validation takes precedence over local preparation errors. Test mocks now represent an open prior result, and internal API-cursor tests configure a real backend converter to expose converter-pinning regressions.

Evidence: just format and just lint pass; 481 offline tests pass, including 48 preparation-error/name-priority combinations and actual dtype/renaming/timestamp reads. Markdown lint and working-tree Sphinx builds pass. The final commit after the offline run changes only documentation wording. No actionable implementation findings remain; independent follow-up and final AWS CI are pending.

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 dtype repair follow-up — CLEAN.

Base: 23a54e1
Head: 6d6fe2f
Previous reviewed head: 7496dcd

Verified both prior objects and the patch-series comparison; all four preceding entries are unchanged. Reviewed the two changed files and traced dtype aliases through default merging, column renaming, timestamp fallback, eager/scan readers, and result-set construction.

The timestamp fallback now excludes explicit dtypes exactly as it excludes schema_overrides, and no longer removes the user’s alias. Tests use milliseconds rather than inferred microseconds, non-numeric data requested as Int64, and renamed timestamp columns; each fails identically under old/new names instead of silently returning inferred types. Existing successful default truncation and valid alias/column-renaming cases still pass. The new iterator test label now names the actual checked method.

just format and just lint pass. The full focused offline selection passes: 486 passed, with three expected library deprecation warnings for dtypes. No actionable implementation findings remain. Independent follow-up and current-head AWS CI are pending.

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 wording follow-up — CLEAN.

Base: 23a54e1
Head: f2460c5
Previous head: 6d6fe2f

The range-diff preserves every preceding entry and appends only two test-docstring lines. No runtime code, assertions, parameter coverage, or public interfaces changed. The wording now names schema_overrides/dtypes and describes the guard directly. just format and just lint pass; the 486 offline passes on the preceding runtime head remain applicable. Independent wording follow-up and current-head AWS CI are pending.

Comment thread docs/cursor.md Outdated
The validation happens before starting a query or a Spark calculation.
For native asyncio cursors, `execute()` validates arguments when awaited.
Pandas and Polars cursors continue to accept their reader options, with names checked against the installed library and the selected CSV, UNLOAD, and chunking mode.
Invalid values and combinations remain subject to the reader's validation.

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 — CLEAN (caller compatibility, operational effects, and claims).

Base: 23a54e1
Head: 09f7fb3

Audited the full changed-file inventory and the PR/documentation claims against actual callers and the installed reader APIs. AST signature comparison confirms that existing execute and public connection/cursor signatures retain their positional order; only BaseCursor.init loses the ignored **kwargs. Async validation runs on await, while Future-based execution rejects unknown names before starting work. Shared constructor callbacks retain their existing support and invocation behavior.

Traced reader-name validation into CSV/UNLOAD and eager/scan paths, including pandas-to-PyArrow Parquet forwarding. Values and combinations remain reader-validated. Traced SQLAlchemy synchronous and async reflection, converter pinning, common default precedence, and preservation of unknown defaults. Core usage and backend module imports still work with optional reader imports blocked; constructing a default pandas converter still requires pandas, as before.

No retry, quota, or AWS performance claim is made. Local evidence is lint, 355 offline tests, and successful documentation builds. AWS suites and an independent static review remain pending; neither is claimed complete. No actionable compatibility or factual-claim findings remain.

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 repair follow-up — CLEAN.

Base: 23a54e1
Head: 0241fc2
Previous reviewed head: 09f7fb3

Audited the updated PR description, docs/cursor.md, validator docstrings, deprecated-reader comments, test assertions, and the bounded patch-series comparison. The initial round-two record overstated selected-reader coverage; the repaired claims explicitly distinguish unknown-name rejection before AWS execution from CSV-mode-specific rejection after metadata determines managed-result fallback.

Checked unload=True DESCRIBE behavior against Formatter.wrap_unload and AthenaResultSet.is_unload, and checked eager/scan options plus renamed aliases against installed Polars reader sources. Rechecked sync, Future, and native asyncio call timing, state preservation, reader value-validation boundaries, unchanged public signatures, and shared callback behavior. The separate view baseline preserves an independent runtime comparison; real-connection tests still check converter pinning and default isolation. No retry, quota, or performance claims are made.

Evidence: 428 offline tests pass, including the old failing dialect unit cases and actual CSV reading with managed/eager options and deprecated row-index names. Lint and documentation checks pass. The interrupted AWS run is reported as interrupted with failures, not success. Required live suites and independent follow-up remain pending. No actionable compatibility or factual-claim findings remain.

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 second repair follow-up — CLEAN.

Base: 23a54e1
Head: 7496dcd
Previous reviewed head: 0241fc2

Audited the revised documentation, reader-helper comments and docstrings, test expectations, and PR claims separately from implementation review. The docs now distinguish unknown-name rejection before AWS execution from mode-specific CSV checks during result-set creation, including Future.result() timing. No claim of successful AWS execution is made for either interrupted local run.

Checked alias behavior against installed Polars wrapper source and actual eager/scan output; ambiguous user-provided old/new dtype pairs remain reader-validated combinations. Checked restoration of original preparation-failure state behavior and error visibility, retained defaults/public signatures, independent SQLAlchemy view-baseline construction, configured-converter isolation, shared callbacks, and optional import boundaries. No retries, quota improvements, or performance changes are claimed.

Evidence: 481 offline cases passed on fbb2832; the only subsequent change clarifies one documentation sentence, with final lint/docs checks passing. The full AWS coverage will be obtained from current-head CI, whose tox configuration executes just test pyathena, just test sqla, and just test sqla-async. CI path filters select all three suites plus the PyAthena Spark and SQLAlchemy directories for this patch. No actionable compatibility or factual-claim findings remain. Independent follow-up and AWS CI remain pending.

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 dtype repair follow-up — CLEAN.

Base: 23a54e1
Head: 6d6fe2f
Previous reviewed head: 7496dcd

Audited the changed helper/test docstrings and the reader-value guarantee against installed Polars wrapper source, the explicit-schema fallback guard, and real CSV parsing. The new name and its alias now have the same error visibility and requested-type behavior. Library validation of invalid values/combinations remains visible as OperationalError through the existing wrapper. The no-override timestamp precision fallback remains unchanged.

Checked the full two-file repair and necessary callers; constructor/execute signatures, state ordering, reader selection, managed CSV fallback, optional imports, and AWS request/retry behavior are unchanged by this repair. The record states the actual 486 offline passes and expected deprecation warnings and does not infer AWS success. No actionable compatibility or factual-claim findings remain. Independent follow-up and AWS CI remain pending.

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 wording follow-up — CLEAN.

Base: 23a54e1
Head: f2460c5
Previous head: 6d6fe2f

Checked the changed claim against _get_timestamp_dtypes and its caller: both named options return an empty fallback mapping, so ComputeError is re-raised and wrapped as OperationalError. The text makes no claim about Polars inferring types or AWS/runtime validation. Existing default timestamp fallback is unchanged. No actionable factual-claim findings remain. Independent wording follow-up and AWS CI are pending.

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.

Final validation completed on reviewed head f2460c5. Ready-triggered AWS CI run https://github.com/pyathena-dev/PyAthena/actions/runs/37352006458 succeeded on Python 3.14.7: just test pyathena (3,744 passed, 12 skipped), just test sqla (589 passed, 759 skipped), and just test sqla-async (589 passed, 759 skipped). Current-head lint, documentation, license checks, and offline CI also passed. The PR is Ready, mergeable, and CLEAN; the reviewed head matches the CI head and the worktree is clean. These completed runs replace the earlier pending AWS evidence, not the independently recorded static review scope.

return raw_connection.driver_connection.cursor( # type: ignore[union-attr]
Cursor, converter=Cursor.get_default_converter()
)
return raw_connection.driver_connection._internal_cursor(Cursor) # type: ignore[union-attr]

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 static review — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: 42543ff2-b082-4c60-b3f9-176757cbc8fc.

Base: 23a54e1
Head: 09f7fb3

FINDINGS — Medium: Existing dialect test doubles still provide only cursor(), so 13 information_schema and view-definition unit cases raise AttributeError after this call changes to _internal_cursor(). Affected factories are in tests/pyathena/sqlalchemy/test_base.py:185,353,390,443,483,576,607. Their old converter-call assertions also need updating. Verified by the local AWS-suite run. The production Connection and async adapter implement this method. A further integration baseline at test_base.py:1417 passes backend unload defaults to public Cursor and must instead create an isolated baseline connection.

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 repair record.

Current reviewed head: f2460c5

Updated every affected dialect test factory to implement _internal_cursor(), preserving the existing reflection/error/cache assertions. The view-definition runtime baseline now uses a separate API Connection. Repaired in 0241fc2; all affected unit cases pass.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread pyathena/pandas/result_set.py Outdated
unload: Whether the cursor reads UNLOAD Parquet results.

Raises:
TypeError: If an argument is not supported by the selected reader.

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 static review — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: 42543ff2-b082-4c60-b3f9-176757cbc8fc.

Base: 23a54e1
Head: 09f7fb3

FINDINGS — Low: Reader selection based only on the configured unload flag rejects valid CSV options for non-SELECT/WITH statements. For example, PandasCursor(unload=True).execute("DESCRIBE t", nrows=10) reads CSV but this validator selects Parquet. The corresponding Polars validator has the same mismatch. Also, chunked Polars queries without an output location use eager read_csv for managed results, so eager-only options are incorrectly rejected before execution. Reader choice must follow the prepared statement and allow the managed-result fallback until result metadata is known. The reviewer also identified an unverified deprecated-Polars-name compatibility risk; installed reader decorators confirm that aliases exist.

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 repair record.

Current reviewed head: f2460c5

Reader validation now follows the prepared statement, so unload=True non-SELECT statements retain CSV options. Chunked Polars CSV allows managed eager options before metadata is known and validates the selected reader when constructing the result set. The constructor-stage check prevents one-shot generator failure. Repaired across 0241fc2 and fbb2832; regression cases pass.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread tests/pyathena/test_kwargs.py Outdated
async def test_unknown_execute_keyword_preserves_state(offline_connection, cursor_class):
cursor = cursor_class(**_constructor_kwargs(offline_connection, cursor_class))
cursor._query_id = "previous"
cursor._result_set = MagicMock()

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 static review — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: 42543ff2-b082-4c60-b3f9-176757cbc8fc.

Base: 23a54e1
Head: 09f7fb3

FINDINGS — Low: Spark preservation tests need a prior _calculation_id and _calculation_execution; their real fields currently stay unset and would not expose a misplaced reset. The reviewer also flagged internal-cursor test cleanup at lines 272-284. The specific claim that factory failure triggers UnboundLocalError is rejected: the factory call is outside try. Its actual failure path skips connection cleanup, and will be repaired with guarded cleanup. Tests/builds/AWS were not executed by the independent reviewer; the author verified these findings separately.

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 repair record.

Current reviewed head: f2460c5

Spark preservation tests now seed the real calculation ID/execution fields. Internal-cursor creation runs inside guarded cleanup, and the prior result mock explicitly models is_closed=False. The original UnboundLocalError claim was rejected with source evidence; skipped connection cleanup was repaired.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread pyathena/polars/result_set.py Outdated
allowed |= keyword_parameters(polars.read_csv) - {"source"}
# Polars still accepts these renamed parameters through its reader decorators.
for old, new in {
"dtypes": "schema_overrides",

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 repair follow-up — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: d2f8dd1e-ff00-4e4d-8d1f-cff51dab65ab.

Base: 23a54e1
Head: 0241fc2
Previous reviewed head: 09f7fb3

FINDINGS — Medium: The newly accepted dtypes alias collides with automatically supplied schema_overrides in both CSV readers. Polars rejects receiving both names; the result set wraps the failure as OperationalError after AWS execution. Verified against the installed reader decorators and default-merging code. This reproduces a base behavior but is a regression from the previous strict validation head. Preserve the alias by letting it replace inferred schema defaults, including renamed columns and timestamp fallback.

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 repair record.

Current reviewed head: f2460c5

The dtypes alias replaces the automatically supplied schema_overrides default and follows the same column-renaming and timestamp-error rules as the new name. Valid eager/scan aliases preserve leading-zero strings and renamed columns; paired invalid-type cases preserve OperationalError. Repaired in fbb2832 and 6d6fe2f.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread pyathena/pandas/cursor.py
on_start_query_execution=on_start_query_execution,
result_set_type_hints=result_set_type_hints,
)
operation, unload_location = self._prepare_unload(operation, options.s3_staging_dir)

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 repair follow-up — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: d2f8dd1e-ff00-4e4d-8d1f-cff51dab65ab.

Base: 23a54e1
Head: 0241fc2
Previous reviewed head: 09f7fb3

FINDINGS — Low: Moving reset below local option resolution and UNLOAD preparation keeps an old result after valid-keyword calls fail with an empty query or missing staging directory. Equivalent sync/native-async Polars and pandas methods are affected. Restore the previous reset on preparation failure while keeping keyword failures state-preserving. Future-based cursors retain their original no-reset behavior.

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 repair record.

Current reviewed head: f2460c5

Preparation failures now restore the original reset/close behavior for sync and native-async cursors; keyword errors keep prior state and take precedence over local preparation errors. Future-based cursors retain their original no-reset behavior. All 48 combinations of cursor style, failure cause, and unknown-name presence pass.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread pyathena/polars/result_set.py Outdated
"""
import polars as pl

validate_execute_kwargs(

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 repair follow-up — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: d2f8dd1e-ff00-4e4d-8d1f-cff51dab65ab.

Base: 23a54e1
Head: 0241fc2
Previous reviewed head: 09f7fb3

FINDINGS — Low: Scan-specific CSV validation is inside a one-shot generator. With a chunked S3 result and eager-only columns=, the first read raises TypeError, then subsequent reads silently return empty results. Move actual scan validation to result-set/iterator construction before creating the generator. This should fail execute(), await execute(), or future.result() deterministically. The initial repair also receives two minor nits: make the TypeError test match the offending name and fix its docstring line wrap; these will be addressed.

The reviewer covered the bounded repair and relevant source/tests/dependency contracts, ran no tests/code/AWS, and found no other actionable issues. Snapshot hashes and worktree status confirm no reviewer edits.

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 repair record.

Current reviewed head: f2460c5

Actual scan-reader validation now runs in _create_dataframe_iterator before the generator exists. Invalid eager-only options fail result-set creation; repeated checks keep raising the same TypeError rather than exhausting a generator and returning empty rows. Repaired in fbb2832; repeated-error tests pass.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

Comment thread pyathena/polars/result_set.py Outdated
# Athena writes up to 12 fractional digits, which Polars does not parse
# into a Datetime whose time unit holds fewer, so the data is read again
# with the timestamp columns as text.
kwargs.pop("dtypes", 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.

Relayed independent static repair review — Claude Code, claude-opus-5-5, Max profile, effort high, firstParty (not Enterprise). Session: 31c82e42-b828-45b6-ba27-9360c35a324a.

Base: 23a54e1
Head: 7496dcd
Previous head: 0241fc2

FINDINGS — Medium: Timestamp rereading still treats dtypes differently from schema_overrides. _get_timestamp_dtypes skips fallback for the new name but not the alias; the new pop then discards the user’s requested types. A timestamp result with dtypes={"v": pl.Int64} and non-numeric v silently returns strings instead of raising. Datetime("ms") is likewise replaced with inferred microseconds; renamed timestamp columns can be left as raw strings. Verified against the guard at pyathena/polars/result_set.py:495 and the new pop at line 661. Add dtypes to the guard, remove the pop, and assert the same reader failure under both names.

No other actionable defects were found in the bounded update. Covered alias/default merging, early iterator validation and repeated errors, all six preparation-failure paths and error priority, converter pinning, test assertions, and timing claims. Static review only: no tests, Polars, or AWS were executed. Snapshot hashes are unchanged. Two non-blocking observations are recorded: the new iterator test label is stale and will be corrected; preparation exceptions remain as TypeError traceback context, while the correct primary exception and state behavior are preserved.

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 repair record.

Current reviewed head: f2460c5

Added dtypes to the explicit-override fallback exclusion and removed the destructive pop. Paired old/new-name cases cover milliseconds instead of inferred microseconds, invalid Int64 text, and renamed timestamp columns. Both names preserve reader failures. Repaired in 6d6fe2f.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

GetQueryResults rows are mocked.
``with_column_names`` is a scan-only option and is rejected by the eager reader.
Explicit schema overrides, including the dtypes alias, retain reader errors
instead of retrying with inferred types.

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 static dtype follow-up — Claude Code, claude-opus-5-5, Max profile, effort high, firstParty (not Enterprise). Session: 0118e756-fe6c-404b-a37c-b35e4f8abc74.

Base: 23a54e1
Head: 6d6fe2f
Previous reviewed head: 7496dcd

FINDINGS — Low wording inaccuracy only: tests/pyathena/polars/test_result_set.py:366-367 calls the old fallback types inferred. The fallback uses Athena metadata types, so the wording should say metadata types. No regressions were found in changed code.

Covered explicit dtype/alias exclusion, safety of removing the pop, preservation of caller types and errors, renamed columns, unchanged default fallback, real old/new-name test pairs, iterator test label, and dependency versions. Review was static: no code, tests, Polars or AWS were executed; parser error expectations were inferred from primary Python source and existing tests. Snapshot hashes and clean worktree status confirm no reviewer edits.

A possible full-schema parameter interaction was identified as pre-existing and unverified without the Rust parser; it is outside this keyword-validation repair and is not reported as a new defect.

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 repair record.

Current reviewed head: f2460c5

Replaced the ambiguous type-source description with the directly checked contract: schema_overrides/dtypes disable timestamp retries and preserve reader errors. Only two docstring lines change in f2460c5. The final independent wording follow-up is CLEAN.

Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready.

reads them, so they are not, and the read fails as before. No AWS calls; the
GetQueryResults rows are mocked.
``with_column_names`` is a scan-only option and is rejected by the eager reader.
The ``schema_overrides`` option and its ``dtypes`` alias disable timestamp

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 chain completed — CLEAN for the final wording repair.

Reviewer: Claude Code, claude-opus-5-5, Max profile, effort high; verified claude.ai/firstParty Max authentication (not Enterprise). Final session: d0876b58-8de1-4fd7-87e5-599db1db68f5.
Base: 23a54e1
Head: f2460c5
Previous head: 6d6fe2f

The initial full review and bounded repair reviews are recorded in their finding threads. Every verified finding has been repaired; the previous dtype follow-up found no code regression, and this final follow-up confirms the corrected option-name, retry-disable, and error-preservation claims. All earlier patch-series entries are unchanged. Both self-review perspectives also completed each repair.

The final reviewer was limited to Read/Grep/Glob and this clean snapshot, without edits, execution, tests, GitHub, PR context, commit messages, memory, or credentials. A Glob attempt against the parent temporary directory was denied; the review completed using permitted snapshot files. Snapshot hashes are unchanged, and the PR worktree remains clean at the reviewed head. This is static review, not AWS or runtime validation. The author has 486 offline passes; current-head AWS CI remains to run after Ready.

Actual narrow result:
CLEAN. I found nothing wrong with the two changed docstring lines (tests/pyathena/polars/test_result_set.py:366-367).

What I checked

  • "disable timestamp retries": _get_timestamp_dtypes (pyathena/polars/result_set.py:494-499) returns {} if schema_overrides or dtypes is a key in self._kwargs. Then _read_csv (pyathena/polars/result_set.py:657-660) re-raises instead of reading the file again with the timestamp columns as text (:661-668). So either option on its own turns the retry off. It's a presence check, which fits calling them "options".
  • "dtypes alias": the alias mapping is at pyathena/polars/result_set.py:76-83. In _read_kwargs (:377-378), passing only dtypes drops the default schema_overrides, so dtypes is what reaches Polars. Both names are spelled exactly as in the code.
  • "preserve reader errors": the original ComputeError is re-raised and caught at :672-674, then raised again as OperationalError(*e.args) from e. The message and cause are kept, wrapped in the DB-API error type. That's what the tests check: all six dtypes/schema_overrides cases (:345-356) expect OperationalError matching "could not parse", and the plain {} case (:344) shows the retry working when neither option is set.
  • Coverage: the claim holds whether the bad column is the timestamp t, the non-timestamp v (Int64 on "x"), or a renamed column with new_columns. The retry is disabled in each case, no matter which column caused the error.

Not reviewed, as you asked: the unchanged docstring lines (including the with_column_names sentence), runtime behavior, and entries 1–5 of the series.

Limitation: this was a static review of the snapshot only. I didn't run anything or check which exception types and messages the installed Polars actually raises, so "could not parse" and the ComputeError path were taken from the source and test assertions, not seen at runtime.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 5, 2026 17:54
@laughingman7743
laughingman7743 marked this pull request as draft October 7, 2026 00:11
Comment thread pyathena/common.py Outdated
if not getattr(self, "_unload", False) or not isinstance(operation, str):
return False
prefixes = ("UNLOAD",) if prepared else ("SELECT", "WITH", "UNLOAD")
return operation.strip().upper().startswith(prefixes)

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 DRY follow-up: CLEAN. Merge-base 23a54e1; published head 5b48a62; previous reviewed head f2460c5. Both old objects exist and the earlier patch entries are unchanged. Covered the shared predicate, all six pandas/Polars sync/Future/aio delegates, preparation success/failure, supported and unknown reader names, exceptions, previous-result cleanup, and the added public execute-path tests. SQL normalization and the distinct prepared/unprepared prefixes remain consistent with the previous paths; validation/reset/AWS ordering is unchanged for the supported contracts. No new imports or optional dependency requirements. just format and just lint passed; focused offline coverage passed 510 tests with three expected dtypes deprecation warnings. Full updated-head AWS validation remains pending.

Comment thread tests/pyathena/test_kwargs.py Outdated
options = {"use_threads": False} if unload_reader else {"sep": "|"}
try:
with pytest.raises(ProgrammingError, match="s3_staging_dir is required"):
await _execute(cursor, operation=operation, **options)

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 DRY follow-up: CLEAN. Merge-base 23a54e1; published head 5b48a62; previous reviewed head f2460c5. Audited claims separately: the helper handles raw SELECT/WITH/UNLOAD only before preparation and prepared UNLOAD afterward, requires the cursor's unload setting, and tolerates invalid input during preparation-error validation. Existing supported reader options, return shapes, callback handling, error precedence, and no-AWS-on-unknown-name guarantees retain their call ordering. The 24 added cases check accepted library names on staging failure across all six execution variants; existing tests cover wrong-mode names, invalid operation/options, state preservation, and successful preparation. Current origin/master changes on these affected paths only make existing public parameters keyword-only and do not alter the new helper contract. Static compatibility review and 510 offline passes are distinct from AWS evidence; previous CI belongs to f2460c5 and is not claimed for this new 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.

Final validation for the DRY refactor completed on reviewed head 5b48a62. AWS CI https://github.com/pyathena-dev/PyAthena/actions/runs/37551498860 succeeded on Python 3.14.7: just test pyathena (3,891 passed, 12 skipped), just test sqla (589 passed, 759 skipped), and just test sqla-async (589 passed, 759 skipped). Current-head lint, documentation lint/build, license checks, and offline CI also passed. The PR remains Ready, mergeable, and has mergeStateStatus=CLEAN; the published head matches the reviewed/CI head and the worktree is clean. This completes the current-head AWS evidence previously recorded as pending; the independent review remains a separate static CLEAN verdict within its bounded scope.

Comment thread pyathena/common.py Outdated
if not getattr(self, "_unload", False) or not isinstance(operation, str):
return False
prefixes = ("UNLOAD",) if prepared else ("SELECT", "WITH", "UNLOAD")
return operation.strip().upper().startswith(prefixes)

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 within the bounded appended DRY update. Reviewer: Claude Code, actual canonical model claude-opus-5-5, firstParty provider, Max profile (verified claude.ai subscriptionType=max; not Enterprise), effort high. Session: 25c5728f-9356-4a84-957a-5e9c5bda0d8b. Old/new merge-base: 23a54e1; old head: f2460c5; new published head: 5b48a62. Range-diff confirms the previous six patch entries are unchanged. Covered the new predicate and twelve delegates across all six pandas/Polars execution variants, the added staging-failure tests, and necessary formatter/result-set/validator/MRO/state-management context. No actionable introduced defects found. The non-string return edge case requires a formatter violating its declared return contract; the pre-preparation assumption about custom formatter prefixes predates this update. The added tests specifically exercise preparation failure; existing tests also cover successful preparation and DESCRIBE. Static review only with Read/Grep/Glob: no commands, tests, builds, GitHub, memory, credentials, or subagents. Installed reader signatures were not separately inspected by the reviewer; author's 510 offline passes provide distinct runtime evidence. The exported snapshot excludes .env/.git, hash verification passed, and the PR worktree remained clean.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 7, 2026 00:21
@laughingman7743
laughingman7743 marked this pull request as draft October 9, 2026 02:31
Comment thread pyathena/_kwargs.py Outdated
from typing import Any


@lru_cache(maxsize=64)

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 — performance follow-up: CLEAN.

Review merge-base: 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f; published head: 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee.
Previous reviewed head: 5b48a622667322689dee3fc973a6d1c26e3e8353 at the same merge-base. Both old objects exist; git range-diff confirms all seven previous commits unchanged and one appended performance commit.

Covered all nine changed files and traced the leaf signature cache into pandas/Polars validators and synchronous/async SQLAlchemy internal cursor creation. Checked immutable cached names, fresh returned sets, bounded retention, unhashable-callable fallback, constructor replacement, all six reader cursor variants, preparation failures, validation-before-state/AWS ordering, effective chunk settings, and Unicode/whitespace prefix semantics. No actionable regressions found; there are no public signature or reader-option changes in this follow-up.

Validation: just format, just lint (including mypy, license headers, and CloudFormation validation), and the focused offline suite passed: 538 tests, three existing Polars dtypes deprecation warnings. Tests cover inspection reuse without mutable-set contamination, unhashable readers, constructor replacement, explicit Unicode classification cases, and skipping classification with empty keywords in both preparation-success and failure paths across all six variants. Prior reader-option and state-preservation coverage still passes.

Limits: author self-review, not independent review. Local tests block AWS requests. Cold cache misses still inspect signatures; in-place signature mutations require the documented private cache clear. Current-head AWS CI remains pending.

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.

Design consultation: Claude Fable 5.1 (claude-fable-5-1), effort high, verified first-party max profile (not Enterprise). Session: e756833b-6cc3-40cb-94f1-c69ad71443d0; completed successfully with the requested actual model/provider.

Read-only consultation on the tracked-source snapshot at merge-base 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f, previous head 5b48a622667322689dee3fc973a6d1c26e3e8353, plus the separate native keyword-only patch and local benchmark artifacts. Read/Grep/Glob only; no edits, commands, tests, GitHub access, credentials, memory, or subagents.

The reviewer suggested bounded signature-name caching, an empty-keyword fast path, and inspecting the SQL prefix. The authored follow-up implements these with immutable cache entries and fresh sets, retains the constructor MRO walk, and preserves existing Unicode uppercase semantics. The separate keyword-only API change does not remove the previous repeated introspection if reverted, so it remains outside this repair. Measurements and limits are recorded in the second self-review and PR description.

This was a design consultation before implementation, not the independent review of the resulting patch. The separate final Opus 5.5 Max static review is in progress.

Comment thread pyathena/common.py Outdated
*reader_args: Additional reader settings needed by the validator.
prepared: Whether ``_prepare_unload()`` completed.
"""
if kwargs:

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 — performance follow-up: CLEAN.

Review merge-base: 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f; published head: 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee. Verified the frozen snapshot, published-head equality, both previous objects, and the seven unchanged commits plus one appended repair against previous head 5b48a622667322689dee3fc973a6d1c26e3e8353.

Claim inventory: installed-reader option compatibility; validation before state mutation/AWS; immutable bounded caching and replacement behavior; empty-keyword fast paths; prefix classification; measured performance; the separation from native keyword-only API binding; and actual review/CI status. Audited all nine repair files, their constructor/reader/reflection callers, unchanged cursor documentation, the PR description, commit, benchmark scripts, and raw measurements.

The cache stores signature-derived names, not query results, AWS metadata, or user values. Fresh sets isolate per-mode removals and Polars aliases; retaining the MRO walk observes replaced constructors. Optional dependency imports remain lazy. A miss still performs introspection, unhashable callables are uncached, and same-object signature mutation requires the documented explicit clear. All six variants retain preparation-error precedence, supported reader options, and their existing sync/async/Future state behavior. Prefix checks preserve existing whitespace/case/Unicode semantics; normal formatter processing remains unchanged.

Measured warm CPU costs on the same Python/library versions, 2,000 calls per repeat, seven repeats, median: pandas CSV validation 39.874 -> 1.107 microseconds; constructor-name discovery 153.367 -> 4.623; 100 KiB SQL predicate 32.556 -> 0.319. Actual synchronous execute paths with AWS and result readers replaced by fakes: pandas CSV/dtype 54.815 -> 11.921, pandas UNLOAD/use_threads 61.697 -> 25.889, chunked Polars CSV 23.480 -> 12.648. These establish local CPU improvements, not AWS end-to-end latency or first-import performance. Short-string predicate overhead is not claimed to disappear; empty execute keywords bypass it entirely.

The separate merged keyword-only change uses native Python binding and contains no runtime signature inspection. Its isolated binding experiment measures roughly 0.01–0.07 microseconds of additional cost for the tested signatures, not zero cost and not complete API latency. Reverting it would leave this PR's former repeated introspection intact; no revert is included.

Validation: 538 focused offline tests and full local lint passed. Current Draft lint/license/offline checks passed; documentation build and independent review are pending at recording time. Prior-head AWS results are explicitly not presented as validation of this head. All three current-head AWS suites, including SQLAlchemy and Spark selection from the core path filters, must succeed after Ready. No AWS retry or request-count reduction is introduced. Limits: author claim/operational review, not independent review or live AWS evidence.

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.

Final performance-follow-up validation: all applicable checks passed on published/reviewed head 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee, merge-base 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f.

Ready-triggered Test run 37877232431, Python 3.14.8: just test pyathena 3,919 passed / 12 skipped; just test sqla 589 passed / 759 skipped; just test sqla-async 589 passed / 759 skipped. All three AWS jobs completed successfully, including the PyAthena suite's selected SQLAlchemy and Spark coverage. Draft-run skipped AWS jobs are expected from workflow conditions; they are superseded as validation by this complete Ready-triggered run.

Local format/full lint and 538 focused offline tests passed; current-head documentation lint/build, license, and offline CI passed. Both author review perspectives and the separate Opus 5.5 Max independent static review are CLEAN for this follow-up. The frozen source snapshot and PR worktree remained unchanged.

Final GitHub state: Ready (isDraft=false), MERGEABLE, merge state CLEAN, exact reviewed head. Source worktrees are clean. Performance claims remain limited to the explicitly scoped local benchmarks; no AWS end-to-end performance improvement is inferred from CI duration. PR description updated with the final validation.

Comment thread pyathena/common.py Outdated
start = _OPERATION_START.search(operation)
if start is None:
return False
return operation[start.start() : start.start() + 6].upper().startswith(prefixes)

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 static review — performance follow-up: CLEAN.

Reviewer: Claude Opus 5.5 (claude-opus-5-5), effort high, verified first-party max profile (not Enterprise). Session: 530606ba-826d-4f2e-a6ca-432ca0e81e9d. The CLI completed successfully; actual model and provider metadata match the request.

Review merge-base: 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f; published head: 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee. Previous reviewed head: 5b48a622667322689dee3fc973a6d1c26e3e8353, same merge-base; both old objects verified and all seven prior commits unchanged in the range-diff. The clean tracked-source snapshot, literal repair/full-series diffs, and source/test contracts were supplied without PR description/discussion, commit messages, author conclusions, credentials, or memory. Read/Grep/Glob only; no edits, execution, tests, GitHub access, or subagents. Snapshot hashes and the clean PR worktree were verified unchanged afterward.

Covered all nine appended-update files, caching/constructor MRO callers, pandas/Polars reader-name mutations, sync/async SQLAlchemy internal cursors and reflection, Unicode/whitespace classification, prepared/unprepared reader routing, effective chunk settings, empty-keyword/preparation-failure paths, state/AWS ordering, and regression assertions. No actionable defects found.

The reviewer noted non-blocking cache-clear wording, the fixed six-character prefix width, the lack of an inspection-count assertion in the unhashable-callable test, and bounded retention of constructor functions/classes. These are retained in this patch: current prefixes are all at most six characters and explicitly tested; the fallback regression verifies unhashable callables remain accepted; retention is capped at 64 callables. The private cache-clear instruction is intentional for same-object signature mutation. None changes the CLEAN verdict.

Limits: static source review, not runtime or AWS proof. The reviewer did not execute against installed reader signatures or free-threaded Python; local installed-library coverage and CPU measurements are separate author validation. Current-head Ready-triggered AWS CI remains to be collected.

Actual reviewer result follows:

Result: CLEAN

I found no actionable defects in the appended update (patch 8, 5b48a622… → 5c96c925…). The range-diff shows patches 1–7 are unchanged, and the snapshot files match repair.patch and review.patch.

What I covered

Signature caching (pyathena/_kwargs.py:16-46)

  • Cache keys and replacement: functions and the object.__init__ slot wrapper are cached by identity. The cache holds a strong reference to each cached callable, so its id can't be reused while the entry exists. Assigning a new __init__ creates a new key, as the docstring says.
  • Mutation isolation: every call returns set(frozenset), a fresh set. So the in-place edits in the validators (allowed -= …, |= and .add(old) at pandas/result_set.py:56-67, polars/result_set.py:71-84) and names.update(...) at _kwargs.py:61 never touch the cache.
  • Unhashable callables: if hash() raises TypeError, the code calls the undecorated function directly, so nothing is cached and the error isn't hidden.
  • Exceptions: lru_cache does not cache them, so signature() errors reach the caller as before.
  • Concurrency: lru_cache is thread-safe. At worst two threads compute the same value twice.
  • Optional dependencies: the module still uses only the standard library. import_module("pandas")/("polars") still only runs when keywords are present. Python ≥3.11 (pyproject.toml:17) supports frozenset[str] and __wrapped__.
  • Callers: Connection._internal_cursor (connection.py:645-652), the async adapter (aio/sqlalchemy/base.py:279-282) and SQLAlchemy reflection (sqlalchemy/base.py:539, 566, 674) only subtract or read fresh sets, so their behavior is unchanged.

UNLOAD classification (common.py:1198-1215)

  • Whitespace: Python's regex \s and str.strip() use the same whitespace definition (str.isspace()), so the first non-whitespace position is identical. That holds for U+2003 and U+001C, and U+FEFF is excluded in both.
  • Unicode case: str.upper() maps each character independently (only lower/casefold look at context) and never shortens text. So uppercasing the first 6 characters gives a prefix of the full uppercased string with length ≥ 6. With the longest prefix being 6 characters (SELECT/UNLOAD), the result is exactly the same as the old strip().upper().startswith(...), including ſ→S, ı→I and İ unchanged. Trailing whitespace stripping can't matter because no prefix contains whitespace.
  • Prepared vs. unprepared: results agree with Formatter.wrap_unload (formatter.py:175-179) and with its "\nUNLOAD (" output.
  • Performance: cost is now proportional to leading whitespace plus 6 characters, instead of the whole string.

Skipping validation when no keywords are passed (common.py:1217-1240, and the six execute methods: pandas/cursor.py:228-242, pandas/async_cursor.py:257-269, aio/pandas/cursor.py:197-211, polars/cursor.py:219-235, polars/async_cursor.py:260-274, aio/polars/cursor.py:177-193)

  • Both validators already returned immediately for empty kwargs (pandas/result_set.py:51, polars/result_set.py:64), and _is_unload_query can't raise. So skipping only removes the SQL scan; it doesn't change any outcome.
  • Polars still passes the effective chunk size, kwargs.get("chunksize", self._chunksize), on both paths.
  • The order of validation, _reset_state() and _execute (the AWS call) is unchanged. Synchronous and asyncio cursors still reset state on a preparation failure; Future-based ones don't.
  • When an unknown keyword hits the failure path, the TypeError is still raised with the original error attached as its context, and state isn't reset. This behavior comes from patch 7, not this update.

Tests (tests/pyathena/test_kwargs.py:68-134, 188-213)

  • The cache test patches the module-level signature, which the cached helper looks up at call time, and uses a fresh local function, so the "called once" assertion is valid.
  • The classification table's expected values match the reasoning above.
  • The skip test would fail on the old code, which called _is_unload_query unconditionally. patch.object on the asyncio _execute uses an AsyncMock.

Non-blocking notes (not defects)

  • _kwargs.py:37: "require clearing …cache_clear()" should read "require calling". The docstring also exposes a private helper.
  • common.py:1215: the hard-coded 6 relies on the longest prefix being 6 characters. Adding a longer prefix later would silently produce wrong results.
  • test_kwargs.py:81-90: the unhashable test doesn't check that the cache was bypassed (e.g. that signature ran twice).
  • The cache keeps up to 64 __init__ functions alive. Ones that use super() also keep their class alive. This is bounded and harmless.

Limitation

This is a static review only. I used Read, Grep and Glob within this snapshot, and ran no code or tests. The Unicode, re/str.isspace() and lru_cache conclusions come from my knowledge of CPython, not from running anything. I didn't check behavior against the installed pandas, Polars or pyarrow signatures, or under free-threaded Python.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 9, 2026 03:00
@laughingman7743
laughingman7743 marked this pull request as draft October 9, 2026 13:55
Comment thread pyathena/common.py Outdated
*reader_args,
)

def _prepare_reader_query(

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.

Reader-preparation follow-up — self-review round one: CLEAN.

Literal current review range: f3532e9fb57fd769441e533f0b8375fd854ece69..3daaedf06100384cde8eea65d438069539c6ecc7.
Previous completed review range: 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f..5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee.
Both old objects were verified. The bounded patch-series comparison shows all eight previously reviewed commits unchanged and one added reader-preparation commit (769b115c3d9b00bdd9764f8d8746fcde54458711). Current master was merged without conflicts; affected upstream contracts were inspected rather than treating the previous CI as current evidence.

Coverage: all 12 follow-up files (BaseCursor helper, six pandas/Polars sync/Future/aio execute methods, both reader validators, kwargs and both result-set regression files), necessary unchanged formatting/execution/result-set callers, and upstream keyword-only signatures and ExecuteOptions resolution. The earlier full-series coverage remains bounded by its recorded scopes; this is a repair and integration follow-up, not a claim of executing every backend locally.

The shared helper resolves the same fields, prepares once, and validates once on each normal/preparation-failure path. Successful preparation selects the actual returned operation's reader; failed preparation selects the original operation's reader. Unknown names still take precedence over preparation errors and preserve prior state. Valid preparation failures reset synchronous/aio state; Future variants retain their existing state. KeyboardInterrupt/SystemExit bypass Exception handling and preserve state. Callback and type-hint options, None/false/zero behavior, and public execute signatures are preserved.

Immutable backend cursor-setting sets allow validators to return before optional module lookup/signature inspection only when every supplied name is already a PyAthena-owned setting. Mixed reader options or unknown names still take the installed-reader validation path. No module or accepted-user-value cache was introduced. Replaced readers remain detectable by their callable identity; in-place signature mutation retains the existing explicit cache-clear requirement.

Validation on this published tree: just format, just lint, just docs lint, and just docs build passed. Focused offline pytest: 682 passed, three existing dtypes deprecation warnings. New regressions cover custom prepared-mode changes, interrupts, import omission, and reader replacement; existing six-variant failure/reset, supported-option, callback, and constructor cases remain covered. No actionable findings or repairs from this round. CPU measurements, independent static review, and current-head AWS CI remain separate pending evidence.

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.

Round-one repair follow-up: CLEAN at bb993fa8c3985bee117006f07c6912517d9bdea6, merge-base f3532e9fb57fd769441e533f0b8375fd854ece69.

Inspected git diff 3daaedf06100384cde8eea65d438069539c6ecc7..bb993fa8c3985bee117006f07c6912517d9bdea6 plus its six execute callers and regression tests. The single changed file folds the unreleased _validate_reader_kwargs helper into shared preparation, removing a nested call without changing the two distinct reader-mode decisions, error precedence, kwargs forwarding, empty-keyword guard, or reset/interrupt behavior. The previous full follow-up coverage and inspected upstream contracts remain applicable.

just format, just lint, and the same focused offline suite passed on this final source: 682 passed, three existing dtypes warnings. Local docs lint/build passed on the integrated predecessor; documentation sources are unchanged by this one-file repair, and final-head docs CI is collected separately. No actionable findings. The 103-case final measurements keep the same upstream on both sides and explicitly report remaining default/reader-option cost, as well as cursor-setting-only improvements. Independent review and final-head AWS CI are still pending.

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.

Round-one native-import follow-up: CLEAN.

Current merge-base/head: f3532e9fb57fd769441e533f0b8375fd854ece69..c42d65d04a54d3fe1b5cb90122635b495a64d09d.
Inspected git diff bb993fa8c3985bee117006f07c6912517d9bdea6..c42d65d04a54d3fe1b5cb90122635b495a64d09d: both reader validators and both result-set test files, plus the six shared-preparation callers and unchanged optional-reader/option/formatter contracts. The earlier complete follow-up and upstream coverage is retained; the three new preparation/import commits are separately auditable and all eight original-series commits remain unchanged.

The validators use native optional imports instead of import_module. Imports remain inside the dependency-requiring path after the empty/cursor-setting-only guard; no eager module import or separate module-pointer cache was added. Each validation still reads the live callable attribute and selects the installed reader signature. Polars' explicitly annotated Callable variable allows the CSV/Parquet callable union to be inspected without changing runtime behavior. The dynamic pandas CSV-engine name import elsewhere is unchanged because its name is selected at runtime.

The import-omission tests now observe builtins.import and reader signature inspection, so they cover the native-import path rather than mocking a removed import_module alias. CSV reader replacement tests for pandas and Polars remain, and added PyArrow read_table replacement coverage accepts the new name and rejects the removed name. Existing failure precedence, state/reset/interrupt cases, supported options and callbacks remain covered.

Final-source just format and just lint passed. The same focused offline suite plus the new Parquet replacement case passed: 683 passed, three existing Polars dtypes deprecation warnings. No actionable findings or additional repairs. Independent review is not counted as completed: the previous Max attempt hit its weekly limit; the final snapshot is prepared for a retry after the reported reset. Final-head AWS validation is still pending.

Comment thread pyathena/common.py Outdated
return False
return operation[start.start() : start.start() + 6].upper().startswith(prefixes)

def _prepare_reader_query(

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.

Reader-preparation follow-up — self-review round two: CLEAN.

Literal base/head: f3532e9fb57fd769441e533f0b8375fd854ece69..bb993fa8c3985bee117006f07c6912517d9bdea6.
Previous reviewed base/head: 23a54e16ecfe7cf8f64f42193d92cbb44f86cf2f..5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee. Patch-series comparison preserves all eight previously reviewed commits and adds the two bounded reader-preparation changes; current-master integration was checked for affected contracts.

Claim inventory and falsification checks: early rejection versus cursor state/AWS start, supported pandas/Polars reader names and modes, PyAthena-owned names, imported-reader replacement and lazy optional dependencies, callback/hint/value preservation, sync/Future/aio error ordering and interrupts, keyword-only public signatures after upstream integration, and benchmark attribution/performance limits. Traced all 12 repair files and their upstream/unchanged option, formatting, execution, result-reader, and constructor/reflection callers. Checked the current PR's complete changed-file inventory for expanded contracts; the unchanged initial series remains covered by its prior records rather than relabeled as a new full implementation pass.

The synthesized comparison tree merges the previous published validation head with the exact same current master used by the candidate, including #1110. It is identified as a tree, not presented as a published revision. Checked the 103-case harness, nine fresh-process samples per cohort, alternating order, actual import identities, retained BaseCursor._execute and formatting, fake provider/reader/scheduling boundaries, 2000 versus 100 iteration counts, warm-up/GC behavior, and all median/min/max calculations. No AWS performance extrapolation or statistical significance claim is made. Additional shared-preparation cost on default/reader-option paths is disclosed; measurements support cursor-setting-only savings, not a general speedup.

The new immutable cursor-name sets bypass module/signature discovery only for names already handled by PyAthena. Reader replacement still selects a new callable-signature cache entry; no separate module cache or cached mutable options were added. Optional-dependency availability and reader values remain lazy library concerns, not a promised preflight. Ordinary import_module calls use Python's existing module cache and initialization synchronization.

Supported kwargs preserve the result-reader routing, including managed chunked Polars eager fallback and deprecated dtype aliases. Invalid names still fail before AWS/state changes; valid preparation failures keep the existing sync/aio reset versus Future preservation distinction. KeyboardInterrupt/SystemExit remain outside Exception handling. Upstream ExecuteOptions direct construction and keyword-only bindings remain intact. No requests, retry settings, polling or cancellation policy changed.

Final-source format/lint and 682 offline tests passed (three existing dtypes warnings). Docs sources were unchanged by the final one-file repair; local docs checks belong to the integrated predecessor and current-head docs CI is recorded separately. Earlier-head AWS CI is explicitly historical; final-head AWS coverage is not yet complete. No actionable findings, no additional repairs, and no claimed end-to-end runtime proof from this static/CPU audit.

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 status: INCOMPLETE at head bb993fa8c3985bee117006f07c6912517d9bdea6, merge-base f3532e9fb57fd769441e533f0b8375fd854ece69.

The first-party Max account/profile was verified (loggedIn=true, authMethod=claude.ai, apiProvider=firstParty, subscriptionType=max). Requested model/effort: claude-opus-5-5, high. The bounded tracked-source snapshot excluded .git, .env, credentials, memory, PR discussion and prior review conclusions; Read/Grep/Glob only, safe/restricted modes, no code execution or writes.

CLI session 32048682-a26a-448f-b8b6-d2982de6789d exited 1 with is_error=true, empty modelUsage, and the actual result: "You've hit your weekly limit · resets 12am (Asia/Tokyo)". This attempt did not produce a review verdict and is not counted as CLEAN. The frozen snapshot and worktree were verified unchanged. The PR stays Draft while waiting for the requested Max review to become available; no replacement model/account was used.

Both self-review rounds, the final 682-test offline run, and the recorded 103-case CPU measurements remain completed. Current-head Docs lint/build, Test lint, license headers and benchmark tooling offline passed; Draft AWS skips are intentional. Current-head AWS validation follows the completed independent review and Ready transition.

Comment thread pyathena/pandas/result_set.py Outdated

Raises:
TypeError: If an argument is not supported by the selected reader.
"""

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.

Reader-preparation and optional-module follow-up measurements.

Published head: bb993fa8c3985bee117006f07c6912517d9bdea6.
Review merge-base/current master: f3532e9fb57fd769441e533f0b8375fd854ece69.
Comparison source: tree b1d87c391dce1484633d1872a900c9cc6a900bec, produced by a conflict-free git merge-tree --write-tree 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee f3532e9fb57fd769441e533f0b8375fd854ece69. This is a synthesized pre-follow-up tree, not a separately published commit. Both cohorts contain the same current master, including merged #1110, so its options optimization is not counted as this follow-up's improvement.

CPython 3.13.1, arm64; pandas 3.0.6, Polars 1.44.2, PyArrow 25.0.1, SQLAlchemy 2.0.46. 103 cases, 9 fresh-subprocess repeats, alternating cohort order, 2,000 iterations per main case. The three default_interrupt_mode cases use 100 iterations. Every sample and its case inventory were checked; the reported ranges are observed minima/maxima, not confidence intervals. timeit disables GC during timing.

Retained: actual connection/cursor/execute bodies, BaseCursor._execute, option resolution, UNLOAD formatting, request preparation, and reader keyword validation. Replaced: SDK session/client construction, AWS submission/polling, result readers, Future worker scheduling, and asyncio reader thread offloads. Future measurements collect the fake executor's immediately produced result and do not measure real executor latency. asyncio batches retain event-loop run_until_complete overhead amortized across the batch. Main cases use kill_on_interrupt=False. Three separately named default_interrupt_mode cases retain start-request thread/task scheduling with True. Setup, imports, and cold signature-cache misses are excluded by preparation/warm-up.

import_module itself is called on every ordinary reader-validation invocation. CPython's _find_and_load checks sys.modules and waits for imports still initializing; the already initialized module is returned without rerunning its code. Module identity was checked against sys.modules for each cached-import case. Warm cached calls measured approximately 0.29–0.30 microseconds. No module cache was added by this patch.

Cursor-setting-only validation now returns before module lookup and signature inspection: pandas CSV median 1.020 -> 0.145 microseconds; Polars CSV 2.368 -> 0.153. Actual CSV execute with cursor settings: pandas 9.271 -> 8.323; Polars 10.406 -> 8.047. These are local CPU-path results.

Shared preparation adds work on other paths: PandasCursor defaults 7.278 -> 7.577; PolarsCursor defaults 7.219 -> 7.258; Pandas CSV reader option 8.814 -> 9.085; Polars CSV reader option 9.168 -> 9.311. Their observed ranges overlap; this is not a claim of equal cost or universal improvement. Default interrupt-mode pandas medians 51.280 -> 52.463, ranges 49.206–59.380 / 47.802–56.425. No speedup is demonstrated for that mode, and no AWS end-to-end latency claim is made.

Only cursor-setting names bypass reader discovery. Mixed reader arguments or typos still inspect the selected installed reader's cached signature. Dependency availability and reader values/combinations remain lazy library concerns; this is keyword-name validation, not an optional-dependency preflight.

All case results (microseconds/call, median and observed range):

Case Before This head
connect_local_session 16.072 [15.759–16.311] 16.121 [15.687–17.053]
Connection_local_session 15.701 [15.471–16.383] 15.861 [15.321–16.351]
AioConnection_local_session 15.995 [15.638–16.649] 16.087 [15.603–17.065]
Cursor.construct_close 12.865 [12.602–13.056] 12.792 [12.679–13.281]
Cursor.execute_defaults 6.533 [6.467–6.965] 6.564 [6.470–6.847]
Cursor.execute_keywords 6.758 [6.640–7.135] 6.724 [6.582–7.117]
Cursor.execute_prebuilt 8.242 [8.087–8.595] 8.144 [7.978–8.552]
AsyncCursor.construct_close 13.231 [13.016–13.582] 13.361 [13.159–14.026]
AsyncCursor.execute_defaults 8.291 [8.004–8.699] 8.145 [8.074–8.633]
AsyncCursor.execute_keywords 8.319 [8.241–8.840] 8.357 [8.235–8.596]
AsyncCursor.execute_prebuilt 9.778 [9.625–10.460] 9.862 [9.674–10.406]
AioCursor.construct_close 12.820 [12.614–13.355] 12.930 [12.577–13.255]
AioCursor.execute_defaults 7.109 [6.939–7.285] 7.138 [6.844–7.342]
AioCursor.execute_keywords 7.236 [7.017–7.486] 7.313 [7.015–7.612]
AioCursor.execute_prebuilt 8.765 [8.384–9.183] 8.836 [8.443–8.992]
PandasCursor.construct_close 7.893 [7.812–8.129] 7.915 [7.798–8.328]
PandasCursor.execute_defaults 7.278 [7.190–7.591] 7.577 [7.340–7.912]
PandasCursor.execute_keywords 7.470 [7.350–7.668] 7.766 [7.559–7.993]
PandasCursor.execute_prebuilt 8.953 [8.782–9.386] 9.250 [8.989–9.537]
AsyncPandasCursor.construct_close 9.449 [9.341–9.742] 9.661 [9.449–10.000]
AsyncPandasCursor.execute_defaults 8.891 [8.649–9.343] 9.199 [8.946–9.526]
AsyncPandasCursor.execute_keywords 9.055 [8.883–9.319] 9.194 [8.914–10.007]
AsyncPandasCursor.execute_prebuilt 10.443 [10.250–10.903] 10.789 [10.422–11.051]
AioPandasCursor.construct_close 7.832 [7.714–8.141] 7.999 [7.776–8.248]
AioPandasCursor.execute_defaults 8.069 [7.936–8.382] 8.388 [8.105–8.703]
AioPandasCursor.execute_keywords 8.270 [8.127–9.525] 8.561 [8.294–9.120]
AioPandasCursor.execute_prebuilt 9.696 [9.544–11.029] 10.013 [9.731–10.514]
ArrowCursor.construct_close 9.014 [8.817–9.473] 9.154 [9.002–9.728]
ArrowCursor.execute_defaults 6.946 [6.741–7.169] 6.982 [6.773–7.479]
ArrowCursor.execute_keywords 7.138 [6.954–7.357] 7.149 [7.072–7.541]
ArrowCursor.execute_prebuilt 8.598 [8.426–8.763] 8.607 [8.413–9.055]
AsyncArrowCursor.construct_close 10.829 [10.662–11.086] 10.903 [10.717–11.460]
AsyncArrowCursor.execute_defaults 8.490 [8.281–8.703] 8.457 [8.309–9.095]
AsyncArrowCursor.execute_keywords 8.616 [8.407–8.858] 8.658 [8.503–8.885]
AsyncArrowCursor.execute_prebuilt 10.088 [9.968–10.506] 10.145 [9.829–10.665]
AioArrowCursor.construct_close 9.069 [8.966–9.516] 9.099 [9.028–9.468]
AioArrowCursor.execute_defaults 7.535 [7.374–7.952] 7.578 [7.345–8.074]
AioArrowCursor.execute_keywords 7.799 [7.555–7.934] 7.761 [7.595–8.131]
AioArrowCursor.execute_prebuilt 9.252 [9.029–9.590] 9.248 [8.932–9.681]
PolarsCursor.construct_close 8.327 [8.184–8.719] 8.254 [8.177–8.386]
PolarsCursor.execute_defaults 7.219 [6.946–7.511] 7.258 [7.163–7.484]
PolarsCursor.execute_keywords 7.462 [7.255–7.744] 7.477 [7.230–7.765]
PolarsCursor.execute_prebuilt 8.942 [8.698–9.180] 8.939 [8.672–9.225]
AsyncPolarsCursor.construct_close 9.919 [9.800–10.146] 10.131 [9.773–10.574]
AsyncPolarsCursor.execute_defaults 8.687 [8.454–9.048] 8.884 [8.577–9.185]
AsyncPolarsCursor.execute_keywords 8.734 [8.561–9.579] 8.944 [8.815–9.508]
AsyncPolarsCursor.execute_prebuilt 10.250 [10.051–10.823] 10.447 [10.176–10.814]
AioPolarsCursor.construct_close 8.291 [8.105–8.784] 8.302 [8.119–8.761]
AioPolarsCursor.execute_defaults 7.790 [7.570–8.330] 7.982 [7.840–8.212]
AioPolarsCursor.execute_keywords 7.878 [7.848–8.204] 8.153 [8.038–8.527]
AioPolarsCursor.execute_prebuilt 9.359 [9.201–10.067] 9.543 [9.395–10.256]
S3FSCursor.construct_close 12.516 [12.379–13.205] 12.689 [12.350–12.823]
S3FSCursor.execute_defaults 6.702 [6.530–7.369] 6.675 [6.553–6.941]
S3FSCursor.execute_keywords 6.872 [6.707–7.278] 6.893 [6.783–7.196]
S3FSCursor.execute_prebuilt 8.334 [8.235–9.182] 8.392 [8.181–8.624]
AsyncS3FSCursor.construct_close 14.413 [14.095–15.789] 14.247 [14.100–14.721]
AsyncS3FSCursor.execute_defaults 8.231 [8.082–8.638] 8.272 [8.116–8.636]
AsyncS3FSCursor.execute_keywords 8.479 [8.154–8.965] 8.398 [8.217–8.703]
AsyncS3FSCursor.execute_prebuilt 9.863 [9.613–10.343] 9.868 [9.668–10.088]
AioS3FSCursor.construct_close 12.629 [12.474–12.818] 12.752 [12.393–12.920]
AioS3FSCursor.execute_defaults 7.370 [7.222–7.570] 7.481 [7.181–7.636]
AioS3FSCursor.execute_keywords 7.537 [7.356–8.034] 7.638 [7.363–7.870]
AioS3FSCursor.execute_prebuilt 9.000 [8.824–9.323] 9.155 [8.796–9.499]
pandas.validate_csv_cursor_settings 1.020 [1.001–1.052] 0.145 [0.141–0.158]
pandas.validate_csv_reader_setting 0.948 [0.932–0.990] 0.970 [0.934–1.052]
PandasCursor.execute_csv_cursor_settings 9.271 [9.088–9.702] 8.323 [8.185–8.598]
PandasCursor.execute_csv_reader_setting 8.814 [8.692–9.165] 9.085 [8.878–9.442]
AsyncPandasCursor.execute_csv_cursor_settings 10.816 [10.721–11.264] 9.835 [9.656–10.035]
AsyncPandasCursor.execute_csv_reader_setting 10.366 [10.298–10.804] 10.675 [10.464–10.971]
AioPandasCursor.execute_csv_cursor_settings 10.049 [9.887–10.463] 9.134 [9.023–9.480]
AioPandasCursor.execute_csv_reader_setting 9.677 [9.443–10.199] 9.894 [9.698–10.056]
pandas.validate_parquet_cursor_settings 1.752 [1.679–1.785] 0.147 [0.141–0.156]
pandas.validate_parquet_reader_setting 1.694 [1.642–1.739] 1.713 [1.650–1.829]
PandasCursor.execute_parquet_cursor_settings 23.136 [22.832–23.969] 21.229 [20.942–22.480]
PandasCursor.execute_parquet_reader_setting 22.589 [22.368–24.055] 23.024 [22.687–25.365]
AsyncPandasCursor.execute_parquet_cursor_settings 24.880 [24.584–26.554] 23.124 [22.610–24.905]
AsyncPandasCursor.execute_parquet_reader_setting 24.597 [24.161–26.236] 24.752 [24.318–26.152]
AioPandasCursor.execute_parquet_cursor_settings 24.066 [23.699–25.253] 22.283 [21.764–23.194]
AioPandasCursor.execute_parquet_reader_setting 23.642 [23.309–25.887] 23.804 [23.598–25.354]
polars.validate_csv_cursor_settings 2.368 [2.326–2.420] 0.153 [0.149–0.157]
polars.validate_csv_reader_setting 2.303 [2.271–2.336] 2.348 [2.321–2.427]
PolarsCursor.execute_csv_cursor_settings 10.406 [10.215–10.677] 8.047 [7.887–8.452]
PolarsCursor.execute_csv_reader_setting 9.168 [9.033–9.424] 9.311 [9.119–9.703]
AsyncPolarsCursor.execute_csv_cursor_settings 12.160 [11.833–12.427] 9.502 [9.408–10.157]
AsyncPolarsCursor.execute_csv_reader_setting 10.917 [10.657–11.233] 11.068 [10.726–11.352]
AioPolarsCursor.execute_csv_cursor_settings 11.161 [10.878–11.405] 8.712 [8.660–8.989]
AioPolarsCursor.execute_csv_reader_setting 9.970 [9.679–10.375] 10.128 [9.773–10.387]
polars.validate_parquet_cursor_settings 1.427 [1.377–1.454] 0.154 [0.151–0.161]
polars.validate_parquet_reader_setting 1.356 [1.308–1.396] 1.400 [1.351–1.462]
PolarsCursor.execute_parquet_cursor_settings 22.465 [22.025–23.664] 20.888 [20.739–21.851]
PolarsCursor.execute_parquet_reader_setting 22.057 [21.596–22.806] 22.336 [22.176–23.065]
AsyncPolarsCursor.execute_parquet_cursor_settings 24.179 [23.922–25.676] 22.671 [22.419–23.385]
AsyncPolarsCursor.execute_parquet_reader_setting 23.845 [23.639–25.134] 24.425 [23.887–25.485]
AioPolarsCursor.execute_parquet_cursor_settings 23.428 [22.633–24.625] 22.040 [21.437–22.524]
AioPolarsCursor.execute_parquet_reader_setting 22.762 [22.458–23.672] 23.304 [22.871–24.456]
cached_import.pandas 0.291 [0.290–0.304] 0.296 [0.291–0.318]
cached_import.polars 0.294 [0.291–0.303] 0.294 [0.290–0.303]
cached_import.pyarrow.parquet 0.296 [0.293–0.305] 0.301 [0.293–0.307]
ExecuteOptions.resolve_none_all_none 1.319 [1.307–1.353] 1.323 [1.310–1.405]
ExecuteOptions.resolve_prebuilt_all_none 2.915 [2.852–3.021] 2.936 [2.877–3.055]
Cursor.execute_default_interrupt_mode 53.435 [50.015–63.977] 51.501 [45.212–59.015]
PandasCursor.execute_default_interrupt_mode 51.280 [49.206–59.380] 52.463 [47.802–56.425]
AioCursor.execute_default_interrupt_mode 51.689 [50.157–71.610] 51.459 [49.755–76.194]
Reproduction and complete measurement harness

Use the same interpreter/dependencies for both source trees. Save the child and driver below as measure-api.py and run-comparison.py under /private/tmp/pyathena-1093-reader-preparation. Export the synthesized baseline, and use a clean checkout of the published head at the driver's worktree path. Write the provided provenance JSON as benchmark-source.json under that temporary directory.

git merge-tree --write-tree 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee f3532e9fb57fd769441e533f0b8375fd854ece69
mkdir -p /private/tmp/pyathena-1093-reader-preparation/before-on-master
git archive b1d87c391dce1484633d1872a900c9cc6a900bec | tar -xf - -C /private/tmp/pyathena-1093-reader-preparation/before-on-master
uv run python /private/tmp/pyathena-1093-reader-preparation/run-comparison.py > results.json

Provenance JSON:

{
  "before_tree": "b1d87c391dce1484633d1872a900c9cc6a900bec",
  "before_inputs": [
    "5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee",
    "f3532e9fb57fd769441e533f0b8375fd854ece69"
  ],
  "after_head": "bb993fa8c3985bee117006f07c6912517d9bdea6",
  "upstream": "f3532e9fb57fd769441e533f0b8375fd854ece69",
  "scope": "Both sources include the same current master, including the merged options optimization. Before is a conflict-free merge-tree of the previous published validation head and that master, exported without a git worktree; after is the current PR head."
}

Child harness:

import asyncio
from concurrent.futures import Future
from importlib import import_module
from inspect import iscoroutinefunction
import json
from pathlib import Path
import platform
import sys
import timeit
from types import SimpleNamespace

source = Path(sys.argv[1]).resolve()
sys.path.insert(0, str(source))
import pyathena
from pyathena.aio.connection import AioConnection
from pyathena.connection import Connection
from pyathena.model import AthenaQueryExecution
from pyathena.options import ExecuteOptions
from pyathena.spark.common import SparkBaseCursor

assert Path(pyathena.__file__).resolve().parent.parent == source
iterations = int(sys.argv[2])

class NoAWSClient:
    def close(self):
        pass

    def __getattr__(self, name):
        raise AssertionError('Unexpected AWS client access: ' + name)

class LocalSession:
    def client(self, *args, **kwargs):
        return NoAWSClient()

class LocalResult:
    def __init__(self, *args, **kwargs):
        self.is_closed = False

    def close(self):
        self.is_closed = True

    @classmethod
    async def create(cls, *args, **kwargs):
        return cls(*args, **kwargs)

class ImmediateExecutor:
    def submit(self, fn, *args, **kwargs):
        result = Future()
        result.set_result(fn(*args, **kwargs))
        return result

    def shutdown(self, *args, **kwargs):
        pass

async def immediate_thread(fn, *args, **kwargs):
    return fn(*args, **kwargs)

asyncio.to_thread = immediate_thread
SparkBaseCursor._start_session = lambda *args, **kwargs: 'session'
query = SimpleNamespace(state=AthenaQueryExecution.STATE_SUCCEEDED)
async def start_async(*args, **kwargs):
    return 'query'
async def poll_async(*args, **kwargs):
    return query

session = LocalSession()
connection_args = dict(region_name='us-east-1', s3_staging_dir='s3://bucket/path/', work_group='wg', session=session, kill_on_interrupt=False)
connection = Connection(**connection_args)
loop = asyncio.new_event_loop()
results = {}

def measure(name, call, count=None):
    count = iterations if count is None else count
    for _ in range(30):
        call()
    results[name] = timeit.timeit(call, number=count) / count * 1e6

def create_connection(factory):
    conn = factory(**connection_args)
    conn.close()

measure('connect_local_session', lambda: create_connection(pyathena.connect))
measure('Connection_local_session', lambda: create_connection(Connection))
measure('AioConnection_local_session', lambda: create_connection(AioConnection))

cursor_definitions = [
    ('pyathena.cursor', 'Cursor'),
    ('pyathena.async_cursor', 'AsyncCursor'),
    ('pyathena.aio.cursor', 'AioCursor'),
]
for backend in ('pandas', 'arrow', 'polars', 's3fs'):
    name = {'pandas': 'Pandas', 'arrow': 'Arrow', 'polars': 'Polars', 's3fs': 'S3FS'}[backend]
    cursor_definitions.extend([
        ('pyathena.' + backend + '.cursor', name + 'Cursor'),
        ('pyathena.' + backend + '.async_cursor', 'Async' + name + 'Cursor'),
        ('pyathena.aio.' + backend + '.cursor', 'Aio' + name + 'Cursor'),
    ])

for module_name, class_name in cursor_definitions:
    module = import_module(module_name)
    cursor_class = getattr(module, class_name)
    for name, value in vars(module).copy().items():
        if name.startswith('Athena') and name.endswith('ResultSet'):
            setattr(module, name, LocalResult)
    if 'Pandas' in class_name:
        import_module('pandas')
    if 'Polars' in class_name:
        import_module('polars')
    if 'Arrow' in class_name:
        import_module('pyarrow')

    def close(cursor):
        if iscoroutinefunction(cursor.close):
            loop.run_until_complete(cursor.close())
        else:
            cursor.close()

    def create_cursor():
        cursor = connection.cursor(cursor_class)
        close(cursor)
    measure(class_name + '.construct_close', create_cursor)

    cursor = connection.cursor(cursor_class)
    if hasattr(cursor, '_result_set_class'):
        cursor._result_set_class = LocalResult
    if class_name.startswith('Async'):
        cursor._executor.shutdown(wait=True)
        cursor._executor = ImmediateExecutor()
    if iscoroutinefunction(cursor.execute):
        cursor._start_query_execution = start_async
        cursor._poll = poll_async
        async def execute_batch(settings):
            for _ in range(iterations):
                await cursor.execute('SELECT 1', **settings)
        for label, settings in [('defaults', {}), ('keywords', {'work_group': 'wg', 'cache_size': 0, 'paramstyle': 'pyformat'}), ('prebuilt', {'options': ExecuteOptions(work_group='wg')})]:
            loop.run_until_complete(execute_batch(settings))
            duration = timeit.timeit(lambda: loop.run_until_complete(execute_batch(settings)), number=1)
            results[class_name + '.execute_' + label] = duration / iterations * 1e6
    else:
        cursor._start_query_execution = lambda *args, **kwargs: 'query'
        cursor._poll = lambda *args, **kwargs: query
        for label, settings in [('defaults', {}), ('keywords', {'work_group': 'wg', 'cache_size': 0, 'paramstyle': 'pyformat'}), ('prebuilt', {'options': ExecuteOptions(work_group='wg')})]:
            def execute():
                value = cursor.execute('SELECT 1', **settings)
                if isinstance(value, tuple):
                    value[1].result()
            measure(class_name + '.execute_' + label, execute)
    close(cursor)

# Focused reader preparation paths with real option resolution and request formatting.
for backend in ('pandas', 'polars'):
    name = 'Pandas' if backend == 'pandas' else 'Polars'
    reader_options = {'sep': '|'} if backend == 'pandas' else {'separator': '|'}
    parquet_options = {'use_threads': False} if backend == 'pandas' else {'parallel': 'none'}
    cursor_options = {'chunksize': 10, 'block_size': 64, 'cache_type': 'bytes', 'max_workers': 2}
    validator = import_module('pyathena.' + backend + '.result_set').validate_execute_kwargs
    for mode in (False, True):
        mode_label = 'parquet' if mode else 'csv'
        for label, settings in [('cursor_settings', cursor_options), ('reader_setting', parquet_options if mode else reader_options)]:
            if backend == 'pandas':
                call = lambda: validator('Cursor.execute', settings, mode)
            else:
                call = lambda: validator('Cursor.execute', settings, mode, 10)
            measure(backend + '.validate_' + mode_label + '_' + label, call)
        for module_name, class_name in [
            ('pyathena.' + backend + '.cursor', name + 'Cursor'),
            ('pyathena.' + backend + '.async_cursor', 'Async' + name + 'Cursor'),
            ('pyathena.aio.' + backend + '.cursor', 'Aio' + name + 'Cursor'),
        ]:
            cursor = connection.cursor(getattr(import_module(module_name), class_name), unload=mode)
            if hasattr(cursor, '_result_set_class'):
                cursor._result_set_class = LocalResult
            if class_name.startswith('Async'):
                cursor._executor.shutdown(wait=True)
                cursor._executor = ImmediateExecutor()
            if iscoroutinefunction(cursor.execute):
                cursor._start_query_execution = start_async
                cursor._poll = poll_async
            else:
                cursor._start_query_execution = lambda *args, **kwargs: 'query'
                cursor._poll = lambda *args, **kwargs: query
            for label, settings in [('cursor_settings', cursor_options), ('reader_setting', parquet_options if mode else reader_options)]:
                if iscoroutinefunction(cursor.execute):
                    async def reader_batch():
                        for _ in range(iterations):
                            await cursor.execute('SELECT 1', **settings)
                    loop.run_until_complete(reader_batch())
                    duration = timeit.timeit(lambda: loop.run_until_complete(reader_batch()), number=1)
                    results[class_name + '.execute_' + mode_label + '_' + label] = duration / iterations * 1e6
                else:
                    def reader_execute():
                        value = cursor.execute('SELECT 1', **settings)
                        if isinstance(value, tuple):
                            value[1].result()
                    measure(class_name + '.execute_' + mode_label + '_' + label, reader_execute)
            close(cursor)

for module_name in ('pandas', 'polars', 'pyarrow.parquet'):
    loaded = import_module(module_name)
    assert import_module(module_name) is loaded and sys.modules[module_name] is loaded
    measure('cached_import.' + module_name, lambda: import_module(module_name))

overrides = dict(work_group=None, s3_staging_dir=None, cache_size=None, cache_expiration_time=None, result_reuse_enable=None, result_reuse_minutes=None, paramstyle=None, on_start_query_execution=None, result_set_type_hints=None)
measure('ExecuteOptions.resolve_none_all_none', lambda: ExecuteOptions.resolve(None, **overrides))
base = ExecuteOptions(work_group='wg')
measure('ExecuteOptions.resolve_prebuilt_all_none', lambda: ExecuteOptions.resolve(base, **overrides))

default_connection = Connection(**dict(connection_args, kill_on_interrupt=True))
for module_name, class_name in [('pyathena.cursor','Cursor'),('pyathena.pandas.cursor','PandasCursor'),('pyathena.aio.cursor','AioCursor')]:
    cursor = default_connection.cursor(getattr(import_module(module_name),class_name))
    if hasattr(cursor, '_result_set_class'):
        cursor._result_set_class = LocalResult
    if iscoroutinefunction(cursor.execute):
        cursor._start_query_execution = start_async
        cursor._poll = poll_async
        async def default_batch():
            for _ in range(100):
                await cursor.execute('SELECT 1')
        loop.run_until_complete(default_batch())
        results[class_name+'.execute_default_interrupt_mode'] = timeit.timeit(lambda: loop.run_until_complete(default_batch()),number=1)/100*1e6
    else:
        cursor._start_query_execution = lambda *args, **kwargs: 'query'
        cursor._poll = lambda *args, **kwargs: query
        measure(class_name+'.execute_default_interrupt_mode', lambda: cursor.execute('SELECT 1'),count=100)
    close(cursor)
default_connection.close()
connection.close()
loop.close()
print(json.dumps({'python': platform.python_version(), 'machine': platform.machine(), 'source': str(source), 'iterations': iterations, 'libraries': {name: import_module(name).__version__ for name in ('pandas', 'polars', 'pyarrow', 'sqlalchemy')}, 'results_us': results}))

Driver:

import json
from pathlib import Path
from statistics import median
import subprocess
import sys

root = Path('/private/tmp/pyathena-1093-reader-preparation')
worktree = Path('/private/tmp/worktrees/PyAthena/fix-1093-reject-unknown-cursor-keywords')
cohorts = [('before', root / 'before-on-master'), ('after', worktree)]
samples = {name: [] for name, _ in cohorts}
for repeat in range(9):
    order = cohorts[repeat % len(cohorts):] + cohorts[:repeat % len(cohorts)]
    for name, source in order:
        command = [sys.executable, str(root / 'measure-api.py'), str(source), '2000']
        result = json.loads(subprocess.check_output(command, cwd=worktree, text=True))
        samples[name].append(result)
summary = {'source_provenance': json.loads((root / 'benchmark-source.json').read_text()), 'repeats': 9, 'iterations_per_repeat': 2000, 'default_interrupt_mode_iterations': 100, 'order': 'Rotate source cohort order each repeat; sequential fresh subprocesses with identical interpreter and dependencies.', 'scope': 'Actual connection/cursor/execute bodies, retaining BaseCursor._execute and request formatting. Main cases use kill_on_interrupt=False to isolate CPU work. Three separately named default_interrupt_mode cases use True and retain start-request thread/task scheduling. AWS session/client construction, submission/polling, result readers, Future scheduling, and asyncio reader thread offloads use local fakes. Not AWS end-to-end latency or full asynchronous scheduling performance.', 'samples': samples, 'medians_us': {}}
for name, values in samples.items():
    summary['medians_us'][name] = {case: {'median': median([v['results_us'][case] for v in values]), 'min': min(v['results_us'][case] for v in values), 'max': max(v['results_us'][case] for v in values)} for case in values[0]['results_us']}
print(json.dumps(summary, indent=2))

Comment thread pyathena/pandas/result_set.py Outdated
"""
if not kwargs or kwargs.keys() <= _CURSOR_KWARGS:
return
import pandas

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.

Round-two native-import follow-up: CLEAN.

Literal current base/head: f3532e9fb57fd769441e533f0b8375fd854ece69..c42d65d04a54d3fe1b5cb90122635b495a64d09d.
Audited the final four-file import/test change against the previously covered 12-file reader-preparation scope, affected current-master contracts, and the final description/measurement claims. Checked import laziness, optional dependency/reader-value boundaries, live reader replacement, signature-cache ownership, unchanged public keyword-only bindings, options/callback/state/failure behavior, and attribution of the two source cohorts.

Both native import and import_module use Python's existing module cache/initialization synchronization; neither statement guarantees zero lookup cost or independent dependency preflight. Native imports stay inside the optional dependency path. Cursor-setting-only names need no reader discovery, while reader names and unknown names still use live installed callables. No user values or separate module objects are cached by this patch. The None handling documented by ExecuteOptions still applies to all forwarded fields, including the Future helper's unused None callback override.

Audited the final 106-case child harness and driver: same pinned upstream on both sides, baseline merge-tree identified as a synthesized tree rather than a published commit, nine fresh-process samples per cohort in alternating order, actual _execute/option/request formatting retained, SDK/provider/result/Future/reader-offload boundaries faked, 2000 main versus 100 interrupt-mode iterations, warm-up/import exclusion, and timeit's GC behavior. Recomputed every one of the 212 median/min/max aggregates from the stored samples. Three added native-import probes check module identity against the import_module result; they measure warm module lookups, not initialization.

Warm reader-validation medians improved: pandas CSV 0.951 -> 0.712 microseconds, pandas Parquet 1.696 -> 1.290, chunked Polars CSV 2.296 -> 2.098. Native pandas lookup measured 0.070 versus import_module 0.296 microseconds in the candidate environment. Cursor-setting-only actual CSV execute medians were pandas 9.146 -> 8.319 and Polars 10.522 -> 8.013. Default execute medians still rose slightly (pandas 7.339 -> 7.434; Polars 7.212 -> 7.286) with overlapping observed ranges. No equal-cost, universal speedup, statistical-significance or AWS latency claim is made; default-interrupt-mode results remain separately reported without a speedup claim.

Final-source format/lint and 683 offline tests passed, with three existing dtypes warnings. Earlier docs checks and AWS evidence are not relabeled as current-head evidence; final-head CI is collected separately. No actionable findings, no further repairs. Static review and local fake-provider timings do not establish real AWS end-to-end performance. The required independent Max review remains incomplete until an actual successful verdict is collected.

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.

Final-source validation complete. PR #1102 is Ready, MERGEABLE, merge state CLEAN; it remains open and has not been merged by this work.

Exact published/reviewed head: c42d65d04a54d3fe1b5cb90122635b495a64d09d; review merge-base: f3532e9fb57fd769441e533f0b8375fd854ece69.
Both self-review perspectives and the completed Opus 5.5 first-party Max/high independent review are CLEAN. Its non-blocking observations, author verification and static limitations are preserved in the inline record. The initial weekly-limit response was not counted; actual successful retry session 1d2c8d53-2c96-4708-8011-07fe5f424a83 supplied the verdict.

Ready-triggered AWS Test run 37950215264 completed SUCCESS on Python 3.14.8 for this head:

  • PyAthena: 3971 passed, 12 skipped, 16 warnings, 554.55 seconds. Its skip-spark=false and skip-sqla=false inputs were confirmed in the logs.
  • SQLAlchemy synchronous: 589 passed, 759 skipped, 16 warnings, 511.93 seconds.
  • SQLAlchemy asynchronous: 589 passed, 759 skipped, 16 warnings, 480.45 seconds.
  • The run's lint and changes-selection jobs also passed. All three AWS suites ran; the listed skipped counts are pytest output, separate from the historical Draft-only skipped jobs in the check rollup.

Current-head Docs lint/build, license checks and benchmark tooling offline checks also passed. Final-source local just format, just lint, and the focused 683-test offline suite passed. No validation failure or additional source repair occurred after independent review. Snapshot hash manifest, clean candidate worktree and published-head equality were rechecked after CI.

Final 106-case measurements and reproduction remain local elapsed timings with fake provider/scheduling boundaries, nine fresh-process repeats and the same merged #1110 on both sides. Native import and import_module both reuse initialized modules; native imports reduce warm lookup cost, and cursor-setting-only validation bypasses lookup/inspection. Small default-path increases, sampling/provenance limitations and unexplained portions of Parquet execute differences are disclosed. These timings and CI pass results do not establish an AWS end-to-end performance improvement.

Comment thread pyathena/pandas/result_set.py Outdated
"""
if not kwargs or kwargs.keys() <= _CURSOR_KWARGS:
return
import pandas

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.

Final native-import and shared-preparation measurements.

Head: c42d65d04a54d3fe1b5cb90122635b495a64d09d; review merge-base/current master: f3532e9fb57fd769441e533f0b8375fd854ece69.
Baseline tree: b1d87c391dce1484633d1872a900c9cc6a900bec, constructed by conflict-free merge-tree from old validation head 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee and that same master. This is a synthesized comparison tree, not a published baseline commit. Both cohorts contain merged #1110.

CPython 3.13.1 arm64; pandas 3.0.6, Polars 1.44.2, PyArrow 25.0.1, SQLAlchemy 2.0.46. 106 cases, 9 fresh-process samples per cohort, alternating order, 2000 main iterations; 3 separately named default_interrupt_mode cases use 100. All 212 median/min/max aggregates were recomputed from raw samples. Ranges are observed minima/maxima, not confidence intervals.

Actual connection/cursor/execute bodies, BaseCursor._execute, option resolution and request/UNLOAD formatting are retained. SDK session/client construction, AWS submission/polling, result readers, Future worker scheduling and asyncio reader thread offloads are local fakes. Future timing collects an immediately completed fake result and does not measure real executor latency. asyncio loop entry overhead is amortized across the batch. Main cases use kill_on_interrupt=False. Default_interrupt_mode probes retain start-request thread/task scheduling with True. Setup, imports and cold signature misses are excluded by warm-up; timeit disables GC during timing.

Reader validators now use ordinary optional import statements. Both import paths return the initialized cached module without rerunning module code; imported-module identities were checked. Native pandas/Polars lookup measured about 0.07 microseconds versus import_module about 0.30; native PyArrow Parquet about 0.16 versus import_module about 0.30. There is no added module-pointer cache, and live reader replacement remains visible. The dynamic engine-name import in pandas result construction is unchanged.

Warm reader validation improved (pandas CSV 0.951 -> 0.712; pandas Parquet 1.696 -> 1.290; chunked Polars CSV 2.296 -> 2.098 microseconds). Cursor-setting-only validation returns before import/inspection; actual CSV execute medians with those settings improved (pandas 9.146 -> 8.319, Polars 10.522 -> 8.013). Default execute medians rose slightly (pandas 7.339 -> 7.434, Polars 7.212 -> 7.286); their ranges overlap, which does not prove equal cost. Reader-option execute ranges also overlap. No general speedup, statistical-significance or AWS end-to-end latency claim is made. Default interrupt mode is reported separately without a speedup claim.

Case, microseconds/call Before median [range] This head median [range]
connect_local_session 15.988 [15.497–16.665] 15.970 [15.498–16.445]
Connection_local_session 15.733 [15.213–16.373] 15.634 [15.255–16.117]
AioConnection_local_session 15.865 [15.451–16.304] 16.004 [15.396–16.644]
Cursor.construct_close 12.828 [12.643–13.222] 12.752 [12.600–13.199]
Cursor.execute_defaults 6.498 [6.383–6.969] 6.477 [6.401–6.740]
Cursor.execute_keywords 6.683 [6.558–6.911] 6.634 [6.575–7.063]
Cursor.execute_prebuilt 8.146 [7.983–9.185] 8.196 [8.004–8.544]
AsyncCursor.construct_close 13.209 [13.052–13.923] 13.277 [13.136–14.118]
AsyncCursor.execute_defaults 8.129 [7.891–8.433] 8.175 [7.970–8.425]
AsyncCursor.execute_keywords 8.273 [8.096–8.505] 8.304 [8.169–8.485]
AsyncCursor.execute_prebuilt 9.708 [9.513–10.070] 9.883 [9.494–10.148]
AioCursor.construct_close 12.738 [12.605–13.274] 12.861 [12.643–13.535]
AioCursor.execute_defaults 6.980 [6.744–7.197] 7.199 [6.792–7.244]
AioCursor.execute_keywords 7.181 [6.984–7.548] 7.217 [7.085–7.457]
AioCursor.execute_prebuilt 8.596 [8.392–9.002] 8.659 [8.465–9.046]
PandasCursor.construct_close 8.024 [7.865–8.238] 7.961 [7.772–8.416]
PandasCursor.execute_defaults 7.339 [7.246–7.570] 7.434 [7.318–7.684]
PandasCursor.execute_keywords 7.574 [7.374–7.616] 7.661 [7.529–7.831]
PandasCursor.execute_prebuilt 8.945 [8.817–9.228] 9.160 [8.971–9.406]
AsyncPandasCursor.construct_close 9.653 [9.386–9.785] 9.566 [9.405–9.836]
AsyncPandasCursor.execute_defaults 8.837 [8.642–9.215] 9.109 [8.805–9.639]
AsyncPandasCursor.execute_keywords 8.960 [8.756–9.361] 9.175 [8.891–9.568]
AsyncPandasCursor.execute_prebuilt 10.343 [10.207–10.727] 10.612 [10.322–10.869]
AioPandasCursor.construct_close 7.868 [7.774–8.159] 7.910 [7.784–8.355]
AioPandasCursor.execute_defaults 8.142 [7.953–8.415] 8.383 [7.983–8.696]
AioPandasCursor.execute_keywords 8.321 [8.141–9.047] 8.499 [8.160–8.804]
AioPandasCursor.execute_prebuilt 9.632 [9.590–10.452] 9.970 [9.789–10.179]
ArrowCursor.construct_close 9.116 [8.986–9.500] 9.103 [8.913–9.314]
ArrowCursor.execute_defaults 6.929 [6.786–7.137] 6.951 [6.752–7.309]
ArrowCursor.execute_keywords 7.075 [6.976–7.180] 7.160 [6.913–7.304]
ArrowCursor.execute_prebuilt 8.520 [8.407–8.821] 8.576 [8.414–8.703]
AsyncArrowCursor.construct_close 10.887 [10.657–11.484] 10.904 [10.671–11.065]
AsyncArrowCursor.execute_defaults 8.415 [8.265–8.802] 8.491 [8.195–8.636]
AsyncArrowCursor.execute_keywords 8.595 [8.434–8.741] 8.631 [8.346–8.777]
AsyncArrowCursor.execute_prebuilt 9.993 [9.838–10.586] 10.049 [9.739–10.403]
AioArrowCursor.construct_close 9.094 [8.925–9.570] 9.026 [8.933–9.245]
AioArrowCursor.execute_defaults 7.460 [7.408–7.850] 7.479 [7.356–7.797]
AioArrowCursor.execute_keywords 7.683 [7.577–8.216] 7.765 [7.535–7.924]
AioArrowCursor.execute_prebuilt 9.203 [9.015–9.600] 9.190 [8.943–9.857]
PolarsCursor.construct_close 8.335 [8.276–8.568] 8.330 [8.151–8.507]
PolarsCursor.execute_defaults 7.212 [6.987–7.332] 7.286 [7.148–7.466]
PolarsCursor.execute_keywords 7.297 [7.175–7.681] 7.442 [7.234–7.619]
PolarsCursor.execute_prebuilt 8.822 [8.593–8.950] 8.982 [8.848–9.277]
AsyncPolarsCursor.construct_close 9.970 [9.858–10.324] 10.010 [9.871–11.062]
AsyncPolarsCursor.execute_defaults 8.589 [8.467–8.977] 8.820 [8.571–11.064]
AsyncPolarsCursor.execute_keywords 8.689 [8.591–8.865] 8.896 [8.651–10.017]
AsyncPolarsCursor.execute_prebuilt 10.289 [10.017–10.715] 10.365 [10.196–10.708]
AioPolarsCursor.construct_close 8.282 [8.203–8.587] 8.231 [8.107–8.466]
AioPolarsCursor.execute_defaults 7.865 [7.623–8.106] 7.868 [7.704–8.337]
AioPolarsCursor.execute_keywords 8.017 [7.794–8.224] 8.114 [7.882–8.432]
AioPolarsCursor.execute_prebuilt 9.358 [9.193–9.705] 9.645 [9.339–10.083]
S3FSCursor.construct_close 12.536 [12.377–12.811] 12.713 [12.504–13.017]
S3FSCursor.execute_defaults 6.634 [6.525–6.854] 6.722 [6.562–7.106]
S3FSCursor.execute_keywords 6.876 [6.668–7.040] 6.853 [6.759–7.281]
S3FSCursor.execute_prebuilt 8.215 [8.127–8.447] 8.390 [8.160–8.802]
AsyncS3FSCursor.construct_close 14.339 [13.910–15.454] 14.189 [14.130–14.758]
AsyncS3FSCursor.execute_defaults 8.118 [8.015–8.789] 8.237 [8.035–8.579]
AsyncS3FSCursor.execute_keywords 8.246 [8.193–8.517] 8.300 [8.151–8.704]
AsyncS3FSCursor.execute_prebuilt 9.681 [9.549–10.004] 9.814 [9.632–10.148]
AioS3FSCursor.construct_close 12.446 [12.249–12.834] 12.515 [12.414–12.899]
AioS3FSCursor.execute_defaults 7.268 [7.144–7.370] 7.241 [7.184–7.693]
AioS3FSCursor.execute_keywords 7.449 [7.364–7.822] 7.548 [7.372–7.946]
AioS3FSCursor.execute_prebuilt 8.830 [8.736–9.151] 8.944 [8.746–9.243]
pandas.validate_csv_cursor_settings 1.018 [0.999–1.069] 0.142 [0.140–0.147]
pandas.validate_csv_reader_setting 0.951 [0.941–0.972] 0.712 [0.694–0.766]
PandasCursor.execute_csv_cursor_settings 9.146 [9.069–9.417] 8.319 [8.223–8.510]
PandasCursor.execute_csv_reader_setting 8.884 [8.637–9.078] 8.696 [8.555–8.953]
AsyncPandasCursor.execute_csv_cursor_settings 10.825 [10.675–11.277] 9.808 [9.740–10.275]
AsyncPandasCursor.execute_csv_reader_setting 10.342 [10.252–10.912] 10.220 [10.027–10.820]
AioPandasCursor.execute_csv_cursor_settings 10.062 [9.955–10.330] 9.127 [8.865–9.475]
AioPandasCursor.execute_csv_reader_setting 9.667 [9.427–10.563] 9.545 [9.324–10.028]
pandas.validate_parquet_cursor_settings 1.789 [1.712–1.874] 0.144 [0.140–0.151]
pandas.validate_parquet_reader_setting 1.696 [1.654–1.787] 1.290 [1.260–1.381]
PandasCursor.execute_parquet_cursor_settings 23.432 [22.873–24.205] 21.011 [20.800–22.733]
PandasCursor.execute_parquet_reader_setting 22.930 [22.469–23.704] 22.477 [22.298–24.173]
AsyncPandasCursor.execute_parquet_cursor_settings 25.302 [24.611–26.051] 22.899 [22.704–24.360]
AsyncPandasCursor.execute_parquet_reader_setting 24.618 [24.163–25.341] 24.326 [23.804–25.979]
AioPandasCursor.execute_parquet_cursor_settings 24.383 [23.727–25.377] 22.106 [21.807–23.468]
AioPandasCursor.execute_parquet_reader_setting 24.002 [23.125–26.249] 23.232 [23.000–24.797]
polars.validate_csv_cursor_settings 2.382 [2.334–2.544] 0.152 [0.149–0.163]
polars.validate_csv_reader_setting 2.296 [2.260–2.398] 2.098 [2.075–2.241]
PolarsCursor.execute_csv_cursor_settings 10.522 [10.214–10.888] 8.013 [7.906–8.348]
PolarsCursor.execute_csv_reader_setting 9.104 [8.955–9.442] 8.921 [8.835–9.355]
AsyncPolarsCursor.execute_csv_cursor_settings 12.074 [11.840–12.659] 9.591 [9.405–9.907]
AsyncPolarsCursor.execute_csv_reader_setting 10.869 [10.482–11.185] 10.624 [10.503–11.153]
AioPolarsCursor.execute_csv_cursor_settings 11.159 [10.956–12.222] 8.645 [8.584–9.111]
AioPolarsCursor.execute_csv_reader_setting 9.861 [9.739–10.414] 9.681 [9.523–9.948]
polars.validate_parquet_cursor_settings 1.415 [1.396–1.505] 0.152 [0.150–0.163]
polars.validate_parquet_reader_setting 1.339 [1.329–1.430] 1.124 [1.086–1.153]
PolarsCursor.execute_parquet_cursor_settings 23.095 [22.262–23.951] 20.818 [20.296–21.687]
PolarsCursor.execute_parquet_reader_setting 22.695 [21.757–23.069] 21.896 [21.615–23.190]
AsyncPolarsCursor.execute_parquet_cursor_settings 24.996 [24.018–25.836] 22.741 [22.447–24.248]
AsyncPolarsCursor.execute_parquet_reader_setting 24.153 [23.491–25.629] 23.803 [23.494–26.614]
AioPolarsCursor.execute_parquet_cursor_settings 24.229 [23.073–24.678] 21.690 [21.316–23.273]
AioPolarsCursor.execute_parquet_reader_setting 23.674 [22.718–24.592] 22.741 [22.401–23.699]
native_import.pandas 0.071 [0.070–0.075] 0.070 [0.069–0.080]
native_import.polars 0.070 [0.069–0.076] 0.070 [0.070–0.076]
native_import.pyarrow.parquet 0.161 [0.157–0.167] 0.155 [0.150–0.161]
cached_import.pandas 0.297 [0.290–0.305] 0.296 [0.290–0.315]
cached_import.polars 0.301 [0.290–0.321] 0.296 [0.291–0.316]
cached_import.pyarrow.parquet 0.306 [0.293–0.313] 0.298 [0.295–0.306]
ExecuteOptions.resolve_none_all_none 1.335 [1.305–1.403] 1.323 [1.297–1.400]
ExecuteOptions.resolve_prebuilt_all_none 2.899 [2.866–3.059] 2.901 [2.812–2.968]
Cursor.execute_default_interrupt_mode 54.468 [44.608–63.671] 50.946 [39.936–56.303]
PandasCursor.execute_default_interrupt_mode 53.078 [50.963–68.188] 53.907 [45.965–59.078]
AioCursor.execute_default_interrupt_mode 54.060 [49.994–63.443] 50.403 [49.179–61.449]
Reproduction and full child/driver

Save the child and driver below under /private/tmp/pyathena-1093-reader-preparation, together with the provenance JSON. Use a clean checkout of the candidate head at the driver's worktree path and identical dependencies/interpreter for both cohorts. Export the baseline using the exact input commits:

git merge-tree --write-tree 5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee f3532e9fb57fd769441e533f0b8375fd854ece69
mkdir -p /private/tmp/pyathena-1093-reader-preparation/before-on-master
git archive b1d87c391dce1484633d1872a900c9cc6a900bec | tar -xf - -C /private/tmp/pyathena-1093-reader-preparation/before-on-master
uv run python /private/tmp/pyathena-1093-reader-preparation/run-comparison.py > results.json

Provenance (benchmark-source.json):

{
  "before_tree": "b1d87c391dce1484633d1872a900c9cc6a900bec",
  "before_inputs": [
    "5c96c9257f64f6fa5c7fa1bc1cee47d8f496ebee",
    "f3532e9fb57fd769441e533f0b8375fd854ece69"
  ],
  "after_head": "c42d65d04a54d3fe1b5cb90122635b495a64d09d",
  "upstream": "f3532e9fb57fd769441e533f0b8375fd854ece69",
  "scope": "Both sources include the same current master, including the merged options optimization. Before is a conflict-free merge-tree of the previous published validation head and that master, exported without a git worktree; after is the current PR head."
}

Child (measure-api.py):

import asyncio
from concurrent.futures import Future
from importlib import import_module
from inspect import iscoroutinefunction
import json
from pathlib import Path
import platform
import sys
import timeit
from types import SimpleNamespace

source = Path(sys.argv[1]).resolve()
sys.path.insert(0, str(source))
import pyathena
from pyathena.aio.connection import AioConnection
from pyathena.connection import Connection
from pyathena.model import AthenaQueryExecution
from pyathena.options import ExecuteOptions
from pyathena.spark.common import SparkBaseCursor

assert Path(pyathena.__file__).resolve().parent.parent == source
iterations = int(sys.argv[2])

class NoAWSClient:
    def close(self):
        pass

    def __getattr__(self, name):
        raise AssertionError('Unexpected AWS client access: ' + name)

class LocalSession:
    def client(self, *args, **kwargs):
        return NoAWSClient()

class LocalResult:
    def __init__(self, *args, **kwargs):
        self.is_closed = False

    def close(self):
        self.is_closed = True

    @classmethod
    async def create(cls, *args, **kwargs):
        return cls(*args, **kwargs)

class ImmediateExecutor:
    def submit(self, fn, *args, **kwargs):
        result = Future()
        result.set_result(fn(*args, **kwargs))
        return result

    def shutdown(self, *args, **kwargs):
        pass

async def immediate_thread(fn, *args, **kwargs):
    return fn(*args, **kwargs)

asyncio.to_thread = immediate_thread
SparkBaseCursor._start_session = lambda *args, **kwargs: 'session'
query = SimpleNamespace(state=AthenaQueryExecution.STATE_SUCCEEDED)
async def start_async(*args, **kwargs):
    return 'query'
async def poll_async(*args, **kwargs):
    return query

session = LocalSession()
connection_args = dict(region_name='us-east-1', s3_staging_dir='s3://bucket/path/', work_group='wg', session=session, kill_on_interrupt=False)
connection = Connection(**connection_args)
loop = asyncio.new_event_loop()
results = {}

def measure(name, call, count=None):
    count = iterations if count is None else count
    for _ in range(30):
        call()
    results[name] = timeit.timeit(call, number=count) / count * 1e6

def create_connection(factory):
    conn = factory(**connection_args)
    conn.close()

measure('connect_local_session', lambda: create_connection(pyathena.connect))
measure('Connection_local_session', lambda: create_connection(Connection))
measure('AioConnection_local_session', lambda: create_connection(AioConnection))

cursor_definitions = [
    ('pyathena.cursor', 'Cursor'),
    ('pyathena.async_cursor', 'AsyncCursor'),
    ('pyathena.aio.cursor', 'AioCursor'),
]
for backend in ('pandas', 'arrow', 'polars', 's3fs'):
    name = {'pandas': 'Pandas', 'arrow': 'Arrow', 'polars': 'Polars', 's3fs': 'S3FS'}[backend]
    cursor_definitions.extend([
        ('pyathena.' + backend + '.cursor', name + 'Cursor'),
        ('pyathena.' + backend + '.async_cursor', 'Async' + name + 'Cursor'),
        ('pyathena.aio.' + backend + '.cursor', 'Aio' + name + 'Cursor'),
    ])

for module_name, class_name in cursor_definitions:
    module = import_module(module_name)
    cursor_class = getattr(module, class_name)
    for name, value in vars(module).copy().items():
        if name.startswith('Athena') and name.endswith('ResultSet'):
            setattr(module, name, LocalResult)
    if 'Pandas' in class_name:
        import_module('pandas')
    if 'Polars' in class_name:
        import_module('polars')
    if 'Arrow' in class_name:
        import_module('pyarrow')

    def close(cursor):
        if iscoroutinefunction(cursor.close):
            loop.run_until_complete(cursor.close())
        else:
            cursor.close()

    def create_cursor():
        cursor = connection.cursor(cursor_class)
        close(cursor)
    measure(class_name + '.construct_close', create_cursor)

    cursor = connection.cursor(cursor_class)
    if hasattr(cursor, '_result_set_class'):
        cursor._result_set_class = LocalResult
    if class_name.startswith('Async'):
        cursor._executor.shutdown(wait=True)
        cursor._executor = ImmediateExecutor()
    if iscoroutinefunction(cursor.execute):
        cursor._start_query_execution = start_async
        cursor._poll = poll_async
        async def execute_batch(settings):
            for _ in range(iterations):
                await cursor.execute('SELECT 1', **settings)
        for label, settings in [('defaults', {}), ('keywords', {'work_group': 'wg', 'cache_size': 0, 'paramstyle': 'pyformat'}), ('prebuilt', {'options': ExecuteOptions(work_group='wg')})]:
            loop.run_until_complete(execute_batch(settings))
            duration = timeit.timeit(lambda: loop.run_until_complete(execute_batch(settings)), number=1)
            results[class_name + '.execute_' + label] = duration / iterations * 1e6
    else:
        cursor._start_query_execution = lambda *args, **kwargs: 'query'
        cursor._poll = lambda *args, **kwargs: query
        for label, settings in [('defaults', {}), ('keywords', {'work_group': 'wg', 'cache_size': 0, 'paramstyle': 'pyformat'}), ('prebuilt', {'options': ExecuteOptions(work_group='wg')})]:
            def execute():
                value = cursor.execute('SELECT 1', **settings)
                if isinstance(value, tuple):
                    value[1].result()
            measure(class_name + '.execute_' + label, execute)
    close(cursor)

# Focused reader preparation paths with real option resolution and request formatting.
for backend in ('pandas', 'polars'):
    name = 'Pandas' if backend == 'pandas' else 'Polars'
    reader_options = {'sep': '|'} if backend == 'pandas' else {'separator': '|'}
    parquet_options = {'use_threads': False} if backend == 'pandas' else {'parallel': 'none'}
    cursor_options = {'chunksize': 10, 'block_size': 64, 'cache_type': 'bytes', 'max_workers': 2}
    validator = import_module('pyathena.' + backend + '.result_set').validate_execute_kwargs
    for mode in (False, True):
        mode_label = 'parquet' if mode else 'csv'
        for label, settings in [('cursor_settings', cursor_options), ('reader_setting', parquet_options if mode else reader_options)]:
            if backend == 'pandas':
                call = lambda: validator('Cursor.execute', settings, mode)
            else:
                call = lambda: validator('Cursor.execute', settings, mode, 10)
            measure(backend + '.validate_' + mode_label + '_' + label, call)
        for module_name, class_name in [
            ('pyathena.' + backend + '.cursor', name + 'Cursor'),
            ('pyathena.' + backend + '.async_cursor', 'Async' + name + 'Cursor'),
            ('pyathena.aio.' + backend + '.cursor', 'Aio' + name + 'Cursor'),
        ]:
            cursor = connection.cursor(getattr(import_module(module_name), class_name), unload=mode)
            if hasattr(cursor, '_result_set_class'):
                cursor._result_set_class = LocalResult
            if class_name.startswith('Async'):
                cursor._executor.shutdown(wait=True)
                cursor._executor = ImmediateExecutor()
            if iscoroutinefunction(cursor.execute):
                cursor._start_query_execution = start_async
                cursor._poll = poll_async
            else:
                cursor._start_query_execution = lambda *args, **kwargs: 'query'
                cursor._poll = lambda *args, **kwargs: query
            for label, settings in [('cursor_settings', cursor_options), ('reader_setting', parquet_options if mode else reader_options)]:
                if iscoroutinefunction(cursor.execute):
                    async def reader_batch():
                        for _ in range(iterations):
                            await cursor.execute('SELECT 1', **settings)
                    loop.run_until_complete(reader_batch())
                    duration = timeit.timeit(lambda: loop.run_until_complete(reader_batch()), number=1)
                    results[class_name + '.execute_' + mode_label + '_' + label] = duration / iterations * 1e6
                else:
                    def reader_execute():
                        value = cursor.execute('SELECT 1', **settings)
                        if isinstance(value, tuple):
                            value[1].result()
                    measure(class_name + '.execute_' + mode_label + '_' + label, reader_execute)
            close(cursor)

def native_pandas_import():
    import pandas
    return pandas

def native_polars_import():
    import polars
    return polars

def native_parquet_import():
    import pyarrow.parquet as parquet
    return parquet

for name, call in [('pandas', native_pandas_import), ('polars', native_polars_import), ('pyarrow.parquet', native_parquet_import)]:
    assert call() is import_module(name)
    measure('native_import.' + name, call)

for module_name in ('pandas', 'polars', 'pyarrow.parquet'):
    loaded = import_module(module_name)
    assert import_module(module_name) is loaded and sys.modules[module_name] is loaded
    measure('cached_import.' + module_name, lambda: import_module(module_name))

overrides = dict(work_group=None, s3_staging_dir=None, cache_size=None, cache_expiration_time=None, result_reuse_enable=None, result_reuse_minutes=None, paramstyle=None, on_start_query_execution=None, result_set_type_hints=None)
measure('ExecuteOptions.resolve_none_all_none', lambda: ExecuteOptions.resolve(None, **overrides))
base = ExecuteOptions(work_group='wg')
measure('ExecuteOptions.resolve_prebuilt_all_none', lambda: ExecuteOptions.resolve(base, **overrides))

default_connection = Connection(**dict(connection_args, kill_on_interrupt=True))
for module_name, class_name in [('pyathena.cursor','Cursor'),('pyathena.pandas.cursor','PandasCursor'),('pyathena.aio.cursor','AioCursor')]:
    cursor = default_connection.cursor(getattr(import_module(module_name),class_name))
    if hasattr(cursor, '_result_set_class'):
        cursor._result_set_class = LocalResult
    if iscoroutinefunction(cursor.execute):
        cursor._start_query_execution = start_async
        cursor._poll = poll_async
        async def default_batch():
            for _ in range(100):
                await cursor.execute('SELECT 1')
        loop.run_until_complete(default_batch())
        results[class_name+'.execute_default_interrupt_mode'] = timeit.timeit(lambda: loop.run_until_complete(default_batch()),number=1)/100*1e6
    else:
        cursor._start_query_execution = lambda *args, **kwargs: 'query'
        cursor._poll = lambda *args, **kwargs: query
        measure(class_name+'.execute_default_interrupt_mode', lambda: cursor.execute('SELECT 1'),count=100)
    close(cursor)
default_connection.close()
connection.close()
loop.close()
print(json.dumps({'python': platform.python_version(), 'machine': platform.machine(), 'source': str(source), 'iterations': iterations, 'libraries': {name: import_module(name).__version__ for name in ('pandas', 'polars', 'pyarrow', 'sqlalchemy')}, 'results_us': results}))

Driver (run-comparison.py):

import json
from pathlib import Path
from statistics import median
import subprocess
import sys

root = Path('/private/tmp/pyathena-1093-reader-preparation')
worktree = Path('/private/tmp/worktrees/PyAthena/fix-1093-reject-unknown-cursor-keywords')
cohorts = [('before', root / 'before-on-master'), ('after', worktree)]
samples = {name: [] for name, _ in cohorts}
for repeat in range(9):
    order = cohorts[repeat % len(cohorts):] + cohorts[:repeat % len(cohorts)]
    for name, source in order:
        command = [sys.executable, str(root / 'measure-api.py'), str(source), '2000']
        result = json.loads(subprocess.check_output(command, cwd=worktree, text=True))
        samples[name].append(result)
summary = {'source_provenance': json.loads((root / 'benchmark-source.json').read_text()), 'repeats': 9, 'iterations_per_repeat': 2000, 'default_interrupt_mode_iterations': 100, 'order': 'Rotate source cohort order each repeat; sequential fresh subprocesses with identical interpreter and dependencies.', 'scope': 'Actual connection/cursor/execute bodies, retaining BaseCursor._execute and request formatting. Main cases use kill_on_interrupt=False to isolate CPU work. Three separately named default_interrupt_mode cases use True and retain start-request thread/task scheduling. AWS session/client construction, submission/polling, result readers, Future scheduling, and asyncio reader thread offloads use local fakes. Not AWS end-to-end latency or full asynchronous scheduling performance.', 'samples': samples, 'medians_us': {}}
for name, values in samples.items():
    summary['medians_us'][name] = {case: {'median': median([v['results_us'][case] for v in values]), 'min': min(v['results_us'][case] for v in values), 'max': max(v['results_us'][case] for v in values)} for case in values[0]['results_us']}
print(json.dumps(summary, indent=2))

Comment thread pyathena/common.py Outdated
return False
return operation[start.start() : start.start() + 6].upper().startswith(prefixes)

def _prepare_reader_query(

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, with the non-blocking notes and limitations preserved below.

Reviewer: Claude Code, actual canonical model claude-opus-5-5, first-party max profile, effort high; no Enterprise account or API/provider override. Authentication was verified immediately before launch. Session: 1d2c8d53-2c96-4708-8011-07fe5f424a83. The retry completed successfully after the reported 00:00 Asia/Tokyo reset; the earlier weekly-limit response was not counted as a review.

Frozen current merge-base/head: f3532e9fb57fd769441e533f0b8375fd854ece69..c42d65d04a54d3fe1b5cb90122635b495a64d09d.
Bounded follow-up: the 12-file diff from comparison tree b1d87c391dce1484633d1872a900c9cc6a900bec to this head, with affected current-master and unchanged contracts. All eight original-series commits remain unchanged. The tracked snapshot excluded credentials, .env, memory and unrelated artifacts. Author conclusions, commit messages and PR discussion were omitted. Read/Grep/Glob only, plan/read-only mode, no builds/tests/writes/GitHub/MCP/subagents. Both self-review rounds preceded this review.

The actual successful result was read in full. Post-review verification confirmed the package hash manifest and clean candidate worktree were unchanged, with exact base/head equality.


Verdict: CLEAN

I found no defects introduced by the three follow-up commits. There are three non-blocking notes (one behavioral, two on test strength), and the benchmark supports some conclusions but not others. This was a read-only, static review: I only used Read, Grep and Glob inside the snapshot. Nothing was executed. Plan mode asked for a plan file, but you ruled out edits and the Write and ExitPlanMode tools aren't available here, so the review is below.

Coverage

  • Shared helper: pyathena/common.py:1218-1291, compared with the baseline _validate_reader_kwargs (baseline-source/pyathena/common.py:1218-1241). Also traced _is_unload_query (common.py:1199-1216), _prepare_unload (:1293-1317), _execute (:1362+) and Formatter.wrap_unload (formatter.py:127+).
  • All six callers:
    • pandas/cursor.py:215-231
    • pandas/async_cursor.py:245-263
    • aio/pandas/cursor.py:184-201
    • polars/cursor.py:206-222
    • polars/async_cursor.py:248-264
    • aio/polars/cursor.py:164-186
  • Both validators: pandas/result_set.py:38-73 and polars/result_set.py:42-95. Also the Polars result-set uses at polars/result_set.py:563-569 and :640.
  • Unchanged contracts traced:
    • options.py:84-120 (upstream resolve fast path, upstream.patch:668-679)
    • _kwargs.py:16-78 (signature cache)
    • result_set.py:935-940 (_reset_state)
    • keyword-only execute and constructors (upstream.patch:230-269, 458-497)
    • docs/cursor.md:14-22
    • pyproject.toml:58-66 (extras)
  • Tests:
    • test_kwargs.py:569, :615, plus the existing tests at :200-510
    • pandas/test_result_set.py:723, :736, :751
    • polars/test_result_set.py:438, :451

Behavior audit (no defects)

  • Equivalence with the baseline: the helper reproduces the old inline code exactly:
    • Options are resolved, then UNLOAD is prepared.
    • On an Exception, a non-empty kwargs dict is validated against the unprepared operation (prepared=False, so SELECT/WITH/UNLOAD select Parquet). Then on_prepare_error runs, then the original error is re-raised.
    • On success, validation uses the prepared operation (prepared defaults to True, common.py:1199).
    • In the failure path operation is still the original. A failed tuple assignment never rebinds it, and a failure in resolve happens before the assignment.
  • Rejection order and prior state:
    • An unknown keyword raises TypeError before on_prepare_error, so earlier state is kept. This matches the baseline and test_kwargs.py:282-334.
    • Sync and aio cursors pass self._reset_state, which is synchronous (result_set.py:935), so there is no un-awaited coroutine.
    • Future-based cursors pass no callback and keep their earlier no-reset behavior.
    • Successful paths still reset after the helper returns.
  • Exception vs. interrupts: except Exception leaves KeyboardInterrupt and SystemExit unvalidated and unreset. The new test at :615 locks this in.
  • Options, callbacks and type hints:
    • Calls go through ExecuteOptions.resolve by class literal, so a subclass instance passed as options still dispatches to its own merge().
    • None overrides are ignored, so Future-based cursors omitting on_start_query_execution keep any value from options (unchanged).
    • Callback and hint references pass through by identity (upstream test_options.py).
    • Every helper option is keyword-only after *reader_args, and all callers pass them by name, so the upstream * changes are compatible.
  • Effective chunksize: Polars callers still pass kwargs.get("chunksize", self._chunksize). The result-set checks (allow_eager_fallback=False) are unchanged.
  • Fast path: kwargs.keys() <= _CURSOR_KWARGS returns early only when every key is a cursor setting. The full path always added _CURSOR_KWARGS to the allowed names, so keyword results are identical. Mixed kwargs still take the full path, as does the Polars alias loop and the managed read_csv fallback.
  • Native imports:
    • In-function import pandas / import polars matches the module's existing idiom (e.g. pandas/result_set.py:109, polars/result_set.py:141).
    • The reader is still looked up as pandas.read_csv at call time, so replacing it is honored. The lru_cache is keyed by function identity, so a replacement gets a new cache entry (_kwargs.py:16-46).
    • One edge-case difference: import pyarrow.parquet as parquet resolves through the pyarrow.parquet attribute before falling back to sys.modules. So patch.dict(sys.modules, {"pyarrow.parquet": fake}) after pyarrow is already imported is no longer seen by the validator; import_module would have seen it. This is informational only and not a supported contract.
    • Native imports go through builtins.__import__; import_module did not. That only matters for import hooks, and the new tests rely on it.

Non-blocking notes

  1. Low — earlier failure removed when an optional dependency is missing (pandas/result_set.py:61-66).
    • Scenario: only pyathena[pandas] is installed (no pyarrow, pyproject.toml:58-60) and you call PandasCursor(unload=True).execute("SELECT ...", chunksize=50_000).
    • Before: ModuleNotFoundError was raised before StartQueryExecution.
    • Now: the UNLOAD runs on Athena (billed, writes Parquet to S3), and the failure only happens when results are read.
    • This is not a regression of any contract: the baseline already ran the query for the same call with no kwargs, or with chunksize set on the constructor. The early failure was incidental. It is worth knowing if anyone relied on it.
  2. Test strength:
    • test_kwargs.py:569 distinguishes "validate the original operation with prepared=False" from correct behavior. It cannot catch "validate the original with prepared=True" (SELECT 1 and DESCRIBE t both pick CSV). The existing tests at :394 and :418 do catch that, so overall coverage is adequate.
    • The polars import test's "pyarrow.parquet" check (polars/test_result_set.py:441-444) is vacuous, because that validator never imports pyarrow.
    • If the code went back to import_module, the __import__ spy would pass silently. The keyword_parameters.assert_not_called() check still catches it.
  3. Characterization tests: all of the new tests would also pass on the baseline. They protect the refactor rather than show a fixed failure, which is appropriate for a refactor with a fast path.

Benchmark evidence

Attribution: the 106 cases add up correctly: 3 connection, 60 cursor (15 classes × 4), 32 reader-preparation, 6 import, 2 resolve, 3 interrupt-mode.

  • Control cases: the import and resolve cases run the same code in both cohorts. resolve came out at 1.33 vs 1.32 µs and 2.90 vs 2.90 µs. Connection cases were within 0.1–0.2 µs.

  • Fast path (clear result, ranges don't overlap): cursor-settings validation dropped as follows:

    Validator Before (µs) After (µs)
    pandas CSV 1.02 0.14
    pandas Parquet 1.79 0.14
    Polars CSV 2.38 0.15
    Polars Parquet 1.41 0.15
  • Native import: reader-setting validation improved by 0.20–0.41 µs. That matches the measured cost difference: cached import_module is about 0.30 µs vs about 0.07 µs for a native import (0.155 µs for pyarrow.parquet). The two Parquet imports explain the 0.41 µs pandas Parquet gain.

  • Cursor-level gains are larger than the validator gains for Parquet: 2.3–2.5 µs vs 1.26–1.64 µs. The roughly 1 µs left over isn't explained by the evidence; it may be noise from the UUID/UNLOAD path or allocation effects.

  • Possible small cost on the no-reader-kwargs path: pandas/Polars execute_{defaults,keywords,prebuilt} are about +0.18 µs on average, against about +0.06 µs for unchanged cursors. That fits the extra 13-keyword call, but the per-case ranges overlap and the control spread reaches +0.22 µs (AioCursor.execute_defaults). The evidence neither confirms nor rules out a regression of about 0.1–0.2 µs.

  • Interrupt-mode cases (100 iterations, range 40–68 µs) support no conclusion.

Harness behavior:

  • Retained layers: real execute bodies, option resolution, formatting/UNLOAD wrapping, validators and _execute.
  • Faked layers: AWS client, start/poll, result sets, and asyncio.to_thread. The ImmediateExecutor runs work synchronously, so Future scheduling and thread offload are not measured.
  • Warm-up: 30 calls for sync cases; one full batch of 2000 for asyncio.
  • GC: timeit disables GC during timing.
  • Imports and caches: pandas, polars and pyarrow are imported, and signature caches warmed, before timing. Cold imports and first inspect.signature costs are not measured.
  • Interpreter and cohorts: Python 3.13.1 on arm64, 9 fresh subprocesses per cohort, alternating order.

Provenance limitations:

  • The harness prints str(source) as a resolved absolute path (measure-api.py:245), but the samples say "source": "before"/"after" (samples.json:22, 1104). Either the samples were edited afterwards or a different harness revision produced them.
  • No file or harness hashes were recorded.
  • "after" is a live git worktree, so uncommitted changes can't be ruled out.
  • "before" is an exported synthesized merge-tree that the samples can't verify.
  • Aio cursors were created from a sync Connection.
  • Local timing like this says nothing about AWS end-to-end latency or how asynchronous scheduling performs.

Static-review limitations

I ran no tests, type checking, linting or benchmarks. Python import-machinery details are reasoned from CPython semantics, not observed. I assumed followup.patch matches the comparison tree → head diff stated in review-shas.txt.


Author verification and disposition of the non-blocking notes:

  • Missing optional dependencies remain a lazy result-reading concern. The cursor-setting-only guard removes an incidental earlier dependency failure for some kwargs forms; this boundary is stated in the description. Unknown reader names still reject before submission. This is consistent with the existing no-extra-kwargs and constructor-settings paths. No additional optional-dependency preflight is introduced.
  • Preparation-mode coverage is combined with the existing original/prepared-mode tests, as the reviewer noted. The extra pyarrow name in the Polars import spy is harmless. Ordinary imports and callable replacement are the supported paths; a divergent parent-module attribute versus sys.modules monkeypatch is outside that contract. No source repair is required.
  • The statement that every new test passes on the baseline is too broad. Static comparison of baseline validators shows that non-empty cursor-only settings unconditionally call keyword_parameters; the new assert_not_called assertion rejects that baseline path. The state/mode/interrupt and replacement cases are intentional characterization coverage. No baseline test execution is claimed.
  • The review copy deliberately normalizes only each sample's absolute source path to the before/after cohort label. Compared it with the retained raw results: every timing, aggregate, environment field and other value is identical. This explains the provenance observation; a different harness revision did not produce that difference.
  • Verified all 293 tracked files in each source cohort against the comparison-tree/candidate Git blob IDs after the review. The reviewed harness/driver/provenance files match their retained pre-review SHA-256 manifest. Child harness: 0b10d65b9780c50232d71a5bd4994aa723f93197ebf725288e3ba64e65a8314a; driver: 6b932d83a292ec0fc5cc5881e4a13b5657f05ed4cb261b190c5679a6b71990f5; retained raw samples: 61ee45e9650ac845bf25174e0dfae5f81963235a65c4ced2f24499cc0def4cfe. These checks establish current file identity, not atomic immutability throughout the earlier measurement run.
  • The benchmark deliberately uses local fake providers and a sync Connection for aio cursor bodies; it does not cover asynchronous connection lifecycle or scheduling. The unexplained remainder of the Parquet execute differences is not attributed to the import optimization. Small default-path costs remain possible; overlapping observed ranges do not establish equal cost. Interrupt-mode timings and these local timings support no AWS end-to-end performance conclusion.

No actionable introduced defects were identified, so no source repair or independent repair follow-up was needed. Final-source format/lint and 683 offline tests have passed. Current-head AWS validation will be recorded separately after Ready.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 9, 2026 15:14
@laughingman7743
laughingman7743 marked this pull request as draft October 9, 2026 15:45
laughingman7743 and others added 2 commits October 10, 2026 00:45
Pandas and Polars readers already reject unknown keyword names.
Checking them before the query required signature inspection,
UNLOAD classification, Polars alias handling, and a signature cache,
so keep the pandas and Polars cursors as on master.

Move the execute() keyword check and the constructor keyword names
onto BaseCursor, drop pyathena/_kwargs.py, and move the tests next to
the keyword-only tests in the connection test modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A custom default cursor class can consume its own options through
**kwargs, which constructor introspection cannot see. Apply only the
cursor_kwargs that the internal cursor class accepts.

Document that cursor subclasses must not pass their own settings to
the base constructor, and how pandas and Polars report unknown reader
option names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread pyathena/common.py
"""The connection that created this cursor."""
return self._connection

def _validate_execute_kwargs(

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) — FINDINGS, repaired.

Base: f3532e9fb57fd769441e533f0b8375fd854ece69
Head: a8d79da40e176d553555233c0e5fa5cbb110569d (initial pass at 45639cd638ea3f3d71a7fe105084619607fa0296)

Scope: the reworked PR after removing pandas/Polars reader pre-validation. Covered every execute() override in pyathena/ (12 validate; the 6 pandas/Polars variants forward to readers unchanged from master), all constructor chains reaching BaseCursor.__init__ (Spark, WithResultSet, dict cursors), executemany() delegation, Connection.cursor()/_cursor()/_internal_cursor(), the sync and async SQLAlchemy adapters, and the new tests in tests/pyathena/test_connection.py, tests/pyathena/aio/test_connection.py, and tests/pyathena/sqlalchemy/test_base.py. Also ran /code-review (medium) on the PR.

Repaired in a8d79da4:

  • pyathena/connection.py:669: _internal_cursor() filtered out only names declared by the default class's constructors, so a custom default cursor that consumes cursor_kwargs via **kwargs (e.g. kwargs.pop("trace_id")) broke SQLAlchemy reflection with TypeError. Now it applies only the names Cursor accepts; regression test test_internal_cursor_leaves_out_custom_cursor_options.
  • docs/usage.md:47: the pandas/Polars statement overclaimed; names are not checked when no result file is read and the reader error surfaces as OperationalError.

Deferred with reasons:

  • cursor_kwargs={"arraysize": 5000} with a DataFrame cursor makes the internal Cursor raise ProgrammingError: pre-existing on master (it already forwarded arraysize).
  • executemany(op, [], work_gruop=...) does not raise: no query runs; adding per-method validation for an empty batch is not worth the extra surface.
  • Explicit keyword-only result-set parameters instead of allow-lists: blocked by the abstract BaseCursor.execute(**kwargs) signature; a separate API design change.
  • Caching _constructor_keyword_names(): only the SQLAlchemy reflection path calls it, next to an Athena query.
  • AsyncSparkCursor builds its executor before super().__init__() can raise: no threads are started before submission.

Validation at a8d79da4: just format, just lint, just docs lint passed; the offline selection in the PR body passed (434).

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.

Round-one repair follow-up at 2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b (range a8d79da40e176d553555233c0e5fa5cbb110569d..2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b): CLEAN. Connection._cursor() takes the cursor class positional-only; both callers pass it positionally, AioConnection inherits the path, and test_cursor_kwargs_named_cursor fails without the change ("multiple values for argument 'cursor'"). Offline selection 435 passed, lint passed.

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.

Round-one repair follow-up at dd296bf5d231c5025f2f428384eec44a35ae876b (range 2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b..dd296bf5d231c5025f2f428384eec44a35ae876b, merge-base unchanged f3532e9fb57fd769441e533f0b8375fd854ece69): CLEAN. The deferred arraysize item is now fixed at the maintainer's request: Connection._internal_cursor() also leaves out arraysize, so cursor_kwargs={"arraysize": 5000} on a DataFrame/Arrow connection no longer makes the internal Cursor/AioCursor raise ProgrammingError; cursor_kwargs itself is not mutated and user cursors still receive it. The sync, aio, and both SQLAlchemy dialect tests fail without the change (4 failed) and pass with it. executemany([]) is filed as #1112; the Polars dtypes alias (outside this PR) as #1111. Lint passed; offline selection 435 passed.

Comment thread docs/usage.md

Unknown keyword arguments raise `TypeError` in cursor constructors and in `execute()` instead of being ignored.
The connection's `cursor_kwargs` are passed to every cursor that it creates, so the requested cursor class must accept them.
A cursor subclass that receives settings of its own must not pass them on to the base class constructor.

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, callers, operations) — FINDINGS, repaired.

Base: f3532e9fb57fd769441e533f0b8375fd854ece69
Head: a8d79da40e176d553555233c0e5fa5cbb110569d

Claims checked:

  • Readers reject unknown names: verified locally with pandas 3.0.6 / Polars 1.44.2 / PyArrow 25.0.1 that pd.read_csv, pd.read_parquet (pyarrow read_table), pl.read_csv, pl.scan_csv, pl.read_parquet, and pl.scan_parquet raise TypeError; the result sets wrap it as OperationalError (pyathena/pandas/result_set.py, pyathena/polars/result_set.py except Exception). _read_csv returns an empty frame before calling the reader when there is no readable output, so the docs now say so.
  • Validation happens before state change or AWS: tests snapshot vars(cursor) and fail on any _make_api_call; Future-based cursors validate before _execute/submit; Spark before _calculate.
  • on_start_query_execution/on_poll still accepted by every cursor: test_cursor_accepts_connection_callbacks (sync and aio).
  • Existing caller: dbt-athena connections_legacy.py (dbt-labs/dbt-adapters main) builds AthenaCursor(**kwargs) → Cursor.__init__(**kwargs) with cursor_kwargs={"debug_query_state", "num_iceberg_retries"} and reads them back from connection.cursor_kwargs. That now raises TypeError. It pins pyathena>=2.25,<3.35, and 4.0.0 already requires it to change (BREAKING: Make connection and cursor arguments keyword-only #1101, Remove the legacy keyword arguments from the private _execute() #1088). Added the subclass rule to docs/usage.md:45 and the example to the PR's release-note text.
  • AWS operator: no request-count or retry change; unknown names fail before any API call, otherwise the query path is the master path.

No unmeasured performance claim remains: the reworked PR adds a dict-key loop to non-reader execute() and no signature inspection on query paths.

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.

Round-two repair follow-up at 2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b: CLEAN. Claim corrected in docs/usage.md:47 and the PR body: managed query results (_fetch_all_rows_as_csv) invoke the reader without a result file, so the unchecked case is "no results to read" (no output and empty UNLOAD manifests). No other claim changed.

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.

Round-two repair follow-up at dd296bf5d231c5025f2f428384eec44a35ae876b: CLEAN. Docstring claim checked: DataFrame/Arrow/S3FS cursors have no arraysize upper bound (WithResultSet), while Cursor/AioCursor cap it at DEFAULT_FETCH_SIZE (1000). The internal cursor only reads reflection rows, so using its default page size changes no user-visible result. PR body updated.

Make the cursor class parameter of Connection._cursor() positional-only
so that cursor_kwargs={"cursor": ...} still reaches a custom cursor.
Managed query results are read without a result file, so describe the
unchecked case as having no results to read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread pyathena/connection.py
return self._cursor(cursor, **{**self.cursor_kwargs, **kwargs})

def _cursor(
self, cursor: type[FunctionalCursor] | None, /, **kwargs: Any

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 — Codex CLI (codex-companion task, default model and effort), read-only static review. Not authored by this reviewer.

Initial review: base f3532e9fb57fd769441e533f0b8375fd854ece69, head a8d79da40e176d553555233c0e5fa5cbb110569d, detached snapshot of tracked files (no .env), prompt without PR number, description, commits, or prior findings; no edits, builds, tests, GitHub, or memory access. Snapshot remained clean.
Covered: all cursor families across sync, Future-based Async, and native Aio (constructor forwarding, supported options, rejection timing, cursor state), pandas/Polars readers, SQLAlchemy reflection and async adaptation, docs, tests, conventions.

FINDINGS:

  1. P2 pyathena/connection.py:615 — Connection.cursor() now calls _cursor(cursor, **merged), so a custom cursor option named cursor from cursor_kwargs raises "multiple values for argument 'cursor'" (Connection and AioConnection). Verified; repaired in 2bc1ef0e by making the selector positional-only (connection.py:618), with test_cursor_kwargs_named_cursor.
  2. P3 docs/usage.md:47 — managed result storage serializes GetQueryResults rows and invokes the reader without a result file, so "not checked when no result file is read" was wrong. Verified; reworded to "no results to read".
    Note (non-blocking): the supported-keyword tests stop at _execute; forwarding is covered by existing result-set test_read_options tests.

Follow-up: bounded review of a8d79da40e176d553555233c0e5fa5cbb110569d..2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b traced into Connection.cursor, _internal_cursor, AioConnection, and the SQLAlchemy adapters — CLEAN; both findings repaired. Static review only.

Author validation at 2bc1ef0e: just format, just lint, just docs lint passed; offline selection in the PR body 435 passed.

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 follow-up — Codex CLI (codex-companion task, default model/effort), read-only static review of 2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b..dd296bf5d231c5025f2f428384eec44a35ae876b on a detached snapshot, traced into Connection, AioConnection, both SQLAlchemy reflection paths, the async adapter, and the updated tests: CLEAN. Static review only; snapshot unchanged.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 9, 2026 16:12
@laughingman7743
laughingman7743 marked this pull request as draft October 9, 2026 16:29
A DataFrame cursor allows an arraysize above Cursor's page size limit
of 1000, so applying it to the internal API cursor made SQLAlchemy
reflection raise ProgrammingError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@laughingman7743
laughingman7743 marked this pull request as ready for review October 9, 2026 16:38
@laughingman7743
laughingman7743 merged commit dafee9a into master Oct 10, 2026
14 checks passed
@laughingman7743
laughingman7743 deleted the fix/1093-reject-unknown-cursor-keywords branch October 10, 2026 03:06
laughingman7743 added a commit that referenced this pull request Oct 10, 2026
Spark work_group rejection now comes from the generic unknown-keyword
check added in #1102, so one case covers it. Fallback cases use explicit
expected values instead of normalizing them in the test body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
laughingman7743 added a commit that referenced this pull request Oct 10, 2026
Spark work_group rejection now comes from the generic unknown-keyword
check added in #1102, so one case covers it. Fallback cases use explicit
expected values instead of normalizing them in the test body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reject unknown keyword arguments in cursor constructors and execute()

1 participant