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

TypeWhere to start
Bug reportOpen an issue
Feature requestOpen an issue or start a discussion
Documentation fixOpen a PR directly — no issue needed for typos/clarity
Code fix or featureOpen an issue first, then a PR
Performance improvementInclude 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:

  1. target/ (preferred)
  2. .cargo-target/ (project-local fallback)
  3. $HOME/.cache/pg_trickle-target
  4. ${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 by tests/build_e2e_image.sh).
  • A plain docker run of the dev image is not equivalent to a full VS Code devcontainer session with features/lifecycle hooks enabled.

Making a Pull Request

  1. Fork the repository and create a branch: git checkout -b fix/my-fix
  2. Make your changes following the conventions in AGENTS.md
  3. Run just fmt && just lint — both must pass with zero warnings
  4. Add or update tests — see AGENTS.md § Testing
  5. 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 inputsSelected work beyond the contracts and proof
Rust, extension SQL, build inputs, test fixtures/harnessUnit/catalog/DVM component tests; ready PRs add packaged and bounded full E2E
Documentation, release-contract data, ordinary maintenance scriptsDocumentation checks and script tests; no extension rebuild
Benchmark sources or inputsBenchmark tracking on main; measurements also remain nightly/manual
Shipping Dockerfile or PostgreSQL defaultsShipping-image qualification
Monitoring configurationCollector contract
dbt getting-started project or adapterGetting-started example on main; full dbt integration remains scheduled/manual
Demo/CNPG examplesStatic 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 / JobPRPush to mainDaily scheduleManual dispatch
Unit tests (Linux)✅✅✅✅
Unit tests (macOS + Windows)❌❌✅✅
Catalog + DVM component tests✅✅✅✅
Light E2E (3 shards)Ready only✅✅✅
Bounded full E2EReady runtime PRs❌Full inventoryFull inventory
PR correctness aggregate✅❌❌❌
TPC-H❌❌✅✅
Upgrade completeness check✅✅✅✅
Upgrade E2E❌❌✅✅
Benchmarks❌✅✅✅
Benchmark regression gate❌❌✅Opt-in
pgbench overhead gate❌❌✅✅
Shipping image upgradeReady onlyShipping 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 replayReady only❌✅✅
Windows compile checkReady 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() or panic!() in non-test code
  • All unsafe blocks require a // SAFETY: comment
  • Errors go through PgTrickleError in src/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

TargetWhat it exercisesAdded in
parser_fuzzDVM SQL parser (OpTree construction)v0.1.0
cron_fuzzCron expression parserv0.26.0
guc_fuzzGUC string→enum coercionv0.26.0
cdc_fuzzCDC trigger payload decodingv0.26.0
wal_fuzzWAL/SQLSTATE error classifierv0.39.0
dag_fuzzDAG/merge SQL and snapshot column-listv0.39.0
sql_builder_fuzzSQL builder + typed parser facadev0.44.0
merge_sql_fuzzMerge SQL codegen with random change streamsv0.49.0
row_id_fuzzRow identity tracking with random operator treesv0.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.md in 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.