From 29b6590e5946a09e2cc04ecb8c8bfaa8d9b0288e Mon Sep 17 00:00:00 2001
From: prajnamort <prajnamort@gmail.com>
Date: Wed, 25 Mar 2020 15:50:01 +0800
Subject: [PATCH] Check operator when creating UNIQUE index on PARTITION table

Currently, we only check if every column in the partition key
is included in the unique index. But this is not enough when
custom opclass exists. Since there can be cases, that some
values belong to different partitions should be considered
equal according to unique index's opclass.

So, we should check the compatiblity of equality operator
between partition key's opclass and index column's opclass.
Only if they are the same operator should we consider them as
compatible.
---
 src/backend/commands/indexcmds.c       |  34 +++++++++-
 src/test/regress/expected/indexing.out | 114 +++++++++++++++++++++++++++++++++
 src/test/regress/sql/indexing.sql      | 107 +++++++++++++++++++++++++++++++
 3 files changed, 253 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 4e8263af4b..e7c3960011 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -877,8 +877,38 @@ DefineIndex(Oid relationId,
 			{
 				if (key->partattrs[i] == indexInfo->ii_IndexAttrNumbers[j])
 				{
-					found = true;
-					break;
+					/*
+					 * Check the equality operator in both the index's operator
+					 * class and the partition key's operator class. They must
+					 * be the same operator for us to consider them compatible.
+					 */
+					int ptkey_eq_strategy;
+					Oid ptkey_eqop;
+					Oid idxcol_opclass;
+					Oid idxcol_opfamily;
+					Oid idxcol_opclass_intype;
+					Oid idxcol_eqop;
+
+					if (key->strategy == PARTITION_STRATEGY_HASH)
+						ptkey_eq_strategy = HTEqualStrategyNumber;
+					else
+						ptkey_eq_strategy = BTEqualStrategyNumber;
+					ptkey_eqop = get_opfamily_member(key->partopfamily[i],
+													 key->partopcintype[i],
+													 key->partopcintype[i],
+													 ptkey_eq_strategy);
+					idxcol_opclass = classObjectId[j];
+					idxcol_opfamily = get_opclass_family(idxcol_opclass);
+					idxcol_opclass_intype = get_opclass_input_type(idxcol_opclass);
+					idxcol_eqop = get_opfamily_member(idxcol_opfamily,
+													  idxcol_opclass_intype,
+													  idxcol_opclass_intype,
+													  BTEqualStrategyNumber);
+					if (ptkey_eqop == idxcol_eqop)
+					{
+						found = true;
+						break;
+					}
 				}
 			}
 			if (!found)
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index f78865ef81..ffba7fbd83 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -1336,3 +1336,117 @@ Indexes:
     "parted_index_col_drop11_b_idx" btree (b)
 
 drop table parted_index_col_drop;
+--
+-- Test for checking operator class compatibility between partition key and
+-- unique index. If they are imcompatible, the unique index should not be
+-- allowed to created.
+--
+CREATE FUNCTION abscmp(int, int) RETURNS INT AS $$
+begin return btint4cmp(abs($1),abs($2)); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abshashint4(int) RETURNS INT AS $$
+begin return hashint4(abs($1)); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abshashint4extended(int, bigint) RETURNS BIGINT AS $$
+begin return hashint4extended(abs($1),$2); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abslt(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) < 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abslte(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) <= 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abseq(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) = 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION absgte(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) >= 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION absgt(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) > 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE OPERATOR |<| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |>|, HASHES);
+CREATE OPERATOR |<=| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |>=|, HASHES);
+CREATE OPERATOR |=| (PROCEDURE = abseq, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |=|, HASHES, MERGES);
+CREATE OPERATOR |>=| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |<=|, HASHES);
+CREATE OPERATOR |>| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |<|, HASHES);
+CREATE OPERATOR CLASS abs_int_btree_ops FOR TYPE int4
+    USING btree AS
+    OPERATOR 1 |<|,
+    OPERATOR 2 |<=|,
+    OPERATOR 3 |=|,
+    OPERATOR 4 |>=|,
+    OPERATOR 5 |>|,
+    FUNCTION 1 abscmp(int, int);
+CREATE OPERATOR CLASS abs_int_hash_ops FOR TYPE int4
+    USING hash AS
+    OPERATOR 1 |=|,
+    FUNCTION 1 abshashint4(int),
+    FUNCTION 2 abshashint4extended(int, bigint);
+-- default BTREE partition & default BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY LIST (a);
+CREATE TABLE ptop_test_0 PARTITION OF ptop_test FOR VALUES IN ('0');
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test FOR VALUES IN ('1');
+CREATE TABLE ptop_test_m1 PARTITION OF ptop_test FOR VALUES IN ('-1');
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- imcompatible
+ERROR:  insufficient columns in UNIQUE constraint definition
+DETAIL:  UNIQUE constraint on table "ptop_test" lacks column "a" which is part of the partition key.
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- compatible
+DROP INDEX ptop_test_unq_a;
+DROP TABLE ptop_test;
+-- custom BTREE partition & custom BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY LIST (a abs_int_btree_ops);
+CREATE TABLE ptop_test_0 PARTITION OF ptop_test FOR VALUES IN ('0');
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test FOR VALUES IN ('1', '-1');
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- imcompatible
+ERROR:  insufficient columns in UNIQUE constraint definition
+DETAIL:  UNIQUE constraint on table "ptop_test" lacks column "a" which is part of the partition key.
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- compatible
+DROP INDEX ptop_test_unq_abs_a;
+DROP TABLE ptop_test;
+-- default HASH partition & default BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY HASH (a);
+CREATE TABLE ptop_test_p0 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 1);
+CREATE TABLE ptop_test_p2 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 2);
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- imcompatible
+ERROR:  insufficient columns in UNIQUE constraint definition
+DETAIL:  UNIQUE constraint on table "ptop_test" lacks column "a" which is part of the partition key.
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- compatible
+DROP INDEX ptop_test_unq_a;
+DROP TABLE ptop_test;
+-- custom HASH partition & custom BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY HASH (a abs_int_hash_ops);
+CREATE TABLE ptop_test_p0 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 1);
+CREATE TABLE ptop_test_p2 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 2);
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- imcompatible
+ERROR:  insufficient columns in UNIQUE constraint definition
+DETAIL:  UNIQUE constraint on table "ptop_test" lacks column "a" which is part of the partition key.
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- compatible
+DROP INDEX ptop_test_unq_abs_a;
+DROP TABLE ptop_test;
+-- Clean up
+DROP OPERATOR CLASS abs_int_btree_ops USING btree;
+DROP OPERATOR FAMILY abs_int_btree_ops USING btree;
+DROP OPERATOR CLASS abs_int_hash_ops USING hash;
+DROP OPERATOR FAMILY abs_int_hash_ops USING hash;
+DROP OPERATOR |<| (int, int);
+DROP OPERATOR |<=| (int, int);
+DROP OPERATOR |=| (int, int);
+DROP OPERATOR |>=| (int, int);
+DROP OPERATOR |>| (int, int);
+DROP FUNCTION abscmp(int, int);
+DROP FUNCTION abslt(int, int);
+DROP FUNCTION abslte(int, int);
+DROP FUNCTION abseq(int, int);
+DROP FUNCTION absgte(int, int);
+DROP FUNCTION absgt(int, int);
+DROP FUNCTION abshashint4(int);
+DROP FUNCTION abshashint4extended(int, bigint);
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 35d159f41b..8dcf3d9c92 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -734,3 +734,110 @@ alter table parted_index_col_drop drop column c;
 \d parted_index_col_drop2
 \d parted_index_col_drop11
 drop table parted_index_col_drop;
