Contents
Testing & QA Instructions — @QA
Scope:
sql/,tap/,isolation/,expected/,ci/scripts/,bench_tde.sh
Mandatory Validation Gates
ALL gates must pass before any change is considered complete:
# Gate 1: Full regression (110 tests: 70 baseline + 20 v1.5 + 38 v1.6 + 2 skip-guarded, vault provider)
make ci-regress
# Gate 1b: Wallet provider regression (110 tests, kms_provider=local)
make ci-wallet
# Gate 2: Page checksum compatibility
make ci-checksums
# Gate 3: TAP tests (Perl, with mock Vault)
make ci-tap
# Gate 4: Isolation tests (concurrency / MVCC)
make ci-isolation
# Gate 5: Vault integration (Compose-based)
make ci-vault
# Gate 6: Zero compiler warnings
make PG_CONFIG=$(which pg_config) 2>&1 | grep -c "warning:" | grep "^0$"
# Gate 7: Full local pipeline (all of the above + bench)
make ci-all
Test Suite Map
sql/regression_test.sql contains 70 tests (52 v1.4 baseline + 18 v1.7
TAM/TOAST additions in tests 53–70). The v1.5 and v1.6 supplement files each
carry their own internal numbering. All three files together make up the
110-test suite run by make ci-regress / make ci-wallet.
| Test # | Category | What It Validates | File |
|---|---|---|---|
| 1–11 | Crypto primitives | GCM encrypt/decrypt, IV uniqueness, tamper detection, rotation | regression_test.sql |
| 12 | TAM INSERT+SELECT | Basic round-trip on encrypted_heap table |
regression_test.sql |
| 13 | On-disk absence | Raw file scan confirms no plaintext on disk | regression_test.sql |
| 14 | TAM UPDATE | ctid preservation, tuple refetch, HOT chains | regression_test.sql |
| 15 | DELETE | Row removal without crash | regression_test.sql |
| 16 | All-NULL row | user_len == 0 edge case (no Assert crash) |
regression_test.sql |
| 17 | Index scan | index_fetch_tuple + rd_tableam impersonation |
regression_test.sql |
| 18 | COPY/bulk | multi_insert path via COPY FROM |
regression_test.sql |
| 19 | Multi-column | int, text, bool, numeric, timestamptz types | regression_test.sql |
| 20 | Key rotation | DEK-A rows rejected after rotating to DEK-B | regression_test.sql |
| 21 | ANALYZE | Statistics computed on decrypted data | regression_test.sql |
| 22 | SELECT FOR UPDATE | tuple_lock path |
regression_test.sql |
| 23 | BitmapHeapScan | scan_bitmap_next_tuple via forced bitmap scan |
regression_test.sql |
| 24 | TABLESAMPLE | scan_sample_next_tuple via SYSTEM(100) |
regression_test.sql |
| 25–43 | UPSERT, MERGE, TRUNCATE, REINDEX, ALTER, JOINs, CTEs, HW accel, Vault, logical decoding | v1.1–v1.3 coverage | regression_test.sql |
| 44 | health_check() 7-col v1.5+ schema |
Realigned to (version, enabled, kms_provider, dek_available, aad_binding, wallet_open, checked_at) |
regression_test.sql |
| 45–46 | BGW token renewal, multi_insert batch | v1.3 coverage | regression_test.sql |
| 47 | health_check() state transitions |
Uses dek_available only |
regression_test.sql |
| 48 | Logical decoding (skipped if wal_level != logical) |
v1.2 | regression_test.sql |
| 49 | Wire format v2 round-trip | v1.4 | regression_test.sql |
| 50 | tde_btree CREATE INDEX + equality |
v1.4 | regression_test.sql |
| 51 | health_check.kms_provider GUC coherence |
Replaces v1.4 wrapped_dek_perms test |
regression_test.sql |
| 52 | tde_btree UNIQUE constraint |
v1.4 | regression_test.sql |
| 53–58 | TOAST large-value round-trips (4 kB text, 8 kB jsonb, UPDATE, bulk COPY, inline/EXTERNAL storage paths) | v1.7 TAM/TOAST additions | regression_test.sql |
| 59 | VACUUM dead tuples | VACUUM + pg_stat_force_next_flush; dead tuple counter matches delete count |
regression_test.sql |
| 60 | VACUUM FULL + tde_tuple_has_external_slow |
pg_vault_tde_relation_copy_for_cluster; DELETE after VACUUM FULL finds TOAST chunks even if HEAP_HASEXTERNAL was cleared on the rewritten tuple |
regression_test.sql |
| 61 | CLUSTER | Same relation_copy_for_cluster path driven by CLUSTER ON index; full round-trip on re-clustered relation |
regression_test.sql |
| 62 | TOAST + index scan | Large-value column + index_fetch_tuple; verifies decrypt survives detoast across index lookup |
regression_test.sql |
| 63 | TOAST + BitmapHeapScan | Large-value column + scan_bitmap_next_tuple; forced bitmap scan |
regression_test.sql |
| 64 | TOAST + SELECT FOR UPDATE | Large-value column + tuple_lock; verifies TOAST-bearing rows handled by tuple_lock |
regression_test.sql |
| 65 | TOAST + TABLESAMPLE | Large-value column + scan_sample_next_tuple; SYSTEM(100) |
regression_test.sql |
| 66 | TOAST + ANALYZE | Large-value column + scan_analyze_next_tuple; verifies stats computed on decrypted text |
regression_test.sql |
| 67 | TOAST + multi_insert | Bulk COPY with large-value column; multi_insert + TOAST round-trip |
regression_test.sql |
| 68 | Multi-column TOAST | Table with two large-value columns; both columns decoded correctly after TOAST | regression_test.sql |
| 69 | UPDATE large↔large / large↔small | old_has_external branch in tuple_update; UPDATE large→large, large→small, small→large |
regression_test.sql |
| 70 | toast_am GUC |
pg_vault_tde_toast_am returns correct AM OID based on toast_encryption GUC; TOAST table AM matches expectation |
regression_test.sql |
| 53–72 | Per-table DEK catalog, TOAST large-value, DEK isolation, tde_btree native ops, wire format v3 AAD, online rotation BGW | v1.5 coverage | regression_test_v15.sql |
| 73–110 | Wallet init/unlock/lock, wallet passphrase, rotate_kek, bundle export/import, TOAST storage paths (EXTERNAL/EXTENDED), VACUUM FULL + TOAST, CLUSTER, all seven read paths with TOAST, ANALYZE, multi_insert, UPDATE old_has_external, ALTER TABLE AM switch, online rotation, CREATE TABLE AS, WITH HOLD cursor plaintext spill | v1.6 coverage | regression_test_v16.sql |
Skip semantics under
make ci-regress(vault provider): Test 48 SKIP ifwal_level != logical; Test 61 (v15) SKIP if thepageinspectcontrib extension is not available; Tests 74–80 SKIP ifkms_provider != local; tests 84–85 SKIP ifpg_vault_tde.dev_mode != on; test 103 SKIP ifpg_vault_tde.toast_encryption != on. Net result: 110/110 PASS with the corresponding skips.Under
make ci-wallet(kms_provider=local): Tests 73–79 PASS; test 80 SKIPS unlesswallet_passphrase_envis wired up; tests 81–110 also PASS (the v1.6 patch resolved the change_passphrase SPI invalidation crash and the rotate_kek MAC mismatch).
Test Writing Template
When adding a new TAM/IAM callback, follow this template:
-- Test NN: [Feature Name] ([callback_name] path)
-- Validates: [what this test proves]
-- Setup: create encrypted table
CREATE TABLE test_nn_feature (
id serial PRIMARY KEY,
val text
) USING encrypted_heap;
-- Insert known data
INSERT INTO test_nn_feature (val) VALUES ('known_plaintext');
-- Force specific scan path
SET enable_seqscan = off; -- if testing index scan
SET enable_indexscan = off; -- if testing bitmap scan
-- etc.
-- Verify round-trip
SELECT val FROM test_nn_feature WHERE id = 1;
-- Expected: 'known_plaintext'
-- Cleanup
DROP TABLE test_nn_feature;
For per-table DEK isolation tests, create separate tables and verify each decrypts independently:
-- Each table gets its own DEK at CREATE TABLE time
CREATE TABLE test_dek_a (...) USING encrypted_heap;
CREATE TABLE test_dek_b (...) USING encrypted_heap;
INSERT INTO test_dek_a ...;
INSERT INTO test_dek_b ...;
-- Both tables should decrypt correctly and independently
SELECT * FROM test_dek_a; -- OK
SELECT * FROM test_dek_b; -- OK
Anti-Patterns (DO NOT)
DO NOT mix rows encrypted with different DEKs in the same table for sequential scan tests — the scan hits wrong-DEK rows first and raises GCM errors before reaching target rows.
DO NOT assume
slot->tts_tidis valid afterExecFetchSlotHeapTuple(slot, false, ...)— usebslot->base.tuple->t_selfwhile buffer pin is held.DO NOT test
pg_vault_tde_decode_sloton already-decoded slots without the double-decode guard.DO NOT use
VACUUM FULLin rotation tests — it rewrites tuples, which re-encrypts them with the current DEK, masking the rotation test.DO NOT rely on specific tuple ordering in sequential scans — heap page allocation and HOT chains affect order.
TAP Test Conventions
# tap/NN_feature.t
use strict;
use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More tests => N;
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
$node->append_conf('postgresql.conf', "shared_preload_libraries = 'pg_vault_tde'");
$node->start;
$node->safe_psql('postgres', "CREATE EXTENSION pg_vault_tde;");
# ... test logic ...
$node->stop;
Isolation Test Conventions
Isolation specs live in isolation/ and use the PostgreSQL isolation tester:
# isolation/feature_name.spec
setup {
CREATE EXTENSION IF NOT EXISTS pg_vault_tde;
}
teardown {
DROP TABLE IF EXISTS test_table;
}
session s1
step s1_begin { BEGIN; }
step s1_insert { INSERT INTO ...; }
step s1_commit { COMMIT; }
session s2
step s2_read { SELECT * FROM ...; }
permutation s1_begin s1_insert s2_read s1_commit
Benchmark Script (bench_tde.sh)
Arguments: bash bench_tde.sh <row_count>
Workloads compared: INSERT, SELECT, UPDATE, index scan, TABLESAMPLE.
Both plain_heap and encrypted_heap are tested. Use
pg_vault_tde.enabled = off (requires a server restart) to isolate TAM
overhead from crypto cost.
Test × Callback Coverage Matrix
This matrix maps EVERY TAM/IAM callback to its covering test(s). A callback with no test is a gap that MUST be filled before release.
| Callback | Test(s) | Gap? |
|---|---|---|
tuple_insert |
12 | No |
tuple_update |
14, 69 | No |
tuple_delete |
15, 60 | No |
multi_insert |
18, 67 | No |
scan_getnextslot |
12, 14, 16, 19 | No |
index_fetch_tuple |
17, 62 | No |
scan_bitmap_next_tuple |
23, 63 | No |
scan_analyze_next_tuple |
21, 66 | No |
scan_sample_next_tuple |
24, 65 | No |
tuple_fetch_row_version |
14, 64 | No |
tuple_lock |
22, 64 | No |
relation_copy_for_cluster |
60, 61 | No |
tde_tuple_has_external_slow (DELETE after VACUUM FULL) |
60 | No |
ambuild (IAM) |
17 | No |
aminsert (IAM) |
17 | No |
Expected Output File Conventions
Every regression test MUST have a corresponding expected output file:
sql/regression_test.sql → expected/pg_vault_tde_init.out
When adding new tests (current numbering in regression_test.sql goes up to 70):
1. Run the test manually: psql -f sql/regression_test.sql > expected/pg_vault_tde_init.out 2>&1
2. Review the output for correctness
3. Commit the .out file alongside the .sql file
4. NEVER hand-edit .out files — always regenerate
5. v1.6 tests (73–110) live in sql/regression_test_v16.sql; v1.5 tests (53–72)
live in sql/regression_test_v15.sql. The baseline file sql/regression_test.sql
now contains tests 1–70: the original 52 v1.4 tests plus tests 53–70 added as
v1.7 TAM/TOAST coverage.