Skip to content

Measure PolarsCursor on both Polars 1.x and 2.x in the benchmark harness #1292

Closed

Description

@laughingman7743

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 Map that 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.0 works on the benchmark environment. It is an unlocked overlay.
  • Dependency groups polars1 = ["polars<2"] and polars2 = ["polars>=2"] in benchmarks/pyproject.toml resolve in uv lock when 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 polars1 gives Polars 1.44.2.
    • --no-default-groups --group polars2 gives 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 dev group forces polars<2 on Python < 3.14.
  • --group dev --group polars1 from benchmarks/ is rejected as conflicting with pyathena:dev. The benchmark package's own dev group 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.

  1. Locked dependency groups.
    • Add polars1 = ["polars>=1.39.0,<2"] and polars2 = ["polars>=2,<3"] to benchmarks/pyproject.toml, with the three-way conflict above in the root pyproject.toml.
    • Rename the benchmark's dev group (for example to test), so that it no longer collides with pyathena:dev.
    • Every benchmark sync selects its group explicitly. This covers just benchmark, .github/workflows/benchmarks.yaml, and EC2 bootstrap.
    • The root uv sync --group dev and the root CI stay unchanged.
  2. One stack, one virtual environment per group on each host.
    • Bootstrap syncs polars1 into /opt/pyathena/.venv and polars2 into a second environment.
    • jobs --environment polars2 adds an environment field to the job and a -polars2 suffix to the job ID.
    • run_job() runs the job with that environment's interpreter, and a job without the field uses sys.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. So Case.id, the trial keys, and report.py need no change, and existing job files and queues keep working.
    • environment.json already records the installed versions. Results cite the Polars version from there, not from the lock hash, which no longer identifies the installed set.
  3. Scope. Only polars and sqla_polars run in the polars2 environment, 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 test and 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

  1. laughingman7743 commented on Oct 18, 2026

    @laughingman7743
    MemberAuthor

    Decisions on the open questions (2026-10-18):

    • Test environment. Run the harness tests in both environments. just benchmark test and .github/workflows/benchmarks.yaml cover polars1 and polars2.
    • uv version. No change is needed. With the lock made by uv 0.13.1, uv 0.12.5 (the EC2 UvVersion default) ran uv sync --locked --no-default-groups --group polars1 and --group polars2 on Python 3.12 and installed Polars 1.44.2 and 2.0.0, respectively.
    • Ordering. The combined run of Run the combined benchmark before the 4.2.0 release #1288 starts after every issue filed so far has been resolved, so it measures Polars 1.x after Read and write timestamps at Athena's precision for each path #1289 item 1 too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions