8000 Teach contrib/amcheck to check the unique constraint violation · postgres/postgres@5ae2087 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5ae2087

Browse files
committed
Teach contrib/amcheck to check the unique constraint violation
Add the 'checkunique' argument to bt_index_check() and bt_index_parent_check(). When the flag is specified the procedures will check the unique constraint violation for unique indexes. Only one heap entry for all equal keys in the index should be visible (including posting list entries). Report an error otherwise. pg_amcheck called with the --checkunique option will do the same check for all the indexes it checks. Author: Anastasia Lubennikova <lubennikovaav@gmail.com> Author: Pavel Borisov <pashkin.elfe@gmail.com> Author: Maxim Orlov <orlovmg@gmail.com> Reviewed-by: Mark Dilger <mark.dilger@enterprisedb.com> Reviewed-by: Zhihong Yu <zyu@yugabyte.com> Reviewed-by: Peter Geoghegan <pg@bowt.ie> Reviewed-by: Aleksander Alekseev <aleksander@timescale.com> Discussion: https://postgr.es/m/CALT9ZEHRn5xAM5boga0qnrCmPV52bScEK2QnQ1HmUZDD301JEg%40mail.gmail.com
1 parent 8b62b44 commit 5ae2087

File tree

13 files changed

+830
-23
lines changed

13 files changed

+830
-23
lines changed

contrib/amcheck/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ OBJS = \
77
verify_nbtree.o
88

99
EXTENSION = amcheck
10-
DATA = amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
10+
DATA = amcheck--1.3--1.4.sql amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
1111
PGFILEDESC = "amcheck - function for verifying relation integrity"
1212

1313
REGRESS = check check_btree check_heap

contrib/amcheck/amcheck--1.3--1.4.sql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* contrib/amcheck/amcheck--1.3--1.4.sql */
2+
3+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
4+
\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.4'" to load this file. \quit
5+
6+
-- In order to avoid issues with dependencies when updating amcheck to 1.4,
7+
-- create new, overloaded versions of the 1.2 bt_index_parent_check signature,
8+
-- and 1.1 bt_index_check signature.
9+
10+
--
11+
-- bt_index_parent_check()
12+
--
13+
CREATE FUNCTION bt_index_parent_check(index regclass,
14+
heapallindexed boolean, rootdescend boolean, checkunique boolean)
15+
RETURNS VOID
16+
AS 'MODULE_PATHNAME', 'bt_index_parent_check'
17+
LANGUAGE C STRICT PARALLEL RESTRICTED;
18+
--
19+
-- bt_index_check()
20+
--
21+
CREATE FUNCTION bt_index_check(index regclass,
22+
heapallindexed boolean, checkunique boolean)
23+
RETURNS VOID
24+
AS 'MODULE_PATHNAME', 'bt_index_check'
25+
LANGUAGE C STRICT PARALLEL RESTRICTED;
26+
27+
-- We don't want this to be available to public
28+
REVOKE ALL ON FUNCTION bt_index_parent_check(regclass, boolean, boolean, boolean) FROM PUBLIC;
29+
REVOKE ALL ON FUNCTION bt_index_check(regclass, boolean, boolean) FROM PUBLIC;

contrib/amcheck/amcheck.control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# amcheck extension
22
comment = 'functions for verifying relation integrity'
3-
default_version = '1.3'
3+
default_version = '1.4'
44
module_pathname = '$libdir/amcheck'
55
relocatable = true

contrib/amcheck/expected/check_btree.out

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,54 @@ SELECT bt_index_check('bttest_a_expr_idx', true);
199199

200200
(1 row)
201201

