Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 8c852ba

Browse files
committed
Allow some exclusion constraints on partitions
Previously we only allowed unique B-tree constraints on partitions (and only if the constraint included all the partition keys). But we could allow exclusion constraints with the same restriction. We also require that those columns be compared for equality, not something like &&. Author: Paul A. Jungwirth <pj@illuminatedcomputing.com> Reviewed-by: Ronan Dunklau <ronan.dunklau@aiven.io> Reviewed-by: Peter Eisentraut <peter@eisentraut.org> Discussion: https://www.postgresql.org/message-id/flat/ec8b1d9b-502e-d1f8-e909-1bf9dffe6fa5@illuminatedcomputing.com
1 parent ce0b0fa commit 8c852ba

File tree

13 files changed

+284
-73
lines changed

13 files changed

+284
-73
lines changed

contrib/btree_gist/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
3838

3939
REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
4040
time timetz date interval macaddr macaddr8 inet cidr text varchar char \
41-
bytea bit varbit numeric uuid not_equal enum bool
41+
bytea bit varbit numeric uuid not_equal enum bool partitions
4242

4343
SHLIB_LINK += $(filter -lm, $(LIBS))
4444

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
-- Make sure we can create an exclusion constraint
2+
-- across a partitioned table.
3+
-- That code looks at strategy numbers that can differ in regular gist vs btree_gist,
4+
-- so we want to make sure it works here too.
5+
create table parttmp (
6+
id int,
7+
valid_at daterange,
8+
exclude using gist (id with =, valid_at with &&)
9+
) partition by range (id);
10+
create table parttmp_1_to_10 partition of parttmp for values from (1) to (10);
11+
create table parttmp_11_to_20 partition of parttmp for values from (11) to (20);
12+
insert into parttmp (id, valid_at) values
13+
(1, '[2000-01-01, 2000-02-01)'),
14+
(1, '[2000-02-01, 2000-03-01)'),
15+
(2, '[2000-01-01, 2000-02-01)'),
16+
(11, '[2000-01-01, 2000-02-01)'),
17+
(11, '[2000-02-01, 2000-03-01)'),
18+
(12, '[2000-01-01, 2000-02-01)');
19+
select * from parttmp order by id, valid_at;
20+
id | valid_at
21+
----+-------------------------
22+
1 | [01-01-2000,02-01-2000)
23+
1 | [02-01-2000,03-01-2000)
24+
2 | [01-01-2000,02-01-2000)
25+
11 | [01-01-2000,02-01-2000)
26+
11 | [02-01-2000,03-01-2000)
27+
12 | [01-01-2000,02-01-2000)
28+
(6 rows)
29+
30+
select * from parttmp_1_to_10 order by id, valid_at;
31+
id | valid_at
32+
----+-------------------------
33+
1 | [01-01-2000,02-01-2000)
34+
1 | [02-01-2000,03-01-2000)
35+
2 | [01-01-2000,02-01-2000)
36+
(3 rows)
37+
38+
select * from parttmp_11_to_20 order by id, valid_at;
39+
id | valid_at
40+
----+-------------------------
41+
11 | [01-01-2000,02-01-2000)
42+
11 | [02-01-2000,03-01-2000)
43+
12 | [01-01-2000,02-01-2000)
44+
(3 rows)
45+
46+
update parttmp set valid_at = valid_at * '[2000-01-15,2000-02-15)' where id = 1;
47+
select * from parttmp order by id, valid_at;
48+
id | valid_at
49+
----+-------------------------
50+
1 | [01-15-2000,02-01-2000)
51+
1 | [02-01-2000,02-15-2000)
52+
2 | [01-01-2000,02-01-2000)
53+
11 | [01-01-2000,02-01-2000)
54+
11 | [02-01-2000,03-01-2000)
55+
12 | [01-01-2000,02-01-2000)
56+
(6 rows)
57+
58+
select * from parttmp_1_to_10 order by id, valid_at;
59+
id | valid_at
60+
----+-------------------------
61+
1 | [01-15-2000,02-01-2000)
62+
1 | [02-01-2000,02-15-2000)
63+
2 | [01-01-2000,02-01-2000)
64+
(3 rows)
65+
66+
select * from parttmp_11_to_20 order by id, valid_at;
67+
id | valid_at
68+
----+-------------------------
69+
11 | [01-01-2000,02-01-2000)
70+
11 | [02-01-2000,03-01-2000)
71+
12 | [01-01-2000,02-01-2000)
72+
(3 rows)
73+
74+
-- make sure the excluson constraint excludes:
75+
insert into parttmp (id, valid_at) values
76+
(2, '[2000-01-15, 2000-02-01)');
77+
ERROR: conflicting key value violates exclusion constraint "parttmp_1_to_10_id_valid_at_excl"
78+
DETAIL: Key (id, valid_at)=(2, [01-15-2000,02-01-2000)) conflicts with existing key (id, valid_at)=(2, [01-01-2000,02-01-2000)).
79+
drop table parttmp;
80+
-- should fail with a good error message:
81+
create table parttmp (id int, valid_at daterange, exclude using gist (id with <>, valid_at with &&)) partition by range (id);
82+
ERROR: cannot match partition key to index on column "id" using non-equal operator "<>"

