Skip to content

Commit 2bd4318

Browse files
authored
Merge pull request #90 from NikolayS/rework-amcheck-reports
Rework amcheck: 3 reports (c1/c2/c3), include system indexes, add GIN
2 parents 124a2bd + f8c9331 commit 2bd4318

File tree

5 files changed

+195
-51
lines changed

5 files changed

+195
-51
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ Then connect to any Postgres server via psql and type `:dba` to open the interac
4646
### Corruption checks
4747
| ID | Report |
4848
|----|--------|
49-
| c1 | B-tree index integrity check (amcheck, non-blocking) |
50-
| c2 | Full B-tree + heap integrity check (amcheck, takes locks — use on standby!) |
49+
| c1 | Quick: btree + GIN (PG18) + heap (PG14) check — safe for production (AccessShareLock) |
50+
| c2 | Parent: btree parent-child check — detects glibc/collation corruption (⚠️ ShareLock) |
51+
| c3 | Full: heapallindexed + parent + heap — proves every tuple is indexed (⚠️⚠️ slow + ShareLock) |
5152

5253
### Memory
5354
| ID | Report |
Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
-- Corruption: B-tree index integrity check (amcheck, non-blocking)
1+
-- Corruption: quick check — btree, GIN (PG18+), heap (PG14+). Safe for production.
22
-- Requires: CREATE EXTENSION amcheck
3-
-- Uses bt_index_check() — lightweight, safe for production primaries.
4-
-- Does NOT lock tables (only AccessShareLock on indexes).
5-
-- Checks internal page consistency of all btree indexes.
6-
-- On PG14+, also runs verify_heapam() to detect heap corruption.
3+
-- All checks use AccessShareLock only (same as SELECT) — no write blocking.
4+
-- Checks page-level consistency of btree indexes (bt_index_check).
5+
-- On PG18+, also checks GIN indexes (gin_index_check).
6+
-- On PG14+, also checks heap and TOAST integrity (verify_heapam).
77

88
do $$
99
declare
@@ -12,6 +12,9 @@ declare
1212
idx_count int := 0;
1313
err_count int := 0;
1414
skip_count int := 0;
15+
gin_count int := 0;
16+
gin_err_count int := 0;
17+
gin_skip_count int := 0;
1518
tbl_count int := 0;
1619
tbl_err_count int := 0;
1720
tbl_skip_count int := 0;
@@ -26,9 +29,9 @@ begin
2629

2730
select current_setting('server_version_num')::int into pg_version;
2831

32+
-- === B-tree indexes ===
2933
raise notice '';
30-
raise notice '=== B-tree index integrity (bt_index_check) ===';
31-
raise notice 'Checking all btree indexes in the current database...';
34+
raise notice '=== B-tree index check (bt_index_check, AccessShareLock) ===';
3235
raise notice '';
3336

3437
for rec in
@@ -43,8 +46,6 @@ begin
4346
join pg_namespace n on n.oid = c.relnamespace
4447
join pg_am a on a.oid = c.relam
4548
where a.amname = 'btree'
46-
and n.nspname not in ('pg_catalog', 'information_schema')
47-
and n.nspname !~ '^pg_toast'
4849
and c.relpersistence != 't'
4950
and i.indisvalid
5051
order by n.nspname, t.relname, c.relname
@@ -54,30 +55,75 @@ begin
5455
idx_count := idx_count + 1;
5556
exception
5657
when insufficient_privilege then
57-
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.index_name;
5858
skip_count := skip_count + 1;
5959
when others then
60-
raise warning '❌ CORRUPTION in %.% (table %.%): %',
61-
rec.schema_name, rec.index_name,
62-
rec.schema_name, rec.table_name,
63-
sqlerrm;
60+
raise warning '❌ CORRUPTION in %.%: %',
61+
rec.schema_name, rec.index_name, sqlerrm;
6462
err_count := err_count + 1;
6563
end;
6664
end loop;
6765

6866
if err_count = 0 and skip_count = 0 then
69-
raise notice '✅ All % btree indexes passed integrity check.', idx_count;
67+
raise notice '✅ All % btree indexes OK.', idx_count;
7068
elsif err_count = 0 then
71-
raise notice '✅ % btree indexes passed, % skipped (insufficient privileges).', idx_count, skip_count;
69+
raise notice '✅ % btree indexes OK, % skipped (insufficient privileges).', idx_count, skip_count;
7270
else
7371
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
7472
end if;
7573

