From edbee3c9c5cbb1919c0f086ab200d9670a5f8e92 Mon Sep 17 00:00:00 2001 From: laughingman7743 Date: Sun, 26 Jul 2026 09:39:13 +0900 Subject: [PATCH] test(sqlalchemy): cover AthenaTimestamp.process and simplify AthenaDate isinstance Follow-up to #743, which fixed a missing return in AthenaDate.process. AthenaTimestamp.process is the sibling of that method and had no unit test, so nothing guarded its millisecond truncation: strftime("%f") emits six digits and the [:-3] slice narrows them to the three Athena TIMESTAMP supports. Add cases for that truncation, for a whole-second datetime, and for the non-datetime fallback, plus the matching fallback case for AthenaDate. Also drop the redundant datetime from AthenaDate.process's isinstance check: datetime is a subclass of date, so isinstance(value, date) already matches both. The annotation is narrowed to date | Any for the same reason. Swept the whole tree for the #742 bug class (an expression statement whose value is computed and discarded) with an AST walk. No ruff rule catches it - B018 deliberately skips strings so it cannot see a bare f-string - and the only other hits were a PEP 258 attribute docstring and an intentional __getitem__ inside pytest.raises. #742 was the sole real instance. --- pyathena/sqlalchemy/types.py | 6 +++-- tests/pyathena/sqlalchemy/test_types.py | 30 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pyathena/sqlalchemy/types.py b/pyathena/sqlalchemy/types.py index 89c6a4b0..906b6c16 100644 --- a/pyathena/sqlalchemy/types.py +++ b/pyathena/sqlalchemy/types.py @@ -79,8 +79,10 @@ class AthenaDate(TypeEngine[date]): render_bind_cast = True @staticmethod - def process(value: date | datetime | Any) -> str: - if isinstance(value, (date, datetime)): + def process(value: date | Any) -> str: + # datetime is a subclass of date, so this branch also covers datetime, + # which is truncated to its date part. + if isinstance(value, date): return f"DATE '{value:%Y-%m-%d}'" return f"DATE '{value!s}'" diff --git a/tests/pyathena/sqlalchemy/test_types.py b/tests/pyathena/sqlalchemy/test_types.py index 9a4f3943..3eb9c10c 100644 --- a/tests/pyathena/sqlalchemy/test_types.py +++ b/tests/pyathena/sqlalchemy/test_types.py @@ -12,6 +12,7 @@ AthenaDate, AthenaMap, AthenaStruct, + AthenaTimestamp, get_double_type, ) @@ -178,3 +179,32 @@ class TestAthenaDate: ) def test_process_renders_date_only_literal(self, value, expected): assert AthenaDate.process(value) == expected + + def test_process_falls_back_to_str(self): + assert AthenaDate.process("2017-01-01") == "DATE '2017-01-01'" + + +class TestAthenaTimestamp: + @pytest.mark.parametrize( + ("value", "expected"), + [ + # Athena TIMESTAMP has millisecond precision, so the six digits + # strftime("%f") emits are truncated to three. + ( + datetime(2017, 1, 1, 12, 34, 56, 789012), + "TIMESTAMP '2017-01-01 12:34:56.789'", + ), + ( + datetime(2017, 1, 1, 12, 34, 56), + "TIMESTAMP '2017-01-01 12:34:56.000'", + ), + ], + ) + def test_process_renders_millisecond_precision_literal(self, value, expected): + assert AthenaTimestamp.process(value) == expected + + def test_process_falls_back_to_str(self): + assert ( + AthenaTimestamp.process("2017-01-01 12:34:56.789") + == "TIMESTAMP '2017-01-01 12:34:56.789'" + )