This repo is a Cargo workspace (core/ = the pgpq crate, py/ = the pgpq
Python package, json/ = the arrow-json Python package) plus a
uv workspace for the Python development
environment.
- Rust — current stable. The
rust-versionin the rootCargo.toml([workspace.package]) is the library floor; the dev-dependencies used bycargo testhave a higher one (see the comment next torust-version), so building the test suite needs a reasonably recent stable. - uv — manages the Python environment. Do not
use
pipdirectly. - PostgreSQL client/server binaries — needed only for the Python test suite (see below). The Rust integration tests download their own Postgres.
- Optional: a nightly toolchain plus
cargo-fuzzfor the fuzz targets, andcargo-llvm-covfor local coverage.
make init # create the venv from the committed uv.lock + install pre-commit
make build-develop # maturin develop both extension modules into the venv
make test # cargo test + pytest
make lint # uv lock --check + dependency-group check + pre-commit (cargo fmt, clippy, ruff)cargo test alone runs the entire Rust suite (unit tests, byte-exact snapshot
tests, the typed roundtrip harness against an embedded Postgres, and a
property-based suite — set PROPTEST_CASES=1000 for a longer soak).
The development environment is locked: the root pyproject.toml is a virtual
uv workspace root (no [project] table — it is never published) whose
[dependency-groups] mirror the union of the [test]/[bench] extras of
py/pyproject.toml and json/pyproject.toml. CI installs only the root
group (uv sync --locked --only-group test --no-install-workspace), so:
A test dependency must be added in BOTH places — the package extra in
py/pyproject.toml(orjson/pyproject.toml) and the root[dependency-groups]— then re-runuv lockand commituv.lock.
Adding it only to the package extra installs nothing anywhere — neither the
Makefile's uv sync nor maturin develop consults the extras — but it can
appear to work locally when the package happens to be present transitively
(or via the bench group, which local make init installs and CI does not).
CI, which installs only the root test group, then fails with
ModuleNotFoundError. uv lock --check catches a stale lock but not a
missing mirror entry, so make lint (and the CI lint job) also runs
scripts/check_dep_groups.py, which fails if a root group is no longer a
superset of the corresponding extras.
The --no-sync flags on uv run in the Makefile are load-bearing: the
maturin-built extension modules are not part of the lock, and an implicit sync
would prune them from the venv.
Two different mechanisms, two different requirements:
-
Rust integration tests use
postgresql_embedded, which downloads a Postgres build from GitHub releases into~/.theseuson first run. The download is unauthenticated by default and can hit GitHub's rate limit — setGITHUB_TOKENin your environment to avoid that (any token with public-repo read access works). -
Python tests use
testing.postgresql, which needs realinitdb/postgresbinaries onPATH. On macOS with Homebrew:brew install postgresql@17 export PATH="$(brew --prefix postgresql@17)/bin:$PATH" export LC_ALL=en_US.UTF-8 # without a UTF-8 locale, initdb fails on macOS # (or comes up SQL_ASCII and text columns decode as bytes)
Alternatively, once the Rust tests have run, the embedded download works too:
export PATH="$HOME/.theseus/postgresql/<version>/bin:$PATH".
cargo llvm-cov --workspace --locked --lcov --output-path lcov.info # Rust
uv run --no-sync pytest py --cov=pgpq --cov-report=term # Python wrappersThe Python numbers cover only the pure-Python wrapper modules; the compiled extension shows up in the Rust coverage instead. CI uploads both to Codecov (informational only, never a required check).
core/tests/testdata/*.arrow inputs are generated by
core/tests/generate_test_data.py; core/tests/snapshots/*.bin are byte-exact
encodings of them, and core/tests/snapshots_csv/*.csv are what real Postgres
exports after a binary COPY load. If a snapshot is missing, the test writes it
and fails — inspect the new file and commit it. A changed snapshot means the
wire format changed and deserves scrutiny, not a reflexive regeneration.
cargo bench -p pgpq --bench yellow_cab_dataset # downloads a ~40MB parquet on first run
cargo +nightly fuzz run encode_record_batch # from core/, needs cargo-fuzzThe fuzz crate (core/fuzz/) is its own workspace and is excluded from normal
builds; stable cargo test/clippy never touch it.