202+
-- UNIQUE constraint check
203+
SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
204+
bt_index_check
205+
----------------
206+
207+
(1 row)
208+
209+
SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
210+
bt_index_check
211+
----------------
212+
213+
(1 row)
214+
215+
SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
216+
bt_index_parent_check
217+
-----------------------
218+
219+
(1 row)
220+
221+
SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
222+
bt_index_parent_check
223+
-----------------------
224+
225+
(1 row)
226+
227+
-- Check that null values in an unique index are not treated as equal
228+
CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
229+
INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
230+
SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
231+
bt_index_check
232+
----------------
233+
234+
(1 row)
235+
236+
CREATE INDEX on bttest_unique_nulls (b,c);
237+
SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
238+
bt_index_check
239+
----------------
240+
241+
(1 row)
242+
202243
-- cleanup
203244
DROP TABLE bttest_a;
204245
DROP TABLE bttest_b;
205246
DROP TABLE bttest_multi;
206247
DROP TABLE delete_test_table;
207248
DROP TABLE toast_bug;
208249
DROP FUNCTION ifun(int8);
250+
DROP TABLE bttest_unique_nulls;
209251
DROP OWNED BY regress_bttest_role; -- permissions
210252
DROP ROLE regress_bttest_role;

contrib/amcheck/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ install_data(
2323
'amcheck--1.0--1.1.sql',
2424
'amcheck--1.1--1.2.sql',
2525
'amcheck--1.2--1.3.sql',
26+
'amcheck--1.3--1.4.sql',
2627
kwargs: contrib_data_args,
2728
)
2829

@@ -42,6 +43,7 @@ tests += {
4243
't/001_verify_heapam.pl',
4344
't/002_cic.pl',
4445
't/003_cic_2pc.pl',
46+
't/004_verify_nbtree_unique.pl',
4547
],
4648
},
4749
}

contrib/amcheck/sql/check_btree.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,26 @@ CREATE INDEX bttest_a_expr_idx ON bttest_a ((ifun(id) + ifun(0)))
135135

136136
SELECT bt_index_check('bttest_a_expr_idx', true);
137137