76-
-- Heap verification (PG14+ only)
74+
-- === GIN indexes (PG18+) ===
75+
if pg_version >= 180000 then
76+
raise notice '';
77+
raise notice '=== GIN index check (gin_index_check, AccessShareLock) ===';
78+
raise notice '';
79+
80+
for rec in
81+
select
82+
n.nspname as schema_name,
83+
c.relname as index_name,
84+
t.relname as table_name,
85+
c.oid as index_oid
86+
from pg_index i
87+
join pg_class c on c.oid = i.indexrelid
88+
join pg_class t on t.oid = i.indrelid
89+
join pg_namespace n on n.oid = c.relnamespace
90+
join pg_am a on a.oid = c.relam
91+
where a.amname = 'gin'
92+
and c.relpersistence != 't'
93+
and i.indisvalid
94+
order by n.nspname, t.relname, c.relname
95+
loop
96+
begin
97+
perform gin_index_check(rec.index_oid);
98+
gin_count := gin_count + 1;
99+
exception
100+
when insufficient_privilege then
101+
gin_skip_count := gin_skip_count + 1;
102+
when others then
103+
raise warning '❌ CORRUPTION in %.%: %',
104+
rec.schema_name, rec.index_name, sqlerrm;
105+
gin_err_count := gin_err_count + 1;
106+
end;
107+
end loop;
108+
109+
if gin_count + gin_err_count + gin_skip_count = 0 then
110+
raise notice 'No GIN indexes found.';
111+
elsif gin_err_count = 0 and gin_skip_count = 0 then
112+
raise notice '✅ All % GIN indexes OK.', gin_count;
113+
elsif gin_err_count = 0 then
114+
raise notice '✅ % GIN indexes OK, % skipped (insufficient privileges).', gin_count, gin_skip_count;
115+
else
116+
raise warning '❌ % of % GIN indexes have corruption!', gin_err_count, gin_count + gin_err_count + gin_skip_count;
117+
end if;
118+
else
119+
raise notice '';
120+
raise notice 'ℹ️ GIN index checking requires PostgreSQL 18+. Skipped.';
121+
end if;
122+
123+
-- === Heap verification (PG14+) ===
77124
if pg_version >= 140000 then
78125
raise notice '';
79-
raise notice '=== Heap integrity (verify_heapam) ===';
80-
raise notice 'Checking all user tables for heap corruption...';
126+
raise notice '=== Heap check (verify_heapam, AccessShareLock) ===';
81127
raise notice '';
82128

83129
for rec in
@@ -88,7 +134,6 @@ begin
88134
from pg_class c
89135
join pg_namespace n on n.oid = c.relnamespace
90136
where c.relkind = 'r'
91-
and n.nspname not in ('pg_catalog', 'information_schema')
92137
and c.relpersistence != 't'
93138
order by n.nspname, c.relname
94139
loop
@@ -107,7 +152,6 @@ begin
107152
end loop;
108153
exception
109154
when insufficient_privilege then
110-
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.table_name;
111155
tbl_skip_count := tbl_skip_count + 1;
112156
when others then
113157
raise warning 'ERROR checking %.%: %', rec.schema_name, rec.table_name, sqlerrm;
@@ -117,9 +161,9 @@ begin
117161
end loop;
118162

119163
if tbl_err_count = 0 and tbl_skip_count = 0 then
120-
raise notice '✅ All % tables passed heap integrity check.', tbl_count;
164+
raise notice '✅ All % tables passed heap check.', tbl_count;
121165
elsif tbl_err_count = 0 then
122-
raise notice '✅ % tables passed, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
166+
raise notice '✅ % tables OK, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
123167
else
124168
raise warning '❌ % of % tables have corruption!', tbl_err_count, tbl_count;
125169
end if;