+
+--
+-- Test for checking operator class compatibility between partition key and
+-- unique index. If they are imcompatible, the unique index should not be
+-- allowed to created.
+--
+CREATE FUNCTION abscmp(int, int) RETURNS INT AS $$
+begin return btint4cmp(abs($1),abs($2)); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abshashint4(int) RETURNS INT AS $$
+begin return hashint4(abs($1)); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abshashint4extended(int, bigint) RETURNS BIGINT AS $$
+begin return hashint4extended(abs($1),$2); end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abslt(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) < 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abslte(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) <= 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION abseq(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) = 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION absgte(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) >= 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE FUNCTION absgt(int, int) RETURNS BOOL AS $$
+begin return abscmp($1,$2) > 0; end;
+$$ LANGUAGE plpgsql STRICT IMMUTABLE;
+CREATE OPERATOR |<| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |>|, HASHES);
+CREATE OPERATOR |<=| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |>=|, HASHES);
+CREATE OPERATOR |=| (PROCEDURE = abseq, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |=|, HASHES, MERGES);
+CREATE OPERATOR |>=| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |<=|, HASHES);
+CREATE OPERATOR |>| (PROCEDURE = abslt, LEFTARG = int, RIGHTARG = int, COMMUTATOR = |<|, HASHES);
+CREATE OPERATOR CLASS abs_int_btree_ops FOR TYPE int4
+    USING btree AS
+    OPERATOR 1 |<|,
+    OPERATOR 2 |<=|,
+    OPERATOR 3 |=|,
+    OPERATOR 4 |>=|,
+    OPERATOR 5 |>|,
+    FUNCTION 1 abscmp(int, int);
+CREATE OPERATOR CLASS abs_int_hash_ops FOR TYPE int4
+    USING hash AS
+    OPERATOR 1 |=|,
+    FUNCTION 1 abshashint4(int),
+    FUNCTION 2 abshashint4extended(int, bigint);
+-- default BTREE partition & default BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY LIST (a);
+CREATE TABLE ptop_test_0 PARTITION OF ptop_test FOR VALUES IN ('0');
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test FOR VALUES IN ('1');
+CREATE TABLE ptop_test_m1 PARTITION OF ptop_test FOR VALUES IN ('-1');
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- imcompatible
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- compatible
+DROP INDEX ptop_test_unq_a;
+DROP TABLE ptop_test;
+-- custom BTREE partition & custom BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY LIST (a abs_int_btree_ops);
+CREATE TABLE ptop_test_0 PARTITION OF ptop_test FOR VALUES IN ('0');
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test FOR VALUES IN ('1', '-1');
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- imcompatible
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- compatible
+DROP INDEX ptop_test_unq_abs_a;
+DROP TABLE ptop_test;
+-- default HASH partition & default BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY HASH (a);
+CREATE TABLE ptop_test_p0 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 1);
+CREATE TABLE ptop_test_p2 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 2);
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- imcompatible
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- compatible
+DROP INDEX ptop_test_unq_a;
+DROP TABLE ptop_test;
+-- custom HASH partition & custom BTREE index
+CREATE TABLE ptop_test (a int, b int, c int) PARTITION BY HASH (a abs_int_hash_ops);
+CREATE TABLE ptop_test_p0 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
+CREATE TABLE ptop_test_p1 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 1);
+CREATE TABLE ptop_test_p2 PARTITION OF ptop_test
+    FOR VALUES WITH (MODULUS 3, REMAINDER 2);
+CREATE UNIQUE INDEX ptop_test_unq_a ON ptop_test (a);  -- imcompatible
+CREATE UNIQUE INDEX ptop_test_unq_abs_a ON ptop_test (a abs_int_btree_ops);  -- compatible
+DROP INDEX ptop_test_unq_abs_a;
+DROP TABLE ptop_test;
+-- Clean up
+DROP OPERATOR CLASS abs_int_btree_ops USING btree;
+DROP OPERATOR FAMILY abs_int_btree_ops USING btree;
+DROP OPERATOR CLASS abs_int_hash_ops USING hash;
+DROP OPERATOR FAMILY abs_int_hash_ops USING hash;
+DROP OPERATOR |<| (int, int);
+DROP OPERATOR |<=| (int, int);
+DROP OPERATOR |=| (int, int);
+DROP OPERATOR |>=| (int, int);
+DROP OPERATOR |>| (int, int);
+DROP FUNCTION abscmp(int, int);
+DROP FUNCTION abslt(int, int);
+DROP FUNCTION abslte(int, int);
+DROP FUNCTION abseq(int, int);
+DROP FUNCTION absgte(int, int);
+DROP FUNCTION absgt(int, int);
+DROP FUNCTION abshashint4(int);
+DROP FUNCTION abshashint4extended(int, bigint);
-- 
2.11.0

