Repository navigation
Backport #1185 and #1169 to 3.x #1198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d0dc76
92ca99f
053120c
60ee3ba
a2e6899
cb34998
e5c4fc4
90a9922
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| ) | ||
|
|
||
| import pyathena | ||
| from pyathena.cursor import Cursor | ||
| from pyathena.sqlalchemy.compiler import ( | ||
| AthenaDDLCompiler, | ||
| AthenaStatementCompiler, | ||
|
|
@@ -52,6 +53,7 @@ | |
| PoolProxiedConnection, | ||
| ) | ||
| from sqlalchemy.engine.interfaces import ( | ||
| DBAPIConnection, | ||
| ReflectedForeignKeyConstraint, | ||
| ReflectedIndex, | ||
| ReflectedPrimaryKeyConstraint, | ||
|
|
@@ -85,6 +87,35 @@ | |
| } | ||
|
|
||
|
|
||
| class _PingConnection: | ||
| """A DBAPI connection whose ``cursor()`` opens an API cursor. | ||
|
|
||
| ``AthenaDialect.do_ping()`` passes it to ``DefaultDialect.do_ping()``, so the ping | ||
| does not run through the connection's default cursor class. Other attributes are | ||
| those of the pinged connection, which ``is_disconnect()`` receives before | ||
| SQLAlchemy 2.0.5. | ||
| """ | ||
|
|
||
| def __init__(self, connection: Any) -> None: | ||
| """Wrap the pinged connection. | ||
|
|
||
| Args: | ||
| connection: The pinged DBAPI connection, or the async adapter around one. | ||
| """ | ||
| self._connection = connection | ||
|
|
||
| def cursor(self) -> Any: | ||
| """Open an API cursor on the pinged connection. | ||
|
|
||
| Returns: | ||
| The cursor, wrapped by the async adapter for an async connection. | ||
| """ | ||
| return self._connection.cursor(Cursor) | ||
|
|
||
| def __getattr__(self, name: str) -> Any: | ||
| return getattr(self._connection, name) | ||
|
|
||
|
|
||
| class AthenaDialect(DefaultDialect): | ||
| """SQLAlchemy dialect for Amazon Athena. | ||
|
|
||
|
|
@@ -284,6 +315,14 @@ def _get_table(self, connection, table_name: str, schema: str | None = None, **k | |
| raise exc.NoSuchTableError(table_name) from e | ||
| raise | ||
|
|
||
| def do_ping(self, dbapi_connection: DBAPIConnection) -> bool: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Self-review round one (implementation behavior): CLEAN. Scope: base 5970145, head a2e6899; every changed file (dialect
Out of scope, recorded only: with Polars 1.36.1, |
||
| # When unload is enabled in the default cursor, SELECT 1 is wrapped in UNLOAD | ||
| # which Athena rejects with MISSING_COLUMN_NAME because the column is unnamed. | ||
| # Using the plain cursor here performs the SELECT without the UNLOAD. | ||
| # The ping still runs in DefaultDialect.do_ping(), which before SQLAlchemy 2.0.5 | ||
| # is also where a failed ping is checked for a disconnect. | ||
| return super().do_ping(cast("DBAPIConnection", _PingConnection(dbapi_connection))) | ||
|
|
||
| @reflection.cache | ||
| def _get_tables(self, connection, schema: str | None = None, **kw): | ||
| raw_connection = self._raw_connection(connection) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Copyright 2026 The PyAthena authors | ||
| # | ||
| # Licensed under the MIT License. | ||
| # See LICENSE or https://opensource.org/licenses/MIT. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| import polars as pl | ||
| import pytest | ||
|
|
||
| from pyathena.polars.util import get_athena_type | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("dtype", "expected"), | ||
| [ | ||
| (pl.List(pl.Int32), ("array", 0, 0)), | ||
| (pl.List(pl.Struct({"x": pl.Int32})), ("array", 0, 0)), | ||
| (pl.Struct({"x": pl.Int32}), ("row", 0, 0)), | ||
| (pl.Struct({"x": pl.List(pl.Int32)}), ("row", 0, 0)), | ||
| ], | ||
| ) | ||
| def test_get_athena_type_nested(dtype, expected): | ||
| assert get_athena_type(dtype) == expected | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not hasattr(pl, "Map"), reason="Polars 1.x has no Map type") | ||
| def test_get_athena_type_map(): | ||
| assert get_athena_type(pl.Map(pl.String, pl.Int32)) == ("map", 0, 0) | ||
| assert get_athena_type(pl.Map(pl.String, pl.List(pl.Int32))) == ("map", 0, 0) | ||
| assert get_athena_type(pl.List(pl.Map(pl.String, pl.Int32))) == ("array", 0, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review round two (claims, callers, operations): FINDINGS, repaired in the PR body only; no code change.
Scope: base 5970145, head a2e6899; the PR body, the five commit messages, the changed comments and docstring, and docs/polars.md and docs/sqlalchemy.md on 3.x.
Finding (repaired): the release note said an UNLOAD map was described as
stringbefore andmapnow. With Polars 2.0 on the base, a map whose values are lists or structs was described asarrayorrow, becausestr(dtype)isMap(String, List(Int32))orMap(String, Struct({'a': Int32}))and the old substring checks matched it. The note now states all three previous results. The TEST section's account of the Polars 1.36.1 file-cache panic is narrowed to what was observed. The eviction explanation is marked as inferred from the Polars source (EvictionCandidate::update_ttl()), not traced in the run.Claims checked and held:
**kwargsand the adapter'scursor_kwargs/retry_configproperties come from Fix metadata reflection throttling and reuse listed metadata #777 (commits 7932642, 922b295, and 9d68b59 all belong to Fix metadata reflection throttling and reuse listed metadata #777).Connection._internal_cursor()comes from BREAKING: Reject unknown cursor keyword arguments #1102 (09f7fb3). 3.x has nopyathena.util.override.uv.lock: only thepolarsandpolars-runtime-32entries change. Polars 2.0.0 requires Python >=3.10, so every Python version 3.x supports resolves it for a freshPyAthena[polars]<4install.TestAthenaPolarsResultSet,test_new_columns_with_schema_overrides,test_csv_read_kwargs_replace_defaults,test_txt_new_columns_with_schema_overrides,test_storage_options_replace_defaults: 0 matches)._create_dataframe_iterator()→_iter_parquet_chunks()) never calls_read_parquet_schema(), so it keeps the UNLOAD statement'srowsmetadata; Describe chunked Polars UNLOAD results with the Parquet schema #1012 ("Describe chunked Polars UNLOAD results with the Parquet schema") is the master fix.engine.dialect.do_ping(engine.raw_connection())(superset/commands/database/utils.py). That passes a_ConnectionFairy, whosecursor(*args, **kwargs)forwards the cursor class to the DBAPI connection (SQLAlchemy 2.0.46, pool/base.py). SQLAlchemy 1.4's defaultdo_ping()re-raises a non-disconnect error, and this dialect does not overrideis_disconnect(), so error propagation is unchanged.just tox→just test pyathena(pytest -n 8) on Python 3.10–3.14, so Python 3.10–3.13 keep Polars 1.36.1 as before.pl.read_csv/pl.read_parquet, and PyAthena's S3FileSystem for S3 access); docs/sqlalchemy.md does not mention pings.