sql/c2_amcheck_parent.sql

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
-- Corruption: B-tree parent check — detects collation/glibc corruption. ⚠️ ShareLock!
2+
-- Requires: CREATE EXTENSION amcheck
3+
-- ⚠️ Takes ShareLock on each index — blocks writes while checking!
4+
-- ⚠️ Best used on STANDBYS or during maintenance windows.
5+
--
6+
-- Uses bt_index_parent_check() which verifies parent-child key ordering.
7+
-- This is the most reliable way to detect corruption caused by glibc/ICU
8+
-- version changes that silently alter collation sort order.
9+
-- Also checks sibling page pointers and descends from root (rootdescend).
10+
-- On PG14+, also verifies unique constraint consistency (checkunique).
11+
12+
do $$
13+
declare
14+
rec record;
15+
idx_count int := 0;
16+
err_count int := 0;
17+
skip_count int := 0;
18+
pg_version int;
19+
begin
20+
-- Check extension
21+
if not exists (select 1 from pg_extension where extname = 'amcheck') then
22+
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
23+
return;
24+
end if;
25+
26+
select current_setting('server_version_num')::int into pg_version;
27+
28+
raise warning '';
29+
raise warning '⚠️ WARNING: This check takes ShareLock on each index — blocks writes!';
30+
raise warning '⚠️ Recommended: run on a STANDBY or during a maintenance window.';
31+
raise warning '';
32+
raise notice '=== B-tree parent check (bt_index_parent_check, ShareLock) ===';
33+
raise notice 'Detects: collation/glibc corruption, parent-child inconsistency, sibling pointer errors';
34+
raise notice '';
35+
36+
for rec in
37+
select
38+
n.nspname as schema_name,
39+
c.relname as index_name,
40+
t.relname as table_name,
41+
c.oid as index_oid,
42+
pg_relation_size(c.oid) as index_size
43+
from pg_index i
44+
join pg_class c on c.oid = i.indexrelid
45+
join pg_class t on t.oid = i.indrelid
46+
join pg_namespace n on n.oid = c.relnamespace
47+
join pg_am a on a.oid = c.relam
48+
where a.amname = 'btree'
49+
and c.relpersistence != 't'
50+
and i.indisvalid
51+
order by pg_relation_size(c.oid) asc -- smallest first
52+
loop
53+
begin
54+
if pg_version >= 140000 then
55+
perform bt_index_parent_check(
56+
rec.index_oid,
57+
heapallindexed := false,
58+
rootdescend := true,
59+
checkunique := true
60+
);
61+
elsif pg_version >= 110000 then
62+
perform bt_index_parent_check(
63+
rec.index_oid,
64+
heapallindexed := false,
65+
rootdescend := true
66+
);
67+
else
68+
perform bt_index_parent_check(rec.index_oid);
69+
end if;
70+
idx_count := idx_count + 1;
71+
exception
72+
when insufficient_privilege then
73+
skip_count := skip_count + 1;
74+
when others then
75+
raise warning '❌ CORRUPTION in %.% (table %.%, size %): %',
76+
rec.schema_name, rec.index_name,
77+
rec.schema_name, rec.table_name,
78+
pg_size_pretty(rec.index_size),
79+
sqlerrm;
80+
err_count := err_count + 1;
81+
end;
82+
end loop;
83+
84+
if err_count = 0 and skip_count = 0 then
85+
raise notice '✅ All % btree indexes passed parent check.', idx_count;
86+
elsif err_count = 0 then
87+
raise notice '✅ % btree indexes OK, % skipped (insufficient privileges).', idx_count, skip_count;
88+
else
89+
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
90+
end if;
91+
end;
92+
$$;
Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
-- Corruption: Full B-tree + heap check (amcheck, takes locks – use on standby!)
1+
-- Corruption: FULL check — heapallindexed + parent + heap. ⚠️⚠️ SLOW + ShareLock!
22
-- Requires: CREATE EXTENSION amcheck
3-
-- Uses bt_index_parent_check(heapallindexed := true) — thorough, takes locks.
4-
-- ⚠️ Takes ShareLock on each index. Run on standbys or during maintenance windows.
5-
-- Checks parent-child consistency, sibling pointers, root descent, unique constraints.
6-
-- Verifies all heap tuples have corresponding index entries.
7-
-- On PG14+, also runs verify_heapam() with full TOAST checking.
3+
-- ⚠️⚠️ HEAVY: Takes ShareLock AND scans entire heap for each index!
4+
-- ⚠️⚠️ This WILL be slow on large databases. Use on STANDBYS only.
5+
--
6+
-- bt_index_parent_check with heapallindexed=true: verifies that every single
7+
-- heap tuple has a corresponding index entry. Catches silent data loss where
8+
-- rows exist but are invisible to index scans.
9+
-- On PG14+: also verify_heapam with full TOAST checking.
810

