Testing and Validation
May 11, 2026 ยท View on GitHub
For source-build commands and release workflow notes, see Contribution.
Core Test Layers
Unit tests
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
PostgreSQL integration tests
make installcheck
What Integration Tests Cover
- extension installation
CREATE INDEX USING psql_bm25stext[],varchar[],text,varchar, andint4[]indexing- canonical BM25 search APIs
- raw-query retrieval
@@predicate behavior<=>ordered scans- table-index diagnostics through supported SQL helpers
- automatic maintenance
- manual stale mode
- maintenance-state introspection
- maintenance-policy introspection
- maintenance-policy recommendation
- shared generation cache diagnostics and invalidation checks
Mutable-Maintenance Smoke Tests
Dedicated scripts in the main repository cover:
- restart
- crash recovery
- local physical replication
- concurrent maintenance stability smoke
- query-first eventual clean-but-stale tail convergence smoke
- query-first eventual staged-maintenance cancellation smoke
- query-first eventual self-triggered background maintenance smoke
- payload-health corruption and repair smoke
- non-public extension schema wrapper smoke
- broader concurrent stress
- broader family soak
The concrete smoke scripts live in scripts/ in the main repository.
They cover restart, crash recovery, local physical replication,
concurrent maintenance stability, query-first eventual clean-but-stale tail
convergence, staged-maintenance cancellation, payload-health corruption repair,
broader concurrent stress, non-public extension-schema wrapper resolution, and
family soak behavior.
Focused extension schema smoke:
python3 scripts/test_extension_schema_smoke.py \
--psql /path/to/postgresql/bin/psql
This script verifies fresh install and 0.2.0 to current-version upgrade when the
extension is created in a non-public schema. It also verifies that
ALTER EXTENSION ... SET SCHEMA is rejected, because SQL helper functions
capture the extension schema for safe wrapper resolution.
Hybrid search coverage is part of the main regression test. It uses
synthetic vector-like candidates so the test suite does not require
pgvector or VectorChord:
- RRF fusion
- score fusion with
minmax,zscore,rank, andinverse_distance - mixed BM25-like and vector-distance-like candidates
- BM25 adapter output from
psql_bm25s_query(...)
The schema smoke test also calls the hybrid fusion API from a non-public
extension schema, including the 0.2.0 to current-version upgrade path.
Shared generation diagnostics are covered by the main integration regression.
The regression checks that small indexes stay off the shared-cache descriptor
path, that cache_epoch advances after explicit refresh, and that
maintenance/REINDEX paths produce a new observable generation key.
The cache deployment model and connection-pool benchmark requirements are documented in Shared Generation Cache. Runtime memory sizing, workspace retention, and active warmup guidance are documented in Connection Memory and Index Prewarming.
Focused generation cache lifecycle smoke:
python3 scripts/test_generation_cache_smoke.py \
--psql /path/to/postgresql/bin/psql \
--dbname contrib_regression
This script verifies that operational cache clear removes corrupt descriptor files, interrupted descriptor temp files, stale lock files, and failure markers. It also uses independent psql backends to verify that maintenance publishes a new observable generation key and that the new generation is searchable.
Focused shared-preload generation cache smoke:
python3 scripts/test_shared_preload_generation_cache.py \
--bindir /path/to/postgresql/bin
This script starts a temporary PostgreSQL cluster with
shared_preload_libraries = 'psql_bm25s', configures
psql_bm25s.shared_generation_cache_size, creates a BM25 index, prewarms it,
queries it from independent psql backends, and verifies that the main
shared-memory arena has one ready resident generation.
Focused shared-preload automatic preload smoke:
python3 scripts/test_shared_preload_auto_preload.py \
--bindir /path/to/postgresql/bin
This script verifies the auto_preload reloption validation, priority order,
background preload through the shared-preload worker, and the no-op behavior for
unmarked indexes.
Focused shared-preload standby smoke:
python3 scripts/test_shared_preload_standby_auto_preload.py \
--bindir /path/to/postgresql/bin
This script starts a temporary primary/standby pair, verifies that the standby auto-preloads a marked index without performing maintenance, then rebuilds the index on the primary and verifies that the standby reloads the replicated current generation.
Focused generation cache connection-churn benchmark:
python3 scripts/benchmark_generation_cache_churn.py \
--dataset webis-touche2020 \
--max-cases 50 \
--connections 4 \
--parallelism 4 \
--cases-per-connection 1
This benchmark prepares a large enough index to use the DSM generation cache, clears volatile cache state, then launches fresh PostgreSQL backends against the same index. The expected DSM V2 behavior is single-flight publish, serialized attach, and a valid descriptor after the run.
Focused query-first eventual maintenance smoke:
python3 scripts/test_query_first_eventual_tail_smoke.py \
--psql /path/to/postgresql/bin/psql \
--dbname contrib_regression
This script verifies that query-first eventual maintenance can tolerate concurrent tail inserts by publishing a complete clean-but-stale generation, then retrying maintenance to converge the index to a clean state with the inserted documents searchable.
Focused cancellation smoke:
python3 scripts/test_query_first_eventual_cancel_smoke.py \
--psql /path/to/postgresql/bin/psql \
--dbname contrib_regression
This script terminates a backend during the long staged-build phase, before the final publish. It verifies the old base index remains readable with retryable debt, then reruns maintenance to converge the index.
Focused self-triggered background maintenance smoke:
python3 scripts/test_query_first_eventual_background_smoke.py \
--psql /path/to/postgresql/bin/psql \
--dbname contrib_regression
This script verifies that committed pending debt can wake a dynamic background worker and converge without an explicit pg_cron call.
Focused payload-health corruption smoke:
python3 scripts/test_payload_health_corruption_smoke.py \
--bindir /opt/homebrew/opt/postgresql@18/bin
This script truncates a temporary index relation after build, verifies cheap
payload-health diagnostics report corrupt/rebuild_required, verifies query
fails fast instead of cold-loading a bad payload, then verifies maintenance can
rebuild a new generation from the heap.
Focused compact maintenance builder smoke:
python3 scripts/test_compact_maintenance_builder.py \
--bindir /opt/homebrew/opt/postgresql@18/bin \
--builder compact
This script starts a temporary shared-preload cluster, creates an eventual
text[] index, sets a rebuild memory budget that rejects the standard builder
under the 60% headroom rule but admits the compact builder under the 75%
headroom rule, runs due-index maintenance, and verifies the result reports
builder=compact with the new row searchable after publish.
Focused spill maintenance builder smoke:
python3 scripts/test_compact_maintenance_builder.py \
--bindir /opt/homebrew/opt/postgresql@18/bin \
--builder spill
This runs the same online maintenance path with a tighter memory budget that
rejects standard and compact builders under their headroom rules but admits the
spill builder. It verifies builder=spill and confirms the streamed
append-only generation is searchable.
Benchmark Validation
Benchmark tooling is also part of validation because maintenance policy is benchmark-backed.
Important benchmark scripts also live in scripts/ in the main
repository. They cover automatic maintenance, churn benchmarks, policy
sweeps, policy matrices, long-run profiles, and production-shaped trace
profiles.
For query-first eventual backfill behavior, use:
PSQL_BM25S_BENCH_DSN='dbname=postgres' \
python3 scripts/benchmark_query_first_eventual_backfill.py \
--output /tmp/psql_bm25s_eventual_backfill.json
This benchmark compares eager exact maintenance with query-first eventual maintenance while a non-indexed text column is backfilled and concurrent queries hit the indexed title-token column.
Filtered Ranked SQL Validation Gate
For SQL-surface changes that affect filtered/ranked query composition, validation should include all of the following:
- unit tests and
make installcheck - at least one integration case for:
@@plus<=>@@@plus<=>psql_bm25s_ranked_query(...)
EXPLAIN (FORMAT JSON)inspection through:psql_bm25s_fast_path_plan(...)- or
psql_bm25s_fast_path_explain(...)
The goal is not just functional correctness. The goal is to confirm
that the intended SQL shape still uses a psql_bm25s index-aware plan
instead of silently degrading into a broader executor path.
If a new helper only wraps an existing exact surface, the benchmark gate can stay narrow. At minimum it should cover:
- one canonical
rowsetcase - one combined filter/rank SQL case
If a new helper changes how filtered/ranked SQL is composed, it should also be checked against a representative benchmark slice before the surface is treated as stable.
Field-Aware Helper Validation Slice
The structured field-aware helpers have one focused benchmark slice:
It validates:
- result equivalence against explicit
psql_bm25s_fusion(...) - latency overhead for:
psql_bm25s_fusion_query_weighted(...)psql_bm25s_fusion_query_fields(...)psql_bm25s_fusion_query(...)
Current benchmark summary:
- all helper variants matched the explicit baseline exactly
- all variants stayed sub-millisecond on the focused synthetic slice
Practical Rule
For code changes:
- run unit and integration tests
For maintenance-path changes:
- run unit and integration tests
- run the dedicated smoke scripts
- run the relevant benchmark slice again