
# Copyright (c) 2023-2025, PostgreSQL Global Development Group

# This regression test checks the behavior of the btree validation in the
# presence of breaking sort order changes.
#
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;

my $node = PostgreSQL::Test::Cluster->new('test');
$node->init;
$node->append_conf('postgresql.conf', 'autovacuum = off');
$node->start;

# Create a custom operator class and an index which uses it.
$node->safe_psql(
	'postgres', q(
	CREATE EXTENSION amcheck;

	CREATE FUNCTION ok_cmp (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT
			CASE WHEN $1 < $2 THEN -1
				 WHEN $1 > $2 THEN  1
				 ELSE 0
			END;
	$$;

	---
	--- Check 1: uniqueness violation.
	---
	CREATE FUNCTION ok_cmp1 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT public.ok_cmp($1, $2);
	$$;

	---
	--- Make values 768 and 769 look equal.
	---
	CREATE FUNCTION bad_cmp1 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT
			CASE WHEN ($1 = 768 AND $2 = 769) OR
					  ($1 = 769 AND $2 = 768) THEN 0
				 ELSE public.ok_cmp($1, $2)
			END;
	$$;

	---
	--- Check 2: uniqueness violation without deduplication.
	---
	CREATE FUNCTION ok_cmp2 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT public.ok_cmp($1, $2);
	$$;

	CREATE FUNCTION bad_cmp2 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT
			CASE WHEN $1 = $2 AND $1 = 400 THEN -1
			ELSE public.ok_cmp($1, $2)
		END;
	$$;

	---
	--- Check 3: uniqueness violation with deduplication.
	---
	CREATE FUNCTION ok_cmp3 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT public.ok_cmp($1, $2);
	$$;

	CREATE FUNCTION bad_cmp3 (int4, int4)
	RETURNS int LANGUAGE sql AS
	$$
		SELECT public.bad_cmp2($1, $2);
	$$;

	---
	--- Create data.
	---
	CREATE TABLE bttest_unique1 (i int4);
	INSERT INTO bttest_unique1
		(SELECT * FROM generate_series(1, 1024) gs);

	CREATE TABLE bttest_unique2 (i int4);
	INSERT INTO bttest_unique2(i)
		(SELECT * FROM generate_series(1, 400) gs);
	INSERT INTO bttest_unique2
		(SELECT * FROM generate_series(400, 1024) gs);

	CREATE TABLE bttest_unique3 (i int4);
	INSERT INTO bttest_unique3
		SELECT * FROM bttest_unique2;

	CREATE OPERATOR CLASS int4_custom_ops1 FOR TYPE int4 USING btree AS
		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
		OPERATOR 5 > (int4, int4), FUNCTION 1 ok_cmp1(int4, int4);
	CREATE OPERATOR CLASS int4_custom_ops2 FOR TYPE int4 USING btree AS
		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp2(int4, int4);
	CREATE OPERATOR CLASS int4_custom_ops3 FOR TYPE int4 USING btree AS
		OPERATOR 1 < (int4, int4), OPERATOR 2 <= (int4, int4),
		OPERATOR 3 = (int4, int4), OPERATOR 4 >= (int4, int4),
		OPERATOR 5 > (int4, int4), FUNCTION 1 bad_cmp3(int4, int4);

	CREATE UNIQUE INDEX bttest_unique_idx1
						ON bttest_unique1
						USING btree (i int4_custom_ops1)
						WITH (deduplicate_items = off);
	CREATE UNIQUE INDEX bttest_unique_idx2
						ON bttest_unique2
						USING btree (i int4_custom_ops2)
						WITH (deduplicate_items = off);
	CREATE UNIQUE INDEX bttest_unique_idx3
						ON bttest_unique3
						USING btree (i int4_custom_ops3)
						WITH (deduplicate_items = on);
));

my ($result, $stdout, $stderr);

#
# Test 1.
#  - insert seq values
#  - create unique index
#  - break cmp function
#  - amcheck finds the uniqueness violation
#

# We have not yet broken the index, so we should get no corruption
$result = $node->safe_psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx1', true, true);
));
is($result, '', 'run amcheck on non-broken bttest_unique_idx1');

# Change the operator class to use a function which considers certain different
# values to be equal.
$node->safe_psql(
	'postgres', q(
	UPDATE pg_catalog.pg_amproc SET
		   amproc = 'bad_cmp1'::regproc
	WHERE amproc = 'ok_cmp1'::regproc;
));

($result, $stdout, $stderr) = $node->psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx1', true, true);
));
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/,
	'detected uniqueness violation for index "bttest_unique_idx1"');

#
# Test 2.
#  - break cmp function
#  - insert seq values with duplicates
#  - create unique index
#  - make cmp function correct
#  - amcheck finds the uniqueness violation
#

# Due to bad cmp function we expect amcheck to detect item order violation,
# but no uniqueness violation.
($result, $stdout, $stderr) = $node->psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx2', true, true);
));
ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx2"/,
	'detected item order invariant violation for index "bttest_unique_idx2"');

$node->safe_psql(
	'postgres', q(
	UPDATE pg_catalog.pg_amproc SET
		   amproc = 'ok_cmp2'::regproc
	WHERE amproc = 'bad_cmp2'::regproc;
));

($result, $stdout, $stderr) = $node->psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx2', true, true);
));
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/,
	'detected uniqueness violation for index "bttest_unique_idx2"');

#
# Test 3.
#  - same as Test 2, but with index deduplication
#
# Then uniqueness violation is detected between different posting list
# entries inside one index entry.
#

# Due to bad cmp function we expect amcheck to detect item order violation,
# but no uniqueness violation.
($result, $stdout, $stderr) = $node->psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx3', true, true);
));
ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx3"/,
	'detected item order invariant violation for index "bttest_unique_idx3"');

# For unique index deduplication is possible only for same values, but
# with different visibility.
$node->safe_psql(
	'postgres', q(
	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
	INSERT INTO bttest_unique3 VALUES (400);
	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
	INSERT INTO bttest_unique3 VALUES (400);
	DELETE FROM bttest_unique3 WHERE 380 <= i AND i <= 420;
	INSERT INTO bttest_unique3 (SELECT * FROM generate_series(380, 420));
	INSERT INTO bttest_unique3 VALUES (400);
));

$node->safe_psql(
	'postgres', q(
	UPDATE pg_catalog.pg_amproc SET
		   amproc = 'ok_cmp3'::regproc
	WHERE amproc = 'bad_cmp3'::regproc;
));

($result, $stdout, $stderr) = $node->psql(
	'postgres', q(
	SELECT bt_index_check('bttest_unique_idx3', true, true);
));
ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/,
	'detected uniqueness violation for index "bttest_unique_idx3"');

$node->stop;
done_testing();