138+
-- UNIQUE constraint check
139+
SELECT bt_index_check('bttest_a_idx', heapallindexed => true, checkunique => true);
140+
SELECT bt_index_check('bttest_b_idx', heapallindexed => false, checkunique => true);
141+
SELECT bt_index_parent_check('bttest_a_idx', heapallindexed => true, rootdescend => true, checkunique => true);
142+
SELECT bt_index_parent_check('bttest_b_idx', heapallindexed => true, rootdescend => false, checkunique => true);
143+
144+
-- Check that null values in an unique index are not treated as equal
145+
CREATE TABLE bttest_unique_nulls (a serial, b int, c int UNIQUE);
146+
INSERT INTO bttest_unique_nulls VALUES (generate_series(1, 10000), 2, default);
147+
SELECT bt_index_check('bttest_unique_nulls_c_key', heapallindexed => true, checkunique => true);
148+
CREATE INDEX on bttest_unique_nulls (b,c);
149+
SELECT bt_index_check('bttest_unique_nulls_b_c_idx', heapallindexed => true, checkunique => true);
150+
138151
-- cleanup
139152
DROP TABLE bttest_a;
140153
DROP TABLE bttest_b;
141154
DROP TABLE bttest_multi;
142155
DROP TABLE delete_test_table;
143156
DROP TABLE toast_bug;
144157
DROP FUNCTION ifun(int8);
158+
DROP TABLE bttest_unique_nulls;
145159
DROP OWNED BY regress_bttest_role; -- permissions
146160
DROP ROLE regress_bttest_role;
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
2+
# Copyright (c) 2023, PostgreSQL Global Development Group
3+
4+
# This regression test checks the behavior of the btree validation in the
5+
# presence of breaking sort order changes.
6+
#
7+
use strict;
8+
use warnings;
9+
use PostgreSQL::Test::Cluster;
10+
use PostgreSQL::Test::Utils;
11+
use Test::More;
12+
13+
my $node = PostgreSQL::Test::Cluster->new('test');
14+
$node->init;
15+
$node->append_conf('postgresql.conf', 'autovacuum = off');
16+
$node->start;
17+
18+
# Create a custom operator class and an index which uses it.
19+
$node->safe_psql(
20+
'postgres', q(
21+
CREATE EXTENSION amcheck;
22+
23+
CREATE FUNCTION ok_cmp (int4, int4)
24+
RETURNS int LANGUAGE sql AS
25+
$$
26+
SELECT
27+
CASE WHEN $1 < $2 THEN -1
28+
WHEN $1 > $2 THEN 1
29+
ELSE 0
30+
END;
31+
$$;
32+
33+
---
34+
--- Check 1: uniqueness violation.
35+
---
36+
CREATE FUNCTION ok_cmp1 (int4, int4)
37+
RETURNS int LANGUAGE sql AS
38+
$$
39+
SELECT ok_cmp($1, $2);
40+
$$;
41+
42+
---
43+
--- Make values 768 and 769 look equal.
44+
---
45+
CREATE FUNCTION bad_cmp1 (int4, int4)
46+
RETURNS int LANGUAGE sql AS
47+
$$
48+
SELECT
49+
CASE WHEN ($1 = 768 AND $2 = 769) OR
50+
($1 = 769 AND $2 = 768) THEN 0
51+
ELSE ok_cmp($1, $2)
52+
END;
53+
$$;
54+
55+
---
56+
--- Check 2: uniqueness violation without deduplication.
57+
---
58+
CREATE FUNCTION ok_cmp2 (int4, int4)
59+
RETURNS int LANGUAGE sql AS
60+
$$
61+
SELECT ok_cmp($1, $2);
62+
$$;
63+
64+
CREATE FUNCTION bad_cmp2 (int4, int4)
65+
RETURNS int LANGUAGE sql AS
66+
$$
67+
SELECT
68+
CASE WHEN $1 = $2 AND $1 = 400 THEN -1
69+
ELSE ok_cmp($1, $2)
70+
END;
71+
$$;
72+
73+
---
74+
--- Check 3: uniqueness violation with deduplication.
75+
---
76+
CREATE FUNCTION ok_cmp3 (int4, int4)
77+
RETURNS int LANGUAGE sql AS
78+
$$
79+
SELECT ok_cmp($1, $2);
80+
$$;
81+
82+
CREATE FUNCTION bad_cmp3 (int4, int4)
83+
RETURNS int LANGUAGE sql AS
84+
$$
85+
SELECT bad_cmp2($1, $2);
86+
$$;
87+
88+
---
89+
--- Create data.
90+
---
91+
CREATE TABLE bttest_unique1 (i int4);
92+
INSERT INTO bttest_unique1
93+
(SELECT * FROM generate_series(1, 1024) gs);
94+
95+
CREATE TABLE bttest_unique2 (i int4);
96+
INSERT INTO bttest_unique2(i)
97+
(SELECT * FROM generate_series(1, 400) gs);
98+
INSERT INTO bttest_unique2
99+
(SELECT * FROM generate_series(400, 1024) gs);
100+
101+
CREATE TABLE bttest_unique3 (i int4);
102+
INSERT INTO bttest_unique3
103+
SELECT * FROM bttest_unique2;
104+
105+
CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
106+
OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
107+
OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
108+
OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
109+
CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
110+
OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
111+
OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
112+
OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
113+
CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
114+
OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
115+
OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
116+
OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);
117+
118+
CREATE UNIQUE INDEX bttest_unique_idx1
119+
ON bttest_unique1
120+
USING btree (i int4_custom_ops1)
121+
WITH (deduplicate_items = off);
122+
CREATE UNIQUE INDEX bttest_unique_idx2
123+
ON bttest_unique2
124+
USING btree (i int4_custom_ops2)
125+
WITH (deduplicate_items = off);
126+
CREATE UNIQUE INDEX bttest_unique_idx3
127+
ON bttest_unique3
128+
USING btree (i int4_custom_ops3)
129+
WITH (deduplicate_items = on);
130+
));
131+
132+
my ($result, $stdout, $stderr);
133+
134+
#
135+
# Test 1.
136+
# - insert seq values
137+
# - create unique index
138+
# - break cmp function
139+
# - amcheck finds the uniqueness violation
140+
#
141+
142+
# We have not yet broken the index, so we should get no corruption
143+
$result = $node->safe_psql(
144+
'postgres', q(
145+
SELECT bt_index_check('bttest_unique_idx1', true, true);
146+
));
147+
is($result, '', 'run amcheck on non-broken bttest_unique_idx1');
148+
149+
# Change the operator class to use a function which considers certain different
150+
# values to be equal.
151+
$node->safe_psql(
152+
'postgres', q(
153+
UPDATE pg_catalog.pg_amproc SET
154+
amproc = 'bad_cmp1'::regproc
155+
WHERE amproc = 'ok_cmp1'::regproc;
156+
));
157+
158+
($result, $stdout, $stderr) = $node->psql(
159+
'postgres', q(
160+
SELECT bt_index_check('bttest_unique_idx1', true, true);
161+
));
162+
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
163+
'detected uniqueness violation for index "bttest_unique_idx1"');
164+
165+
#
166+
# Test 2.
167+
# - break cmp function
168+
# - insert seq values with duplicates
169+
# - create unique index
170+
# - make cmp function correct
171+
# - amcheck finds the uniqueness violation
172+
#
173+
174+
# Due to bad cmp function we expect amcheck to detect item order violation,
175+
# but no uniqueness violation.
176+
($result, $stdout, $stderr) = $node->psql(
177+
'postgres', q(
178+
SELECT bt_index_check('bttest_unique_idx2', true, true);
179+
));
180+
ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
181+
'detected item order invariant violation for index "bttest_unique_idx2"');
182+
183+
$node->safe_psql(
184+
'postgres', q(
185+
UPDATE pg_catalog.pg_amproc SET
186+
amproc = 'ok_cmp2'::regproc
187+
WHERE amproc = 'bad_cmp2'::regproc;
188+
));
189+
190+
($result, $stdout, $stderr) = $node->psql(
191+
'postgres', q(
192+
SELECT bt_index_check('bttest_unique_idx2', true, true);
193+
));
194+
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
195+
'detected uniqueness violation for index "bttest_unique_idx2"');
196+
197+
#
198+
# Test 3.
199+
# - same as Test 2, but with index deduplication
200+
#
201+
# Then uniqueness violation is detected between different posting list
202+
# entries inside one index entry.
203+
#
204+
205+
# Due to bad cmp function we expect amcheck to detect item order violation,
206+
# but no uniqueness violation.
207+
($result, $stdout, $stderr) = $node->psql(
208+
'postgres', q(
209+
SELECT bt_index_check('bttest_unique_idx3', true, true);
210+
));
211+
ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
212+
'detected item order invariant violation for index "bttest_unique_idx3"');
213+
214+
# For unique index deduplication is possible only for same values, but
215+
# with different visibility.
216+
$node->safe_psql(
217+
'postgres', q(
218+
DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
219+
INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
220+
INSERT INTO bttest_unique3 VALUES (400);
221+
DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
222+
INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
223+
INSERT INTO bttest_unique3 VALUES (400);
224+
DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
225+
INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
226+
INSERT INTO bttest_unique3 VALUES (400);
227+
));
228+
229+
$node->safe_psql(
230+
'postgres', q(
231+
UPDATE pg_catalog.pg_amproc SET
232+
amproc = 'ok_cmp3'::regproc
233+
WHERE amproc = 'bad_cmp3'::regproc;
234+
));
235+
236+
($result, $stdout, $stderr) = $node->psql(
237+
'postgres', q(
238+
SELECT bt_index_check('bttest_unique_idx3', true, true);
239+
));
240+
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
241+
'detected uniqueness violation for index "bttest_unique_idx3"');
242+
243+
$node->stop;
244+
done_testing();

0 commit comments

Comments
 (0)
0