911
do $$
1012
declare
@@ -27,10 +29,15 @@ begin
2729

2830
select current_setting('server_version_num')::int into pg_version;
2931

30-
raise warning '⚠️ This check takes locks! Use on standbys or during maintenance windows.';
31-
raise notice '';
32-
raise notice '=== Full B-tree index integrity (bt_index_parent_check + heapallindexed) ===';
33-
raise notice 'Checking all btree indexes with parent-child + heap verification...';
32+
raise warning '';
33+
raise warning '⚠️⚠️ WARNING: This is the HEAVIEST corruption check!';
34+
raise warning '⚠️⚠️ Takes ShareLock on each index (blocks writes) AND scans entire heap.';
35+
raise warning '⚠️⚠️ On large databases this can take HOURS. Use on standbys only.';
36+
raise warning '';
37+
38+
-- === Full B-tree check ===
39+
raise notice '=== Full B-tree check (bt_index_parent_check + heapallindexed, ShareLock) ===';
40+
raise notice 'Verifies: parent-child ordering, all heap tuples indexed, unique constraints';
3441
raise notice '';
3542

3643
for rec in
@@ -46,8 +53,6 @@ begin
4653
join pg_namespace n on n.oid = c.relnamespace
4754
join pg_am a on a.oid = c.relam
4855
where a.amname = 'btree'
49-
and n.nspname not in ('pg_catalog', 'information_schema')
50-
and n.nspname !~ '^pg_toast'
5156
and c.relpersistence != 't'
5257
and i.indisvalid
5358
order by pg_relation_size(c.oid) asc -- smallest first
@@ -72,7 +77,6 @@ begin
7277
idx_count := idx_count + 1;
7378
exception
7479
when insufficient_privilege then
75-
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.index_name;
7680
skip_count := skip_count + 1;
7781
when others then
7882
raise warning '❌ CORRUPTION in %.% (table %.%, size %): %',
@@ -85,18 +89,17 @@ begin
8589
end loop;
8690

8791
if err_count = 0 and skip_count = 0 then
88-
raise notice '✅ All % btree indexes passed full integrity check.', idx_count;
92+
raise notice '✅ All % btree indexes passed full check.', idx_count;
8993
elsif err_count = 0 then
90-
raise notice '✅ % btree indexes passed, % skipped (insufficient privileges).', idx_count, skip_count;
94+
raise notice '✅ % btree indexes OK, % skipped (insufficient privileges).', idx_count, skip_count;
9195
else
9296
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
9397
end if;
9498

95-
-- Full heap verification (PG14+ only)
99+
-- === Full heap verification (PG14+) ===
96100
if pg_version >= 140000 then
97101
raise notice '';
98-
raise notice '=== Full heap integrity (verify_heapam + TOAST) ===';
99-
raise notice 'Checking all user tables for heap and TOAST corruption...';
102+
raise notice '=== Full heap check (verify_heapam + TOAST, AccessShareLock) ===';
100103
raise notice '';
101104

102105
for rec in
@@ -108,7 +111,6 @@ begin
108111
from pg_class c
109112
join pg_namespace n on n.oid = c.relnamespace
110113
where c.relkind = 'r'
111-
and n.nspname not in ('pg_catalog', 'information_schema')
112114
and c.relpersistence != 't'
113115
order by n.nspname, c.relname
114116
loop
@@ -132,7 +134,6 @@ begin
132134
end loop;
133135
exception
134136
when insufficient_privilege then
135-
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.table_name;
136137
tbl_skip_count := tbl_skip_count + 1;
137138
when others then
138139
raise warning 'ERROR checking %.%: %', rec.schema_name, rec.table_name, sqlerrm;
@@ -142,9 +143,9 @@ begin
142143
end loop;
143144

144145
if tbl_err_count = 0 and tbl_skip_count = 0 then
145-
raise notice '✅ All % tables passed full heap integrity check.', tbl_count;
146+
raise notice '✅ All % tables passed full heap check.', tbl_count;
146147
elsif tbl_err_count = 0 then
147-
raise notice '✅ % tables passed, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
148+
raise notice '✅ % tables OK, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
148149
else
149150
raise warning '❌ % of % tables have corruption!', tbl_err_count, tbl_count;
150151
end if;

0 commit comments

Comments
 (0)