contrib/btree_gist/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ tests += {
8888
'not_equal',
8989
'enum',
9090
'bool',
91+
'partitions',
9192
],
9293
},
9394
}

contrib/btree_gist/sql/partitions.sql

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
-- Make sure we can create an exclusion constraint
2+
-- across a partitioned table.
3+
-- That code looks at strategy numbers that can differ in regular gist vs btree_gist,
4+
-- so we want to make sure it works here too.
5+
create table parttmp (
6+
id int,
7+
valid_at daterange,
8+
exclude using gist (id with =, valid_at with &&)
9+
) partition by range (id);
10+
11+
create table parttmp_1_to_10 partition of parttmp for values from (1) to (10);
12+
create table parttmp_11_to_20 partition of parttmp for values from (11) to (20);
13+
14+
insert into parttmp (id, valid_at) values
15+
(1, '[2000-01-01, 2000-02-01)'),
16+
(1, '[2000-02-01, 2000-03-01)'),
17+
(2, '[2000-01-01, 2000-02-01)'),
18+
(11, '[2000-01-01, 2000-02-01)'),
19+
(11, '[2000-02-01, 2000-03-01)'),
20+
(12, '[2000-01-01, 2000-02-01)');
21+
22+
select * from parttmp order by id, valid_at;
23+
select * from parttmp_1_to_10 order by id, valid_at;
24+
select * from parttmp_11_to_20 order by id, valid_at;
25+
26+
update parttmp set valid_at = valid_at * '[2000-01-15,2000-02-15)' where id = 1;
27+
28+
select * from parttmp order by id, valid_at;
29+
select * from parttmp_1_to_10 order by id, valid_at;
30+
select * from parttmp_11_to_20 order by id, valid_at;
31+
32+
-- make sure the excluson constraint excludes:
33+
insert into parttmp (id, valid_at) values
34+
(2, '[2000-01-15, 2000-02-01)');
35+
36+
drop table parttmp;
37+
38+
-- should fail with a good error message:
39+
create table parttmp (id int, valid_at daterange, exclude using gist (id with <>, valid_at with &&)) partition by range (id);

doc/src/sgml/ddl.sgml

+7-5
Original file line numberDiff line numberDiff line change
@@ -4216,11 +4216,13 @@ ALTER INDEX measurement_city_id_logdate_key
42164216

