Repository navigation
Redesign the PyAthena test fixtures: define once, generate data, set up once per job #848
Copy link
Copy link
Closed
Labels
Milestone
Description
Use case
The PyAthena suite's shared fixtures have barely changed since 2022 (#295). They are costly to set up, and they are hard to change.
Setup cost
tests/__init__.pygives every process its own random schema name (pyathena_test_<random>).pytest_sessionstartintests/pyathena/conftest.pyruns in every process. It uploadstests/resources/rows/*to S3, creates the database, and runscreate_table.sql.jinja2: 34 statements, namely 16DROP TABLE IF EXISTS(redundant in a fresh schema), 16CREATE EXTERNAL TABLE, and 2CREATE OR REPLACE VIEW.- With
pytest -n 8, one job sets up the same fixtures 9 times: 8 workers plus the pytest-xdist controller, which runs no tests. A full run on 5 Python versions creates 45 copies. In the sample on Reduce the AWS cost of integration tests without reducing real-service coverage #834,CREATE TABLE/DROP TABLEstatements were 15.4% of all CI queries.
Maintainability
- The data is files in
tests/resources/rows/, prepared by hand in advance. The maintainer has confirmed that this approach does not need to be kept:one_row_complex.gzis a single gzipped row in Hive text format, with\002/\003separators for arrays, maps, and structs. It cannot practically be edited by hand.many_rows.tsvhas 10,000 lines.
one_row_complex(16 columns) is referenced 83 times in 9 files. The cursor tests (default, pandas, arrow, polars, s3fs) spell out the full row or DataFrame for each cursor type, and the SQLAlchemy reflection tests assert its column list. Adding a column means editing the Hive text file and every one of those expectations.- 9 of the 16 tables are
execute_many*tables: mutable, one per cursor type, created for every session whether used or not. tests/resources/queries/insert_into_table.sql.jinja2is not referenced anywhere.
Proposed change
Redesign the shared fixtures so that they are defined once, generated, and set up once per job.
- Single definition in Python.
- Describe each shared table in code: name, columns with Athena types, storage format, partitions, and rows as Python values.
- Generate the DDL and the data files from this definition, and upload them. No queries are needed beyond
CREATE EXTERNAL TABLE/CREATE VIEW. - The data format is free to choose; TSV does not need to be kept. Parquet written with pyarrow (already a dev dependency) is a good default: it represents binary, decimal, microsecond timestamps, and nested array/map/struct values natively, with no delimiter escaping.
- Keep text-format tables only where a test depends on text-format behavior, such as NA handling.
- Expectations derived from the definition.
- Tests that read a whole row get their expected values from the definition, with per-cursor conversion helpers (Python objects, pandas, Arrow, Polars).
- Other tests select explicit columns instead of
SELECT *. - Adding a column or a type-coverage table then means adding one entry.
- Set up once per job.
- Skip the setup in the xdist controller and drop the redundant
DROP TABLE IF EXISTSstatements (a small first step). - Then create the read-only fixtures once, in one schema shared by all workers. For example, the controller creates it and passes the name through pytest-xdist
workerinput. - Keep cleanup working after interrupted runs (the scheduled database sweep covers leftovers).
- Skip the setup in the xdist controller and drop the redundant
- Mutable tables per test. Replace the session-wide
execute_many*tables with per-test tables, like the existingexecutemany_tablefixture, so the shared schema stays read-only. - Remove the unused
insert_into_table.sql.jinja2.
This can land in steps. Step 3's first part is independent and cheap; the definition and the expectation helpers can move one table at a time, starting with one_row_complex.
Validation plan (if implementing)
just test pyathenaagainst AWS passes with-n 8and with-n 1, with the same number of tests and no weakened assertions. Compare the collected test IDs and the pass/skip counts before and after.- Count the setup statements and S3 uploads per job before and after.
- Show that adding a column to a shared table is a one-place change, by adding one in the PR that introduces the definition, or in a follow-up.
- Confirm that the schema and the S3 Tables namespace are cleaned up after normal and interrupted runs.
Reactions are currently unavailable
Activity
[-]Set up the PyAthena test fixtures once per job instead of in every xdist process[/-][+]Redesign the PyAthena test fixtures: define once, generate data, set up once per job[/+]laughingman7743 commentedon Sep 27, 2026
A related teardown failure, from the SQLAlchemy compliance suite rather than
tests/pyathena/, may be worth covering in the fixture redesign.What happened
In run 36252040653 (PR #858), job
test-sqla-async / run (3.11), attempt 1:INTERNALERRORinpytest_testnodedown. The follower-database hook_awsathena_drop_dbintests/sqlalchemy/conftest.pyranDROP DATABASE test_b4798d487ea4 CASCADE, which failed withSemanticException [Error 10072]: Database does not exist.The database was dropped
4f17347c-..., the onlyDROP DATABASEfor this name amongStartQueryExecutioncalls between 15:41:55 and 15:42:25 UTC.GetDatabaseGetTablesDeleteDatabaseGetDatabase(invoked by Athena)EntityNotFoundExceptionSo Athena deleted the database, looked it up again, and reported the query as
FAILED.The error came from the service side, after a successful delete.
Effect on the current hook
{ident},{ident}_test_schema, and{ident}_test_schema_2in sequence withoutIF EXISTS._test_schemadatabases were left behind until the 7-day sweep.drop_database.sql.jinja2already usesDROP DATABASE IF EXISTS ... CASCADE.Suggestion for the redesign
DROP DATABASE IF EXISTS ... CASCADEand attempt each of the three drops even if an earlier one fails.IF EXISTSavoids this particular post-delete failure, because it did not reproduce on rerun.Database does not existfrom aDROPas success would also keep a passing test run from failing in teardown.laughingman7743 commentedon Sep 27, 2026
Proposal for the fixture work: add
IF EXISTSto the follower-database drops intests/sqlalchemy/conftest.py.The PyAthena suite's teardown already uses it (
tests/resources/queries/drop_database.sql.jinja2:DROP DATABASE IF EXISTS {{ schema }} CASCADE).The SQLAlchemy suite's
_awsathena_drop_dbdoes not:Suggested change:
Why:
Database does not existfor aDROP DATABASE ... CASCADEthat already deleted the database.IF EXISTS, that turns a passing run into anINTERNALERRORinpytest_testnodedown, and it aborts the remaining two drops, leaving the_test_schemadatabases for the 7-day sweep.IF EXISTSalso matches the PyAthena suite's teardown._awsathena_create_dbcalls this hook insidecontextlib.suppress(Exception)to clear leftovers before creating, soIF EXISTSdoes not change that path.Limit: I could not reproduce the failure, so it is unverified whether
IF EXISTSsuppresses Athena's post-deleteGetDatabasefailure.If it does not, attempting each drop independently and treating
Database does not existas success would still let the three drops complete.laughingman7743 commentedon Sep 28, 2026
Status of the proposed changes:
insert_into_table.sql.jinja2Step 2 was dropped by the maintainer's decision. To derive the expected results of every cursor from the table definitions,
tests/pyathena/expected.pyhad to re-model how each cursor returns each type:It reached about 800 lines, enough that it would need tests of its own. The hand-written expectations are more verbose, but they can be read as they are.
Adding a column to
one_row_complextherefore means changing two things:tests/pyathena/tables.pyNotes from the work that stay useful:
many_rowsandone_rowremain text tables. ForSELECT ... LIMITwithoutORDER BY, Athena returned a 10,000-row Parquet file out of file order. The reflection tests assert the text SerDe ofone_row.test_spark_sqltests are always skipped. Theirpytest.mark.dependency(depends="test_spark_dataframe")passes a string instead of a list. This existing problem is unrelated to this issue.This issue can be closed once #878 is merged.
laughingman7743 commentedon Sep 28, 2026
Closing. Final state of the proposed changes:
insert_into_table.sql.jinja2Step 3's shared fixture schema was dropped by the maintainer's decision. #866 and #873 already brought the session setup from 333 to 88 statements per
pytest -n 8job. The shared schema would have removed 61 more.That saving is small next to the few thousand queries in a job. Athena does not charge for DDL, so it comes to under a tenth of a cent per job, and the wall time does not change.
To get there, the session lifecycle would have needed the following:
workerinputENV.schemaThe saving is not worth that complexity.
Set up the PyAthena test fixtures only in processes that run tests