Contents
Testing
pg_fts is a PostgreSQL index access method: a bug here can corrupt an index, crash a backend (which force-rolls-back every other backend), or silently return wrong search results. Trust in code like this is earned method by method, so this page is an honest accounting of how pg_fts is tested. Every method below runs in CI on both GitHub and Codeberg on every push and pull request.
Correctness: regression + isolation
The core gate is the PostgreSQL regression suite (sql/pg_fts.sql vs
expected/pg_fts.out, plus unicode_fold and idx_scan_stats) and the
isolation suite (specs/bm25_concurrency.spec, specs/bm25_cic.spec), run via
make installcheck against PostgreSQL 17, 18, and 19. The regression suite
exercises the full query language (boolean, phrase, NEAR, prefix, fuzzy, regex),
ranking (<=>, all five BM25 variants, BM25F), the count pushdown, the index
build/insert/vacuum/merge lifecycle, and the type I/O round-trips. The isolation
suite covers MVCC correctness, CREATE INDEX CONCURRENTLY / REINDEX
CONCURRENTLY, and concurrent DML + VACUUM + merge.
Memory safety: AddressSanitizer + a fuzz gate
Two of pg_fts’s shipped bugs (0.3.3, 0.3.4) were memory-safety bugs on the paths that parse untrusted on-disk bytes — a buffer overflow the plain regression suite ran straight past because it only feeds valid data. Those are now guarded two ways:
- Fuzz / corruption harness (
test/fuzz/, gating). Standalone AddressSanitizer + UndefinedBehaviorSanitizer fuzzers drive random and deliberately-corrupted bytes at the functions that parse on-disk data — the frame-of-reference codec (bm25_for_unpack), the stored-document validator (fts_doc_is_valid), and the segment block-header decoder. The property is simple: these must never overflow or crash on any input — they reject it or handle it safely. The harness carries its own teeth: three planted-bug builds (an un-clamped count, a signed-count bypass, a corrupt width) that must abort under ASan, so a regression that removed a guard would be caught by the harness failing to catch it. - In-process AddressSanitizer run (best-effort signal). The regression +
isolation suite also runs against a real PostgreSQL with an ASan-instrumented
pg_fts.so. It is non-gating because loading an instrumented shared library into a stock postmaster is runner-dependent; the fuzz gate is the hard memory-safety check.
Property-based tests (hegel)
test/hegel/ states invariants that must hold for all inputs, checked over
generated and shrunk cases via hegel-c:
the FOR codec round-trips and never writes past the caller’s buffer for any
width or count; the Levenshtein automaton matches a reference; the stored-document
validator accepts every well-formed document and rejects every out-of-bounds
mutation. (This job is best-effort in CI, since it needs the hegel server;
the same surface is gated by the fuzz job.)
Torn-page / corruption crash-safety (TAP)
t/003_corruption.pl builds an index, then writes bad bytes directly into the
index file — a corrupt block-header count, and a coarse content smash — and
asserts that scans and VACUUM over the damaged index do not crash the
backend: a corrupt block is a bounded miss with a WARNING, never a segfault.
t/001_crash_recovery.pl and t/002_replication.pl verify WAL crash recovery
and physical replication of the index.
Coverage
CI measures line coverage of the pg_fts sources (ci/coverage.sh, gating) and
fails if it drops below 90%. The vendored sparsemap (vendor/sm.c, whose
API is mostly unused by pg_fts) is reported separately and not part of the gate.
Coverage is a floor, not the goal — the fuzz, property, and corruption tests
exist to exercise the adversarial inputs that a line-coverage number alone
would not.
Portability
The extension builds and its suite runs on PostgreSQL 17, 18, and 19, via both
PGXS (make PG_CONFIG=...) and the Nix flake (nix flake check). It has been
built on x86-64, ARM64, and RISC-V, and on Linux, FreeBSD, and Windows (MSVC).