42174217
<listitem>
42184218
<para>
4219-
There is no way to create an exclusion constraint spanning the
4220-
whole partitioned table. It is only possible to put such a
4221-
constraint on each leaf partition individually. Again, this
4222-
limitation stems from not being able to enforce cross-partition
4223-
restrictions.
4219+
Similarly an exclusion constraint must include all the
4220+
partition key columns. Furthermore the constraint must compare those
4221+
columns for equality (not e.g. <literal>&amp;&amp;</literal>).
4222+
Again, this limitation stems from not being able to enforce
4223+
cross-partition restrictions. The constraint may include additional
4224+
columns that aren't part of the partition key, and it may compare
4225+
those with any operators you like.
42244226
</para>
42254227
</listitem>
42264228

src/backend/commands/indexcmds.c

+38-21
Original file line numberDiff line numberDiff line change
@@ -712,11 +712,6 @@ DefineIndex(Oid relationId,
712712
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
713713
errmsg("cannot create index on partitioned table \"%s\" concurrently",
714714
RelationGetRelationName(rel))));
715-
if (stmt->excludeOpNames)
716-
ereport(ERROR,
717-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
718-
errmsg("cannot create exclusion constraints on partitioned table \"%s\"",
719-
RelationGetRelationName(rel))));
720715
}
721716

722717
/*
@@ -923,15 +918,16 @@ DefineIndex(Oid relationId,
923918
index_check_primary_key(rel, indexInfo, is_alter_table, stmt);
924919

925920
/*
926-
* If this table is partitioned and we're creating a unique index or a
927-
* primary key, make sure that the partition key is a subset of the
928-
* index's columns. Otherwise it would be possible to violate uniqueness
929-
* by putting values that ought to be unique in different partitions.
921+
* If this table is partitioned and we're creating a unique index, primary
922+
* key, or exclusion constraint, make sure that the partition key is a
923+
* subset of the index's columns. Otherwise it would be possible to
924+
* violate uniqueness by putting values that ought to be unique in
925+
* different partitions.
930926
*
931927
* We could lift this limitation if we had global indexes, but those have
932928
* their own problems, so this is a useful feature combination.
933929
*/
934-
if (partitioned && (stmt->unique || stmt->primary))
930+
if (partitioned && (stmt->unique || stmt->excludeOpNames))
935931
{
936932
PartitionKey key = RelationGetPartitionKey(rel);
937933
const char *constraint_type;
@@ -941,7 +937,7 @@ DefineIndex(Oid relationId,
941937
constraint_type = "PRIMARY KEY";
942938
else if (stmt->unique)
943939
constraint_type = "UNIQUE";
944-
else if (stmt->excludeOpNames != NIL)
940+
else if (stmt->excludeOpNames)
945941
constraint_type = "EXCLUDE";
946942
else
947943
{
@@ -984,11 +980,11 @@ DefineIndex(Oid relationId,
984980
* We'll need to be able to identify the equality operators
985981
* associated with index columns, too. We know what to do with
986982
* btree opclasses; if there are ever any other index types that
987-
* support unique indexes, this logic will need extension.
983+
* support unique indexes, this logic will need extension. But if
984+
* we have an exclusion constraint, it already knows the
985+
* operators, so we don't have to infer them.
988986
*/
989-
if (accessMethodId == BTREE_AM_OID)
990-
eq_strategy = BTEqualStrategyNumber;
991-
else
987+
if (stmt->unique && accessMethodId != BTREE_AM_OID)
992988
ereport(ERROR,
993989
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
994990
errmsg("cannot match partition key to an index using access method \"%s\"",
@@ -1019,17 +1015,38 @@ DefineIndex(Oid relationId,
10191015
&idx_opfamily,
10201016
&idx_opcintype))
10211017
{
1022-
Oid idx_eqop;
1018+
Oid idx_eqop = InvalidOid;
1019+
1020+
if (stmt->unique)
1021+
idx_eqop = get_opfamily_member(idx_opfamily,
1022+
idx_opcintype,
1023+
idx_opcintype,
1024+
BTEqualStrategyNumber);
1025+
else if (stmt->excludeOpNames)
1026+
idx_eqop = indexInfo->ii_ExclusionOps[j];
1027+
Assert(idx_eqop);
10231028

1024-
idx_eqop = get_opfamily_member(idx_opfamily,
1025-
idx_opcintype,
1026-
idx_opcintype,
1027-
eq_strategy);
10281029
if (ptkey_eqop == idx_eqop)
10291030
{
10301031
found = true;
10311032
break;
10321033
}
1034+
else if (stmt->excludeOpNames)
1035+
{
1036+
/*
1037+
* We found a match, but it's not an equality
1038+
* operator. Instead of failing below with an
1039+
* error message about a missing column, fail now
1040+
* and explain that the operator is wrong.
1041+
*/
1042+
Form_pg_attribute att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1);
1043+
1044+
ereport(ERROR,
1045+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1046+
errmsg("cannot match partition key to index on column \"%s\" using non-equal operator \"%s\"",
1047+
NameStr(att->attname),
1048+
get_opname(indexInfo->ii_ExclusionOps[j]))));
1049+
}
10331050
}
10341051
}
10351052
}
@@ -1101,7 +1118,7 @@ DefineIndex(Oid relationId,
11011118
constraint_type = "PRIMARY KEY";
11021119
else if (stmt->unique)
11031120
constraint_type = "UNIQUE";
1104-
else if (stmt->excludeOpNames != NIL)
1121+
else if (stmt->excludeOpNames)
11051122
constraint_type = "EXCLUDE";
11061123
else
11071124
{

src/backend/parser/parse_utilcmd.c

-6
Original file line numberDiff line numberDiff line change
@@ -900,12 +900,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
900900
errmsg("exclusion constraints are not supported on foreign tables"),
901901
parser_errposition(cxt->pstate,
902902
constraint->location)));
903-
if (cxt->ispartitioned)
904-
ereport(ERROR,
905-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
906-
errmsg("exclusion constraints are not supported on partitioned tables"),
907-
parser_errposition(cxt->pstate,
908-
constraint->location)));
909903
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
910904
break;
911905

