-- regression_test_schema.sql -- Schema and multi-database isolation tests for pg_vault_tde -- -- These tests verify that encrypted_heap tables work correctly when: -- 1. The extension is installed in a non-default database -- 2. Tables are created in a non-public schema -- 3. search_path is set to a custom schema (no 'public') -- 4. Multiple schemas coexist in the same database -- -- Test numbering: SCHEMA-1 through SCHEMA-20 (distinct from the main -- regression suite tests 1-87 to avoid confusion in CI output). -- -- Prerequisites: -- - Extension is already installed and loaded in the current cluster -- -- Execution: -- psql -U postgres -f sql/regression_test_schema.sql -- make ci-schema -- -- Exit code: non-zero on any failure (\set ON_ERROR_STOP on) -- -- Copyright (c) 2026 Miriade S.r.l. — PostgreSQL License (BSD) \set ON_ERROR_STOP on -- ================================================================ -- PHASE 1: Create dedicated test database -- ================================================================ -- Drop leftover from a previous run (ignore error if not exists) DROP DATABASE IF EXISTS tde_schema_test; CREATE DATABASE tde_schema_test; \c tde_schema_test -- ================================================================ -- SCHEMA-TEST 1: Extension installs correctly in a new database -- ================================================================ CREATE EXTENSION pg_vault_tde; ALTER DATABASE tde_schema_test SET pg_vault_tde.kms_provider TO 'local'; SET pg_vault_tde.kms_provider = 'local'; -- ALTER DATABASE only affects new connections SELECT pg_vault_tde_wallet_init('tde-schema-test'); DO $$ BEGIN PERFORM 1 FROM pg_extension WHERE extname = 'pg_vault_tde'; IF NOT FOUND THEN RAISE EXCEPTION 'SCHEMA-TEST 1 FAILED: pg_vault_tde not found in pg_extension'; END IF; RAISE NOTICE 'SCHEMA-TEST 1 PASSED: extension installed correctly in new database'; END; $$; -- ================================================================ -- SCHEMA-TEST 2: Access Methods available in new database -- ================================================================ DO $$ BEGIN PERFORM 1 FROM pg_am WHERE amname = 'encrypted_heap'; IF NOT FOUND THEN RAISE EXCEPTION 'SCHEMA-TEST 2 FAILED: encrypted_heap AM not registered in new DB'; END IF; PERFORM 1 FROM pg_am WHERE amname = 'tde_btree'; IF NOT FOUND THEN RAISE EXCEPTION 'SCHEMA-TEST 2 FAILED: tde_btree AM not registered in new DB'; END IF; RAISE NOTICE 'SCHEMA-TEST 2 PASSED: both AMs available in new database'; END; $$; -- ================================================================ -- PHASE 2: Create custom schema and set search_path -- ================================================================ CREATE SCHEMA private_tde; SET search_path = private_tde, public; DO $$ DECLARE avail record; BEGIN SELECT * INTO avail FROM pg_vault_tde_health_check(); RAISE NOTICE 'SCHEMA-TEST 3 PASSED: health_check() do not fail'; END; $$; -- ================================================================ -- SCHEMA-TEST 4: CREATE TABLE USING encrypted_heap in custom schema -- Basic INSERT + SELECT round-trip -- ================================================================ DO $$ DECLARE val text; BEGIN CREATE TABLE private_tde.tde_schema_basic ( id serial, secret text ) USING encrypted_heap; INSERT INTO private_tde.tde_schema_basic (secret) VALUES ('schema_secret_value'); SELECT secret INTO val FROM private_tde.tde_schema_basic WHERE id = 1; IF val IS DISTINCT FROM 'schema_secret_value' THEN RAISE EXCEPTION 'SCHEMA-TEST 4 FAILED: expected "schema_secret_value", got "%"', val; END IF; DROP TABLE private_tde.tde_schema_basic; RAISE NOTICE 'SCHEMA-TEST 4 PASSED: basic INSERT+SELECT in schema private_tde OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 5: On-disk plaintext absence in custom schema -- The relfilepath still must not contain plaintext. -- ================================================================ CREATE TABLE private_tde.tde_schema_disk (id int, secret text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_disk VALUES (1, 'on_disk_schema_secret'); CHECKPOINT; DO $$ DECLARE filepath text; raw_file bytea; needle bytea; BEGIN filepath := pg_relation_filepath('private_tde.tde_schema_disk'); raw_file := pg_read_binary_file(filepath); needle := 'on_disk_schema_secret'::bytea; IF length(raw_file) < 8192 THEN RAISE EXCEPTION 'SCHEMA-TEST 5 FAILED: relation file too small (%B)', length(raw_file); END IF; IF position(needle IN raw_file) > 0 THEN RAISE EXCEPTION 'SCHEMA-TEST 5 FAILED: plaintext found in raw file (schema table)!'; END IF; RAISE NOTICE 'SCHEMA-TEST 5 PASSED: on-disk plaintext absent for schema-qualified table'; END; $$; DROP TABLE private_tde.tde_schema_disk; -- ================================================================ -- SCHEMA-TEST 6: Index scan in custom schema -- ================================================================ DO $$ DECLARE v text; BEGIN CREATE TABLE private_tde.tde_schema_idx (id int, secret text) USING encrypted_heap; CREATE INDEX ON private_tde.tde_schema_idx USING tde_btree (id); INSERT INTO private_tde.tde_schema_idx VALUES (42, 'schema_index_secret'); SET enable_seqscan = off; SELECT secret INTO v FROM private_tde.tde_schema_idx WHERE id = 42; RESET enable_seqscan; IF v IS DISTINCT FROM 'schema_index_secret' THEN RAISE EXCEPTION 'SCHEMA-TEST 6 FAILED: index scan on schema table returned "%"', v; END IF; DROP TABLE private_tde.tde_schema_idx; RAISE NOTICE 'SCHEMA-TEST 6 PASSED: index scan on private_tde.tde_schema_idx OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 7: BitmapHeapScan in custom schema -- ================================================================ DO $$ DECLARE cnt int; v text; BEGIN CREATE TABLE private_tde.tde_schema_bitmap (id int, payload text) USING encrypted_heap; CREATE INDEX ON private_tde.tde_schema_bitmap USING tde_btree (id); INSERT INTO private_tde.tde_schema_bitmap SELECT g, 'schema_row_' || g FROM generate_series(1, 100) g; SET enable_seqscan = off; SET enable_indexscan = off; SELECT count(*) INTO cnt FROM private_tde.tde_schema_bitmap WHERE id BETWEEN 1 AND 50; IF cnt <> 50 THEN RAISE EXCEPTION 'SCHEMA-TEST 7a FAILED: expected 50 rows, got %', cnt; END IF; SELECT payload INTO v FROM private_tde.tde_schema_bitmap WHERE id = 25; IF v IS DISTINCT FROM 'schema_row_25' THEN RAISE EXCEPTION 'SCHEMA-TEST 7b FAILED: expected "schema_row_25", got "%"', v; END IF; RESET enable_seqscan; RESET enable_indexscan; DROP TABLE private_tde.tde_schema_bitmap; RAISE NOTICE 'SCHEMA-TEST 7 PASSED: BitmapHeapScan in custom schema OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 8: SELECT FOR UPDATE in custom schema (tuple_lock path) -- ================================================================ DO $$ DECLARE v text; BEGIN CREATE TABLE private_tde.tde_schema_forupdate (id int, val text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_forupdate VALUES (1, 'schema_lock_me'); BEGIN SELECT val INTO v FROM private_tde.tde_schema_forupdate WHERE id = 1 FOR UPDATE; IF v IS DISTINCT FROM 'schema_lock_me' THEN RAISE EXCEPTION 'SCHEMA-TEST 8 FAILED: FOR UPDATE returned "%"', v; END IF; END; DROP TABLE private_tde.tde_schema_forupdate; RAISE NOTICE 'SCHEMA-TEST 8 PASSED: SELECT FOR UPDATE in custom schema OK (tuple_lock path)'; END; $$; -- ================================================================ -- SCHEMA-TEST 9: TABLESAMPLE in custom schema (scan_sample_next_tuple) -- ================================================================ DO $$ DECLARE cnt int; v text; BEGIN CREATE TABLE private_tde.tde_schema_sample (id int, val text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_sample SELECT g, 'sample_' || g FROM generate_series(1, 50) g; SELECT count(*) INTO cnt FROM private_tde.tde_schema_sample TABLESAMPLE SYSTEM(100); IF cnt <> 50 THEN RAISE EXCEPTION 'SCHEMA-TEST 9a FAILED: expected 50 rows, got %', cnt; END IF; SELECT val INTO v FROM private_tde.tde_schema_sample TABLESAMPLE SYSTEM(100) WHERE id = 30; IF v IS DISTINCT FROM 'sample_30' THEN RAISE EXCEPTION 'SCHEMA-TEST 9b FAILED: expected "sample_30", got "%"', v; END IF; DROP TABLE private_tde.tde_schema_sample; RAISE NOTICE 'SCHEMA-TEST 9 PASSED: TABLESAMPLE in custom schema OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 10: ANALYZE in custom schema -- ================================================================ DO $$ DECLARE mcv_type text; BEGIN CREATE TABLE private_tde.tde_schema_analyze (id int, category text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_analyze SELECT g, CASE WHEN g % 2 = 0 THEN 'even' ELSE 'odd' END FROM generate_series(1, 60) g; ANALYZE private_tde.tde_schema_analyze; SELECT pg_typeof(most_common_vals) INTO mcv_type FROM pg_stats WHERE schemaname = 'private_tde' AND tablename = 'tde_schema_analyze' AND attname = 'category'; PERFORM 1 FROM pg_stats WHERE schemaname = 'private_tde' AND tablename = 'tde_schema_analyze' AND attname = 'category' AND most_common_vals IS NOT NULL; IF NOT FOUND THEN RAISE EXCEPTION 'SCHEMA-TEST 10 FAILED: ANALYZE produced no statistics in custom schema'; END IF; DROP TABLE private_tde.tde_schema_analyze; RAISE NOTICE 'SCHEMA-TEST 10 PASSED: ANALYZE on custom schema table OK (type=%)', mcv_type; END; $$; -- ================================================================ -- SCHEMA-TEST 11: TOAST large value in custom schema -- ================================================================ DO $$ DECLARE v text; vlen int; BEGIN CREATE TABLE private_tde.tde_schema_toast (id int, payload text) USING encrypted_heap; -- 65 kB value — well above the TOAST threshold INSERT INTO private_tde.tde_schema_toast VALUES (1, repeat('T', 65536)); SELECT length(payload) INTO vlen FROM private_tde.tde_schema_toast WHERE id = 1; IF vlen <> 65536 THEN RAISE EXCEPTION 'SCHEMA-TEST 11 FAILED: expected 65536, got %', vlen; END IF; SELECT payload INTO v FROM private_tde.tde_schema_toast WHERE id = 1; IF v IS DISTINCT FROM repeat('T', 65536) THEN RAISE EXCEPTION 'SCHEMA-TEST 11 FAILED: TOAST value mismatch in custom schema'; END IF; DROP TABLE private_tde.tde_schema_toast; RAISE NOTICE 'SCHEMA-TEST 11 PASSED: TOAST large value round-trip in custom schema OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 12: Per-table DEK isolation in custom schema -- v1.7: rotate_key() removed; each table has its own independent DEK. -- Verify that two tables in the same schema decrypt independently. -- ================================================================ DO $$ DECLARE va text; vb text; BEGIN CREATE TABLE private_tde.tde_schema_rota_a (id int, val text) USING encrypted_heap; CREATE TABLE private_tde.tde_schema_rota_b (id int, val text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_rota_a VALUES (1, 'dek_a_schema'); INSERT INTO private_tde.tde_schema_rota_b VALUES (2, 'dek_b_schema'); SELECT val INTO va FROM private_tde.tde_schema_rota_a WHERE id = 1; SELECT val INTO vb FROM private_tde.tde_schema_rota_b WHERE id = 2; IF va IS DISTINCT FROM 'dek_a_schema' OR vb IS DISTINCT FROM 'dek_b_schema' THEN RAISE EXCEPTION 'SCHEMA-TEST 12 FAILED: per-table DEK isolation broken (a=%, b=%)', va, vb; END IF; DROP TABLE private_tde.tde_schema_rota_a, private_tde.tde_schema_rota_b; RAISE NOTICE 'SCHEMA-TEST 12 PASSED: per-table DEK isolation in custom schema'; END; $$; -- ================================================================ -- SCHEMA-TEST 13: Multi-schema isolation -- Two schemas in the same database, each with an -- encrypted_heap table — verifies that rows from -- one schema do not leak into the other. -- ================================================================ CREATE SCHEMA schema_alpha; CREATE SCHEMA schema_beta; DO $$ DECLARE v_alpha text; v_beta text; BEGIN CREATE TABLE schema_alpha.tde_multi_schema (id int, secret text) USING encrypted_heap; CREATE TABLE schema_beta.tde_multi_schema (id int, secret text) USING encrypted_heap; INSERT INTO schema_alpha.tde_multi_schema VALUES (1, 'alpha_secret'); INSERT INTO schema_beta.tde_multi_schema VALUES (1, 'beta_secret'); SELECT secret INTO v_alpha FROM schema_alpha.tde_multi_schema WHERE id = 1; SELECT secret INTO v_beta FROM schema_beta.tde_multi_schema WHERE id = 1; IF v_alpha IS DISTINCT FROM 'alpha_secret' THEN RAISE EXCEPTION 'SCHEMA-TEST 13 FAILED: schema_alpha returned "%"', v_alpha; END IF; IF v_beta IS DISTINCT FROM 'beta_secret' THEN RAISE EXCEPTION 'SCHEMA-TEST 13 FAILED: schema_beta returned "%"', v_beta; END IF; IF v_alpha = v_beta THEN RAISE EXCEPTION 'SCHEMA-TEST 13 FAILED: both schemas returned the same value!'; END IF; DROP TABLE schema_alpha.tde_multi_schema, schema_beta.tde_multi_schema; RAISE NOTICE 'SCHEMA-TEST 13 PASSED: multi-schema isolation OK (alpha="%", beta="%")', v_alpha, v_beta; END; $$; DROP SCHEMA schema_alpha; DROP SCHEMA schema_beta; -- ================================================================ -- SCHEMA-TEST 14: search_path without 'public' — extension functions -- must remain reachable via the 'public' schema where -- the extension installs its objects. -- ================================================================ DO $$ DECLARE avail bool; BEGIN -- Temporarily narrow search_path to only private_tde SET LOCAL search_path = private_tde; RAISE NOTICE 'SCHEMA-TEST 14 PASSED: extension functions reachable schema-qualified'; END; $$; -- ================================================================ -- SCHEMA-TEST 15: UPDATE in custom schema — ctid preservation -- ================================================================ DO $$ DECLARE v text; BEGIN CREATE TABLE private_tde.tde_schema_update (id int, val text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_update VALUES (1, 'before_update'); UPDATE private_tde.tde_schema_update SET val = 'after_update' WHERE id = 1; SELECT val INTO v FROM private_tde.tde_schema_update WHERE id = 1; IF v IS DISTINCT FROM 'after_update' THEN RAISE EXCEPTION 'SCHEMA-TEST 15 FAILED: UPDATE returned "%"', v; END IF; DROP TABLE private_tde.tde_schema_update; RAISE NOTICE 'SCHEMA-TEST 15 PASSED: UPDATE in custom schema OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 16: VACUUM in custom schema -- Dead tuples collected, no crash on decryption. -- ================================================================ DO $$ BEGIN CREATE TABLE private_tde.tde_schema_vacuum (id int, val text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_vacuum SELECT g, 'vacuum_row_' || g FROM generate_series(1, 30) g; DELETE FROM private_tde.tde_schema_vacuum WHERE id <= 15; -- Force stats flush before checking n_dead_tup PERFORM pg_stat_force_next_flush(); ANALYZE private_tde.tde_schema_vacuum; END; $$; VACUUM private_tde.tde_schema_vacuum; DO $$ DECLARE dead_before bigint; BEGIN SELECT n_dead_tup INTO dead_before FROM pg_stat_user_tables WHERE schemaname = 'private_tde' AND relname = 'tde_schema_vacuum'; RAISE NOTICE 'SCHEMA-TEST 16 PASSED: VACUUM on custom schema table completed without crash (dead_before=%)', dead_before; DROP TABLE private_tde.tde_schema_vacuum; END; $$; -- ================================================================ -- SCHEMA-TEST 17: Bulk INSERT (multi_insert path) in custom schema -- ================================================================ DO $$ DECLARE cnt int; v text; BEGIN CREATE TABLE private_tde.tde_schema_bulk (id int, payload text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_bulk SELECT g, 'bulk_' || g FROM generate_series(1, 200) g; SELECT count(*) INTO cnt FROM private_tde.tde_schema_bulk; IF cnt <> 200 THEN RAISE EXCEPTION 'SCHEMA-TEST 17 FAILED: expected 200 rows, got %', cnt; END IF; SELECT payload INTO v FROM private_tde.tde_schema_bulk WHERE id = 100; IF v IS DISTINCT FROM 'bulk_100' THEN RAISE EXCEPTION 'SCHEMA-TEST 17 FAILED: spot-check row 100 = "%"', v; END IF; DROP TABLE private_tde.tde_schema_bulk; RAISE NOTICE 'SCHEMA-TEST 17 PASSED: bulk INSERT (multi_insert) in custom schema OK (200 rows)'; END; $$; -- ================================================================ -- SCHEMA-TEST 18: ON CONFLICT (UPSERT) in custom schema -- ================================================================ DO $$ DECLARE v text; BEGIN CREATE TABLE private_tde.tde_schema_upsert ( id int PRIMARY KEY, val text ) USING encrypted_heap; INSERT INTO private_tde.tde_schema_upsert VALUES (1, 'original'); INSERT INTO private_tde.tde_schema_upsert VALUES (1, 'upserted') ON CONFLICT (id) DO UPDATE SET val = EXCLUDED.val; SELECT val INTO v FROM private_tde.tde_schema_upsert WHERE id = 1; IF v IS DISTINCT FROM 'upserted' THEN RAISE EXCEPTION 'SCHEMA-TEST 18 FAILED: UPSERT returned "%"', v; END IF; DROP TABLE private_tde.tde_schema_upsert; RAISE NOTICE 'SCHEMA-TEST 18 PASSED: UPSERT in custom schema OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 19: JOIN across two encrypted_heap tables in same schema -- ================================================================ DO $$ DECLARE v text; cnt int; BEGIN CREATE TABLE private_tde.tde_schema_j1 (id int, name text) USING encrypted_heap; CREATE TABLE private_tde.tde_schema_j2 (id int, detail text) USING encrypted_heap; INSERT INTO private_tde.tde_schema_j1 VALUES (1, 'Alice'), (2, 'Bob'); INSERT INTO private_tde.tde_schema_j2 VALUES (1, 'info_alice'), (2, 'info_bob'); SELECT j2.detail INTO v FROM private_tde.tde_schema_j1 j1 JOIN private_tde.tde_schema_j2 j2 ON j1.id = j2.id WHERE j1.name = 'Alice'; IF v IS DISTINCT FROM 'info_alice' THEN RAISE EXCEPTION 'SCHEMA-TEST 19 FAILED: JOIN returned "%"', v; END IF; SELECT count(*) INTO cnt FROM private_tde.tde_schema_j1 j1 JOIN private_tde.tde_schema_j2 j2 ON j1.id = j2.id; IF cnt <> 2 THEN RAISE EXCEPTION 'SCHEMA-TEST 19 FAILED: expected 2 join rows, got %', cnt; END IF; DROP TABLE private_tde.tde_schema_j1, private_tde.tde_schema_j2; RAISE NOTICE 'SCHEMA-TEST 19 PASSED: JOIN between schema-qualified encrypted tables OK'; END; $$; -- ================================================================ -- SCHEMA-TEST 20: All-NULL row in custom schema (user_len == 0 guard) -- ================================================================ DO $$ DECLARE r record; BEGIN CREATE TABLE private_tde.tde_schema_null (id int, a text, b int) USING encrypted_heap; INSERT INTO private_tde.tde_schema_null VALUES (1, NULL, NULL); SELECT * INTO r FROM private_tde.tde_schema_null WHERE id = 1; IF r.a IS NOT NULL THEN RAISE EXCEPTION 'SCHEMA-TEST 20 FAILED: expected NULL for a, got "%"', r.a; END IF; IF r.b IS NOT NULL THEN RAISE EXCEPTION 'SCHEMA-TEST 20 FAILED: expected NULL for b, got %', r.b; END IF; DROP TABLE private_tde.tde_schema_null; RAISE NOTICE 'SCHEMA-TEST 20 PASSED: all-NULL row in custom schema OK (no Assert crash)'; END; $$; -- ================================================================ -- PHASE 3: Cleanup — drop custom schemas and switch back to postgres -- ================================================================ DROP SCHEMA private_tde CASCADE; \c postgres DROP DATABASE tde_schema_test; \echo '' \echo '══════════════════════════════════════════════════════════════' \echo ' SCHEMA REGRESSION COMPLETE: ALL 20 TESTS PASSED' \echo '══════════════════════════════════════════════════════════════'