Repository navigation
Measure PolarsCursor on both Polars 1.x and 2.x in the benchmark harness #1292
Description
Part of #1288. Found while adding the temporal shape for #1287 (#1290).
Problem
PyAthena supports polars>=1.39.0, and PolarsCursor reads UNLOAD results differently on the two lines:
- Polars 2.0 reads INT96 timestamps at microseconds. It reads a
Mapthat needs timestamp conversion as a list of structs, which PyAthena casts back. - Polars 1.x reads INT96 at nanoseconds, and PyAthena casts the result. This path is the subject of Read and write timestamps at Athena's precision for each path #1289 item 1, so it will change again.
The benchmark harness can measure only Polars 1.x today. It runs on Python 3.12 and shares the root uv.lock, and the root dev group pins polars<2.0.0; python_version < '3.14'. In one universal lock, that pin makes the benchmark environment resolve Polars 1.44.2, although benchmarks/pyproject.toml depends on unpinned polars. So the combined run of #1288 would not measure the Polars 2.0 path that #1287 names.
The harness also has no notion of the environment a job ran in. EC2 bootstrap runs uv sync --locked --no-dev once (benchmarks/cloudformation/benchmark.yaml), and fleet.run_job() runs every job with sys.executable. Case IDs and job IDs do not name the environment.
Measured (uv 0.13.1, Python 3.12, locally)
uv run --locked --with polars==2.0.0works on the benchmark environment. It is an unlocked overlay.- Dependency groups
polars1 = ["polars<2"]andpolars2 = ["polars>=2"]inbenchmarks/pyproject.tomlresolve inuv lockwhen the root declares one conflict set:[{package = "pyathena", group = "dev"}, {package = "pyathena-benchmarks", group = "polars1"}, {package = "pyathena-benchmarks", group = "polars2"}]. The results:--no-default-groups --group polars1gives Polars 1.44.2.--no-default-groups --group polars2gives 2.0.0.- Without a group, the benchmark environment then resolves 2.0.0.
- A conflict between only the two benchmark groups does not resolve, because the root
devgroup forcespolars<2on Python < 3.14. --group dev --group polars1frombenchmarks/is rejected as conflicting withpyathena:dev. The benchmark package's owndevgroup appears to be matched by name.
Proposal
The proposal was compared with Codex and Claude Fable 5.1 consultations. Both recommended the dependency mechanism and the family scope below. They differed on the integration, and the one below is the option that keeps a single prepared snapshot.
- Locked dependency groups.
- Add
polars1 = ["polars>=1.39.0,<2"]andpolars2 = ["polars>=2,<3"]tobenchmarks/pyproject.toml, with the three-way conflict above in the rootpyproject.toml. - Rename the benchmark's
devgroup (for example totest), so that it no longer collides withpyathena:dev. - Every benchmark sync selects its group explicitly. This covers
just benchmark,.github/workflows/benchmarks.yaml, and EC2 bootstrap. - The root
uv sync --group devand the root CI stay unchanged.
- Add
- One stack, one virtual environment per group on each host.
- Bootstrap syncs
polars1into/opt/pyathena/.venvandpolars2into a second environment. jobs --environment polars2adds anenvironmentfield to the job and a-polars2suffix to the job ID.run_job()runs the job with that environment's interpreter, and a job without the field usessys.executable. An unknown environment fails the job.- Resume (earlier attempts under
results/<job id>/), claims, done markers, and reports are already keyed by job ID. SoCase.id, the trial keys, andreport.pyneed no change, and existing job files and queues keep working. environment.jsonalready records the installed versions. Results cite the Polars version from there, not from the lock hash, which no longer identifies the installed set.
- Bootstrap syncs
- Scope. Only
polarsandsqla_polarsrun in thepolars2environment, with all their transports and shapes; the other families run once. The run selection includes whole (unchunked) UNLOAD reads, because chunked reads use pyarrow when it is installed.
A stack per environment was the alternative. It needs no environment field, but the second stack needs its own scratch bucket, results tree, and prepare. Because preparation selects rows with LIMIT and no ordering, its snapshot can hold different rows.
Open questions
- Test environment. Which Polars line should
just benchmark testand the offline CI use: 1.x, 2.x, or both? - uv version. EC2 bootstrap defaults to uv 0.12.5 (
UvVersion), and the lock above was made with 0.13.1. Check that 0.12.5 honors the conflict, or raise the default. - Ordering. Measure Polars 1.x after Read and write timestamps at Athena's precision for each path #1289 item 1 is fixed, because the fix changes that path.
- Release notes / RESULTS.md. State which families ran on which Polars version.
Activity
laughingman7743 commentedon Oct 18, 2026
Decisions on the open questions (2026-10-18):
just benchmark testand.github/workflows/benchmarks.yamlcoverpolars1andpolars2.UvVersiondefault) ranuv sync --locked --no-default-groups --group polars1and--group polars2on Python 3.12 and installed Polars 1.44.2 and 2.0.0, respectively.Validate job environments before publishing and type-check both envir…
Share the uv invocation and the fake orchestrator setup (GH-1292)
State when a benchmark sync needs a Polars group (GH-1292)
Constrain pyarrow on Python 3.15 for every resolution of the lock (GH…
Merge pull request #1294 from pyathena-dev/feat/1292-polars-benchmark…