PostgreSQL test runner and coverage tool
pgcov is a pure-Go CLI tool that discovers *_test.sql files, instruments PL/pgSQL and SQL function bodies for statement-level coverage tracking, executes tests in isolated temporary databases, and generates coverage reports in JSON, LCOV, and HTML formats. It talks to PostgreSQL directly over the wire protocol via pgx/v5 — no psql, no server extensions, no CGO.
- 🧪 Automatic Test Discovery: Recursively finds
*_test.sqlfiles and the source files co-located with them - 🔒 Complete Test Isolation: Each test runs in its own temporary database (
pgcov_test_<timestamp>_<random>), dropped after the run - 📊 Coverage Tracking: Statement-level coverage via
pg_notifyinstrumentation signals captured overLISTEN/NOTIFY - 📈 Multiple Report Formats: JSON, LCOV, and HTML output for CI/CD integration
- ⚡ Parallel Execution: Optional concurrent test execution with the
--parallelflag - 🐘 PostgreSQL Native: Direct protocol access via
pgx/v5, no external CLI tools - 🪶 Pure Go: SQL is parsed with
pashagolub/pglex; a plaingo buildworks on every platform, no C toolchain needed
- Go: 1.25 or later (for building)
- PostgreSQL: 13 or later, running and accessible (the server version is checked on connect)
- Permissions: The database user needs the
CREATEDBprivilege for test isolation
git clone https://github.com/cybertec-postgresql/pgcov.git
cd pgcov
go build -o pgcov ./cmd/pgcovgo install github.com/cybertec-postgresql/pgcov/cmd/pgcov@latestpgcov is pure Go — there is no CGO requirement and no C compiler involved.
Pass a connection string to every pgcov run invocation (URI or keyword/value format):
pgcov run . --connection "postgresql://postgres:yourpassword@localhost:5432/postgres"
# or keyword/value format
pgcov run . --connection "host=localhost port=5432 user=postgres password=yourpassword dbname=postgres"Fields omitted from the connection string fall back to the standard PG* environment variables (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, ...) and libpq defaults, handled by pgx.
Test files must match the *_test.sql pattern and be co-located with the source files they exercise:
myproject/
├── auth/
│ ├── authenticate.sql # Source (will be instrumented)
│ └── auth_test.sql # Test
Discovery is recursive from the given path — there is no ./... syntax:
# Current directory (recurses into subdirectories)
pgcov run . --connection "..."
# Specific directory (also recurses)
pgcov run ./tests/ --connection "..."On success pgcov prints a summary and persists the raw coverage data:
Tests: 3 passed, 0 failed, 3 total
Coverage: 87.50%
Time: 1.234s
Coverage data written to .pgcov/coverage.json
# JSON (default format) to stdout
pgcov report
# HTML format (human-readable)
pgcov report --format=html -o coverage.html
# LCOV format (for CI)
pgcov report --format=lcov -o coverage.lcovpgcov run [path] [flags] # Run tests and collect coverage (path defaults to .)
pgcov report [flags] # Generate a coverage report from saved coverage data
pgcov help [command] # Show help
pgcov --version # Show version| Flag | Default | Description |
|---|---|---|
--connection, -c |
(required) | PostgreSQL connection string (URI or keyword/value); omitted fields fall back to PG* environment variables |
--timeout |
30s |
Per-test timeout (10s, 1m, 90s) |
--parallel |
1 |
Maximum concurrent tests; 1 = sequential, values above 100 are rejected |
--coverage-file |
.pgcov/coverage.json |
Coverage data output path |
--setup |
— | SQL file(s) — globs allowed — run verbatim in each test's temporary database before the instrumented sources are loaded. Repeatable; order preserved |
--verbose |
false |
Enable debug output |
| Flag | Default | Description |
|---|---|---|
--format |
json |
Output format: json, lcov, or html |
--output, -o |
- (stdout) |
Output file path (- writes to stdout) |
--coverage-file |
.pgcov/coverage.json |
Coverage data input path |
Each test only deploys sources from its own directory into its temporary database. When sources depend on shared or "global" schema living elsewhere, load it with --setup. Setup scripts run verbatim — no instrumentation, not counted as covered source — in every test's temporary database before the instrumented sources:
pgcov run . --connection "..." \
--setup "schema/global.sql" \
--setup "shared_types/*.sql"Glob matches are sorted for determinism, the flag is repeatable, and pattern order is preserved. A pattern that matches nothing is an error.
Within a single directory, source files are deployed in lexical filename order, because pgcov walks the tree with Go's filepath.Walk (which sorts entries lexicographically inside each directory). There is no pgcov.toml manifest or per-file ordering annotation — the filename is the only lever you control.
When sources have interdependencies, prefix the filenames with two-digit numbers so they sort the way you want:
auth/
├── 01_schema.sql # Tables, types — must load first
├── 02_functions.sql # Functions depend on tables from 01
├── 03_views.sql # Views depend on functions from 02
└── auth_test.sql # Test (excluded from the lexical sort pattern)
Sources are loaded in that order for every test in the directory. If a prerequisite lives outside the test directory (shared/global schema), load it verbatim with the repeatable --setup flag — setup runs before the instrumented sources and is not counted as covered source.
| Code | Meaning |
|---|---|
0 |
All tests passed |
1 |
Test failures, timeouts, or runtime errors |
2 |
Invalid configuration |
pgcov validates the configuration up front and reports actionable errors:
# Missing connection string
$ pgcov run .
Error: configuration error for connection: PostgreSQL connection string is required
Suggestion: Set via --connection flag or standard PG* environment variables (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE).
# Invalid parallelism
$ pgcov run . --connection "..." --parallel=-1
Error: configuration error for parallel: parallelism must be at least 1, got: -1
Suggestion: Use --parallel=N where N is number of tests to run concurrently. Use 1 for sequential execution.
# Invalid timeout
$ pgcov run . --connection "..." --timeout=-5s
Error: configuration error for timeout: timeout must be positive
Suggestion: Use --timeout flag with format like '30s', '1m', '90s'. Default is 30s.-- auth_test.sql
-- Setup: Create schema and test data
CREATE TABLE users (id INT PRIMARY KEY, name TEXT);
INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');
-- Test: Verify behavior
DO $$
BEGIN
IF NOT authenticate(1) THEN
RAISE EXCEPTION 'Test failed: User 1 should authenticate';
END IF;
IF authenticate(999) THEN
RAISE EXCEPTION 'Test failed: Invalid user should not authenticate';
END IF;
RAISE NOTICE 'All tests passed';
END;
$$;Source files in the same directory as test files are automatically instrumented:
-- authenticate.sql
CREATE OR REPLACE FUNCTION authenticate(user_id INT) RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS(SELECT 1 FROM users WHERE id = user_id);
END;
$$ LANGUAGE plpgsql;name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- name: Install pgcov
run: go install github.com/cybertec-postgresql/pgcov/cmd/pgcov@latest
- name: Run tests
run: pgcov run . --connection "postgresql://postgres:postgres@localhost:5432/postgres"
- name: Generate LCOV report
run: pgcov report --format=lcov -o coverage.lcov
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage.lcovSee examples/ci-integration for GitHub Actions and GitLab CI variants.
- CLI Layer:
runandreportsubcommands and flag wiring (urfave/cli/v3),cmd/pgcov - Discovery Layer: Recursive filesystem scan; files ending in
_test.sqlare tests, all other.sqlfiles are sources, and each test is scoped to sources from its own directory - Parser Layer: Pure-Go SQL statement splitting and classification via
pashagolub/pglex - Instrumentation Layer: Statements inside PL/pgSQL function bodies get a
PERFORM pg_notify('pgcov', '<relPath>:<startOffset>:<byteLength>')coverage signal; statements in SQL-language functions are wrapped in aWITH _pgcov_signal AS (SELECT pg_notify(...))CTE; all other DDL/DML counts as implicitly covered - Database Layer: One temporary database per test (
pgcov_test_<yyyymmdd_hhmmss>_<random hex>), created from the configured server and dropped after the run (pgx/v5) - Runner Layer: Sequential executor, or a worker pool when
--parallelis greater than 1; a dedicated connectionLISTENs on thepgcovchannel for coverage signals during each test - Coverage Layer: Signals are aggregated into per-file byte-range hit counts; every instrumented position is seeded with 0 hits so uncovered branches show up in reports
- Reporter Layer: Output formatting for JSON (default), LCOV, and HTML
Unit tests need nothing but Go. Integration tests use testcontainers-go and require a running Docker daemon (they start a postgres:16-alpine container):
# All tests
go test ./...
# Verbose output
go test -v ./...
# A specific integration test
go test -v ./internal/ -run TestEndToEndWithTestcontainers
# Longer timeout (integration tests pull and start containers)
go test -timeout 5m ./...
# With Go coverage
go test -cover ./...Other integration tests in the same package: TestRunnerIsolation, TestOrderIndependence, TestTestIndependence, TestSQLFunctionInstrumentation.
# Development build
go build -o pgcov ./cmd/pgcov
# Release build with optimizations
go build -ldflags="-s -w" -o pgcov ./cmd/pgcov
# Format and lint
go fmt ./...
go vet ./...
golangci-lint run # matches CITest container startup failures:
# Verify Docker is running
docker ps
# Pull the PostgreSQL image manually
docker pull postgres:16-alpineThe repository ships .vscode configuration covering common workflows:
- settings.json — gopls environment and Go test defaults
- launch.json — debug configurations: launch the CLI, debug the package or file under the cursor, and run the integration test suite (5-minute timeout)
- tasks.json — build (default:
Ctrl+Shift+B), test, coverage, lint, format, andgo mod tidytasks
See .vscode/README.md for details.
MIT
Contributions welcome — see CONTRIBUTING.md and open an issue or pull request.
- Documentation:
docs/— quickstart, CLI contract - Issues: GitHub Issues
- Examples: Examples directory