Viewing on GitHub? The contributing guide lives in CONTRIBUTING.md. This stub is served by the pg_trickle docs site — the include below renders there.
Contributing to pg_trickle
Thank you for your interest in contributing! pg_trickle is an Apache 2.0-licensed open-source project and welcomes contributions of all kinds.
Before You Start
- Check the open issues and discussions to avoid duplicating work.
- For non-trivial changes, open an issue first to discuss the approach.
- Read AGENTS.md — it is the authoritative guide for all coding conventions, error handling rules, module layout, and test requirements.
- Read docs/ARCHITECTURE.md to understand the system.
- Read ROADMAP.md to see what work is planned.
Ways to Contribute
| Type | Where to start |
|---|---|
| Bug report | Open an issue |
| Feature request | Open an issue or start a discussion |
| Documentation fix | Open a PR directly — no issue needed for typos/clarity |
| Code fix or feature | Open an issue first, then a PR |
| Performance improvement | Include benchmark numbers (see just bench) |
Development Setup
# Install pgrx
cargo install cargo-pgrx --version "=0.18.0"
cargo pgrx init --pg18 /usr/lib/postgresql/18/bin/pg_config
# Build
cargo build
# Format + lint (required before every PR)
# CI-004: `just lint` now includes docs-lint as the final step.
just fmt
just lint
# Run tests
just test-unit # fast, no DB
just test-integration # Testcontainers
just test-dvm # generated DVM SQL against Testcontainers PostgreSQL
just test-light-e2e # installed extension in stock postgres
just test-e2e # full E2E (builds Docker image)
just test-pgbouncer # PgBouncer transaction-pool compatibility tests
For differential correctness, prefer multi-cycle mutation tests that compare the stream table with a fresh query result. Concurrency tests need a barrier or an observed database state to prove operations overlapped. Poll for completion instead of relying on fixed sleeps.
Full setup instructions are in INSTALL.md.
Devcontainer / Containerized Development
If you are developing in a devcontainer, use the default non-root vscode user
and run the normal commands from the workspace root:
just fmt
just lint
just test-unit
just test-unit uses scripts/run_unit_tests.sh, which now selects a writable
and cache-friendly target directory in this order:
target/(preferred).cargo-target/(project-local fallback)$HOME/.cache/pg_trickle-target${TMPDIR:-/tmp}/pg_trickle-target(last resort)
This avoids permission failures on bind mounts and preserves incremental builds when source or test files change.
If you see permission errors in containerized runs, verify you are not forcing a different container user/UID than expected by your workspace mount.
Run E2E tests in devcontainer
E2E tests use Testcontainers and require Docker access from inside the
devcontainer (provided by the Docker-in-Docker feature in
.devcontainer/devcontainer.json).
Run from the workspace root inside the devcontainer:
just build-e2e-image
just test-e2e
Notes:
- The E2E harness starts containers via
testcontainers(tests/e2e/mod.rs). - The default E2E image is
pg_trickle_e2e:latest(built bytests/build_e2e_image.sh). - A plain
docker runof the dev image is not equivalent to a full VS Code devcontainer session with features/lifecycle hooks enabled.
Making a Pull Request
- Fork the repository and create a branch:
git checkout -b fix/my-fix - Make your changes following the conventions in AGENTS.md
- Run
just fmt && just lint— both must pass with zero warnings - Add or update tests — see AGENTS.md § Testing
- Open a PR against
main
The PR template will walk you through the checklist.
CI Coverage on PRs
Every PR runs static/release contracts, script unit tests, upgrade completeness, and the checked LSN proof. The contracts also classify changed files using .github/ci-paths.yml. Expensive CI jobs wait for the contracts to pass and run only when the relevant inputs changed:
| Changed inputs | Selected work beyond the contracts and proof |
|---|---|
| Rust, extension SQL, build inputs, test fixtures/harness | Unit/catalog/DVM component tests; ready PRs add packaged and bounded full E2E |
| Documentation, release-contract data, ordinary maintenance scripts | Documentation checks and script tests; no extension rebuild |
| Benchmark sources or inputs | Benchmark tracking on main; measurements also remain nightly/manual |
| Shipping Dockerfile or PostgreSQL defaults | Shipping-image qualification |
| Monitoring configuration | Collector contract |
| dbt getting-started project or adapter | Getting-started example on main; full dbt integration remains scheduled/manual |
| Demo/CNPG examples | Static checks; CNPG deployment verification remains scheduled/manual |
Build inputs include build.rs, .cargo/**, pg_trickle.control, Cargo
manifests, and the scripts used by compiled tests. Unclassified paths retain
runtime coverage. A mixed PR selects the union of the relevant checks. The
standalone lint, benchmark-tracking, and coverage workflows also filter their
inputs, so a documentation-only merge does not rebuild Rust targets.
Draft PRs defer packaging, light/full E2E, shipping-image qualification, Windows compilation, and fuzz corpus replay. Marking a draft ready for review starts those jobs; converting it back to a draft cancels the previous run and returns to the smaller set of checks.
Ready runtime PRs additionally run all 91 light targets in three shards against
stock postgres:18.6 and 100 full-harness targets from
the checked allocation. Permanent regressions,
transaction visibility, multiplicity, recovery, permissions, workers and
full-only cases remain pre-merge checks. DVM component tests run in the
integration job using the existing pg_stub/Testcontainers runner. Refresh,
CDC and scheduler paths select additional failure-recovery repetitions;
unsafe-boundary paths keep instrumented qualification.
The bounded full suite disables automatic retries; investigate a failure
before explicitly rerunning it.
The coverage map explains the unique behavior at each
layer, every deferred suite's workflow and failure ownership, verification,
timing comparisons and the proposed required check. PR correctness rejects
selected failed, cancelled or unexpectedly skipped jobs. No branch/ruleset
settings are changed by this allocation.
Criterion regression comparisons and the pgbench overhead gate run nightly and remain available manually. Nightly Criterion runs measure both the candidate and the last successful scheduled CI revision on the same runner; the first nightly run falls back to the previous commit with a warning. The compared commit IDs are recorded in the job summary. TPC-H, dbt, CNPG, and the extra macOS/Windows unit jobs also retain their scheduled/manual coverage.
Workflow trigger matrix
PR and push entries below are subject to the input selection described above. Nightly and manual CI runs retain the full matrix regardless of changed paths.
| Workflow / Job | PR | Push to main | Daily schedule | Manual dispatch |
|---|---|---|---|---|
| Unit tests (Linux) | ✅ | ✅ | ✅ | ✅ |
| Unit tests (macOS + Windows) | ❌ | ❌ | ✅ | ✅ |
| Catalog + DVM component tests | ✅ | ✅ | ✅ | ✅ |
| Light E2E (3 shards) | Ready only | ✅ | ✅ | ✅ |
| Bounded full E2E | Ready runtime PRs | ❌ | Full inventory | Full inventory |
| PR correctness aggregate | ✅ | ❌ | ❌ | ❌ |
| TPC-H | ❌ | ❌ | ✅ | ✅ |
| Upgrade completeness check | ✅ | ✅ | ✅ | ✅ |
| Upgrade E2E | ❌ | ❌ | ✅ | ✅ |
| Benchmarks | ❌ | ✅ | ✅ | ✅ |
| Benchmark regression gate | ❌ | ❌ | ✅ | Opt-in |
| pgbench overhead gate | ❌ | ❌ | ✅ | ✅ |
| Shipping image upgrade | Ready only | Shipping inputs | ✅ | ✅ |
| DAG bench (calc / throughput) | ❌ | ❌ | ✅ | ✅ |
| DAG bench (parallel workers) | ❌ | ❌ | ✅ | ✅ |
| E2E bench — refresh matrix | ❌ | ❌ | Weekly (Sun) | ✅ |
| E2E bench — zero-change latency | ❌ | ❌ | Weekly (Sun) | ✅ |
| E2E bench — CDC overhead | ❌ | ❌ | Weekly (Sun) | ✅ |
| E2E bench — TPC-H FULL vs DIFF | ❌ | ❌ | Weekly (Sun) | ✅ |
| dbt integration | ❌ | ❌ | ✅ | ✅ |
| CNPG smoke test | ❌ | ❌ | ✅ | ✅ |
| Soak test (stability-tests.yml) | ❌ | ❌ | ✅ | ✅ |
| Multi-database (stability-tests.yml) | ❌ | ❌ | ✅ | ✅ |
| Lint (fmt + clippy + docs-lint) | ✅ | ✅ | ✅ | ✅ |
| Unsafe block inventory | ✅ | ✅ | ✅ | ✅ |
| GUC + SQL API catalog drift (O40-1) | ✅ | ✅ | ✅ | ✅ |
| cargo-deny (dependency policy) | ✅¹ | ✅ | ✅ | ✅ |
| cargo audit (security) | ✅ | ✅ | ✅ | ✅ |
| Semgrep static analysis | ✅ | ✅ | ✅ | ✅ |
| Secret scanning (gitleaks) | ✅ | ✅ | ✅ | ✅ |
| Fuzz corpus replay | Ready only | ❌ | ✅ | ✅ |
| Windows compile check | Ready only | ❌ | ❌ | ❌ |
¹ cargo-deny runs on PRs only when Cargo.toml, Cargo.lock, or deny.toml
change (path filter). If your PR does not touch these files, the check is skipped
but not blocking.
To trigger the full CI matrix on your PR branch (recommended for DVM engine, refresh, or CDC changes):
gh workflow run ci.yml --ref <your-branch>
Manual dispatch also runs the heavy tests on a draft branch. To opt in to the Criterion comparison against an exact base ref or commit:
gh workflow run ci.yml --ref <your-branch> \
-f run_benchmark_regression=true -f benchmark_base=<base-ref-or-sha>
Omitting benchmark_base on manual runs compares against main.
To run all tests locally before pushing:
just test-all # unit + integration + e2e
# PR-equivalent fast path:
just test-unit
just test-integration
just test-dvm
just test-light-e2e
just build-e2e-image
python3 scripts/ci_pr_suite.py
# Validate the inventory and gate logic without Docker:
python3 scripts/ci_pr_suite.py --list
python3 -m unittest discover -s scripts -p 'test_ci*.py'
# TPC-H correctness tests (requires e2e Docker image):
cargo test --test e2e_tpch_tests -- --ignored --test-threads=1 --nocapture
See AGENTS.md § Testing for the full CI coverage matrix.
Coding Conventions (summary)
- No
unwrap()orpanic!()in non-test code - All
unsafeblocks require a// SAFETY:comment - Errors go through
PgTrickleErrorinsrc/error.rs - New SQL functions use
#[pg_extern(schema = "pgtrickle")] - Tests use Testcontainers — never a local PostgreSQL instance
Full details are in AGENTS.md.
Commit Messages
Use Conventional Commits:
fix: correct pgoutput action parsing for tables named INSERT_LOG
feat: add CUBE explosion guard (max 64 UNION ALL branches)
docs: document JOIN key change limitation in SQL_REFERENCE
test: add E2E test for keyless table duplicate-row behaviour
Fuzz Testing
pg_trickle uses cargo-fuzz (libFuzzer) to exercise core parser and pipeline
logic. Fuzz targets live in fuzz/fuzz_targets/.
Available targets
| Target | What it exercises | Added in |
|---|---|---|
parser_fuzz | DVM SQL parser (OpTree construction) | v0.1.0 |
cron_fuzz | Cron expression parser | v0.26.0 |
guc_fuzz | GUC string→enum coercion | v0.26.0 |
cdc_fuzz | CDC trigger payload decoding | v0.26.0 |
wal_fuzz | WAL/SQLSTATE error classifier | v0.39.0 |
dag_fuzz | DAG/merge SQL and snapshot column-list | v0.39.0 |
sql_builder_fuzz | SQL builder + typed parser facade | v0.44.0 |
merge_sql_fuzz | Merge SQL codegen with random change streams | v0.49.0 |
row_id_fuzz | Row identity tracking with random operator trees | v0.49.0 |
Running all fuzz targets
# Run each target for 60 s (requires nightly toolchain):
just fuzz-all
# Run for a custom duration (seconds):
just fuzz-all 120
# Run a single target:
cargo +nightly fuzz run merge_sql_fuzz -- -max_total_time=60
Corpus directories are at fuzz/corpus/<target_name>/. Regression cases are
stored in proptest-regressions/.
Every new feature ships with documentation. This is a hard requirement, not a nice-to-have.
- New SQL functions → entry in
docs/SQL_REFERENCE.md. - New GUC variable → entry in
docs/CONFIGURATION.md. - New user-facing capability → at minimum a paragraph in the relevant chapter; for headline features, a dedicated page.
- New page → add it to
docs/SUMMARY.mdin the correct chapter.
PRs that introduce a #[pg_extern] function or a new GUC without
documentation will be asked to add it before merge.
Updating base image digests
Docker base images are pinned to exact SHA256 digests in all Dockerfiles
(Dockerfile.demo, Dockerfile.ghcr, tests/Dockerfile.e2e,
tests/Dockerfile.e2e-asan) for
reproducibility and supply-chain security (OPS-10-03).
To update the digests when a new PostgreSQL patch release is available:
# Requires docker with manifest support
scripts/update_base_image_digests.sh
The script resolves the current linux/amd64 digest for
postgres:18.6-bookworm, patches all Dockerfiles in-place, and prints the
commit command. Run this script quarterly or when a PostgreSQL patch release
is needed. Include the digest-update commit in the release PR.
If you are building for linux/arm64 or another platform, edit the
TARGET_PLATFORM variable in the script or pin to the manifest index digest
(returned by docker manifest inspect postgres:18.6-bookworm --verbose).
License
By contributing you agree that your contributions will be licensed under the Apache License 2.0.