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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/spark.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ with Connection(region_name="us-west-2",

This cursor allows you to send PySpark code blocks and use Spark DataFrame and SQL.

### Breaking changes in PyAthena 4.0

`SparkCursor.execute()`, `AsyncSparkCursor.execute()`, and `AioSparkCursor.execute()` raise
`NotSupportedError` when `parameters` is not `None`, including an empty dictionary or list.
Earlier versions silently ignored these parameters and executed the code block without them.
Omit `parameters` or pass `None` when executing Spark code.

These methods no longer accept `work_group`; passing it raises `TypeError`, even if its value is `None`.
Earlier versions silently ignored this argument.
Set the Spark workgroup when creating the connection or cursor instead.
The calculation runs in the Spark session selected by the cursor's `session_id` or the execution's `session_id` override.

(spark-session-lifecycle)=

### Session lifecycle
Expand Down
21 changes: 19 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ cursor = connection.cursor()
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.
Expand Down Expand Up @@ -69,6 +68,24 @@ Passing `max_workers` to those four constructors raises `TypeError`.
Passing it to `execute()` raises `TypeError` after the query has run; with `AsyncPandasCursor` and `AsyncPolarsCursor`, the returned future raises it.
`AsyncPolarsCursor` used its `max_workers` for both the thread pool and the S3 read workers; pass both arguments to keep the former values.

## Environment fallbacks in PyAthena 4.0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Self-review round two (claims and callers) — base dafee9a4264ae2bb765cc7c5e1e3ef842bd6cfb8, head 6d39f1489b9f0f2fae568858c8468f508845fc07. Result: CLEAN.

Checked against the code:

  • The fallback table: omitted or None reads the environment variable, "" becomes None, and a nonempty string is used as given.
  • The ProgrammingError raised when neither setting resolves.
  • The statement that this applies to connect(), aio_connect(), Connection, and AioConnection.
  • The statement that s3_staging_dir="" behaves as before.
  • The claims in docs/spark.md: NotSupportedError for any non-None parameters, TypeError for work_group even when it is None, and calculations running in the session selected by the cursor or by the session_id override.

Every Spark example in the docs sets work_group on the connection; none passes it to execute(). work_group passed when creating a cursor reaches StartSession through BaseCursor._work_group.

Local evidence at this head:

  • just lint and just docs lint passed.
  • uv run --env-file .env pytest --noconftest -p no:rerunfailures -q tests/pyathena/test_connection.py tests/pyathena/aio/test_connection.py tests/pyathena/spark/test_common.py: 319 passed, offline.

I did not run Spark AWS integration tests locally; they are left to the AWS CI run after Ready.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Rebase follow-up, self-review round two — head 86f4443c6f3f742d250cf4a044a09c0056e05e0c. Result: CLEAN. The claims are unchanged. Reading docs/usage.md in order, the migration sections for keyword arguments, worker settings, and environment fallbacks each stand on their own, and nothing in #1103 contradicts the fallback or Spark statements.

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.

Repair follow-up, self-review round two: head 6e9149616721c2b4e7a6ba356ab2b4ce5cb10108. Result: CLEAN. The implementation and docs are unchanged. I updated the PR description's TEST section with the new test locations and commands, and its counts match the runs above.


For connection settings, `s3_staging_dir` and `work_group` use the same fallback rules:

| Argument | Resolved setting |
|----------|------------------|
| Omitted or `None` | Read the corresponding environment variable. |
| `""` | Use `None` and ignore the corresponding environment variable. |
| Nonempty string | Use the supplied value. |

The environment variables are `AWS_ATHENA_S3_STAGING_DIR` and `AWS_ATHENA_WORK_GROUP`.
These rules apply to `connect()`, `aio_connect()`, `Connection`, and `AioConnection`.
At least one resolved setting must be nonempty; otherwise connection creation raises `ProgrammingError`.

This is a breaking change for `work_group=""`, which previously read `AWS_ATHENA_WORK_GROUP`.
Omit `work_group` or pass `None` to retain that fallback.
The behavior of `s3_staging_dir=""` is unchanged, including its use with managed query result storage.

## 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 Expand Up @@ -844,7 +861,7 @@ AWS_ATHENA_S3_STAGING_DIR
: The S3 location where Athena automatically stores the query results and metadata information. Required if you have not set up workgroups. Not required if a workgroup has been set up. When connecting to a workgroup with [managed query result storage](https://docs.aws.amazon.com/athena/latest/ug/managed-results.html), pass ``s3_staging_dir=""`` to explicitly disable this environment variable fallback (see [Managed query result storage](#managed-query-result-storage)).

AWS_ATHENA_WORK_GROUP
: The setting of the workgroup to execute the query.
: The workgroup used to execute queries when the connection's `work_group` argument is omitted or `None`. Pass `work_group=""` to disable this fallback.

## Credentials

Expand Down
2 changes: 2 additions & 0 deletions pyathena/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ def connect(**kwargs) -> Connection[Any]:
catalog_name: Athena data catalog name. Defaults to "awsdatacatalog".
work_group: Athena workgroup name. Can be used instead of s3_staging_dir
if the workgroup has a result location configured.
Pass an empty string to skip the ``AWS_ATHENA_WORK_GROUP``
environment variable fallback.
poll_interval: Time in seconds between polling for query completion.
Defaults to 1.0.
encryption_option: S3 encryption option for query results. Can be
Expand Down
11 changes: 8 additions & 3 deletions pyathena/aio/spark/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,24 +341,29 @@ async def execute(
session_id: str | None = None,
description: str | None = None,
client_request_token: str | None = None,
work_group: str | None = None,
**kwargs,
) -> AioSparkCursor:
"""Execute PySpark code asynchronously.

Args:
operation: PySpark code to execute.
parameters: Unused, kept for API compatibility.
parameters: Must be None; Spark calculations do not support parameters.
session_id: Spark session ID override.
description: Calculation description.
client_request_token: Idempotency token.
work_group: Unused, kept for API compatibility.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Self reference for method chaining.

Raises:
TypeError: If an unknown keyword argument is given.
NotSupportedError: If ``parameters`` is not None.
OperationalError: If the calculation does not complete successfully.
"""
self._validate_execute_kwargs(kwargs)
if parameters is not None:
raise NotSupportedError("Spark cursors do not support parameters.")
# A failure below must not leave the previous calculation on the cursor.
self._calculation_id = None
self._calculation_execution = None
Expand Down
11 changes: 8 additions & 3 deletions pyathena/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def __init__(
Args:
s3_staging_dir: S3 location to store query results. Required if not
using workgroups or if workgroup doesn't have result location.
None uses the ``AWS_ATHENA_S3_STAGING_DIR`` environment variable.
Pass an empty string to explicitly disable S3 staging and skip
the ``AWS_ATHENA_S3_STAGING_DIR`` environment variable fallback.
This is required when connecting to a workgroup with managed
Expand All @@ -220,6 +221,9 @@ def __init__(
catalog_name: Data catalog name. Defaults to "awsdatacatalog".
work_group: Athena workgroup name. Can substitute for s3_staging_dir
if workgroup has result location configured.
None uses the ``AWS_ATHENA_WORK_GROUP`` environment variable.
Pass an empty string to skip that environment variable fallback
and leave the workgroup unset.
poll_interval: Seconds between query status polls. Defaults to 1.0.
encryption_option: S3 encryption for results ("SSE_S3", "SSE_KMS", "CSE_KMS").
kms_key: KMS key ID when using SSE_KMS or CSE_KMS encryption.
Expand Down Expand Up @@ -260,7 +264,8 @@ def __init__(
Note:
Either s3_staging_dir or work_group must be specified. Environment
variables AWS_ATHENA_S3_STAGING_DIR and AWS_ATHENA_WORK_GROUP are
checked if parameters are not provided.
checked only when the corresponding argument is omitted or None.
An explicit empty string disables that fallback and becomes None.

When using a workgroup with managed query result storage, pass
``s3_staging_dir=""`` to prevent the environment variable fallback
Expand All @@ -282,8 +287,8 @@ def __init__(
self.region_name = region_name
self.schema_name = schema_name
self.catalog_name = catalog_name
if work_group:
self.work_group: str | None = work_group
if work_group is not None:
self.work_group: str | None = work_group or 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.

Self-review round two (claims, compatibility, and operational behavior): CLEAN.

Frozen diff: 971dd12..6af17de. Audited the PR body, commit claim, all changed docstrings, both migration documents, and regression evidence across the full 10-file inventory.

Verified prior silent Spark behavior against the base revision, rejection through **kwargs including work_group=None, the guard's ordering before existing state/reset and submission paths, retained None/positional calls and supported options, and the unchanged successful return/cancellation contracts. Traced connect(), aio_connect(), AioConnection.create()/inheritance, Connection.cursor(), Spark session WorkGroup selection, and SQL request construction. Confirmed that explicit empty connection settings disable environment fallback, both disabled settings raise before session construction, and s3_staging_dir='' still supports managed query result storage.

Checked migration instructions against callers and searched related docs for obsolete claims. Documentation builds passed; migration sections render, and warning messages also appear in the unchanged master build. Checked that the core-file workflow filters select Spark/PyAthena plus synchronous and asynchronous SQLAlchemy suites after Ready. Local validation is explicitly offline and has not been described as AWS proof. No actionable findings or repairs; current-head AWS CI remains the final delivery gate.

else:
self.work_group = os.getenv(self._ENV_WORK_GROUP)
self.poll_interval = poll_interval
Expand Down
21 changes: 20 additions & 1 deletion pyathena/spark/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from multiprocessing import cpu_count
from typing import TYPE_CHECKING, Any, cast

from pyathena import NotSupportedError
from pyathena.model import AthenaCalculationExecution
from pyathena.spark.common import SparkBaseCursor
from pyathena.util import override
Expand Down Expand Up @@ -209,10 +210,28 @@ def execute(
session_id: str | None = None,
description: str | None = None,
client_request_token: str | None = None,
work_group: str | None = None,
**kwargs,
) -> tuple[str, "Future[AthenaQueryExecution | AthenaCalculationExecution]"]:
"""Start a PySpark calculation and return its ID and polling future.

Args:
operation: PySpark code to execute.
parameters: Must be None; Spark calculations do not support parameters.
session_id: Spark session ID override.
description: Calculation description.
client_request_token: Idempotency token.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
The calculation ID and a future containing its terminal execution.

Raises:
TypeError: If an unknown keyword argument is given.
NotSupportedError: If ``parameters`` is not None.
"""
self._validate_execute_kwargs(kwargs)
if parameters is not None:
raise NotSupportedError("Spark cursors do not support parameters.")
calculation_id = self._calculate(
session_id=session_id if session_id else self._session_id,
code_block=operation,
Expand Down
23 changes: 21 additions & 2 deletions pyathena/spark/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import logging
from typing import Any, cast

from pyathena import OperationalError, ProgrammingError
from pyathena import NotSupportedError, OperationalError, ProgrammingError
from pyathena.model import AthenaCalculationExecution, AthenaCalculationExecutionStatus
from pyathena.spark.common import SparkBaseCursor, WithCalculationExecution
from pyathena.util import override
Expand Down Expand Up @@ -108,10 +108,29 @@ def execute(
session_id: str | None = None,
description: str | None = None,
client_request_token: str | None = None,
work_group: str | None = None,
**kwargs,
) -> SparkCursor:
"""Execute PySpark code and wait for the calculation to complete.

Args:
operation: PySpark code to execute.
parameters: Must be None; Spark calculations do not support parameters.
session_id: Spark session ID override.
description: Calculation description.
client_request_token: Idempotency token.
**kwargs: Unknown keyword arguments raise TypeError.

Returns:
Self reference for method chaining.

Raises:
TypeError: If an unknown keyword argument is given.
NotSupportedError: If ``parameters`` is not None.
OperationalError: If the calculation does not complete successfully.
"""
self._validate_execute_kwargs(kwargs)
if parameters is not None:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Independent review (relayed)

  • Reviewer: Codex CLI 0.161.0, model gpt-6.1-sol, model_reasoning_effort=high, --sandbox read-only. Session 01a123cf-46aa-7f52-bd99-03c20d897f02.
  • Scope: base dafee9a4264ae2bb765cc7c5e1e3ef842bd6cfb8, head 6d39f1489b9f0f2fae568858c8468f508845fc07, reviewed in a detached worktree snapshot. The prompt included no PR number, PR description, commit messages, or prior findings.
  • After the review, the snapshot and the PR worktree were both unchanged at the reviewed head.
  • This is a static review; the reviewer ran no builds or tests.

Reviewer output:

Covered surfaces:

  • All three Spark execute() implementations; BaseCursor._validate_execute_kwargs; calculation submission, polling, state handling, and session/workgroup selection.
  • Connection fallback resolution and _cursor; connect(), aio_connect(), AioConnection; synchronous and async SQLAlchemy connection argument forwarding.
  • Changed documentation and docstrings against current and base source; repository docstring and test conventions.
  • All changed tests, plus existing unknown-keyword/state-preservation tests and adjacent Spark failure-path tests.

CLEAN

No actionable regressions or pre-existing defects found in the reviewed paths.

Argument validation consistently precedes state mutation, calculation submission, and executor scheduling. Explicit empty connection settings suppress environment fallback, and the resolved values reach cursors without another fallback lookup. Documentation matches these behaviors.

By static comparison, the Spark rejection tests, empty-workgroup tests, and both-empty-settings test fail on the base behavior. Accepted-None and unchanged fallback cases appropriately remain passing controls. Tests use independently specified expectations and follow repository conventions; testing both wrappers and constructors provides useful forwarding coverage.

Small coverage gaps remain: non-None parameters supplied by keyword, preservation of a populated previous calculation execution, and combined invalid arguments exercising exception precedence. These do not establish an implementation defect.

Read-only, offline source review only. No builds or tests were run.

Author disposition: I accept the result. The coverage gaps it notes are not actionable:

  • Passing a non-None parameters by keyword binds the same way as passing it positionally.
  • Keeping the previous _calculation_execution follows from validation running before any state change. The generic unknown-keyword test from BREAKING: Reject unknown cursor keyword arguments #1102 also asserts that cursor state stays unchanged.
  • The order of exceptions when several arguments are invalid is not documented 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.

Independent follow-up (relayed): Codex CLI 0.161.0, gpt-6.1-sol, effort high, --sandbox read-only, session 01a123de-afcc-7a11-98db-b94c12e9bbe1. This was a narrow follow-up on the range-diff dafee9a4..6d39f148 → fa8febb3..86f4443c plus the upstream diff between the two bases, reviewed in a detached snapshot that was unchanged afterward. Static review only.

CLEAN — no actionable findings. Code and tests are unchanged; the sole range-diff difference is documentation context around the newly added worker-settings section. Argument rejection still precedes calculation submission and state mutation. Upstream changes do not affect the Connection fallback contracts. The keyword, worker, environment-fallback, and managed-storage guidance remains coherent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Independent follow-ups (relayed): Codex CLI 0.161.0, gpt-6.1-sol, effort high, --sandbox read-only. Static review in detached snapshots, each unchanged afterward.

  • 86f4443c..169c8a03, session 01a123f1-6a0e-7453-8aad-b654ccc754ef, result FINDINGS. "tests/pyathena/spark/test_async_cursor.py:263: the relocated rejection test omits both _calculation_id = \"previous\" and the assertion that cursor.calculation_id remains \"previous\"… if AsyncSparkCursor.execute() clears _calculation_id before rejecting parameters={}, the new test passes." It found no other issues: placement and mocks follow the neighboring tests, and nothing is left over in test_common.py.
  • 169c8a03..6e914961, session 01a123f3-2546-7cf3-beff-ffc3f6ec5dcc, result CLEAN. "The previous finding is resolved… The relocated tests retain the removed shared tests' rejection cases, None acceptance variants, and calculation-call arguments across all three Spark cursors."

raise NotSupportedError("Spark cursors do not support parameters.")

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) — base dafee9a4264ae2bb765cc7c5e1e3ef842bd6cfb8, head 6d39f1489b9f0f2fae568858c8468f508845fc07. Result: FINDINGS, now repaired.

This review covers the branch after it was rebased onto master, which now includes #1102.

Covered: execute() in SparkCursor, AsyncSparkCursor, and AioSparkCursor; Connection.__init__ fallbacks for work_group and s3_staging_dir; callers of both fallbacks (connect(), aio_connect(), AioConnection.create(), and SQLAlchemy create_connect_args); the docs; and the tests.

  • Since BREAKING: Reject unknown cursor keyword arguments #1102, the Spark execute() methods reject unknown kwargs through BaseCursor._validate_execute_kwargs. Removing work_group from their signatures is therefore enough to raise TypeError. The rebase drops the PR's separate SparkBaseCursor._validate_execute_arguments helper and keeps only an inline parameters is not None check. Both checks still run before any cursor state is reset or any calculation is started.
  • Tests: the Spark cases now cover parameters {}, [], and {"value": 1}, work_group=None, and parameters omitted or None. The class-specific branches and the session_id/return-shape assertions, which this change does not touch, are gone. The connection fallback tests set both environment variables and list explicit expected values instead of normalizing them in the test body. The aio file keeps one test for argument forwarding, since AioConnection.create() runs the same __init__. Each new test fails on the code before the fix.
  • Finding (repaired in 6d39f148): the work_group description in the connect() docstring (pyathena/__init__.py:101) did not mention the empty-string behavior.
  • No change needed: SQLAlchemy URL parsing drops blank query values (?work_group= becomes {}), so the SQLAlchemy URL path behaves the same before and after the change.

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.

Rebase follow-up, self-review round one — new base fa8febb350f112c1200ba805ef94cb2e60910c87 (#1103 merged), new head 86f4443c6f3f742d250cf4a044a09c0056e05e0c. Result: CLEAN.

I compared git range-diff dafee9a4..6d39f148 fa8febb3..86f4443c after verifying that both old commits still exist. Commits 2 and 3 are identical. Commit 1 differs only in where the docs section sits in docs/usage.md: the conflict with #1103's new "Worker settings in PyAthena 4.0" section was resolved by keeping both sections, with the worker section first. #1103 changes only the pandas and Polars cursors, the benchmarks, and their tests. It does not touch Spark execute() or the Connection fallbacks.

Checks run at the new head: just lint and just docs lint passed, and the same offline pytest command (connection, aio connection, Spark common) gave 319 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.

Repair follow-up, self-review round one: 86f4443c → 169c8a03 → 6e914961 (base fa8febb350f112c1200ba805ef94cb2e60910c87). Tests only.

The Spark execute() argument tests were in tests/pyathena/spark/test_common.py under a TestSparkExecute class, which matches no class. They now live in TestSparkCursor, TestAsyncSparkCursor, and TestAioSparkCursor, next to the existing offline __new__-based tests in each file. The checks are in each cursor's execute(), not in spark/common.py, so that is where the tests belong. The shared fixture and the sync/aio _execute helper were removed with the old tests.

The independent follow-up found that the AsyncSparkCursor rejection test no longer checked that the previous calculation ID is kept. I verified the finding: AsyncSparkCursor inherits calculation_id from SparkBaseCursor, and the interrupt path sets it. Fixed in 6e914961: every rejection test, for both parameters and work_group, now sets _calculation_id = "previous" and asserts it is unchanged.

Validation:

  • just lint passed.
  • The moved tests: 21 passed. Copied onto the master source, the 12 rejection cases fail and the 9 accepted-None controls pass.
  • The connection, aio connection, and Spark common tests: 298 passed, offline.

# A failure below must not leave the previous calculation on the cursor.
self._calculation_id = None
self._calculation_execution = None
Expand Down
46 changes: 46 additions & 0 deletions tests/pyathena/aio/spark/test_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,52 @@ async def test_execute_cancelled_while_starting_without_kill_on_interrupt(self):
cancel.assert_not_awaited()
assert cursor.calculation_id is None

@pytest.mark.parametrize("parameters", [{}, [], {"value": 1}])
async def test_execute_rejects_parameters(self, parameters):
"""Any parameters other than None raise before a calculation starts (no AWS)."""
cursor = AioSparkCursor.__new__(AioSparkCursor) # bypass __init__ to avoid AWS calls
cursor._calculation_id = "previous"
cursor._calculate = AsyncMock()

with pytest.raises(NotSupportedError, match="do not support parameters"):
await cursor.execute("code", parameters)

cursor._calculate.assert_not_awaited()
assert cursor.calculation_id == "previous"

async def test_execute_rejects_work_group(self):
"""work_group is not an execute() argument, even when None (no AWS)."""
cursor = AioSparkCursor.__new__(AioSparkCursor) # bypass __init__ to avoid AWS calls
cursor._calculation_id = "previous"
cursor._calculate = AsyncMock()

with pytest.raises(TypeError, match="unexpected keyword argument 'work_group'"):
await cursor.execute("code", work_group=None)

cursor._calculate.assert_not_awaited()
assert cursor.calculation_id == "previous"

@pytest.mark.parametrize(
("args", "kwargs"), [((), {}), ((None,), {}), ((), {"parameters": None})]
)
async def test_execute_accepts_none_parameters(self, args, kwargs):
"""Omitted or None parameters run the calculation (no AWS)."""
cursor = AioSparkCursor.__new__(AioSparkCursor) # bypass __init__ to avoid AWS calls
cursor._session_id = "session_id"
cursor._calculate = AsyncMock(return_value="calculation_id")
cursor._poll = AsyncMock(
return_value=MagicMock(state=AthenaCalculationExecutionStatus.STATE_COMPLETED)
)

await cursor.execute("code", *args, **kwargs)

cursor._calculate.assert_awaited_once_with(
session_id="session_id",
code_block="code",
description=None,
client_request_token=None,
)

async def test_executemany(self, aio_spark_cursor):
with pytest.raises(NotSupportedError):
await aio_spark_cursor.executemany("SELECT 1", [])
Expand Down
24 changes: 22 additions & 2 deletions tests/pyathena/aio/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
READER_CURSOR_CLASSES = [AioPandasCursor, AioPolarsCursor]


@pytest.mark.parametrize("factory", [pyathena.aio_connect, AioConnection.create])
async def test_empty_work_group_disables_environment_fallback(factory, monkeypatch):
monkeypatch.setenv("AWS_ATHENA_WORK_GROUP", "environment-workgroup")
conn = await factory(
region_name="us-east-1",
s3_staging_dir="s3://bucket/path/",
work_group="",
aws_access_key_id="access_key",
aws_secret_access_key="secret_key",
)
try:
assert conn.work_group is None
finally:
conn.close()


@pytest.mark.parametrize("factory", [pyathena.aio_connect, AioConnection.create])
async def test_connect_keyword_only(factory):
assert all(
Expand Down Expand Up @@ -73,9 +89,13 @@ def test_cursor_execute_keyword_only(cursor_class):
] == ["self", "operation", "parameters"]
parameters = {"value": 1}
cursor = object.__new__(cursor_class)
bound = execute_signature.bind(cursor, "SELECT %(value)s", parameters, work_group="group")
bound = execute_signature.bind(cursor, "SELECT %(value)s", parameters)
assert bound.arguments["parameters"] is parameters
assert bound.arguments["work_group"] == "group"
if cursor_class is AioSparkCursor:
assert "work_group" not in execute_signature.parameters
else:
bound = execute_signature.bind(cursor, "SELECT %(value)s", parameters, work_group="group")
assert bound.arguments["work_group"] == "group"
execute_signature.bind(cursor, operation="SELECT 1")
with pytest.raises(TypeError, match="positional"):
cursor.execute("SELECT %(value)s", parameters, "group")
Expand Down
60 changes: 58 additions & 2 deletions tests/pyathena/spark/test_async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import textwrap
import threading
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

import pytest

from pyathena import OperationalError
from pyathena import NotSupportedError, OperationalError
from pyathena.model import AthenaCalculationExecutionStatus, AthenaSessionStatus
from pyathena.spark.async_cursor import AsyncSparkCursor
from tests import ENV
Expand Down Expand Up @@ -256,3 +256,59 @@ def test_close_retries_termination_after_failure(self):
cursor.close()

assert cursor._terminate_session.call_count == 2

@pytest.mark.parametrize("parameters", [{}, [], {"value": 1}])
def test_execute_rejects_parameters(self, parameters):
"""Any parameters other than None raise before a calculation starts (no AWS)."""
cursor = AsyncSparkCursor.__new__(AsyncSparkCursor) # bypass __init__ to avoid AWS calls
cursor._calculation_id = "previous"
cursor._executor = MagicMock()

with (
patch.object(AsyncSparkCursor, "_calculate") as calculate,
pytest.raises(NotSupportedError, match="do not support parameters"),
):
cursor.execute("code", parameters)

calculate.assert_not_called()
cursor._executor.submit.assert_not_called()
assert cursor.calculation_id == "previous"

def test_execute_rejects_work_group(self):
"""work_group is not an execute() argument, even when None (no AWS)."""
cursor = AsyncSparkCursor.__new__(AsyncSparkCursor) # bypass __init__ to avoid AWS calls
cursor._calculation_id = "previous"
cursor._executor = MagicMock()

with (
patch.object(AsyncSparkCursor, "_calculate") as calculate,
pytest.raises(TypeError, match="unexpected keyword argument 'work_group'"),
):
cursor.execute("code", work_group=None)

calculate.assert_not_called()
cursor._executor.submit.assert_not_called()
assert cursor.calculation_id == "previous"

@pytest.mark.parametrize(
("args", "kwargs"), [((), {}), ((None,), {}), ((), {"parameters": None})]
)
def test_execute_accepts_none_parameters(self, args, kwargs):
"""Omitted or None parameters start the calculation and its polling (no AWS)."""
cursor = AsyncSparkCursor.__new__(AsyncSparkCursor) # bypass __init__ to avoid AWS calls
cursor._session_id = "session_id"
cursor._executor = MagicMock()

with patch.object(
AsyncSparkCursor, "_calculate", return_value="calculation_id"
) as calculate:
query_id, future = cursor.execute("code", *args, **kwargs)

calculate.assert_called_once_with(
session_id="session_id",
code_block="code",
description=None,
client_request_token=None,
)
assert query_id == "calculation_id"
assert future is cursor._executor.submit.return_value
Loading
Loading