Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ cursor.execute("SELECT %(value)s", {"value": 1}, work_group="YOUR_WORK_GROUP")
Existing keyword arguments retain their defaults and behavior.
The cursor class can still be selected positionally with `connection.cursor(DictCursor)`.

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.

The pandas and Polars cursors pass the `execute()` keyword arguments that they do not use themselves to the reader, such as `pandas.read_csv()`.
The reader rejects an unknown name when it reads the query results, which PyAthena raises as `OperationalError`; the name is not checked when there are no results to read.

## Managed query result storage

When using a workgroup with [managed query result storage](https://docs.aws.amazon.com/athena/latest/ug/managed-results.html) enabled,
Expand Down
4 changes: 3 additions & 1 deletion pyathena/aio/arrow/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,15 @@ async def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Result-set overrides: ``block_size``, ``connect_timeout``,
and ``request_timeout``. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``connect_timeout`` and ``request_timeout`` override the cursor's values.

Returns:
Self reference for method chaining.
"""
self._validate_execute_kwargs(kwargs, ("block_size", "connect_timeout", "request_timeout"))
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
3 changes: 2 additions & 1 deletion pyathena/aio/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ async def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Self reference for method chaining.
"""
self._validate_execute_kwargs(kwargs)
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
3 changes: 2 additions & 1 deletion pyathena/aio/s3fs/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,14 @@ async def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Supported S3FS result-set overrides. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``csv_reader`` overrides the cursor's value.

Returns:
Self reference for method chaining.
"""
self._validate_execute_kwargs(kwargs, ("block_size", "csv_reader"))
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
3 changes: 2 additions & 1 deletion pyathena/aio/spark/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,12 @@ async def execute(
description: Calculation description.
client_request_token: Idempotency token.
work_group: Unused, kept for API compatibility.
**kwargs: Additional parameters.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Self reference for method chaining.
"""
self._validate_execute_kwargs(kwargs)
# A failure below must not leave the previous calculation on the cursor.
self._calculation_id = None
self._calculation_execution = None
Expand Down
14 changes: 14 additions & 0 deletions pyathena/aio/sqlalchemy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,20 @@ def cursor(self, cursor: Any = None, **kwargs: Any) -> AsyncAdapt_pyathena_curso
raw_cursor = self._connection.cursor(_ASYNC_CURSOR_CLASSES.get(cursor, cursor), **kwargs)
return AsyncAdapt_pyathena_cursor(raw_cursor)

def _internal_cursor(self, cursor: Any) -> AsyncAdapt_pyathena_cursor:
"""Create a cursor with the wrapped connection's ``_internal_cursor()`` and adapt it.

A synchronous cursor class is replaced as in ``cursor()``.

Args:
cursor: The cursor class to create.

Returns:
The created cursor wrapped in ``AsyncAdapt_pyathena_cursor``.
"""
raw_cursor = self._connection._internal_cursor(_ASYNC_CURSOR_CLASSES.get(cursor, cursor))
return AsyncAdapt_pyathena_cursor(raw_cursor)

def close(self) -> None:
"""Close the wrapped connection."""
self._connection.close()
Expand Down
4 changes: 3 additions & 1 deletion pyathena/arrow/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,15 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Result-set overrides: ``block_size``, ``connect_timeout``,
and ``request_timeout``. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``connect_timeout`` and ``request_timeout`` override the cursor's values.

Returns:
Tuple of (query_id, future) where future resolves to AthenaArrowResultSet.
"""
self._validate_execute_kwargs(kwargs, ("block_size", "connect_timeout", "request_timeout"))
options = ExecuteOptions.resolve(
options,
work_group=work_group,
Expand Down
4 changes: 3 additions & 1 deletion pyathena/arrow/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Result-set overrides: ``block_size``, ``connect_timeout``,
and ``request_timeout``. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``connect_timeout`` and ``request_timeout`` override the cursor's values.

Expand All @@ -181,6 +182,7 @@ def execute(
>>> cursor.execute("SELECT * FROM sales WHERE year = 2023")
>>> table = cursor.as_arrow() # Returns Apache Arrow Table
"""
self._validate_execute_kwargs(kwargs, ("block_size", "connect_timeout", "request_timeout"))
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
3 changes: 2 additions & 1 deletion pyathena/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Tuple of (query_id, future) where:
Expand All @@ -254,6 +254,7 @@ def execute(
>>> # Do other work while query runs...
>>> result_set = future.result() # Wait for completion
"""
self._validate_execute_kwargs(kwargs)
options = ExecuteOptions.resolve(
options,
work_group=work_group,
Expand Down
42 changes: 39 additions & 3 deletions pyathena/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import threading
import time
from abc import ABCMeta, abstractmethod
from collections.abc import Callable
from collections.abc import Callable, Collection
from concurrent.futures import Future, wait
from datetime import UTC, datetime, timedelta
from inspect import Parameter, signature
from typing import TYPE_CHECKING, Any, TypeVar, cast

from botocore.exceptions import BotoCoreError, ClientError
Expand Down Expand Up @@ -212,7 +213,6 @@ def __init__(
result_reuse_minutes: int,
on_start_query_execution: Callable[[str], None] | None = None,
on_poll: OnPollCallback | None = None,
**kwargs,
) -> None:
"""Initialize the cursor with the settings it uses to run queries.

Expand All @@ -236,7 +236,6 @@ def __init__(
waits for the query, by cursors whose ``execute()`` supports it.
on_poll: Callback invoked once per poll iteration with the current
execution object.
**kwargs: Ignored.
"""
super().__init__()
self._connection = connection
Expand Down Expand Up @@ -272,11 +271,48 @@ def get_default_converter(unload: bool = False) -> DefaultTypeConverter | Any:
"""
return DefaultTypeConverter()

@classmethod
def _constructor_keyword_names(cls) -> set[str]:
"""Get the keyword names accepted by the constructors in this class's MRO.

Returns:
The named parameters of each ``__init__`` defined in the MRO,
excluding ``self``.
"""
names: set[str] = set()
for base in cls.__mro__:
if "__init__" in vars(base):
names.update(
name
for name, parameter in signature(vars(base)["__init__"]).parameters.items()
if parameter.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
)
names.discard("self")
return names

@property
def connection(self) -> Connection[Any]:
"""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.

self, kwargs: dict[str, Any], allowed: Collection[str] = ()
) -> None:
"""Reject the keyword arguments that ``execute()`` does not support.

Args:
kwargs: The extra keyword arguments given to ``execute()``.
allowed: The extra keyword names that this cursor supports.

Raises:
TypeError: If a keyword name is not in ``allowed``.
"""
for name in kwargs:
if name not in allowed:
raise TypeError(
f"{type(self).__name__}.execute() got an unexpected keyword argument '{name}'"
)

def _build_start_query_execution_request(
self,
query: str,
Expand Down
36 changes: 35 additions & 1 deletion pyathena/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,21 @@ def cursor(
>>> pandas_cursor = connection.cursor(PandasCursor)
>>> df = pandas_cursor.execute("SELECT * FROM my_table").fetchall()
"""
kwargs = {**self.cursor_kwargs, **kwargs}
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.

) -> FunctionalCursor | ConnectionCursor:
"""Create a cursor without applying the connection's ``cursor_kwargs``.

Args:
cursor: The cursor class. If None, the connection's default cursor class.
**kwargs: The cursor constructor arguments. Omitted connection settings
default to the connection's values.

Returns:
The created cursor.
"""
_cursor = cursor or self.cursor_class
converter = kwargs.pop("converter", self._converter)
if not converter:
Expand All @@ -639,6 +653,26 @@ def cursor(
**kwargs,
)

def _internal_cursor(self, cursor: type[FunctionalCursor]) -> FunctionalCursor:
"""Create a cursor for queries that PyAthena runs on its own behalf.

Only the connection's ``cursor_kwargs`` that the cursor class accepts are
applied, leaving out options of the default cursor class such as ``unload``
of a DataFrame cursor. ``arraysize`` is left out too, since a DataFrame
cursor allows values above this cursor's page size limit. The cursor
class's default converter is used.

Args:
cursor: The cursor class.

Returns:
The created cursor.
"""
accepted = cursor._constructor_keyword_names() - {"arraysize"}
kwargs = {k: v for k, v in self.cursor_kwargs.items() if k in accepted}
kwargs["converter"] = cursor.get_default_converter()
return cast(FunctionalCursor, self._cursor(cursor, **kwargs))

def close(self) -> None:
"""Close the connection.

Expand Down
3 changes: 2 additions & 1 deletion pyathena/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Self reference for method chaining.
Expand All @@ -163,6 +163,7 @@ def execute(
... }
... )
"""
self._validate_execute_kwargs(kwargs)
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
4 changes: 3 additions & 1 deletion pyathena/s3fs/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,10 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Supported S3FS result-set overrides. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``csv_reader`` overrides the cursor's value.
``filesystem_class`` overrides the filesystem used to read results.

Returns:
Tuple of (query_id, Future[AthenaS3FSResultSet]).
Expand All @@ -227,6 +228,7 @@ def execute(
>>> result_set = future.result()
>>> rows = result_set.fetchall()
"""
self._validate_execute_kwargs(kwargs, ("block_size", "csv_reader", "filesystem_class"))
options = ExecuteOptions.resolve(
options,
work_group=work_group,
Expand Down
4 changes: 3 additions & 1 deletion pyathena/s3fs/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ def execute(
options: Shared execution options as an
:class:`~pyathena.options.ExecuteOptions` instance. Individual
keyword arguments take precedence over ``options`` fields.
**kwargs: Additional execution parameters.
**kwargs: Supported S3FS result-set overrides. Unknown names raise TypeError.
``block_size`` sets the read block size for this query, and
``csv_reader`` overrides the cursor's value.
``filesystem_class`` overrides the filesystem used to read results.

Returns:
Self reference for method chaining.
Expand All @@ -176,6 +177,7 @@ def execute(
>>> cursor.execute("SELECT * FROM my_table WHERE id = %(id)s", {"id": 123})
>>> rows = cursor.fetchall()
"""
self._validate_execute_kwargs(kwargs, ("block_size", "csv_reader", "filesystem_class"))
self._reset_state()
options = ExecuteOptions.resolve(
options,
Expand Down
1 change: 1 addition & 0 deletions pyathena/spark/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def execute(
work_group: str | None = None,
**kwargs,
) -> tuple[str, "Future[AthenaQueryExecution | AthenaCalculationExecution]"]:
self._validate_execute_kwargs(kwargs)
calculation_id = self._calculate(
session_id=session_id if session_id else self._session_id,
code_block=operation,
Expand Down
1 change: 1 addition & 0 deletions pyathena/spark/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def execute(
work_group: str | None = None,
**kwargs,
) -> SparkCursor:
self._validate_execute_kwargs(kwargs)
# A failure below must not leave the previous calculation on the cursor.
self._calculation_id = None
self._calculation_execution = None
Expand Down
4 changes: 1 addition & 3 deletions pyathena/sqlalchemy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,7 @@ def _internal_cursor(raw_connection: PoolProxiedConnection) -> Any:
returns its wrapper, so this is typed by the interface used here rather
than by the class requested.
"""
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.


def _column(self, name: str | None, type_: str, comment: str | None, partition: bool | None):
return {
Expand Down
Loading
Loading