Repository navigation
v4.2.0
Breaking changes
- A closed cursor raises
ProgrammingError("Cursor is closed.")fromexecute(),executemany(), the fetch methods, iteration,cancel(), the metadata methods, and its other methods (#1274). Before, fetches returnedNoneor[]andexecute()ran a new query. Cursors have a newis_closedproperty. - A closed result set raises
ProgrammingErrorfrom its fetch methods, iteration,as_pandas(),as_arrow(),as_polars(), anditer_chunks(), instead of returningNone,[], or an empty DataFrame or table (#1274). next()on an asyncio cursor or result set raisesTypeErrorinstead of returning an un-awaited coroutine (#1274).- After a fetch raises, later
fetchone(),fetchmany(), andfetchall()calls raiseProgrammingError(#1238). Before, the pandas and Polars result sets returned no more rows, and the other result sets continued after the skipped rows. Invalid arguments, such as a non-integerfetchmany()size, do not count as a failed fetch. - Calling one cursor or result set from several threads is not supported, as
threadsafety = 2declares (#1238). This includescancel()from a timer thread andclose()during a fetch. To stop a query from another thread, use its query ID. - A cancelled fetch of
AioPandasCursor,AioArrowCursor,AioPolarsCursor, orAioS3FSCursordrops the result set from the cursor, so later fetches raiseProgrammingError("No result set.")(#1238). A cancelledas_*()call of these cursors does the same (#1286). AioPandasCursor.as_pandas(),AioPolarsCursor.as_polars()/as_arrow(), andAioArrowCursor.as_arrow()/as_polars()are coroutines: useawait cursor.as_pandas()(#1286).AioPandasCursor.as_pandas()always returns a DataFrame. To read chunks one at a time, use the new asynciter_chunks()ofAioPandasCursorandAioPolarsCursor.- After
close()ofAsyncCursoror one of its variants (dict, pandas, Arrow, Polars, S3FS), the futures ofexecute(),description(), andpoll()that still wait for their query raiseProgrammingError(#1279, #1300). The query is not cancelled. A result set built after the close is closed before its future resolves. The futures ofcancel()andquery_execution()still run. A result set whose future resolved before the close stays open and belongs to the caller. AsyncCursor.description(query_id)reads only the result set metadata, or the Parquet schema of an UNLOAD result, and no longer reads the result rows (#1300). It uses the new metadata reader classes, such asAthenaResultSetMetadataReader. A custom result set class that overrides thedescriptionproperty no longer changes whatdescription(query_id)returns; make that change in its metadata reader class instead.- PandasCursor's temporal dtypes now depend only on the Athena type (#1273). The same applies to
AsyncPandasCursorandAioPandasCursor.timestampis alwaysdatetime64[us], and digits beyond microseconds are truncated. It was[ns]for 7 or more fractional digits,[s]when all values were NULL or withengine="pyarrow"and no fraction,[ns]withengine="pyarrow"and a fraction, andstrwhen a column also held values outside the nanosecond range.timestamp with time zoneisdatetime64[us, UTC]. It had the data's zone, or wasobjectfor several zones or an all-NULL column.datefrom UNLOAD results isdatetime64[us], as from CSV results. It wasobject(date).- The fetch methods return
datetime,date, and awaredatetimein UTC, andNonefor NULL. They returnedpd.TimestampandNaT. - When
parse_dates,dtype, orconvertersis given toexecute(), PyAthena leaves the temporal columns to pandas, and the fetch methods return pandas' values for them. engine="pyarrow"is now also used for results withtimestamp with time zonecolumns.
- A
timestamp with time zonevalue raisesDataErrorwhen its zone name is unknown, or when its zone repeats or skips its local time (daylight saving time) (#1224). Before, it returned a naivedatetimeor the earlier instant. Convert such values to UTC in SQL (AT TIME ZONE 'UTC') or readto_unixtime().tzdata>=2024.2is required. S3File.multipart_uploadis read-only, andS3File.multipart_upload_partsis removed (#1232).S3File.completed_partsreturns the uploaded or copied parts in part-number order.commit()raisesS3MultipartStateErrorafterdiscard()or after a failed write or commit. Before, it uploaded the buffered data after a discard and did nothing after a failure.- A failed read of an UNLOAD data manifest, or of a Spark calculation's output file through
get_std_out()/get_std_error(), raisesOperationalErrorinstead of the botocore exception (#1278). Callers that catch botocore errors there must catchOperationalError. - Custom result set subclasses:
fetchmany()andfetchall()now take rows through_take_rows(). A subclass whosefetchone()does not use the row buffer must override_take_rows()as well (#1195). Several private result set helpers moved to the metadata reader classes or were removed (#1300).
Behavior changes
- PandasCursor, ArrowCursor, and PolarsCursor read UNLOAD timestamps at microseconds at every nesting level, in whole and chunked reads (#1233). The dtypes are
datetime64[us],timestamp[us], andDatetime("us"), as for CSV results. Values outside 1677-09-21 to 2262-04-11 are now correct; before, they were wrapped or saturated.- PandasCursor:
coerce_int96_timestamp_unitgiven toexecute()still replaces the unit.coerce_int96_timestamp_unit=Noneis the same as not giving it (#1291). - PolarsCursor: a
schemagiven toexecute()is used as given. - PolarsCursor with Polars 1.x reads UNLOAD timestamps at nanoseconds and casts them to microseconds, so values outside 1677-09-21 to 2262-04-11 are wrong. It logs a warning that recommends Polars 2.0 or later. Chunked reads with pyarrow installed and no read options given to
execute()read them correctly (#1282).
- PandasCursor:
- ArrowCursor reads
datefrom CSV results asdate32instead oftimestamp[ms], as from UNLOAD results (#1236).as_polars()givespl.Dateinstead ofpl.Datetime("ms"). - ArrowCursor reads NULL
time,time with time zone, andtimestamp with time zonevalues of CSV results as null instead of''(#1236). The fetch methods still returnNone. - In every cursor, a value that fails to convert raises
DataErrornaming the column (Column 'name': ...) (#1273, #1277). PandasCursor whole reads raise it instead of anOperationalErrorthat wrapped it. ADataErrorfrom a function given inconvertersis still wrapped inOperationalError. - PolarsCursor with
ignore_errors=Truetruncates a CSV timestamp with more than nine fractional digits instead of returning null (#1234). Text that is not a timestamp is still null. - PolarsCursor with Polars 2.0 (#1169):
- When
new_columnsis given together withschema_overrides, it must name every column. - A map column of an UNLOAD result is described as
map, and its values are dicts. ArrowCursor.as_polars()returns map columns aspl.Map.- On every Polars version, a struct column that contains a list is described as
rowinstead ofarray.
- When
- For a chunked CSV result of
PolarsCursor,AioPolarsCursor, andAsyncPolarsCursor, theHeadObjectsize check runs when the result set is created, and its error is raised there instead of at the first fetch (#1190). - The chunk iterators of the pandas and Polars result sets, from
iter_chunks()oras_pandas(), raiseProgrammingErrorat their next read once their result set is closed, for example by another query on the cursor (#1286). Before, they ended without an error. - An UNLOAD result of PandasCursor and PolarsCursor that has data files is described by its columns, as in ArrowCursor (#1300). Before, a result whose read options dropped every row was described as
[]. For a chunked pandas read whose files hold no rows,engine="c"now raisesProgrammingError. to_sql()with the defaultflavor="spark"creates tables whose column names have spaces, tabs, newlines, and,;{}()=replaced with_, matching the names in the Parquet files (#1299). Such columns used to read as NULL. Tables created before keep the old names, and appending does not change them; recreate them, for example withif_exists="replace".- Cancelling an asyncio call stops the retries of its Athena requests in the thread: status polling, metadata,
AioCursorpages, and object construction (#1223). A request already sent still runs to its end. The cleanup requests (StopQueryExecution,StopCalculationExecution,TerminateSession) keep their retries. - Cancelling an
AioS3FileSystemcoroutine stops the retries of its S3 requests (#1231). Cancelling a chunked UNLOAD read ofAioPandasCursororAioPolarsCursorstops the retries of the requests that open its Parquet files (#1297). Closing a chunked UNLOAD result no longer waits for the retries of a Parquet file being opened ahead (#1297). AioS3FileSystemruns its blocking calls, including multipart copies, in a pool of up tomax_workersthreads of the filesystem (#1184). Each of its files uses a pool of up tomax_workersthreads. The event loop's default executor no longer limits them, so a process can use more threads than before. Amax_workersthat is not positive raisesValueErrorwhen the filesystem is created. AfterS3AioExecutor.shutdown(),submit()raisesRuntimeError.S3Filemultipart writes keep at mostmax_workersparts in flight, counting the part copies of an append, and a write waits until a part finishes (#1165, #1232). A part that failed is raised by the nextwrite(),flush(), orclose(), which closes the file without committing it (#1176, #1183, #1232). Before, it was raised only byclose()orcommit(). Inside an fsspec transaction, the error is raised by the write. If the caller catches it, the transaction fails when it commits. Committing again after a successful commit sends no request.- A failed part, completion, or part copy stops the retries of the running requests before the abort, and the requests that have not started are cancelled (#1192, #1231). A failed range of a parallel read cancels the ranges that have not started. A recursive
chmodstops after the first failed update, and the keys it has not updated keep their ACL (#1231). - In the traceback of an error from an S3 request or from a call run by an
S3Executor, the frames belowS3Core.call()or the executor no longer hold local variables, for example inpdb.pm()(#1192). executemany()closes only the result set after a failure, not the cursor (#1274).
Benchmark
The benchmark of this release ran on commit 9e99caa8 (#1288), whose package code is identical to this release: the release commit baee716c differs from it only in one test file (#1311). Its history entries are benchmarks/history/2026-10-19-9e99caa8.md and benchmarks/history/2026-10-19-9e99caa8-filesystem.md (#1310). Highlights:
pandas.read_sql_query()through the result-file drivers is faster, because the row output of PandasCursor and PolarsCursor is faster (#1167, #1177). For 10 million flat rows,awsathena+pandaswithunload=truetook 50.7 seconds instead of 162.6, andawsathena+polarswithunload=true48.7 seconds instead of 92.0.AioS3FileSystemwrites and copies as fast asS3FileSystemwith 40 workers (#1184): a 6 GiB multipart copy ran at 1,889–1,925 MiB/s instead of 521–540.- Chunked reads of 100 million flat rows, about 29 GB of CSV, peaked at 462 MiB (PandasCursor CSV), 1.0 GiB (PolarsCursor CSV and UNLOAD), and 986 MiB (PandasCursor UNLOAD), with
chunksize=100000.
Known issues
- PolarsCursor with Polars 1.x holds a copy of the CSV result file while it reads a CSV result whole, so the peak memory of such reads grew by about the size of the file (#1307). Polars 2.0, chunked reads, and UNLOAD results are not affected.
What's Changed
- Record the benchmark runs of the PyAthena 4.1.0 release commit by @laughingman7743 in #1157
- Bound the parts in flight of S3File multipart writes by @laughingman7743 in #1165
- Defer benchmark fleet takeovers whose quiescing Athena throttled by @laughingman7743 in #1166
- Return PandasCursor rows as tuples without a dictionary per row by @laughingman7743 in #1167
- Read CSV results through fsspec for Polars 2.0 by @laughingman7743 in #1169
- Support Python 3.15 by @laughingman7743 in #1164
- Close the remaining gaps in the benchmark fleet's quiescing by @laughingman7743 in #1175
- Stop a multipart write at a part found failed while it waits by @laughingman7743 in #1176
- Keep a notice observed while deferring a fleet takeover by @laughingman7743 in #1181
- Return PolarsCursor rows as tuples without a dictionary per row by @laughingman7743 in #1177
- Stop appends, sync multipart copies, and commits at a failed part by @laughingman7743 in #1183
- Defer closing a chunk reader while another thread reads it by @laughingman7743 in #1182
- Size the asyncio filesystem's threads by max_workers and add open_async() by @laughingman7743 in #1184
- Take buffered rows in bulk in fetchmany() and fetchall() by @laughingman7743 in #1187
- Give the chunk readers the state that close() clears when they are created by @laughingman7743 in #1190
- fix: run ping with plain cursor by @greg-om in #1185
- Share the buffer refill between fetchone() and the bulk fetches of the result sets by @laughingman7743 in #1195
- Let DefaultDialect.do_ping() run the ping with the API cursor by @laughingman7743 in #1200
- Discard the page of a request that close() interrupts by @laughingman7743 in #1197
- Free the data of failed S3 requests and clean up async copies at loop shutdown by @laughingman7743 in #1192
- Raise DataError for time zone values without one instant by @laughingman7743 in #1224
- Skip empty Arrow record batches and move the fetchmany() clamp by @laughingman7743 in #1222
- Stop the retries of closed result sets and cancelled asyncio calls by @laughingman7743 in #1223
- Test the temporal types of each synchronous cursor by @laughingman7743 in #1226
- Stop the S3 retries of cancelled calls and of failed requests' siblings by @laughingman7743 in #1231
- Convert the timestamp text of chunked Polars CSV reads in the scan by @laughingman7743 in #1234
- Read CSV dates as date32 and temporal text NULL as null in ArrowCursor by @laughingman7743 in #1236
- Read UNLOAD timestamps at microseconds by @laughingman7743 in #1233
- Move S3File writes onto a multipart write session by @laughingman7743 in #1232
- Break result sets after failed or cancelled fetches and drop the cross-thread guards by @laughingman7743 in #1238
- Document which tests the suite needs by @laughingman7743 in #1250
- Remove and rewrite unnecessary SQLAlchemy dialect tests by @laughingman7743 in #1251
- Remove and rewrite unnecessary top-level cursor, connection, converter, and utility tests by @laughingman7743 in #1253
- Remove and rewrite unnecessary synchronous filesystem tests by @laughingman7743 in #1254
- Remove and rewrite unnecessary Polars, Arrow, and S3FS cursor tests by @laughingman7743 in #1252
- Remove and rewrite unnecessary pandas cursor tests by @laughingman7743 in #1256
- Remove and rewrite unnecessary aio and Spark cursor tests by @laughingman7743 in #1258
- Run the cursor_kwargs callbacks in the connection test instead of an S3FS query by @laughingman7743 in #1261
- Remove the AioPandasCursor tests that #1245 covers by @laughingman7743 in #1265
- Bound the caller-thread start test and check hinted JSON values by @laughingman7743 in #1264
- Make the S3FS managed-results and end-of-results tests need their behavior by @laughingman7743 in #1266
- Drop the unused abort branch of _finish_multipart_upload by @laughingman7743 in #1262
- Remove and rewrite unnecessary async filesystem and core tests by @laughingman7743 in #1259
- Remove PyHive-derived DB API tests repeated on the same code path by @laughingman7743 in #1267
- Replace the cross-thread cancel example in the Spark documentation by @laughingman7743 in #1270
- State the current reasons for the Polars chunk reader factories by @laughingman7743 in #1271
- Move AioS3StreamedFile writes onto the multipart write session by @laughingman7743 in #1272
- Move the S3FileSystem multipart copy onto the write session by @laughingman7743 in #1269
- Build the pandas result-set tests through the constructor by @laughingman7743 in #1268
- Give PandasCursor's temporal columns one dtype per Athena type by @laughingman7743 in #1273
- Give cursors a PEP 249 closed state and make query and session starts interrupt-safe by @laughingman7743 in #1274
- Raise OperationalError for failed manifest and Spark output reads (GH-1235) by @laughingman7743 in #1278
- Name the column in DataError from the fetch methods of every cursor by @laughingman7743 in #1277
- Move the AioS3FileSystem multipart copy onto the write session by @laughingman7743 in #1275
- Release objects built for cancelled asyncio constructions and close AsyncCursor result sets created after close (GH-1208) by @laughingman7743 in #1279
- Remove the Polars 1.x pyarrow fallback for whole UNLOAD reads with timestamps (GH-1280) by @laughingman7743 in #1282
- Document the temporal type contract (GH-1221) by @laughingman7743 in #1284
- Treat Polars' default schema and with_column_names as not given (GH-1283) by @laughingman7743 in #1285
- Add a temporal benchmark shape derived from the snapshot timestamp (GH-1287) by @laughingman7743 in #1290
- Treat coerce_int96_timestamp_unit=None as not given for UNLOAD results (GH-1289) by @laughingman7743 in #1291
- Cite the Polars issues behind the INT96 timestamp handling (GH-1293) by @laughingman7743 in #1295
- Release AioS3FileSystem threads with close() and add async reads to the aio cursors (GH-1209) by @laughingman7743 in #1286
- Measure PolarsCursor on Polars 1.x and 2.x in the benchmark harness (GH-1292) by @laughingman7743 in #1294
- Declare the column names that the Spark flavor writes in to_sql() (GH-1298) by @laughingman7743 in #1299
- Stop the retries of the Parquet opens of a chunked read with the read that waits for them (GH-1296) by @laughingman7743 in #1297
- Share close paths, stop AsyncCursor waits after close, and read result set metadata with metadata readers (GH-1281) by @laughingman7743 in #1300
- Count only the file's ResourceWarning in test_open_async_cancelled by @laughingman7743 in #1311
New Contributors
Full Changelog: v4.1.0...v4.2.0