src/test/regress/expected/alter_table.out

+1-6
Original file line numberDiff line numberDiff line change
@@ -3834,16 +3834,11 @@ Referenced by:
38343834
TABLE "ataddindex" CONSTRAINT "ataddindex_ref_id_fkey" FOREIGN KEY (ref_id) REFERENCES ataddindex(id)
38353835

38363836
DROP TABLE ataddindex;
3837-
-- unsupported constraint types for partitioned tables
3837+
-- cannot drop column that is part of the partition key
38383838
CREATE TABLE partitioned (
38393839
a int,
38403840
b int
38413841
) PARTITION BY RANGE (a, (a+b+1));
3842-
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
3843-
ERROR: exclusion constraints are not supported on partitioned tables
3844-
LINE 1: ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
3845-
^
3846-
-- cannot drop column that is part of the partition key
38473842
ALTER TABLE partitioned DROP COLUMN a;
38483843
ERROR: cannot drop column "a" because it is part of the partition key of relation "partitioned"
38493844
ALTER TABLE partitioned ALTER COLUMN a TYPE char(5);

src/test/regress/expected/create_table.out

-8
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,6 @@ CREATE TABLE partitioned (
153153
a2 int
154154
) PARTITION BY LIST (a1, a2); -- fail
155155
ERROR: cannot use "list" partition strategy with more than one column
156-
-- unsupported constraint type for partitioned tables
157-
CREATE TABLE partitioned (
158-
a int,
159-
EXCLUDE USING gist (a WITH &&)
160-
) PARTITION BY RANGE (a);
161-
ERROR: exclusion constraints are not supported on partitioned tables
162-
LINE 3: EXCLUDE USING gist (a WITH &&)
163-
^
164156
-- prevent using prohibited expressions in the key
165157
CREATE FUNCTION retset (a int) RETURNS SETOF int AS $$ SELECT 1; $$ LANGUAGE SQL IMMUTABLE;
166158
CREATE TABLE partitioned (

0 commit comments

Comments
 (0)