split tablecmds.c
This file has over 22,000 lines and is too large to be included in GitHub's
code search results [0]https://docs.github.com/en/search-github/github-code-search/about-github-code-search#limitations. It appears to have been given its current form in
2002 by commit 71dc300. Previously, it was named command.c, which dates
back to the 80s. Is it time to split it into a few different files,
similar to what was done to copy.c in 2020 by commit c532d15?
After briefly skimming through it, some areas that seem like they could
potentially be moved out are partitions, constraints, permission checks,
inheritance, foreign keys, column expressions, table rewriting, attribute
merging, TRUNCATE, and CREATE TABLE. This is far from a concrete proposal,
but I first wanted to gauge interest in $SUBJECT.
[0]: https://docs.github.com/en/search-github/github-code-search/about-github-code-search#limitations
--
nathan
On 2025-Dec-01, Nathan Bossart wrote:
This file has over 22,000 lines and is too large to be included in GitHub's
code search results [0]. It appears to have been given its current form in
2002 by commit 71dc300. Previously, it was named command.c, which dates
back to the 80s. Is it time to split it into a few different files,
similar to what was done to copy.c in 2020 by commit c532d15?After briefly skimming through it, some areas that seem like they could
potentially be moved out are partitions, constraints, permission checks,
inheritance, foreign keys, column expressions, table rewriting, attribute
merging, TRUNCATE, and CREATE TABLE. This is far from a concrete proposal,
but I first wanted to gauge interest in $SUBJECT.
I think it makes sense. It's our largest source file at 690kB
the second being pg_dump.c (at 625kB) and also a candidate for
splitting. The third one, ruleutils.c, is slightly above half size,
381kB!
My first thought would be to move code that deals with catalog changes
to files in catalog/. Also a couple of functions related to tablespaces
could be perhaps be moved to commands/tablespace.c.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"El Maquinismo fue proscrito so pena de cosquilleo hasta la muerte"
(Ijon Tichy en Viajes, Stanislaw Lem)
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
On 2025-Dec-01, Nathan Bossart wrote:
This file has over 22,000 lines and is too large to be included in GitHub's
code search results [0]. It appears to have been given its current form in
2002 by commit 71dc300. Previously, it was named command.c, which dates
back to the 80s. Is it time to split it into a few different files,
similar to what was done to copy.c in 2020 by commit c532d15?
I think it makes sense. It's our largest source file at 690kB
the second being pg_dump.c (at 625kB) and also a candidate for
splitting. The third one, ruleutils.c, is slightly above half size,
381kB!
A lot of the bloat seems to be of recent vintage, too:
$ ls -l REL*/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 125011 Jan 14 2014 REL7_4/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 183924 Jan 14 2014 REL8_0/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 195463 Jan 14 2014 REL8_1/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 211853 Jan 14 2014 REL8_2/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 207113 Aug 31 2013 REL8_3/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 240714 May 6 2014 REL8_4/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 250682 Aug 9 2014 REL9_0/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 294495 Dec 22 2015 REL9_1/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 325781 Aug 9 2017 REL9_2/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 329854 Oct 1 2018 REL9_3/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 355629 Feb 3 2020 REL9_4/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 379490 Jul 14 2020 REL9_5/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 380527 Jul 14 2020 REL9_6/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 437331 Jan 6 2022 REL_10/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 512821 Oct 16 2023 REL_11/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 559329 Nov 8 2024 REL_12/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 579305 Nov 4 10:59 REL_13/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 605939 Nov 10 12:43 REL_14/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 628963 Nov 10 12:43 REL_15/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 628950 Nov 10 12:43 REL_16/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 645181 Nov 10 12:43 REL_17/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 708183 Nov 10 12:43 REL_18/src/backend/commands/tablecmds.c
-rw-rw-r--. 1 postgres postgres 706254 Nov 10 12:43 HEAD/src/backend/commands/tablecmds.c
I didn't do any math about it, but that's got to be a far faster rate
of expansion than the overall PG code base. Maybe partitioning is
largely to blame? Perhaps analyzing what functionality got added
here in the past dozen or so years would yield some ideas for how to
split it.
+1 for a split, if we can figure out a good plan.
regards, tom lane
On Mon, Dec 1, 2025, at 3:18 PM, Álvaro Herrera wrote:
My first thought would be to move code that deals with catalog changes
to files in catalog/. Also a couple of functions related to tablespaces
could be perhaps be moved to commands/tablespace.c.
As Tom said partitioning has a big chunk of lines of code. I bet that's the
biggest portion. It seems a good candidate to be moved to a new file
(partitioning and inheritance). Besides your suggestion, I would add things
that set properties (RLS, reloptions, AM, replica identity, generated columns)
to another file (tableutils.c?).
--
Euler Taveira
EDB https://www.enterprisedb.com/
On 2025-Dec-01, Tom Lane wrote:
I didn't do any math about it, but that's got to be a far faster rate
of expansion than the overall PG code base. Maybe partitioning is
largely to blame? Perhaps analyzing what functionality got added
here in the past dozen or so years would yield some ideas for how to
split it.
Since 2015, with a cutoff of at least 50 lines added or removed:
199 lines added by: bdc3d7fa237 (Return ObjectAddress in many ALTER TABLE sub-routines, 2015-03-25)
61 lines added by: e42375fc812 (Retain comments on indexes and constraints at ALTER TABLE ... TYPE ..., 2015-07-14)
1313 lines added by: f0e44751d71 (Implement table partitioning., 2016-12-07)
58 lines added by: 3957b58b888 (Fix ALTER TABLE / SET TYPE for irregular inheritance, 2017-01-09)
74 lines removed by 2f5c9d9c9ce (Tweak catalog indexing abstraction for upcoming WARM, 2017-01-31)
283 lines added by: 32173270536 (Identity columns, 2017-04-06)
55 lines removed by 3ec76ff1f2c (Don't explicitly mark range partitioning columns NOT NULL., 2017-05-18)
58 lines removed by b08df9cab77 (Teach predtest.c about CHECK clauses to fix partitioning bugs., 2017-06-14)
131 lines added by: 6f6b99d1335 (Allow a partitioned table to have a default partition., 2017-09-08)
66 lines added by: af20e2d728e (Fix ALTER TABLE code to update domain constraints when needed., 2017-11-01)
59 lines removed by ef6087ee5fa (Minor preparatory refactoring for UPDATE row movement., 2018-01-04)
531 lines added by: 8b08f7d4820 (Local partitioned indexes, 2018-01-19)
67 lines added by: eb7ed3f3063 (Allow UNIQUE indexes on partitioned tables, 2018-02-19)
136 lines added by: 86f575948c7 (Allow FOR EACH ROW triggers on partitioned tables, 2018-03-23)
121 lines added by: 3de241dba86 (Foreign keys on partitioned tables, 2018-04-04)
86 lines added by: 5dfd1e5a669 (Logical decoding of TRUNCATE, 2018-04-07)
54 lines added by: dfa60814198 (Fix tablespace handling for partitioned indexes, 2018-11-03)
237 lines removed by 578b229718e (Remove WITH OIDS support, change oid catalog column visibility., 2018-11-20)
77 lines added by: 3b174b1a355 (Fix missing values when doing ALTER TABLE ALTER COLUMN TYPE, 2019-01-10)
306 lines added by: 03afae201f0 (Move CloneForeignKeyConstraints to tablecmds.c, 2019-01-18)
67 lines added by: 0464fdf07f6 (Create action triggers when partitions are detached, 2019-01-21)
69 lines added by: bbb96c3704c (Allow ALTER TABLE .. SET NOT NULL to skip provably unnecessary scans., 2019-03-13)
84 lines removed by d25f519107b (tableam: relation creation, VACUUM FULL/CLUSTER, SET TABLESPACE., 2019-03-28)
131 lines added by: fc22b6623b6 (Generated columns, 2019-03-30)
588 lines added by: f56f8f8da6a (Support foreign keys that reference partitioned tables, 2019-04-03)
82 lines added by: f4a3fdfbdcd (Avoid order-of-execution problems with ALTER TABLE ADD PRIMARY KEY., 2019-04-23)
61 lines added by: 1fa846f1c9a (Fix cloning of row triggers to sub-partitions, 2020-01-02)
159 lines added by: f595117e24a (ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION, 2020-01-14)
280 lines added by: 1281a5c907b (Restructure ALTER TABLE execution to fix assorted bugs., 2020-01-15)
64 lines added by: afccd76f1cc (Fix detaching partitions with cloned row triggers, 2020-04-21)
51 lines added by: 086ffddf365 (Fix several DDL issues of generated columns versus inheritance, 2020-05-06)
55 lines removed by f1fcf2d3b2e (Fix timing issue with ALTER TABLE's validate constraint, 2020-07-14)
84 lines added by: 50289819230 (Fix handling of CREATE TABLE LIKE with inheritance., 2020-08-21)
229 lines added by: bbe0a81db69 (Allow configurable LZ4 TOAST compression., 2021-03-19)
352 lines added by: 71f4c8c6f74 (ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY, 2021-03-25)
92 lines added by: a4d75c86bf1 (Extended statistics on expressions, 2021-03-26)
125 lines added by: 8ff1c94649f (Allow TRUNCATE command to truncate foreign tables., 2021-04-08)
60 lines added by: a970edbed30 (Fix ALTER TABLE / INHERIT with generated columns, 2021-05-04)
120 lines added by: 6f70d7ca1d1 (Have ALTER CONSTRAINT recurse on partitioned tables, 2021-05-05)
105 lines added by: 2ed532ee8c4 (Improve error messages about mismatching relkind, 2021-07-08)
60 lines added by: b0483263dda (Add support for SET ACCESS METHOD in ALTER TABLE, 2021-07-28)
85 lines added by: d6f96ed94e7 (Allow specifying column list for foreign key ON DELETE SET actions, 2021-12-08)
283 lines added by: f4566345cf4 (Create foreign key triggers in partitioned tables too, 2022-01-05)
668 lines added by: e056c557aef (Catalog NOT NULL constraints, 2023-04-07)
668 lines removed by 9ce04b50e12 (Revert "Catalog NOT NULL constraints" and fallout, 2023-04-12)
753 lines added by: b0e96f31198 (Catalog not-null constraints, 2023-08-25)
102 lines added by: 04e485273b5 (Move BuildDescForRelation() from tupdesc.c to tablecmds.c, 2023-10-05)
137 lines added by: 5d06e99a3cf (ALTER TABLE command to change generation expression, 2024-01-04)
159 lines added by: 69958631570 (Support identity columns in partitioned tables, 2024-01-16)
56 lines added by: f7cf9494bad (Split some code out from MergeAttributes(), 2024-01-26)
146 lines added by: 34768ee3616 (Add temporal FOREIGN KEY contraints, 2024-03-24)
105 lines added by: 374c7a22904 (Allow specifying an access method for partitioned tables, 2024-03-25)
320 lines added by: 1adf16b8fba (Implement ALTER TABLE ... MERGE PARTITIONS ... command, 2024-04-07)
411 lines added by: 87c21bb9412 (Implement ALTER TABLE ... SPLIT PARTITION ... command, 2024-04-07)
51 lines added by: d9f686a72ee (Fix restore of not-null constraints with inheritance, 2024-04-18)
828 lines removed by 6f8bb7c1e96 (Revert structural changes to not-null constraints, 2024-05-13)
150 lines removed by 8aee330af55 (Revert temporal primary keys and foreign keys, 2024-05-16)
785 lines removed by 3890d90c150 (Revert support for ALTER TABLE ... MERGE/SPLIT PARTITION(S) commands, 2024-08-24)
146 lines added by: 89f908a6d0a (Add temporal FOREIGN KEY contraints, 2024-09-17)
51 lines added by: d69a3f4d70b (Introduce ATT_PARTITIONED_TABLE in tablecmds.c, 2024-09-19)
167 lines added by: 53af9491a04 (Restructure foreign key handling code for ATTACH/DETACH, 2024-10-22)
390 lines added by: 14e87ffa5c5 (Add pg_constraint rows for not-null constraints, 2024-11-08)
52 lines added by: 9321d2fdf80 (Fix collation handling for foreign keys, 2024-11-15)
59 lines added by: 86374c9a0e3 (Split ATExecValidateConstraint into reusable pieces, 2025-01-16)
58 lines added by: b663b9436e7 (Allow NOT VALID foreign key constraints on partitioned tables, 2025-01-23)
120 lines added by: 83ea6c54025 (Virtual generated columns, 2025-02-07)
85 lines added by: f4e53e10b6c (Add ALTER TABLE ... ALTER CONSTRAINT ... SET [NO] INHERIT, 2025-03-05)
89 lines added by: 1d26c2d2c4b (refactor: Split tryAttachPartitionForeignKey(), 2025-03-11)
57 lines added by: 639238b978f (refactor: Split ATExecAlterConstraintInternal(), 2025-03-25)
309 lines added by: eec0040c4bc (Add support for NOT ENFORCED in foreign key constraints, 2025-04-02)
183 lines added by: a379061a22a (Allow NOT NULL constraints to be added as NOT VALID, 2025-04-07)
50 lines removed by 3231fd04552 (Stop creating constraints during DETACH CONCURRENTLY, 2025-10-11)
git log --since '01/01/2015' --format=reference --reverse src/backend/commands/tablecmds.c | while read line; do sha1=$(echo $line | cut -d" " -f1); sz=$(git show $sha1:src/backend/commands/tablecmds.c | wc); prevsz=$currsz; currsz=$(echo $sz | awk '{print $1}'); if [ ! -z "$prevsz" ]; then diff=$(($currsz - $prevsz)); if [ $diff -ge 50 ]; then echo "$diff lines added by: $line" ; elif [ $diff -le -50 ]; then echo "$((-$diff)) lines removed by $line"; fi; fi; done
(BTW this output makes it clear to me that commit titles do not end with
a period.)
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"You don't solve a bad join with SELECT DISTINCT" #CupsOfFail
https://twitter.com/connor_mc_d/status/1431240081726115845
Hi,
On 2025-12-01 11:25:13 -0600, Nathan Bossart wrote:
This file has over 22,000 lines and is too large to be included in GitHub's
code search results [0]. It appears to have been given its current form in
2002 by commit 71dc300. Previously, it was named command.c, which dates
back to the 80s. Is it time to split it into a few different files,
similar to what was done to copy.c in 2020 by commit c532d15?After briefly skimming through it, some areas that seem like they could
potentially be moved out are partitions, constraints, permission checks,
inheritance, foreign keys, column expressions, table rewriting, attribute
merging, TRUNCATE, and CREATE TABLE. This is far from a concrete proposal,
but I first wanted to gauge interest in $SUBJECT.
Seems reasonable, however I think that splitting all or most of the pieces you
listed into their own files would end up being pointlessly granular....
Greetings,
Andres Freund
On Mon, Dec 01, 2025 at 01:59:01PM -0500, Tom Lane wrote:
I didn't do any math about it, but that's got to be a far faster rate
of expansion than the overall PG code base. Maybe partitioning is
largely to blame? Perhaps analyzing what functionality got added
here in the past dozen or so years would yield some ideas for how to
split it.+1 for a split, if we can figure out a good plan.
I tried to move the partitioning-related code to a new file, and it wasn't
too bad. Note that there are a couple of internal-to-tablecmds.c things
that need to be exported. Besides that, the attached patch is still pretty
rough, and I'm not sure I correctly placed the line in the sand when
determining what stays and what goes, but this at least shows the general
shape of what's needed. (BTW git was generating an atrocious diff for
tablecmds.c. You might need to set the diff algorithm to "minimal" if you
are similarly affected.)
src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 3456 +--------------------------------------------------------------------------------------------
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/partcmds.h | 53 ++
src/include/commands/tablecmds.h | 134 +++-
7 files changed, 3575 insertions(+), 3448 deletions(-)
--
nathan
Attachments:
v1-0001-move-partition-code-in-tablecmds.c-to-new-file.patchtext/plain; charset=us-asciiDownload
From 420a90c72183e2e468698faca0e72d30339f07e0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 1 Dec 2025 16:26:39 -0600
Subject: [PATCH v1 1/1] move partition code in tablecmds.c to new file
---
src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 3456 +------------------------
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/partcmds.h | 53 +
src/include/commands/tablecmds.h | 134 +-
7 files changed, 3575 insertions(+), 3448 deletions(-)
create mode 100644 src/backend/commands/partcmds.c
create mode 100644 src/include/commands/partcmds.h
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 64cb6278409..6ffb3fed2be 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -45,6 +45,7 @@ OBJS = \
matview.o \
opclasscmds.o \
operatorcmds.o \
+ partcmds.o \
policy.o \
portalcmds.o \
prepare.o \
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 5fc35826b1c..155e9e294dc 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -33,6 +33,7 @@ backend_sources += files(
'matview.c',
'opclasscmds.c',
'operatorcmds.c',
+ 'partcmds.c',
'policy.c',
'portalcmds.c',
'prepare.c',
diff --git a/src/backend/commands/partcmds.c b/src/backend/commands/partcmds.c
new file mode 100644
index 00000000000..0f72f698df5
--- /dev/null
+++ b/src/backend/commands/partcmds.c
@@ -0,0 +1,3377 @@
+/*-------------------------------------------------------------------------
+ *
+ * partcmds.c
+ * TODO
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/commands/partcmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "utils/lsyscache.h"
+#include "access/relation.h"
+#include "access/skey.h"
+#include "access/stratnum.h"
+#include "access/table.h"
+#include "catalog/heap.h"
+#include "catalog/index.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/partition.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/partcmds.h"
+#include "commands/tablecmds.h"
+#include "commands/trigger.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "parser/parse_utilcmd.h"
+#include "partitioning/partbounds.h"
+#include "partitioning/partdesc.h"
+#include "storage/lmgr.h"
+#include "tcop/utility.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/memutils.h"
+#include "utils/partcache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/*
+ * RemoveInheritedConstraint
+ *
+ * Removes the constraint and its associated trigger from the specified
+ * relation, which inherited the given constraint.
+ */
+static void
+RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
+ Oid conrelid)
+{
+ ObjectAddresses *objs;
+ HeapTuple consttup;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conrelid));
+
+ scan = systable_beginscan(conrel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &key);
+ objs = new_object_addresses();
+ while ((consttup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
+
+ if (conform->conparentid != conoid)
+ continue;
+ else
+ {
+ ObjectAddress addr;
+ SysScanDesc scan2;
+ ScanKeyData key2;
+ int n PG_USED_FOR_ASSERTS_ONLY;
+
+ ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
+ add_exact_object_address(&addr, objs);
+
+ /*
+ * First we must delete the dependency record that binds the
+ * constraint records together.
+ */
+ n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
+ conform->oid,
+ DEPENDENCY_INTERNAL,
+ ConstraintRelationId,
+ conoid);
+ Assert(n == 1); /* actually only one is expected */
+
+ /*
+ * Now search for the triggers for this constraint and set them up
+ * for deletion too
+ */
+ ScanKeyInit(&key2,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conform->oid));
+ scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
+ true, NULL, 1, &key2);
+ while ((trigtup = systable_getnext(scan2)) != NULL)
+ {
+ ObjectAddressSet(addr, TriggerRelationId,
+ ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
+ add_exact_object_address(&addr, objs);
+ }
+ systable_endscan(scan2);
+ }
+ }
+ /* make the dependency deletions visible */
+ CommandCounterIncrement();
+ performMultipleDeletions(objs, DROP_RESTRICT,
+ PERFORM_DELETION_INTERNAL);
+ systable_endscan(scan);
+}
+
+/*
+ * DropForeignKeyConstraintTriggers
+ *
+ * The subroutine for tryAttachPartitionForeignKey handles the deletion of
+ * action triggers for the foreign key constraint.
+ *
+ * If valid confrelid and conrelid values are not provided, the respective
+ * trigger check will be skipped, and the trigger will be considered for
+ * removal.
+ */
+static void
+DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
+ Oid conrelid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trigger;
+
+ /* Invalid if trigger is not for a referential integrity constraint */
+ if (!OidIsValid(trgform->tgconstrrelid))
+ continue;
+ if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid)
+ continue;
+ if (OidIsValid(confrelid) && trgform->tgrelid != confrelid)
+ continue;
+
+ /* We should be dropping trigger related to foreign key constraint */
+ Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS ||
+ trgform->tgfoid == F_RI_FKEY_CHECK_UPD ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+ trgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL ||
+ trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+ trgform->tgfoid == F_RI_FKEY_NOACTION_UPD);
+
+ /*
+ * The constraint is originally set up to contain this trigger as an
+ * implementation object, so there's a dependency record that links
+ * the two; however, since the trigger is no longer needed, we remove
+ * the dependency link in order to be able to drop the trigger while
+ * keeping the constraint intact.
+ */
+ deleteDependencyRecordsFor(TriggerRelationId,
+ trgform->oid,
+ false);
+ /* make dependency deletion visible to performDeletion */
+ CommandCounterIncrement();
+ ObjectAddressSet(trigger, TriggerRelationId,
+ trgform->oid);
+ performDeletion(&trigger, DROP_RESTRICT, 0);
+ /* make trigger drop visible, in case the loop iterates */
+ CommandCounterIncrement();
+ }
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ /* Only ever look at "check" triggers on the FK side. */
+ if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_FK)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ {
+ Assert(*insertTriggerOid == InvalidOid);
+ *insertTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+#ifndef USE_ASSERT_CHECKING
+ /* In an assert-enabled build, continue looking to find duplicates. */
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+#endif
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * AttachPartitionForeignKey
+ *
+ * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
+ * attaching the constraint, removing redundant triggers and entries from
+ * pg_constraint, and setting the constraint's parent.
+ */
+static void
+AttachPartitionForeignKey(List **wqueue,
+ Relation partition,
+ Oid partConstrOid,
+ Oid parentConstrOid,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+ bool queueValidation;
+ Oid partConstrFrelid;
+ Oid partConstrRelid;
+ bool parentConstrIsEnforced;
+
+ /* Fetch the parent constraint tuple */
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+ parentConstrIsEnforced = parentConstr->conenforced;
+
+ /* Fetch the child constraint tuple */
+ partcontup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+ partConstrFrelid = partConstr->confrelid;
+ partConstrRelid = partConstr->conrelid;
+
+ /*
+ * If the referenced table is partitioned, then the partition we're
+ * attaching now has extra pg_constraint rows and action triggers that are
+ * no longer needed. Remove those.
+ */
+ if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
+ {
+ Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+
+ RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
+ partConstrRelid);
+
+ table_close(pg_constraint, RowShareLock);
+ }
+
+ /*
+ * Will we need to validate this constraint? A valid parent constraint
+ * implies that all child constraints have been validated, so if this one
+ * isn't, we must trigger phase 3 validation.
+ */
+ queueValidation = parentConstr->convalidated && !partConstr->convalidated;
+
+ ReleaseSysCache(partcontup);
+ ReleaseSysCache(parentConstrTup);
+
+ /*
+ * The action triggers in the new partition become redundant -- the parent
+ * table already has equivalent ones, and those will be able to reach the
+ * partition. Remove the ones in the partition. We identify them because
+ * they have our constraint OID, as well as being on the referenced rel.
+ */
+ DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
+ partConstrRelid);
+
+ ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
+ RelationGetRelid(partition));
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers if the constraint is ENFORCED. NOT
+ * ENFORCED constraints do not have these triggers.
+ */
+ if (parentConstrIsEnforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ partConstrOid, partConstrFrelid, partConstrRelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ RelationGetRelid(partition));
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ RelationGetRelid(partition));
+ }
+
+ /*
+ * We updated this pg_constraint row above to set its parent; validating
+ * it will cause its convalidated flag to change, so we need CCI here. In
+ * addition, we need it unconditionally for the rare case where the parent
+ * table has *two* identical constraints; when reaching this function for
+ * the second one, we must have made our changes visible, otherwise we
+ * would try to attach both to this one.
+ */
+ CommandCounterIncrement();
+
+ /* If validation is needed, put it in the queue now. */
+ if (queueValidation)
+ {
+ Relation conrel;
+ Oid confrelid;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
+
+ confrelid = ((Form_pg_constraint) GETSTRUCT(partcontup))->confrelid;
+
+ /* Use the same lock as for AT_ValidateConstraint */
+ QueueFKConstraintValidation(wqueue, conrel, partition, confrelid,
+ partcontup, ShareUpdateExclusiveLock);
+ ReleaseSysCache(partcontup);
+ table_close(conrel, RowExclusiveLock);
+ }
+}
+
+/*
+ * When the parent of a partition receives [the referencing side of] a foreign
+ * key, we must propagate that foreign key to the partition. However, the
+ * partition might already have an equivalent foreign key; this routine
+ * compares the given ForeignKeyCacheInfo (in the partition) to the FK defined
+ * by the other parameters. If they are equivalent, create the link between
+ * the two constraints and return true.
+ *
+ * If the given FK does not match the one defined by rest of the params,
+ * return false.
+ */
+bool
+tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
+ Oid parentConstrOid,
+ int numfks,
+ AttrNumber *mapped_conkey,
+ AttrNumber *confkey,
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ HeapTuple parentConstrTup;
+ Form_pg_constraint parentConstr;
+ HeapTuple partcontup;
+ Form_pg_constraint partConstr;
+
+ parentConstrTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(parentConstrTup))
+ elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
+ parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
+
+ /*
+ * Do some quick & easy initial checks. If any of these fail, we cannot
+ * use this constraint.
+ */
+ if (fk->confrelid != parentConstr->confrelid || fk->nkeys != numfks)
+ {
+ ReleaseSysCache(parentConstrTup);
+ return false;
+ }
+ for (int i = 0; i < numfks; i++)
+ {
+ if (fk->conkey[i] != mapped_conkey[i] ||
+ fk->confkey[i] != confkey[i] ||
+ fk->conpfeqop[i] != conpfeqop[i])
+ {
+ ReleaseSysCache(parentConstrTup);
+ return false;
+ }
+ }
+
+ /* Looks good so far; perform more extensive checks. */
+ partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ if (!HeapTupleIsValid(partcontup))
+ elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
+
+ /*
+ * An error should be raised if the constraint enforceability is
+ * different. Returning false without raising an error, as we do for other
+ * attributes, could lead to a duplicate constraint with the same
+ * enforceability as the parent. While this may be acceptable, it may not
+ * be ideal. Therefore, it's better to raise an error and allow the user
+ * to correct the enforceability before proceeding.
+ */
+ if (partConstr->conenforced != parentConstr->conenforced)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
+ NameStr(parentConstr->conname),
+ NameStr(partConstr->conname),
+ RelationGetRelationName(partition))));
+
+ if (OidIsValid(partConstr->conparentid) ||
+ partConstr->condeferrable != parentConstr->condeferrable ||
+ partConstr->condeferred != parentConstr->condeferred ||
+ partConstr->confupdtype != parentConstr->confupdtype ||
+ partConstr->confdeltype != parentConstr->confdeltype ||
+ partConstr->confmatchtype != parentConstr->confmatchtype)
+ {
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+ return false;
+ }
+
+ ReleaseSysCache(parentConstrTup);
+ ReleaseSysCache(partcontup);
+
+ /* Looks good! Attach this constraint. */
+ AttachPartitionForeignKey(wqueue, partition, fk->conoid,
+ parentConstrOid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
+
+ return true;
+}
+
+/*
+ * CloneFkReferencing
+ * Subroutine for CloneForeignKeyConstraints
+ *
+ * For each FK constraint of the parent relation in the given list, find an
+ * equivalent constraint in its partition relation that can be reparented;
+ * if one cannot be found, create a new constraint in the partition as its
+ * child.
+ *
+ * If wqueue is given, it is used to set up phase-3 verification for each
+ * cloned constraint; omit it if such verification is not needed
+ * (example: the partition is being created anew).
+ */
+static void
+CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
+{
+ AttrMap *attmap;
+ List *partFKs;
+ List *clone = NIL;
+ ListCell *cell;
+ Relation trigrel;
+
+ /* obtain a list of constraints that we need to clone */
+ foreach(cell, RelationGetFKeyList(parentRel))
+ {
+ ForeignKeyCacheInfo *fk = lfirst(cell);
+
+ /*
+ * Refuse to attach a table as partition that this partitioned table
+ * already has a foreign key to. This isn't useful schema, which is
+ * proven by the fact that there have been no user complaints that
+ * it's already impossible to achieve this in the opposite direction,
+ * i.e., creating a foreign key that references a partition. This
+ * restriction allows us to dodge some complexities around
+ * pg_constraint and pg_trigger row creations that would be needed
+ * during ATTACH/DETACH for this kind of relationship.
+ */
+ if (fk->confrelid == RelationGetRelid(partRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot attach table \"%s\" as a partition because it is referenced by foreign key \"%s\"",
+ RelationGetRelationName(partRel),
+ get_constraint_name(fk->conoid))));
+
+ clone = lappend_oid(clone, fk->conoid);
+ }
+
+ /*
+ * Silently do nothing if there's nothing to do. In particular, this
+ * avoids throwing a spurious error for foreign tables.
+ */
+ if (clone == NIL)
+ return;
+
+ if (partRel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("foreign key constraints are not supported on foreign tables")));
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The constraint key may differ, if the columns in the partition are
+ * different. This map is used to convert them.
+ */
+ attmap = build_attrmap_by_name(RelationGetDescr(partRel),
+ RelationGetDescr(parentRel),
+ false);
+
+ partFKs = copyObject(RelationGetFKeyList(partRel));
+
+ foreach(cell, clone)
+ {
+ Oid parentConstrOid = lfirst_oid(cell);
+ Form_pg_constraint constrForm;
+ Relation pkrel;
+ HeapTuple tuple;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber mapped_conkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Constraint *fkconstraint;
+ bool attached;
+ Oid indexOid;
+ ObjectAddress address;
+ ListCell *lc;
+ Oid insertTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
+ bool with_period;
+
+ tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for constraint %u",
+ parentConstrOid);
+ constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ /* Don't clone constraints whose parents are being cloned */
+ if (list_member_oid(clone, constrForm->conparentid))
+ {
+ ReleaseSysCache(tuple);
+ continue;
+ }
+
+ /*
+ * Need to prevent concurrent deletions. If pkrel is a partitioned
+ * relation, that means to lock all partitions.
+ */
+ pkrel = table_open(constrForm->confrelid, ShareRowExclusiveLock);
+ if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ (void) find_all_inheritors(RelationGetRelid(pkrel),
+ ShareRowExclusiveLock, NULL);
+
+ DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey,
+ conpfeqop, conppeqop, conffeqop,
+ &numfkdelsetcols, confdelsetcols);
+ for (int i = 0; i < numfks; i++)
+ mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+
+ /*
+ * Get the "check" triggers belonging to the constraint, if it is
+ * ENFORCED, to pass as parent OIDs for similar triggers that will be
+ * created on the partition in addFkRecurseReferencing(). They are
+ * also passed to tryAttachPartitionForeignKey() below to simply
+ * assign as parents to the partition's existing "check" triggers,
+ * that is, if the corresponding constraints is deemed attachable to
+ * the parent constraint.
+ */
+ if (constrForm->conenforced)
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ /*
+ * Before creating a new constraint, see whether any existing FKs are
+ * fit for the purpose. If one is, attach the parent constraint to
+ * it, and don't clone anything. This way we avoid the expensive
+ * verification step and don't end up with a duplicate FK, and we
+ * don't need to recurse to partitions for this constraint.
+ */
+ attached = false;
+ foreach(lc, partFKs)
+ {
+ ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc);
+
+ if (tryAttachPartitionForeignKey(wqueue,
+ fk,
+ partRel,
+ parentConstrOid,
+ numfks,
+ mapped_conkey,
+ confkey,
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
+ {
+ attached = true;
+ table_close(pkrel, NoLock);
+ break;
+ }
+ }
+ if (attached)
+ {
+ ReleaseSysCache(tuple);
+ continue;
+ }
+
+ /* No dice. Set up to create our own constraint */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ /* ->conname determined below */
+ fkconstraint->deferrable = constrForm->condeferrable;
+ fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ /* ->fk_attrs determined below */
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = constrForm->confmatchtype;
+ fkconstraint->fk_upd_action = constrForm->confupdtype;
+ fkconstraint->fk_del_action = constrForm->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = constrForm->convalidated;
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ mapped_conkey[i] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ indexOid = constrForm->conindid;
+ with_period = constrForm->conperiod;
+
+ /* Create the pg_constraint entry at this level */
+ address = addFkConstraint(addFkReferencingSide,
+ NameStr(constrForm->conname), fkconstraint,
+ partRel, pkrel, indexOid, parentConstrOid,
+ numfks, confkey,
+ mapped_conkey, conpfeqop,
+ conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols,
+ false, with_period);
+
+ /* Done with the cloned constraint's tuple */
+ ReleaseSysCache(tuple);
+
+ /* Create the check triggers, and recurse to partitions, if any */
+ addFkRecurseReferencing(wqueue,
+ fkconstraint,
+ partRel,
+ pkrel,
+ indexOid,
+ address.objectId,
+ numfks,
+ confkey,
+ mapped_conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ false, /* no old check exists */
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid,
+ with_period);
+ table_close(pkrel, NoLock);
+ }
+
+ table_close(trigrel, RowExclusiveLock);
+}
+
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ /* Only ever look at "action" triggers on the PK side. */
+ if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_PK)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ {
+ Assert(*deleteTriggerOid == InvalidOid);
+ *deleteTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+#ifndef USE_ASSERT_CHECKING
+ /* In an assert-enabled build, continue looking to find duplicates */
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+#endif
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * CloneFkReferenced
+ * Subroutine for CloneForeignKeyConstraints
+ *
+ * Find all the FKs that have the parent relation on the referenced side;
+ * clone those constraints to the given partition. This is to be called
+ * when the partition is being created or attached.
+ *
+ * This recurses to partitions, if the relation being attached is partitioned.
+ * Recursion is done by calling addFkRecurseReferenced.
+ */
+static void
+CloneFkReferenced(Relation parentRel, Relation partitionRel)
+{
+ Relation pg_constraint;
+ AttrMap *attmap;
+ ListCell *cell;
+ SysScanDesc scan;
+ ScanKeyData key[2];
+ HeapTuple tuple;
+ List *clone = NIL;
+ Relation trigrel;
+
+ /*
+ * Search for any constraints where this partition's parent is in the
+ * referenced side. However, we must not clone any constraint whose
+ * parent constraint is also going to be cloned, to avoid duplicates. So
+ * do it in two steps: first construct the list of constraints to clone,
+ * then go over that list cloning those whose parents are not in the list.
+ * (We must not rely on the parent being seen first, since the catalog
+ * scan could return children first.)
+ */
+ pg_constraint = table_open(ConstraintRelationId, RowShareLock);
+ ScanKeyInit(&key[0],
+ Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel)));
+ ScanKeyInit(&key[1],
+ Anum_pg_constraint_contype, BTEqualStrategyNumber,
+ F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
+ /* This is a seqscan, as we don't have a usable index ... */
+ scan = systable_beginscan(pg_constraint, InvalidOid, true,
+ NULL, 2, key);
+ while ((tuple = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ clone = lappend_oid(clone, constrForm->oid);
+ }
+ systable_endscan(scan);
+ table_close(pg_constraint, RowShareLock);
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
+ RelationGetDescr(parentRel),
+ false);
+ foreach(cell, clone)
+ {
+ Oid constrOid = lfirst_oid(cell);
+ Form_pg_constraint constrForm;
+ Relation fkRel;
+ Oid indexOid;
+ Oid partIndexId;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber mapped_confkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Constraint *fkconstraint;
+ ObjectAddress address;
+ Oid deleteTriggerOid = InvalidOid,
+ updateTriggerOid = InvalidOid;
+
+ tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for constraint %u", constrOid);
+ constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ /*
+ * As explained above: don't try to clone a constraint for which we're
+ * going to clone the parent.
+ */
+ if (list_member_oid(clone, constrForm->conparentid))
+ {
+ ReleaseSysCache(tuple);
+ continue;
+ }
+
+ /* We need the same lock level that CreateTrigger will acquire */
+ fkRel = table_open(constrForm->conrelid, ShareRowExclusiveLock);
+
+ indexOid = constrForm->conindid;
+ DeconstructFkConstraintRow(tuple,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ for (int i = 0; i < numfks; i++)
+ mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
+
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = NameStr(constrForm->conname);
+ fkconstraint->deferrable = constrForm->condeferrable;
+ fkconstraint->initdeferred = constrForm->condeferred;
+ fkconstraint->location = -1;
+ fkconstraint->pktable = NULL;
+ /* ->fk_attrs determined below */
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = constrForm->confmatchtype;
+ fkconstraint->fk_upd_action = constrForm->confupdtype;
+ fkconstraint->fk_del_action = constrForm->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->is_enforced = constrForm->conenforced;
+ fkconstraint->skip_validation = false;
+ fkconstraint->initially_valid = constrForm->convalidated;
+
+ /* set up colnames that are used to generate the constraint name */
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(fkRel),
+ conkey[i] - 1);
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ /*
+ * Add the new foreign key constraint pointing to the new partition.
+ * Because this new partition appears in the referenced side of the
+ * constraint, we don't need to set up for Phase 3 check.
+ */
+ partIndexId = index_get_partition(partitionRel, indexOid);
+ if (!OidIsValid(partIndexId))
+ elog(ERROR, "index for %u not found in partition %s",
+ indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ if (constrForm->conenforced)
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
+ /* Add this constraint ... */
+ address = addFkConstraint(addFkReferencedSide,
+ fkconstraint->conname, fkconstraint, fkRel,
+ partitionRel, partIndexId, constrOid,
+ numfks, mapped_confkey,
+ conkey, conpfeqop, conppeqop, conffeqop,
+ numfkdelsetcols, confdelsetcols, false,
+ constrForm->conperiod);
+ /* ... and recurse */
+ addFkRecurseReferenced(fkconstraint,
+ fkRel,
+ partitionRel,
+ partIndexId,
+ address.objectId,
+ numfks,
+ mapped_confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ deleteTriggerOid,
+ updateTriggerOid,
+ constrForm->conperiod);
+
+ table_close(fkRel, NoLock);
+ ReleaseSysCache(tuple);
+ }
+
+ table_close(trigrel, RowExclusiveLock);
+}
+
+/*
+ * CloneForeignKeyConstraints
+ * Clone foreign keys from a partitioned table to a newly acquired
+ * partition.
+ *
+ * partitionRel is a partition of parentRel, so we can be certain that it has
+ * the same columns with the same datatypes. The columns may be in different
+ * order, though.
+ *
+ * wqueue must be passed to set up phase 3 constraint checking, unless the
+ * referencing-side partition is known to be empty (such as in CREATE TABLE /
+ * PARTITION OF).
+ */
+void
+CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
+ Relation partitionRel)
+{
+ /* This only works for declarative partitioning */
+ Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+
+ /*
+ * First, clone constraints where the parent is on the referencing side.
+ */
+ CloneFkReferencing(wqueue, parentRel, partitionRel);
+
+ /*
+ * Clone constraints for which the parent is on the referenced side.
+ */
+ CloneFkReferenced(parentRel, partitionRel);
+}
+
+/*
+ * MarkInheritDetached
+ *
+ * Set inhdetachpending for a partition, for ATExecDetachPartition
+ * in concurrent mode. While at it, verify that no other partition is
+ * already pending detach.
+ */
+static void
+MarkInheritDetached(Relation child_rel, Relation parent_rel)
+{
+ Relation catalogRelation;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple inheritsTuple;
+ bool found = false;
+
+ Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+
+ /*
+ * Find pg_inherits entries by inhparent. (We need to scan them all in
+ * order to verify that no other partition is pending detach.)
+ */
+ catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhparent,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(parent_rel)));
+ scan = systable_beginscan(catalogRelation, InheritsParentIndexId,
+ true, NULL, 1, &key);
+
+ while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
+ {
+ Form_pg_inherits inhForm;
+
+ inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple);
+ if (inhForm->inhdetachpending)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"",
+ get_rel_name(inhForm->inhrelid),
+ get_namespace_name(parent_rel->rd_rel->relnamespace),
+ RelationGetRelationName(parent_rel)),
+ errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation."));
+
+ if (inhForm->inhrelid == RelationGetRelid(child_rel))
+ {
+ HeapTuple newtup;
+
+ newtup = heap_copytuple(inheritsTuple);
+ ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true;
+
+ CatalogTupleUpdate(catalogRelation,
+ &inheritsTuple->t_self,
+ newtup);
+ found = true;
+ heap_freetuple(newtup);
+ /* keep looking, to ensure we catch others pending detach */
+ }
+ }
+
+ /* Done */
+ systable_endscan(scan);
+ table_close(catalogRelation, RowExclusiveLock);
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("relation \"%s\" is not a partition of relation \"%s\"",
+ RelationGetRelationName(child_rel),
+ RelationGetRelationName(parent_rel))));
+}
+
+/*
+ * Transform any expressions present in the partition key
+ *
+ * Returns a transformed PartitionSpec.
+ */
+PartitionSpec *
+transformPartitionSpec(Relation rel, PartitionSpec *partspec)
+{
+ PartitionSpec *newspec;
+ ParseState *pstate;
+ ParseNamespaceItem *nsitem;
+ ListCell *l;
+
+ newspec = makeNode(PartitionSpec);
+
+ newspec->strategy = partspec->strategy;
+ newspec->partParams = NIL;
+ newspec->location = partspec->location;
+
+ /* Check valid number of columns for strategy */
+ if (partspec->strategy == PARTITION_STRATEGY_LIST &&
+ list_length(partspec->partParams) != 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use \"list\" partition strategy with more than one column")));
+
+ /*
+ * Create a dummy ParseState and insert the target relation as its sole
+ * rangetable entry. We need a ParseState for transformExpr.
+ */
+ pstate = make_parsestate(NULL);
+ nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock,
+ NULL, false, true);
+ addNSItemToQuery(pstate, nsitem, true, true, true);
+
+ /* take care of any partition expressions */
+ foreach(l, partspec->partParams)
+ {
+ PartitionElem *pelem = lfirst_node(PartitionElem, l);
+
+ if (pelem->expr)
+ {
+ /* Copy, to avoid scribbling on the input */
+ pelem = copyObject(pelem);
+
+ /* Now do parse transformation of the expression */
+ pelem->expr = transformExpr(pstate, pelem->expr,
+ EXPR_KIND_PARTITION_EXPRESSION);
+
+ /* we have to fix its collations too */
+ assign_expr_collations(pstate, pelem->expr);
+ }
+
+ newspec->partParams = lappend(newspec->partParams, pelem);
+ }
+
+ return newspec;
+}
+
+/*
+ * Compute per-partition-column information from a list of PartitionElems.
+ * Expressions in the PartitionElems must be parse-analyzed already.
+ */
+void
+ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
+ List **partexprs, Oid *partopclass, Oid *partcollation,
+ PartitionStrategy strategy)
+{
+ int attn;
+ ListCell *lc;
+ Oid am_oid;
+
+ attn = 0;
+ foreach(lc, partParams)
+ {
+ PartitionElem *pelem = lfirst_node(PartitionElem, lc);
+ Oid atttype;
+ Oid attcollation;
+
+ if (pelem->name != NULL)
+ {
+ /* Simple attribute reference */
+ HeapTuple atttuple;
+ Form_pg_attribute attform;
+
+ atttuple = SearchSysCacheAttName(RelationGetRelid(rel),
+ pelem->name);
+ if (!HeapTupleIsValid(atttuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" named in partition key does not exist",
+ pelem->name),
+ parser_errposition(pstate, pelem->location)));
+ attform = (Form_pg_attribute) GETSTRUCT(atttuple);
+
+ if (attform->attnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use system column \"%s\" in partition key",
+ pelem->name),
+ parser_errposition(pstate, pelem->location)));
+
+ /*
+ * Stored generated columns cannot work: They are computed after
+ * BEFORE triggers, but partition routing is done before all
+ * triggers. Maybe virtual generated columns could be made to
+ * work, but then they would need to be handled as an expression
+ * below.
+ */
+ if (attform->attgenerated)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use generated column in partition key"),
+ errdetail("Column \"%s\" is a generated column.",
+ pelem->name),
+ parser_errposition(pstate, pelem->location)));
+
+ partattrs[attn] = attform->attnum;
+ atttype = attform->atttypid;
+ attcollation = attform->attcollation;
+ ReleaseSysCache(atttuple);
+ }
+ else
+ {
+ /* Expression */
+ Node *expr = pelem->expr;
+ char partattname[16];
+ Bitmapset *expr_attrs = NULL;
+ int i;
+
+ Assert(expr != NULL);
+ atttype = exprType(expr);
+ attcollation = exprCollation(expr);
+
+ /*
+ * The expression must be of a storable type (e.g., not RECORD).
+ * The test is the same as for whether a table column is of a safe
+ * type (which is why we needn't check for the non-expression
+ * case).
+ */
+ snprintf(partattname, sizeof(partattname), "%d", attn + 1);
+ CheckAttributeType(partattname,
+ atttype, attcollation,
+ NIL, CHKATYPE_IS_PARTKEY);
+
+ /*
+ * Strip any top-level COLLATE clause. This ensures that we treat
+ * "x COLLATE y" and "(x COLLATE y)" alike.
+ */
+ while (IsA(expr, CollateExpr))
+ expr = (Node *) ((CollateExpr *) expr)->arg;
+
+ /*
+ * Examine all the columns in the partition key expression. When
+ * the whole-row reference is present, examine all the columns of
+ * the partitioned table.
+ */
+ pull_varattnos(expr, 1, &expr_attrs);
+ if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs))
+ {
+ expr_attrs = bms_add_range(expr_attrs,
+ 1 - FirstLowInvalidHeapAttributeNumber,
+ RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber);
+ expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber);
+ }
+
+ i = -1;
+ while ((i = bms_next_member(expr_attrs, i)) >= 0)
+ {
+ AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber;
+
+ Assert(attno != 0);
+
+ /*
+ * Cannot allow system column references, since that would
+ * make partition routing impossible: their values won't be
+ * known yet when we need to do that.
+ */
+ if (attno < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("partition key expressions cannot contain system column references")));
+
+ /*
+ * Stored generated columns cannot work: They are computed
+ * after BEFORE triggers, but partition routing is done before
+ * all triggers. Virtual generated columns could probably
+ * work, but it would require more work elsewhere (for example
+ * SET EXPRESSION would need to check whether the column is
+ * used in partition keys). Seems safer to prohibit for now.
+ */
+ if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use generated column in partition key"),
+ errdetail("Column \"%s\" is a generated column.",
+ get_attname(RelationGetRelid(rel), attno, false)),
+ parser_errposition(pstate, pelem->location)));
+ }
+
+ if (IsA(expr, Var) &&
+ ((Var *) expr)->varattno > 0)
+ {
+
+ /*
+ * User wrote "(column)" or "(column COLLATE something)".
+ * Treat it like simple attribute anyway.
+ */
+ partattrs[attn] = ((Var *) expr)->varattno;
+ }
+ else
+ {
+ partattrs[attn] = 0; /* marks the column as expression */
+ *partexprs = lappend(*partexprs, expr);
+
+ /*
+ * transformPartitionSpec() should have already rejected
+ * subqueries, aggregates, window functions, and SRFs, based
+ * on the EXPR_KIND_ for partition expressions.
+ */
+
+ /*
+ * Preprocess the expression before checking for mutability.
+ * This is essential for the reasons described in
+ * contain_mutable_functions_after_planning. However, we call
+ * expression_planner for ourselves rather than using that
+ * function, because if constant-folding reduces the
+ * expression to a constant, we'd like to know that so we can
+ * complain below.
+ *
+ * Like contain_mutable_functions_after_planning, assume that
+ * expression_planner won't scribble on its input, so this
+ * won't affect the partexprs entry we saved above.
+ */
+ expr = (Node *) expression_planner((Expr *) expr);
+
+ /*
+ * Partition expressions cannot contain mutable functions,
+ * because a given row must always map to the same partition
+ * as long as there is no change in the partition boundary
+ * structure.
+ */
+ if (contain_mutable_functions(expr))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("functions in partition key expression must be marked IMMUTABLE")));
+
+ /*
+ * While it is not exactly *wrong* for a partition expression
+ * to be a constant, it seems better to reject such keys.
+ */
+ if (IsA(expr, Const))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use constant expression as partition key")));
+ }
+ }
+
+ /*
+ * Apply collation override if any
+ */
+ if (pelem->collation)
+ attcollation = get_collation_oid(pelem->collation, false);
+
+ /*
+ * Check we have a collation iff it's a collatable type. The only
+ * expected failures here are (1) COLLATE applied to a noncollatable
+ * type, or (2) partition expression had an unresolved collation. But
+ * we might as well code this to be a complete consistency check.
+ */
+ if (type_is_collatable(atttype))
+ {
+ if (!OidIsValid(attcollation))
+ ereport(ERROR,
+ (errcode(ERRCODE_INDETERMINATE_COLLATION),
+ errmsg("could not determine which collation to use for partition expression"),
+ errhint("Use the COLLATE clause to set the collation explicitly.")));
+ }
+ else
+ {
+ if (OidIsValid(attcollation))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("collations are not supported by type %s",
+ format_type_be(atttype))));
+ }
+
+ partcollation[attn] = attcollation;
+
+ /*
+ * Identify the appropriate operator class. For list and range
+ * partitioning, we use a btree operator class; hash partitioning uses
+ * a hash operator class.
+ */
+ if (strategy == PARTITION_STRATEGY_HASH)
+ am_oid = HASH_AM_OID;
+ else
+ am_oid = BTREE_AM_OID;
+
+ if (!pelem->opclass)
+ {
+ partopclass[attn] = GetDefaultOpClass(atttype, am_oid);
+
+ if (!OidIsValid(partopclass[attn]))
+ {
+ if (strategy == PARTITION_STRATEGY_HASH)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("data type %s has no default operator class for access method \"%s\"",
+ format_type_be(atttype), "hash"),
+ errhint("You must specify a hash operator class or define a default hash operator class for the data type.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("data type %s has no default operator class for access method \"%s\"",
+ format_type_be(atttype), "btree"),
+ errhint("You must specify a btree operator class or define a default btree operator class for the data type.")));
+ }
+ }
+ else
+ partopclass[attn] = ResolveOpClass(pelem->opclass,
+ atttype,
+ am_oid == HASH_AM_OID ? "hash" : "btree",
+ am_oid);
+
+ attn++;
+ }
+}
+
+/*
+ * PartConstraintImpliedByRelConstraint
+ * Do scanrel's existing constraints imply the partition constraint?
+ *
+ * "Existing constraints" include its check constraints and column-level
+ * not-null constraints. partConstraint describes the partition constraint,
+ * in implicit-AND form.
+ */
+bool
+PartConstraintImpliedByRelConstraint(Relation scanrel,
+ List *partConstraint)
+{
+ List *existConstraint = NIL;
+ TupleConstr *constr = RelationGetDescr(scanrel)->constr;
+ int i;
+
+ if (constr && constr->has_not_null)
+ {
+ int natts = scanrel->rd_att->natts;
+
+ for (i = 1; i <= natts; i++)
+ {
+ CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1);
+
+ /* invalid not-null constraint must be ignored here */
+ if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped)
+ {
+ Form_pg_attribute wholeatt = TupleDescAttr(scanrel->rd_att, i - 1);
+ NullTest *ntest = makeNode(NullTest);
+
+ ntest->arg = (Expr *) makeVar(1,
+ i,
+ wholeatt->atttypid,
+ wholeatt->atttypmod,
+ wholeatt->attcollation,
+ 0);
+ ntest->nulltesttype = IS_NOT_NULL;
+
+ /*
+ * argisrow=false is correct even for a composite column,
+ * because attnotnull does not represent a SQL-spec IS NOT
+ * NULL test in such a case, just IS DISTINCT FROM NULL.
+ */
+ ntest->argisrow = false;
+ ntest->location = -1;
+ existConstraint = lappend(existConstraint, ntest);
+ }
+ }
+ }
+
+ return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint);
+}
+
+/*
+ * ConstraintImpliedByRelConstraint
+ * Do scanrel's existing constraints imply the given constraint?
+ *
+ * testConstraint is the constraint to validate. provenConstraint is a
+ * caller-provided list of conditions which this function may assume
+ * to be true. Both provenConstraint and testConstraint must be in
+ * implicit-AND form, must only contain immutable clauses, and must
+ * contain only Vars with varno = 1.
+ */
+bool
+ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint)
+{
+ List *existConstraint = list_copy(provenConstraint);
+ TupleConstr *constr = RelationGetDescr(scanrel)->constr;
+ int num_check,
+ i;
+
+ num_check = (constr != NULL) ? constr->num_check : 0;
+ for (i = 0; i < num_check; i++)
+ {
+ Node *cexpr;
+
+ /*
+ * If this constraint hasn't been fully validated yet, we must ignore
+ * it here.
+ */
+ if (!constr->check[i].ccvalid)
+ continue;
+
+ /*
+ * NOT ENFORCED constraints are always marked as invalid, which should
+ * have been ignored.
+ */
+ Assert(constr->check[i].ccenforced);
+
+ cexpr = stringToNode(constr->check[i].ccbin);
+
+ /*
+ * Run each expression through const-simplification and
+ * canonicalization. It is necessary, because we will be comparing it
+ * to similarly-processed partition constraint expressions, and may
+ * fail to detect valid matches without this.
+ */
+ cexpr = eval_const_expressions(NULL, cexpr);
+ cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true);
+
+ existConstraint = list_concat(existConstraint,
+ make_ands_implicit((Expr *) cexpr));
+ }
+
+ /*
+ * Try to make the proof. Since we are comparing CHECK constraints, we
+ * need to use weak implication, i.e., we assume existConstraint is
+ * not-false and try to prove the same for testConstraint.
+ *
+ * Note that predicate_implied_by assumes its first argument is known
+ * immutable. That should always be true for both NOT NULL and partition
+ * constraints, so we don't test it here.
+ */
+ return predicate_implied_by(testConstraint, existConstraint, true);
+}
+
+/*
+ * QueuePartitionConstraintValidation
+ *
+ * Add an entry to wqueue to have the given partition constraint validated by
+ * Phase 3, for the given relation, and all its children.
+ *
+ * We first verify whether the given constraint is implied by pre-existing
+ * relation constraints; if it is, there's no need to scan the table to
+ * validate, so don't queue in that case.
+ */
+static void
+QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
+ List *partConstraint,
+ bool validate_default)
+{
+ /*
+ * Based on the table's existing constraints, determine whether or not we
+ * may skip scanning the table.
+ */
+ if (PartConstraintImpliedByRelConstraint(scanrel, partConstraint))
+ {
+ if (!validate_default)
+ ereport(DEBUG1,
+ (errmsg_internal("partition constraint for table \"%s\" is implied by existing constraints",
+ RelationGetRelationName(scanrel))));
+ else
+ ereport(DEBUG1,
+ (errmsg_internal("updated partition constraint for default partition \"%s\" is implied by existing constraints",
+ RelationGetRelationName(scanrel))));
+ return;
+ }
+
+ /*
+ * Constraints proved insufficient. For plain relations, queue a
+ * validation item now; for partitioned tables, recurse to process each
+ * partition.
+ */
+ if (scanrel->rd_rel->relkind == RELKIND_RELATION)
+ {
+ AlteredTableInfo *tab;
+
+ /* Grab a work queue entry. */
+ tab = ATGetQueueEntry(wqueue, scanrel);
+ Assert(tab->partition_constraint == NULL);
+ tab->partition_constraint = (Expr *) linitial(partConstraint);
+ tab->validate_default = validate_default;
+ }
+ else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, true);
+ int i;
+
+ for (i = 0; i < partdesc->nparts; i++)
+ {
+ Relation part_rel;
+ List *thisPartConstraint;
+
+ /*
+ * This is the minimum lock we need to prevent deadlocks.
+ */
+ part_rel = table_open(partdesc->oids[i], AccessExclusiveLock);
+
+ /*
+ * Adjust the constraint for scanrel so that it matches this
+ * partition's attribute numbers.
+ */
+ thisPartConstraint =
+ map_partition_varattnos(partConstraint, 1,
+ part_rel, scanrel);
+
+ QueuePartitionConstraintValidation(wqueue, part_rel,
+ thisPartConstraint,
+ validate_default);
+ table_close(part_rel, NoLock); /* keep lock till commit */
+ }
+ }
+}
+
+/*
+ * AttachPartitionEnsureIndexes
+ * subroutine for ATExecAttachPartition to create/match indexes
+ *
+ * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH
+ * PARTITION: every partition must have an index attached to each index on the
+ * partitioned table.
+ */
+static void
+AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel)
+{
+ List *idxes;
+ List *attachRelIdxs;
+ Relation *attachrelIdxRels;
+ IndexInfo **attachInfos;
+ ListCell *cell;
+ MemoryContext cxt;
+ MemoryContext oldcxt;
+
+ cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "AttachPartitionEnsureIndexes",
+ ALLOCSET_DEFAULT_SIZES);
+ oldcxt = MemoryContextSwitchTo(cxt);
+
+ idxes = RelationGetIndexList(rel);
+ attachRelIdxs = RelationGetIndexList(attachrel);
+ attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs));
+ attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs));
+
+ /* Build arrays of all existing indexes and their IndexInfos */
+ foreach_oid(cldIdxId, attachRelIdxs)
+ {
+ int i = foreach_current_index(cldIdxId);
+
+ attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock);
+ attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]);
+ }
+
+ /*
+ * If we're attaching a foreign table, we must fail if any of the indexes
+ * is a constraint index; otherwise, there's nothing to do here. Do this
+ * before starting work, to avoid wasting the effort of building a few
+ * non-unique indexes before coming across a unique one.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ foreach(cell, idxes)
+ {
+ Oid idx = lfirst_oid(cell);
+ Relation idxRel = index_open(idx, AccessShareLock);
+
+ if (idxRel->rd_index->indisunique ||
+ idxRel->rd_index->indisprimary)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" as partition of partitioned table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" contains unique indexes.",
+ RelationGetRelationName(rel))));
+ index_close(idxRel, AccessShareLock);
+ }
+
+ goto out;
+ }
+
+ /*
+ * For each index on the partitioned table, find a matching one in the
+ * partition-to-be; if one is not found, create one.
+ */
+ foreach(cell, idxes)
+ {
+ Oid idx = lfirst_oid(cell);
+ Relation idxRel = index_open(idx, AccessShareLock);
+ IndexInfo *info;
+ AttrMap *attmap;
+ bool found = false;
+ Oid constraintOid;
+
+ /*
+ * Ignore indexes in the partitioned table other than partitioned
+ * indexes.
+ */
+ if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
+ {
+ index_close(idxRel, AccessShareLock);
+ continue;
+ }
+
+ /* construct an indexinfo to compare existing indexes against */
+ info = BuildIndexInfo(idxRel);
+ attmap = build_attrmap_by_name(RelationGetDescr(attachrel),
+ RelationGetDescr(rel),
+ false);
+ constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), idx);
+
+ /*
+ * Scan the list of existing indexes in the partition-to-be, and mark
+ * the first matching, valid, unattached one we find, if any, as
+ * partition of the parent index. If we find one, we're done.
+ */
+ for (int i = 0; i < list_length(attachRelIdxs); i++)
+ {
+ Oid cldIdxId = RelationGetRelid(attachrelIdxRels[i]);
+ Oid cldConstrOid = InvalidOid;
+
+ /* does this index have a parent? if so, can't use it */
+ if (attachrelIdxRels[i]->rd_rel->relispartition)
+ continue;
+
+ /* If this index is invalid, can't use it */
+ if (!attachrelIdxRels[i]->rd_index->indisvalid)
+ continue;
+
+ if (CompareIndexInfo(attachInfos[i], info,
+ attachrelIdxRels[i]->rd_indcollation,
+ idxRel->rd_indcollation,
+ attachrelIdxRels[i]->rd_opfamily,
+ idxRel->rd_opfamily,
+ attmap))
+ {
+ /*
+ * If this index is being created in the parent because of a
+ * constraint, then the child needs to have a constraint also,
+ * so look for one. If there is no such constraint, this
+ * index is no good, so keep looking.
+ */
+ if (OidIsValid(constraintOid))
+ {
+ cldConstrOid =
+ get_relation_idx_constraint_oid(RelationGetRelid(attachrel),
+ cldIdxId);
+ /* no dice */
+ if (!OidIsValid(cldConstrOid))
+ continue;
+
+ /* Ensure they're both the same type of constraint */
+ if (get_constraint_type(constraintOid) !=
+ get_constraint_type(cldConstrOid))
+ continue;
+ }
+
+ /* bingo. */
+ IndexSetParentIndex(attachrelIdxRels[i], idx);
+ if (OidIsValid(constraintOid))
+ ConstraintSetParentConstraint(cldConstrOid, constraintOid,
+ RelationGetRelid(attachrel));
+ found = true;
+
+ CommandCounterIncrement();
+ break;
+ }
+ }
+
+ /*
+ * If no suitable index was found in the partition-to-be, create one
+ * now. Note that if this is a PK, not-null constraints must already
+ * exist.
+ */
+ if (!found)
+ {
+ IndexStmt *stmt;
+ Oid conOid;
+
+ stmt = generateClonedIndexStmt(NULL,
+ idxRel, attmap,
+ &conOid);
+ DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid,
+ RelationGetRelid(idxRel),
+ conOid,
+ -1,
+ true, false, false, false, false);
+ }
+
+ index_close(idxRel, AccessShareLock);
+ }
+
+out:
+ /* Clean up. */
+ for (int i = 0; i < list_length(attachRelIdxs); i++)
+ index_close(attachrelIdxRels[i], AccessShareLock);
+ MemoryContextSwitchTo(oldcxt);
+ MemoryContextDelete(cxt);
+}
+
+/*
+ * ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
+ *
+ * Return the address of the newly attached partition.
+ */
+ObjectAddress
+ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
+ AlterTableUtilityContext *context)
+{
+ Relation attachrel,
+ catalog;
+ List *attachrel_children;
+ List *partConstraint;
+ SysScanDesc scan;
+ ScanKeyData skey;
+ AttrNumber attno;
+ int natts;
+ TupleDesc tupleDesc;
+ ObjectAddress address;
+ const char *trigger_name;
+ Oid defaultPartOid;
+ List *partBoundConstraint;
+ ParseState *pstate = make_parsestate(NULL);
+
+ pstate->p_sourcetext = context->queryString;
+
+ /*
+ * We must lock the default partition if one exists, because attaching a
+ * new partition will change its partition constraint.
+ */
+ defaultPartOid =
+ get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+ if (OidIsValid(defaultPartOid))
+ LockRelationOid(defaultPartOid, AccessExclusiveLock);
+
+ attachrel = table_openrv(cmd->name, AccessExclusiveLock);
+
+ /*
+ * XXX I think it'd be a good idea to grab locks on all tables referenced
+ * by FKs at this point also.
+ */
+
+ /*
+ * Must be owner of both parent and source table -- parent was checked by
+ * ATSimplePermissions call in ATPrepCmd
+ */
+ ATSimplePermissions(AT_AttachPartition, attachrel,
+ ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
+
+ /* A partition can only have one parent */
+ if (attachrel->rd_rel->relispartition)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is already a partition",
+ RelationGetRelationName(attachrel))));
+
+ if (OidIsValid(attachrel->rd_rel->reloftype))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a typed table as partition")));
+
+ /*
+ * Table being attached should not already be part of inheritance; either
+ * as a child table...
+ */
+ catalog = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(attachrel)));
+ scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true,
+ NULL, 1, &skey);
+ if (HeapTupleIsValid(systable_getnext(scan)))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach inheritance child as partition")));
+ systable_endscan(scan);
+
+ /* ...or as a parent table (except the case when it is partitioned) */
+ ScanKeyInit(&skey,
+ Anum_pg_inherits_inhparent,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(attachrel)));
+ scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL,
+ 1, &skey);
+ if (HeapTupleIsValid(systable_getnext(scan)) &&
+ attachrel->rd_rel->relkind == RELKIND_RELATION)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach inheritance parent as partition")));
+ systable_endscan(scan);
+ table_close(catalog, AccessShareLock);
+
+ /*
+ * Prevent circularity by seeing if rel is a partition of attachrel. (In
+ * particular, this disallows making a rel a partition of itself.)
+ *
+ * We do that by checking if rel is a member of the list of attachrel's
+ * partitions provided the latter is partitioned at all. We want to avoid
+ * having to construct this list again, so we request the strongest lock
+ * on all partitions. We need the strongest lock, because we may decide
+ * to scan them if we find out that the table being attached (or its leaf
+ * partitions) may contain rows that violate the partition constraint. If
+ * the table has a constraint that would prevent such rows, which by
+ * definition is present in all the partitions, we need not scan the
+ * table, nor its partitions. But we cannot risk a deadlock by taking a
+ * weaker lock now and the stronger one only when needed.
+ */
+ attachrel_children = find_all_inheritors(RelationGetRelid(attachrel),
+ AccessExclusiveLock, NULL);
+ if (list_member_oid(attachrel_children, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_TABLE),
+ errmsg("circular inheritance not allowed"),
+ errdetail("\"%s\" is already a child of \"%s\".",
+ RelationGetRelationName(rel),
+ RelationGetRelationName(attachrel))));
+
+ /* If the parent is permanent, so must be all of its partitions. */
+ if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+ attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ /* Temp parent cannot have a partition that is itself not a temp */
+ if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+ attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ /* If the parent is temp, it must belong to this session */
+ if (RELATION_IS_OTHER_TEMP(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach as partition of temporary relation of another session")));
+
+ /* Ditto for the partition */
+ if (RELATION_IS_OTHER_TEMP(attachrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach temporary relation of another session as partition")));
+
+ /*
+ * Check if attachrel has any identity columns or any columns that aren't
+ * in the parent.
+ */
+ tupleDesc = RelationGetDescr(attachrel);
+ natts = tupleDesc->natts;
+ for (attno = 1; attno <= natts; attno++)
+ {
+ Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1);
+ char *attributeName = NameStr(attribute->attname);
+
+ /* Ignore dropped */
+ if (attribute->attisdropped)
+ continue;
+
+ if (attribute->attidentity)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("table \"%s\" being attached contains an identity column \"%s\"",
+ RelationGetRelationName(attachrel), attributeName),
+ errdetail("The new partition may not contain an identity column."));
+
+ /* Try to find the column in parent (matching on column name) */
+ if (!SearchSysCacheExists2(ATTNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ CStringGetDatum(attributeName)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"",
+ RelationGetRelationName(attachrel), attributeName,
+ RelationGetRelationName(rel)),
+ errdetail("The new partition may contain only the columns present in parent.")));
+ }
+
+ /*
+ * If child_rel has row-level triggers with transition tables, we
+ * currently don't allow it to become a partition. See also prohibitions
+ * in ATExecAddInherit() and CreateTrigger().
+ */
+ trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc);
+ if (trigger_name != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition",
+ trigger_name, RelationGetRelationName(attachrel)),
+ errdetail("ROW triggers with transition tables are not supported on partitions.")));
+
+ /*
+ * Check that the new partition's bound is valid and does not overlap any
+ * of existing partitions of the parent - note that it does not return on
+ * error.
+ */
+ check_new_partition_bound(RelationGetRelationName(attachrel), rel,
+ cmd->bound, pstate);
+
+ /* OK to create inheritance. Rest of the checks performed there */
+ CreateInheritance(attachrel, rel, true);
+
+ /* Update the pg_class entry. */
+ StorePartitionBound(attachrel, rel, cmd->bound);
+
+ /* Ensure there exists a correct set of indexes in the partition. */
+ AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
+
+ /* and triggers */
+ CloneRowTriggersToPartition(rel, attachrel);
+
+ /*
+ * Clone foreign key constraints. Callee is responsible for setting up
+ * for phase 3 constraint verification.
+ */
+ CloneForeignKeyConstraints(wqueue, rel, attachrel);
+
+ /*
+ * Generate partition constraint from the partition bound specification.
+ * If the parent itself is a partition, make sure to include its
+ * constraint as well.
+ */
+ partBoundConstraint = get_qual_from_partbound(rel, cmd->bound);
+
+ /*
+ * Use list_concat_copy() to avoid modifying partBoundConstraint in place,
+ * since it's needed later to construct the constraint expression for
+ * validating against the default partition, if any.
+ */
+ partConstraint = list_concat_copy(partBoundConstraint,
+ RelationGetPartitionQual(rel));
+
+ /* Skip validation if there are no constraints to validate. */
+ if (partConstraint)
+ {
+ /*
+ * Run the partition quals through const-simplification similar to
+ * check constraints. We skip canonicalize_qual, though, because
+ * partition quals should be in canonical form already.
+ */
+ partConstraint =
+ (List *) eval_const_expressions(NULL,
+ (Node *) partConstraint);
+
+ /* XXX this sure looks wrong */
+ partConstraint = list_make1(make_ands_explicit(partConstraint));
+
+ /*
+ * Adjust the generated constraint to match this partition's attribute
+ * numbers.
+ */
+ partConstraint = map_partition_varattnos(partConstraint, 1, attachrel,
+ rel);
+
+ /* Validate partition constraints against the table being attached. */
+ QueuePartitionConstraintValidation(wqueue, attachrel, partConstraint,
+ false);
+ }
+
+ /*
+ * If we're attaching a partition other than the default partition and a
+ * default one exists, then that partition's partition constraint changes,
+ * so add an entry to the work queue to validate it, too. (We must not do
+ * this when the partition being attached is the default one; we already
+ * did it above!)
+ */
+ if (OidIsValid(defaultPartOid))
+ {
+ Relation defaultrel;
+ List *defPartConstraint;
+
+ Assert(!cmd->bound->is_default);
+
+ /* we already hold a lock on the default partition */
+ defaultrel = table_open(defaultPartOid, NoLock);
+ defPartConstraint =
+ get_proposed_default_constraint(partBoundConstraint);
+
+ /*
+ * Map the Vars in the constraint expression from rel's attnos to
+ * defaultrel's.
+ */
+ defPartConstraint =
+ map_partition_varattnos(defPartConstraint,
+ 1, defaultrel, rel);
+ QueuePartitionConstraintValidation(wqueue, defaultrel,
+ defPartConstraint, true);
+
+ /* keep our lock until commit. */
+ table_close(defaultrel, NoLock);
+ }
+
+ ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel));
+
+ /*
+ * If the partition we just attached is partitioned itself, invalidate
+ * relcache for all descendent partitions too to ensure that their
+ * rd_partcheck expression trees are rebuilt; partitions already locked at
+ * the beginning of this function.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ ListCell *l;
+
+ foreach(l, attachrel_children)
+ {
+ CacheInvalidateRelcacheByRelid(lfirst_oid(l));
+ }
+ }
+
+ /* keep our lock until commit */
+ table_close(attachrel, NoLock);
+
+ return address;
+}
+
+/*
+ * CloneRowTriggersToPartition
+ * subroutine for ATExecAttachPartition/DefineRelation to create row
+ * triggers on partitions
+ */
+void
+CloneRowTriggersToPartition(Relation parent, Relation partition)
+{
+ Relation pg_trigger;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple tuple;
+ MemoryContext perTupCxt;
+
+ ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent)));
+ pg_trigger = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId,
+ true, NULL, 1, &key);
+
+ perTupCxt = AllocSetContextCreate(CurrentMemoryContext,
+ "clone trig", ALLOCSET_SMALL_SIZES);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ Form_pg_trigger trigForm = (Form_pg_trigger) GETSTRUCT(tuple);
+ CreateTrigStmt *trigStmt;
+ Node *qual = NULL;
+ Datum value;
+ bool isnull;
+ List *cols = NIL;
+ List *trigargs = NIL;
+ MemoryContext oldcxt;
+
+ /*
+ * Ignore statement-level triggers; those are not cloned.
+ */
+ if (!TRIGGER_FOR_ROW(trigForm->tgtype))
+ continue;
+
+ /*
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
+ */
+ if (trigForm->tgisinternal)
+ continue;
+
+ /*
+ * Complain if we find an unexpected trigger type.
+ */
+ if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) &&
+ !TRIGGER_FOR_AFTER(trigForm->tgtype))
+ elog(ERROR, "unexpected trigger \"%s\" found",
+ NameStr(trigForm->tgname));
+
+ /* Use short-lived context for CREATE TRIGGER */
+ oldcxt = MemoryContextSwitchTo(perTupCxt);
+
+ /*
+ * If there is a WHEN clause, generate a 'cooked' version of it that's
+ * appropriate for the partition.
+ */
+ value = heap_getattr(tuple, Anum_pg_trigger_tgqual,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ qual = stringToNode(TextDatumGetCString(value));
+ qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO,
+ partition, parent);
+ qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
+ partition, parent);
+ }
+
+ /*
+ * If there is a column list, transform it to a list of column names.
+ * Note we don't need to map this list in any way ...
+ */
+ if (trigForm->tgattr.dim1 > 0)
+ {
+ int i;
+
+ for (i = 0; i < trigForm->tgattr.dim1; i++)
+ {
+ Form_pg_attribute col;
+
+ col = TupleDescAttr(parent->rd_att,
+ trigForm->tgattr.values[i] - 1);
+ cols = lappend(cols,
+ makeString(pstrdup(NameStr(col->attname))));
+ }
+ }
+
+ /* Reconstruct trigger arguments list. */
+ if (trigForm->tgnargs > 0)
+ {
+ char *p;
+
+ value = heap_getattr(tuple, Anum_pg_trigger_tgargs,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (isnull)
+ elog(ERROR, "tgargs is null for trigger \"%s\" in partition \"%s\"",
+ NameStr(trigForm->tgname), RelationGetRelationName(partition));
+
+ p = (char *) VARDATA_ANY(DatumGetByteaPP(value));
+
+ for (int i = 0; i < trigForm->tgnargs; i++)
+ {
+ trigargs = lappend(trigargs, makeString(pstrdup(p)));
+ p += strlen(p) + 1;
+ }
+ }
+
+ trigStmt = makeNode(CreateTrigStmt);
+ trigStmt->replace = false;
+ trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
+ trigStmt->trigname = NameStr(trigForm->tgname);
+ trigStmt->relation = NULL;
+ trigStmt->funcname = NULL; /* passed separately */
+ trigStmt->args = trigargs;
+ trigStmt->row = true;
+ trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
+ trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
+ trigStmt->columns = cols;
+ trigStmt->whenClause = NULL; /* passed separately */
+ trigStmt->transitionRels = NIL; /* not supported at present */
+ trigStmt->deferrable = trigForm->tgdeferrable;
+ trigStmt->initdeferred = trigForm->tginitdeferred;
+ trigStmt->constrrel = NULL; /* passed separately */
+
+ CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
+ trigForm->tgconstrrelid, InvalidOid, InvalidOid,
+ trigForm->tgfoid, trigForm->oid, qual,
+ false, true, trigForm->tgenabled);
+
+ MemoryContextSwitchTo(oldcxt);
+ MemoryContextReset(perTupCxt);
+ }
+
+ MemoryContextDelete(perTupCxt);
+
+ systable_endscan(scan);
+ table_close(pg_trigger, RowExclusiveLock);
+}
+
+/*
+ * Return an OID list of constraints that reference the given relation
+ * that are marked as having a parent constraints.
+ */
+static List *
+GetParentedForeignKeyRefs(Relation partition)
+{
+ Relation pg_constraint;
+ HeapTuple tuple;
+ SysScanDesc scan;
+ ScanKeyData key[2];
+ List *constraints = NIL;
+
+ /*
+ * If no indexes, or no columns are referenceable by FKs, we can avoid the
+ * scan.
+ */
+ if (RelationGetIndexList(partition) == NIL ||
+ bms_is_empty(RelationGetIndexAttrBitmap(partition,
+ INDEX_ATTR_BITMAP_KEY)))
+ return NIL;
+
+ /* Search for constraints referencing this table */
+ pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+ ScanKeyInit(&key[0],
+ Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition)));
+ ScanKeyInit(&key[1],
+ Anum_pg_constraint_contype, BTEqualStrategyNumber,
+ F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
+
+ /* XXX This is a seqscan, as we don't have a usable index */
+ scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key);
+ while ((tuple = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ /*
+ * We only need to process constraints that are part of larger ones.
+ */
+ if (!OidIsValid(constrForm->conparentid))
+ continue;
+
+ constraints = lappend_oid(constraints, constrForm->oid);
+ }
+
+ systable_endscan(scan);
+ table_close(pg_constraint, AccessShareLock);
+
+ return constraints;
+}
+
+/*
+ * During DETACH PARTITION, verify that any foreign keys pointing to the
+ * partitioned table would not become invalid. An error is raised if any
+ * referenced values exist.
+ */
+static void
+ATDetachCheckNoForeignKeyRefs(Relation partition)
+{
+ List *constraints;
+ ListCell *cell;
+
+ constraints = GetParentedForeignKeyRefs(partition);
+
+ foreach(cell, constraints)
+ {
+ Oid constrOid = lfirst_oid(cell);
+ HeapTuple tuple;
+ Form_pg_constraint constrForm;
+ Relation rel;
+ Trigger trig = {0};
+
+ tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for constraint %u", constrOid);
+ constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ Assert(OidIsValid(constrForm->conparentid));
+ Assert(constrForm->confrelid == RelationGetRelid(partition));
+
+ /* prevent data changes into the referencing table until commit */
+ rel = table_open(constrForm->conrelid, ShareLock);
+
+ trig.tgoid = InvalidOid;
+ trig.tgname = NameStr(constrForm->conname);
+ trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN;
+ trig.tgisinternal = true;
+ trig.tgconstrrelid = RelationGetRelid(partition);
+ trig.tgconstrindid = constrForm->conindid;
+ trig.tgconstraint = constrForm->oid;
+ trig.tgdeferrable = false;
+ trig.tginitdeferred = false;
+ /* we needn't fill in remaining fields */
+
+ RI_PartitionRemove_Check(&trig, rel, partition);
+
+ ReleaseSysCache(tuple);
+
+ table_close(rel, NoLock);
+ }
+}
+
+/*
+ * DropClonedTriggersFromPartition
+ * subroutine for ATExecDetachPartition to remove any triggers that were
+ * cloned to the partition when it was created-as-partition or attached.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+static void
+DropClonedTriggersFromPartition(Oid partitionId)
+{
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+ ObjectAddresses *objects;
+
+ objects = new_object_addresses();
+
+ /*
+ * Scan pg_trigger to search for all triggers on this rel.
+ */
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(partitionId));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trig;
+
+ /* Ignore triggers that weren't cloned */
+ if (!OidIsValid(pg_trigger->tgparentid))
+ continue;
+
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because these will be detached when the foreign keys
+ * themselves are.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
+ /*
+ * This is ugly, but necessary: remove the dependency markings on the
+ * trigger so that it can be removed.
+ */
+ deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+
+ /* remember this trigger to remove it below */
+ ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid);
+ add_exact_object_address(&trig, objects);
+ }
+
+ /* make the dependency removal visible to the deletion below */
+ CommandCounterIncrement();
+ performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+
+ /* done */
+ free_object_addresses(objects);
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+}
+
+/*
+ * Second part of ALTER TABLE .. DETACH.
+ *
+ * This is separate so that it can be run independently when the second
+ * transaction of the concurrent algorithm fails (crash or abort).
+ */
+static void
+DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
+ Oid defaultPartOid)
+{
+ Relation classRel;
+ List *fks;
+ ListCell *cell;
+ List *indexes;
+ Datum new_val[Natts_pg_class];
+ bool new_null[Natts_pg_class],
+ new_repl[Natts_pg_class];
+ HeapTuple tuple,
+ newtuple;
+ Relation trigrel = NULL;
+ List *fkoids = NIL;
+
+ if (concurrent)
+ {
+ /*
+ * We can remove the pg_inherits row now. (In the non-concurrent case,
+ * this was already done).
+ */
+ RemoveInheritance(partRel, rel, true);
+ }
+
+ /* Drop any triggers that were cloned on creation/attach. */
+ DropClonedTriggersFromPartition(RelationGetRelid(partRel));
+
+ /*
+ * Detach any foreign keys that are inherited. This includes creating
+ * additional action triggers.
+ */
+ fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * It's possible that the partition being detached has a foreign key that
+ * references a partitioned table. When that happens, there are multiple
+ * pg_constraint rows for the partition: one points to the partitioned
+ * table itself, while the others point to each of its partitions. Only
+ * the topmost one is to be considered here; the child constraints must be
+ * left alone, because conceptually those aren't coming from our parent
+ * partitioned table, but from this partition itself.
+ *
+ * We implement this by collecting all the constraint OIDs in a first scan
+ * of the FK array, and skipping in the loop below those constraints whose
+ * parents are listed here.
+ */
+ foreach_node(ForeignKeyCacheInfo, fk, fks)
+ fkoids = lappend_oid(fkoids, fk->conoid);
+
+ foreach(cell, fks)
+ {
+ ForeignKeyCacheInfo *fk = lfirst(cell);
+ HeapTuple contup;
+ Form_pg_constraint conform;
+
+ contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
+ if (!HeapTupleIsValid(contup))
+ elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
+ conform = (Form_pg_constraint) GETSTRUCT(contup);
+
+ /*
+ * Consider only inherited foreign keys, and only if their parents
+ * aren't in the list.
+ */
+ if (conform->contype != CONSTRAINT_FOREIGN ||
+ !OidIsValid(conform->conparentid) ||
+ list_member_oid(fkoids, conform->conparentid))
+ {
+ ReleaseSysCache(contup);
+ continue;
+ }
+
+ /*
+ * The constraint on this table must be marked no longer a child of
+ * the parent's constraint, as do its check triggers.
+ */
+ ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * ENFORCED constraint being detached and detach them from the parent
+ * triggers. NOT ENFORCED constraints do not have these triggers;
+ * therefore, this step is not needed.
+ */
+ if (fk->conenforced)
+ {
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ }
+
+ /*
+ * Lastly, create the action triggers on the referenced table, using
+ * addFkRecurseReferenced, which requires some elaborate setup (so put
+ * it in a separate block). While at it, if the table is partitioned,
+ * that function will recurse to create the pg_constraint rows and
+ * action triggers for each partition.
+ *
+ * Note there's no need to do addFkConstraint() here, because the
+ * pg_constraint row already exists.
+ */
+ {
+ Constraint *fkconstraint;
+ int numfks;
+ AttrNumber conkey[INDEX_MAX_KEYS];
+ AttrNumber confkey[INDEX_MAX_KEYS];
+ Oid conpfeqop[INDEX_MAX_KEYS];
+ Oid conppeqop[INDEX_MAX_KEYS];
+ Oid conffeqop[INDEX_MAX_KEYS];
+ int numfkdelsetcols;
+ AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+ Relation refdRel;
+
+ DeconstructFkConstraintRow(contup,
+ &numfks,
+ conkey,
+ confkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ &numfkdelsetcols,
+ confdelsetcols);
+
+ /* Create a synthetic node we'll use throughout */
+ fkconstraint = makeNode(Constraint);
+ fkconstraint->contype = CONSTRAINT_FOREIGN;
+ fkconstraint->conname = pstrdup(NameStr(conform->conname));
+ fkconstraint->deferrable = conform->condeferrable;
+ fkconstraint->initdeferred = conform->condeferred;
+ fkconstraint->is_enforced = conform->conenforced;
+ fkconstraint->skip_validation = true;
+ fkconstraint->initially_valid = conform->convalidated;
+ /* a few irrelevant fields omitted here */
+ fkconstraint->pktable = NULL;
+ fkconstraint->fk_attrs = NIL;
+ fkconstraint->pk_attrs = NIL;
+ fkconstraint->fk_matchtype = conform->confmatchtype;
+ fkconstraint->fk_upd_action = conform->confupdtype;
+ fkconstraint->fk_del_action = conform->confdeltype;
+ fkconstraint->fk_del_set_cols = NIL;
+ fkconstraint->old_conpfeqop = NIL;
+ fkconstraint->old_pktable_oid = InvalidOid;
+ fkconstraint->location = -1;
+
+ /* set up colnames, used to generate the constraint name */
+ for (int i = 0; i < numfks; i++)
+ {
+ Form_pg_attribute att;
+
+ att = TupleDescAttr(RelationGetDescr(partRel),
+ conkey[i] - 1);
+
+ fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
+ makeString(NameStr(att->attname)));
+ }
+
+ refdRel = table_open(fk->confrelid, ShareRowExclusiveLock);
+
+ addFkRecurseReferenced(fkconstraint, partRel,
+ refdRel,
+ conform->conindid,
+ fk->conoid,
+ numfks,
+ confkey,
+ conkey,
+ conpfeqop,
+ conppeqop,
+ conffeqop,
+ numfkdelsetcols,
+ confdelsetcols,
+ true,
+ InvalidOid, InvalidOid,
+ conform->conperiod);
+ table_close(refdRel, NoLock); /* keep lock till end of xact */
+ }
+
+ ReleaseSysCache(contup);
+ }
+ list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
+
+ /*
+ * Any sub-constraints that are in the referenced-side of a larger
+ * constraint have to be removed. This partition is no longer part of the
+ * key space of the constraint.
+ */
+ foreach(cell, GetParentedForeignKeyRefs(partRel))
+ {
+ Oid constrOid = lfirst_oid(cell);
+ ObjectAddress constraint;
+
+ ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
+ deleteDependencyRecordsForClass(ConstraintRelationId,
+ constrOid,
+ ConstraintRelationId,
+ DEPENDENCY_INTERNAL);
+ CommandCounterIncrement();
+
+ ObjectAddressSet(constraint, ConstraintRelationId, constrOid);
+ performDeletion(&constraint, DROP_RESTRICT, 0);
+ }
+
+ /* Now we can detach indexes */
+ indexes = RelationGetIndexList(partRel);
+ foreach(cell, indexes)
+ {
+ Oid idxid = lfirst_oid(cell);
+ Oid parentidx;
+ Relation idx;
+ Oid constrOid;
+ Oid parentConstrOid;
+
+ if (!has_superclass(idxid))
+ continue;
+
+ parentidx = get_partition_parent(idxid, false);
+ Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel)));
+
+ idx = index_open(idxid, AccessExclusiveLock);
+ IndexSetParentIndex(idx, InvalidOid);
+
+ /*
+ * If there's a constraint associated with the index, detach it too.
+ * Careful: it is possible for a constraint index in a partition to be
+ * the child of a non-constraint index, so verify whether the parent
+ * index does actually have a constraint.
+ */
+ constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
+ idxid);
+ parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel),
+ parentidx);
+ if (OidIsValid(parentConstrOid) && OidIsValid(constrOid))
+ ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
+
+ index_close(idx, NoLock);
+ }
+
+ /* Update pg_class tuple */
+ classRel = table_open(RelationRelationId, RowExclusiveLock);
+ tuple = SearchSysCacheCopy1(RELOID,
+ ObjectIdGetDatum(RelationGetRelid(partRel)));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u",
+ RelationGetRelid(partRel));
+ Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition);
+
+ /* Clear relpartbound and reset relispartition */
+ memset(new_val, 0, sizeof(new_val));
+ memset(new_null, false, sizeof(new_null));
+ memset(new_repl, false, sizeof(new_repl));
+ new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0;
+ new_null[Anum_pg_class_relpartbound - 1] = true;
+ new_repl[Anum_pg_class_relpartbound - 1] = true;
+ newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel),
+ new_val, new_null, new_repl);
+
+ ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false;
+ CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple);
+ heap_freetuple(newtuple);
+ table_close(classRel, RowExclusiveLock);
+
+ /*
+ * Drop identity property from all identity columns of partition.
+ */
+ for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno);
+
+ if (!attr->attisdropped && attr->attidentity)
+ ATExecDropIdentity(partRel, NameStr(attr->attname), false,
+ AccessExclusiveLock, true, true);
+ }
+
+ if (OidIsValid(defaultPartOid))
+ {
+ /*
+ * If the relation being detached is the default partition itself,
+ * remove it from the parent's pg_partitioned_table entry.
+ *
+ * If not, we must invalidate default partition's relcache entry, as
+ * in StorePartitionBound: its partition constraint depends on every
+ * other partition's partition constraint.
+ */
+ if (RelationGetRelid(partRel) == defaultPartOid)
+ update_default_partition_oid(RelationGetRelid(rel), InvalidOid);
+ else
+ CacheInvalidateRelcacheByRelid(defaultPartOid);
+ }
+
+ /*
+ * Invalidate the parent's relcache so that the partition is no longer
+ * included in its partition descriptor.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * If the partition we just detached is partitioned itself, invalidate
+ * relcache for all descendent partitions too to ensure that their
+ * rd_partcheck expression trees are rebuilt; must lock partitions before
+ * doing so, using the same lockmode as what partRel has been locked with
+ * by the caller.
+ */
+ if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *children;
+
+ children = find_all_inheritors(RelationGetRelid(partRel),
+ AccessExclusiveLock, NULL);
+ foreach(cell, children)
+ {
+ CacheInvalidateRelcacheByRelid(lfirst_oid(cell));
+ }
+ }
+}
+
+/*
+ * ALTER TABLE DETACH PARTITION
+ *
+ * Return the address of the relation that is no longer a partition of rel.
+ *
+ * If concurrent mode is requested, we run in two transactions. A side-
+ * effect is that this command cannot run in a multi-part ALTER TABLE.
+ * Currently, that's enforced by the grammar.
+ *
+ * The strategy for concurrency is to first modify the partition's
+ * pg_inherit catalog row to make it visible to everyone that the
+ * partition is detached, lock the partition against writes, and commit
+ * the transaction; anyone who requests the partition descriptor from
+ * that point onwards has to ignore such a partition. In a second
+ * transaction, we wait until all transactions that could have seen the
+ * partition as attached are gone, then we remove the rest of partition
+ * metadata (pg_inherits and pg_class.relpartbounds).
+ */
+ObjectAddress
+ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
+ RangeVar *name, bool concurrent)
+{
+ Relation partRel;
+ ObjectAddress address;
+ Oid defaultPartOid;
+
+ /*
+ * We must lock the default partition, because detaching this partition
+ * will change its partition constraint.
+ */
+ defaultPartOid =
+ get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+ if (OidIsValid(defaultPartOid))
+ {
+ /*
+ * Concurrent detaching when a default partition exists is not
+ * supported. The main problem is that the default partition
+ * constraint would change. And there's a definitional problem: what
+ * should happen to the tuples that are being inserted that belong to
+ * the partition being detached? Putting them on the partition being
+ * detached would be wrong, since they'd become "lost" after the
+ * detaching completes but we cannot put them in the default partition
+ * either until we alter its partition constraint.
+ *
+ * I think we could solve this problem if we effected the constraint
+ * change before committing the first transaction. But the lock would
+ * have to remain AEL and it would cause concurrent query planning to
+ * be blocked, so changing it that way would be even worse.
+ */
+ if (concurrent)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot detach partitions concurrently when a default partition exists")));
+ LockRelationOid(defaultPartOid, AccessExclusiveLock);
+ }
+
+ /*
+ * In concurrent mode, the partition is locked with share-update-exclusive
+ * in the first transaction. This allows concurrent transactions to be
+ * doing DML to the partition.
+ */
+ partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock :
+ AccessExclusiveLock);
+
+ /*
+ * Check inheritance conditions and either delete the pg_inherits row (in
+ * non-concurrent mode) or just set the inhdetachpending flag.
+ */
+ if (!concurrent)
+ RemoveInheritance(partRel, rel, false);
+ else
+ MarkInheritDetached(partRel, rel);
+
+ /*
+ * Ensure that foreign keys still hold after this detach. This keeps
+ * locks on the referencing tables, which prevents concurrent transactions
+ * from adding rows that we wouldn't see. For this to work in concurrent
+ * mode, it is critical that the partition appears as no longer attached
+ * for the RI queries as soon as the first transaction commits.
+ */
+ ATDetachCheckNoForeignKeyRefs(partRel);
+
+ /*
+ * Concurrent mode has to work harder; first we add a new constraint to
+ * the partition that matches the partition constraint. Then we close our
+ * existing transaction, and in a new one wait for all processes to catch
+ * up on the catalog updates we've done so far; at that point we can
+ * complete the operation.
+ */
+ if (concurrent)
+ {
+ Oid partrelid,
+ parentrelid;
+ LOCKTAG tag;
+ char *parentrelname;
+ char *partrelname;
+
+ /*
+ * We're almost done now; the only traces that remain are the
+ * pg_inherits tuple and the partition's relpartbounds. Before we can
+ * remove those, we need to wait until all transactions that know that
+ * this is a partition are gone.
+ */
+
+ /*
+ * Remember relation OIDs to re-acquire them later; and relation names
+ * too, for error messages if something is dropped in between.
+ */
+ partrelid = RelationGetRelid(partRel);
+ parentrelid = RelationGetRelid(rel);
+ parentrelname = MemoryContextStrdup(PortalContext,
+ RelationGetRelationName(rel));
+ partrelname = MemoryContextStrdup(PortalContext,
+ RelationGetRelationName(partRel));
+
+ /* Invalidate relcache entries for the parent -- must be before close */
+ CacheInvalidateRelcache(rel);
+
+ table_close(partRel, NoLock);
+ table_close(rel, NoLock);
+ tab->rel = NULL;
+
+ /* Make updated catalog entry visible */
+ PopActiveSnapshot();
+ CommitTransactionCommand();
+
+ StartTransactionCommand();
+
+ /*
+ * Now wait. This ensures that all queries that were planned
+ * including the partition are finished before we remove the rest of
+ * catalog entries. We don't need or indeed want to acquire this
+ * lock, though -- that would block later queries.
+ *
+ * We don't need to concern ourselves with waiting for a lock on the
+ * partition itself, since we will acquire AccessExclusiveLock below.
+ */
+ SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
+ WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+
+ /*
+ * Now acquire locks in both relations again. Note they may have been
+ * removed in the meantime, so care is required.
+ */
+ rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock);
+ partRel = try_relation_open(partrelid, AccessExclusiveLock);
+
+ /* If the relations aren't there, something bad happened; bail out */
+ if (rel == NULL)
+ {
+ if (partRel != NULL) /* shouldn't happen */
+ elog(WARNING, "dangling partition \"%s\" remains, can't fix",
+ partrelname);
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("partitioned table \"%s\" was removed concurrently",
+ parentrelname)));
+ }
+ if (partRel == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("partition \"%s\" was removed concurrently", partrelname)));
+
+ tab->rel = rel;
+ }
+
+ /*
+ * Detaching the partition might involve TOAST table access, so ensure we
+ * have a valid snapshot.
+ */
+ PushActiveSnapshot(GetTransactionSnapshot());
+
+ /* Do the final part of detaching */
+ DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
+
+ PopActiveSnapshot();
+
+ ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
+
+ /* keep our lock until commit */
+ table_close(partRel, NoLock);
+
+ return address;
+}
+
+/*
+ * ALTER TABLE ... DETACH PARTITION ... FINALIZE
+ *
+ * To use when a DETACH PARTITION command previously did not run to
+ * completion; this completes the detaching process.
+ */
+ObjectAddress
+ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
+{
+ Relation partRel;
+ ObjectAddress address;
+ Snapshot snap = GetActiveSnapshot();
+
+ partRel = table_openrv(name, AccessExclusiveLock);
+
+ /*
+ * Wait until existing snapshots are gone. This is important if the
+ * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the
+ * user could immediately run DETACH FINALIZE without actually waiting for
+ * existing transactions. We must not complete the detach action until
+ * all such queries are complete (otherwise we would present them with an
+ * inconsistent view of catalogs).
+ */
+ WaitForOlderSnapshots(snap->xmin, false);
+
+ DetachPartitionFinalize(rel, partRel, true, InvalidOid);
+
+ ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
+
+ table_close(partRel, NoLock);
+
+ return address;
+}
+
+/*
+ * Before acquiring lock on an index, acquire the same lock on the owning
+ * table.
+ */
+struct AttachIndexCallbackState
+{
+ Oid partitionOid;
+ Oid parentTblOid;
+ bool lockedParentTbl;
+};
+
+static void
+RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid,
+ void *arg)
+{
+ struct AttachIndexCallbackState *state;
+ Form_pg_class classform;
+ HeapTuple tuple;
+
+ state = (struct AttachIndexCallbackState *) arg;
+
+ if (!state->lockedParentTbl)
+ {
+ LockRelationOid(state->parentTblOid, AccessShareLock);
+ state->lockedParentTbl = true;
+ }
+
+ /*
+ * If we previously locked some other heap, and the name we're looking up
+ * no longer refers to an index on that relation, release the now-useless
+ * lock. XXX maybe we should do *after* we verify whether the index does
+ * not actually belong to the same relation ...
+ */
+ if (relOid != oldRelOid && OidIsValid(state->partitionOid))
+ {
+ UnlockRelationOid(state->partitionOid, AccessShareLock);
+ state->partitionOid = InvalidOid;
+ }
+
+ /* Didn't find a relation, so no need for locking or permission checks. */
+ if (!OidIsValid(relOid))
+ return;
+
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+ if (!HeapTupleIsValid(tuple))
+ return; /* concurrently dropped, so nothing to do */
+ classform = (Form_pg_class) GETSTRUCT(tuple);
+ if (classform->relkind != RELKIND_PARTITIONED_INDEX &&
+ classform->relkind != RELKIND_INDEX)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("\"%s\" is not an index", rv->relname)));
+ ReleaseSysCache(tuple);
+
+ /*
+ * Since we need only examine the heap's tupledesc, an access share lock
+ * on it (preventing any DDL) is sufficient.
+ */
+ state->partitionOid = IndexGetRelation(relOid, false);
+ LockRelationOid(state->partitionOid, AccessShareLock);
+}
+
+/*
+ * Verify whether the given partition already contains an index attached
+ * to the given partitioned index. If so, raise an error.
+ */
+static void
+refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl)
+{
+ Oid existingIdx;
+
+ existingIdx = index_get_partition(partitionTbl,
+ RelationGetRelid(parentIdx));
+ if (OidIsValid(existingIdx))
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentIdx)),
+ errdetail("Another index \"%s\" is already attached for partition \"%s\".",
+ get_rel_name(existingIdx),
+ RelationGetRelationName(partitionTbl))));
+}
+
+/*
+ * When attaching an index as a partition of a partitioned index which is a
+ * primary key, verify that all the columns in the partition are marked NOT
+ * NULL.
+ */
+static void
+verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
+{
+ for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
+ iinfo->ii_IndexAttrNumbers[i] - 1);
+
+ if (!att->attnotnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("invalid primary key definition"),
+ errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.",
+ NameStr(att->attname),
+ RelationGetRelationName(partition)));
+ }
+}
+
+/*
+ * Verify whether the set of attached partition indexes to a parent index on
+ * a partitioned table is complete. If it is, mark the parent index valid.
+ *
+ * This should be called each time a partition index is attached.
+ */
+static void
+validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ int tuples = 0;
+ HeapTuple inhTup;
+ bool updated = false;
+
+ Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX);
+
+ /*
+ * Scan pg_inherits for this parent index. Count each valid index we find
+ * (verifying the pg_index entry for each), and if we reach the total
+ * amount we expect, we can mark this parent index as valid.
+ */
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key, Anum_pg_inherits_inhparent,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(partedIdx)));
+ scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true,
+ NULL, 1, &key);
+ while ((inhTup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup);
+ HeapTuple indTup;
+ Form_pg_index indexForm;
+
+ indTup = SearchSysCache1(INDEXRELID,
+ ObjectIdGetDatum(inhForm->inhrelid));
+ if (!HeapTupleIsValid(indTup))
+ elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid);
+ indexForm = (Form_pg_index) GETSTRUCT(indTup);
+ if (indexForm->indisvalid)
+ tuples += 1;
+ ReleaseSysCache(indTup);
+ }
+
+ /* Done with pg_inherits */
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ /*
+ * If we found as many inherited indexes as the partitioned table has
+ * partitions, we're good; update pg_index to set indisvalid.
+ */
+ if (tuples == RelationGetPartitionDesc(partedTbl, true)->nparts)
+ {
+ Relation idxRel;
+ HeapTuple indTup;
+ Form_pg_index indexForm;
+
+ idxRel = table_open(IndexRelationId, RowExclusiveLock);
+ indTup = SearchSysCacheCopy1(INDEXRELID,
+ ObjectIdGetDatum(RelationGetRelid(partedIdx)));
+ if (!HeapTupleIsValid(indTup))
+ elog(ERROR, "cache lookup failed for index %u",
+ RelationGetRelid(partedIdx));
+ indexForm = (Form_pg_index) GETSTRUCT(indTup);
+
+ indexForm->indisvalid = true;
+ updated = true;
+
+ CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
+
+ table_close(idxRel, RowExclusiveLock);
+ heap_freetuple(indTup);
+ }
+
+ /*
+ * If this index is in turn a partition of a larger index, validating it
+ * might cause the parent to become valid also. Try that.
+ */
+ if (updated && partedIdx->rd_rel->relispartition)
+ {
+ Oid parentIdxId,
+ parentTblId;
+ Relation parentIdx,
+ parentTbl;
+
+ /* make sure we see the validation we just did */
+ CommandCounterIncrement();
+
+ parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false);
+ parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false);
+ parentIdx = relation_open(parentIdxId, AccessExclusiveLock);
+ parentTbl = relation_open(parentTblId, AccessExclusiveLock);
+ Assert(!parentIdx->rd_index->indisvalid);
+
+ validatePartitionedIndex(parentIdx, parentTbl);
+
+ relation_close(parentIdx, AccessExclusiveLock);
+ relation_close(parentTbl, AccessExclusiveLock);
+ }
+}
+
+/*
+ * ALTER INDEX i1 ATTACH PARTITION i2
+ */
+ObjectAddress
+ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
+{
+ Relation partIdx;
+ Relation partTbl;
+ Relation parentTbl;
+ ObjectAddress address;
+ Oid partIdxId;
+ Oid currParent;
+ struct AttachIndexCallbackState state;
+
+ /*
+ * We need to obtain lock on the index 'name' to modify it, but we also
+ * need to read its owning table's tuple descriptor -- so we need to lock
+ * both. To avoid deadlocks, obtain lock on the table before doing so on
+ * the index. Furthermore, we need to examine the parent table of the
+ * partition, so lock that one too.
+ */
+ state.partitionOid = InvalidOid;
+ state.parentTblOid = parentIdx->rd_index->indrelid;
+ state.lockedParentTbl = false;
+ partIdxId =
+ RangeVarGetRelidExtended(name, AccessExclusiveLock, 0,
+ RangeVarCallbackForAttachIndex,
+ &state);
+ /* Not there? */
+ if (!OidIsValid(partIdxId))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" does not exist", name->relname)));
+
+ /* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */
+ partIdx = relation_open(partIdxId, AccessExclusiveLock);
+
+ /* we already hold locks on both tables, so this is safe: */
+ parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock);
+ partTbl = relation_open(partIdx->rd_index->indrelid, NoLock);
+
+ ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx));
+
+ /* Silently do nothing if already in the right state */
+ currParent = partIdx->rd_rel->relispartition ?
+ get_partition_parent(partIdxId, false) : InvalidOid;
+ if (currParent != RelationGetRelid(parentIdx))
+ {
+ IndexInfo *childInfo;
+ IndexInfo *parentInfo;
+ AttrMap *attmap;
+ bool found;
+ int i;
+ PartitionDesc partDesc;
+ Oid constraintOid,
+ cldConstrId = InvalidOid;
+
+ /*
+ * If this partition already has an index attached, refuse the
+ * operation.
+ */
+ refuseDupeIndexAttach(parentIdx, partIdx, partTbl);
+
+ if (OidIsValid(currParent))
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentIdx)),
+ errdetail("Index \"%s\" is already attached to another index.",
+ RelationGetRelationName(partIdx))));
+
+ /* Make sure it indexes a partition of the other index's table */
+ partDesc = RelationGetPartitionDesc(parentTbl, true);
+ found = false;
+ for (i = 0; i < partDesc->nparts; i++)
+ {
+ if (partDesc->oids[i] == state.partitionOid)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentIdx)),
+ errdetail("Index \"%s\" is not an index on any partition of table \"%s\".",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentTbl))));
+
+ /* Ensure the indexes are compatible */
+ childInfo = BuildIndexInfo(partIdx);
+ parentInfo = BuildIndexInfo(parentIdx);
+ attmap = build_attrmap_by_name(RelationGetDescr(partTbl),
+ RelationGetDescr(parentTbl),
+ false);
+ if (!CompareIndexInfo(childInfo, parentInfo,
+ partIdx->rd_indcollation,
+ parentIdx->rd_indcollation,
+ partIdx->rd_opfamily,
+ parentIdx->rd_opfamily,
+ attmap))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentIdx)),
+ errdetail("The index definitions do not match.")));
+
+ /*
+ * If there is a constraint in the parent, make sure there is one in
+ * the child too.
+ */
+ constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(parentTbl),
+ RelationGetRelid(parentIdx));
+
+ if (OidIsValid(constraintOid))
+ {
+ cldConstrId = get_relation_idx_constraint_oid(RelationGetRelid(partTbl),
+ partIdxId);
+ if (!OidIsValid(cldConstrId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
+ RelationGetRelationName(partIdx),
+ RelationGetRelationName(parentIdx)),
+ errdetail("The index \"%s\" belongs to a constraint in table \"%s\" but no constraint exists for index \"%s\".",
+ RelationGetRelationName(parentIdx),
+ RelationGetRelationName(parentTbl),
+ RelationGetRelationName(partIdx))));
+ }
+
+ /*
+ * If it's a primary key, make sure the columns in the partition are
+ * NOT NULL.
+ */
+ if (parentIdx->rd_index->indisprimary)
+ verifyPartitionIndexNotNull(childInfo, partTbl);
+
+ /* All good -- do it */
+ IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx));
+ if (OidIsValid(constraintOid))
+ ConstraintSetParentConstraint(cldConstrId, constraintOid,
+ RelationGetRelid(partTbl));
+
+ free_attrmap(attmap);
+
+ validatePartitionedIndex(parentIdx, parentTbl);
+ }
+
+ relation_close(parentTbl, AccessShareLock);
+ /* keep these locks till commit */
+ relation_close(partTbl, NoLock);
+ relation_close(partIdx, NoLock);
+
+ return address;
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 23ebaa3f230..c93d022aa6d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -61,6 +61,7 @@
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/sequence.h"
+#include "commands/partcmds.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/trigger.h"
@@ -132,84 +133,6 @@ typedef struct OnCommitItem
static List *on_commits = NIL;
-/*
- * State information for ALTER TABLE
- *
- * The pending-work queue for an ALTER TABLE is a List of AlteredTableInfo
- * structs, one for each table modified by the operation (the named table
- * plus any child tables that are affected). We save lists of subcommands
- * to apply to this table (possibly modified by parse transformation steps);
- * these lists will be executed in Phase 2. If a Phase 3 step is needed,
- * necessary information is stored in the constraints and newvals lists.
- *
- * Phase 2 is divided into multiple passes; subcommands are executed in
- * a pass determined by subcommand type.
- */
-
-typedef enum AlterTablePass
-{
- AT_PASS_UNSET = -1, /* UNSET will cause ERROR */
- AT_PASS_DROP, /* DROP (all flavors) */
- AT_PASS_ALTER_TYPE, /* ALTER COLUMN TYPE */
- AT_PASS_ADD_COL, /* ADD COLUMN */
- AT_PASS_SET_EXPRESSION, /* ALTER SET EXPRESSION */
- AT_PASS_OLD_INDEX, /* re-add existing indexes */
- AT_PASS_OLD_CONSTR, /* re-add existing constraints */
- /* We could support a RENAME COLUMN pass here, but not currently used */
- AT_PASS_ADD_CONSTR, /* ADD constraints (initial examination) */
- AT_PASS_COL_ATTRS, /* set column attributes, eg NOT NULL */
- AT_PASS_ADD_INDEXCONSTR, /* ADD index-based constraints */
- AT_PASS_ADD_INDEX, /* ADD indexes */
- AT_PASS_ADD_OTHERCONSTR, /* ADD other constraints, defaults */
- AT_PASS_MISC, /* other stuff */
-} AlterTablePass;
-
-#define AT_NUM_PASSES (AT_PASS_MISC + 1)
-
-typedef struct AlteredTableInfo
-{
- /* Information saved before any work commences: */
- Oid relid; /* Relation to work on */
- char relkind; /* Its relkind */
- TupleDesc oldDesc; /* Pre-modification tuple descriptor */
-
- /*
- * Transiently set during Phase 2, normally set to NULL.
- *
- * ATRewriteCatalogs sets this when it starts, and closes when ATExecCmd
- * returns control. This can be exploited by ATExecCmd subroutines to
- * close/reopen across transaction boundaries.
- */
- Relation rel;
-
- /* Information saved by Phase 1 for Phase 2: */
- List *subcmds[AT_NUM_PASSES]; /* Lists of AlterTableCmd */
- /* Information saved by Phases 1/2 for Phase 3: */
- List *constraints; /* List of NewConstraint */
- List *newvals; /* List of NewColumnValue */
- List *afterStmts; /* List of utility command parsetrees */
- bool verify_new_notnull; /* T if we should recheck NOT NULL */
- int rewrite; /* Reason for forced rewrite, if any */
- bool chgAccessMethod; /* T if SET ACCESS METHOD is used */
- Oid newAccessMethod; /* new access method; 0 means no change,
- * if above is true */
- Oid newTableSpace; /* new tablespace; 0 means no change */
- bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
- char newrelpersistence; /* if above is true */
- Expr *partition_constraint; /* for attach partition validation */
- /* true, if validating default due to some other attach/detach */
- bool validate_default;
- /* Objects to rebuild after completing ALTER TYPE operations */
- List *changedConstraintOids; /* OIDs of constraints to rebuild */
- List *changedConstraintDefs; /* string definitions of same */
- List *changedIndexOids; /* OIDs of indexes to rebuild */
- List *changedIndexDefs; /* string definitions of same */
- char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */
- char *clusterOnIndex; /* index to use for CLUSTER */
- List *changedStatisticsOids; /* OIDs of statistics to rebuild */
- List *changedStatisticsDefs; /* string definitions of same */
-} AlteredTableInfo;
-
/* Struct describing one new constraint to check in Phase 3 scan */
/* Note: new not-null constraints are handled elsewhere */
typedef struct NewConstraint
@@ -325,17 +248,6 @@ struct DropRelationCallbackState
char actual_relpersistence;
};
-/* Alter table target-type flags for ATSimplePermissions */
-#define ATT_TABLE 0x0001
-#define ATT_VIEW 0x0002
-#define ATT_MATVIEW 0x0004
-#define ATT_INDEX 0x0008
-#define ATT_COMPOSITE_TYPE 0x0010
-#define ATT_FOREIGN_TABLE 0x0020
-#define ATT_PARTITIONED_INDEX 0x0040
-#define ATT_SEQUENCE 0x0080
-#define ATT_PARTITIONED_TABLE 0x0100
-
/*
* ForeignTruncateInfo
*
@@ -350,14 +262,6 @@ typedef struct ForeignTruncateInfo
List *rels;
} ForeignTruncateInfo;
-/* Partial or complete FK creation in addFkConstraint() */
-typedef enum addFkConstraintSides
-{
- addFkReferencedSide,
- addFkReferencingSide,
- addFkBothSides,
-} addFkConstraintSides;
-
/*
* Partition tables are expected to be dropped when the parent partitioned
* table gets dropped. Hence for partitioning we use AUTO dependency.
@@ -431,8 +335,6 @@ static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation
static ObjectAddress ATExecValidateConstraint(List **wqueue,
Relation rel, char *constrName,
bool recurse, bool recursing, LOCKMODE lockmode);
-static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel,
- Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode);
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
@@ -476,8 +378,6 @@ static void ATRewriteTables(AlterTableStmt *parsetree,
List **wqueue, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static void ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap);
-static AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel);
-static void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets);
static void ATSimpleRecursion(List **wqueue, Relation rel,
AlterTableCmd *cmd, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -508,8 +408,6 @@ static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
bool recurse, bool recursing,
LOCKMODE lockmode);
static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
-static bool ConstraintImpliedByRelConstraint(Relation scanrel,
- List *testConstraint, List *provenConstraint);
static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
Node *newDefault, LOCKMODE lockmode);
static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
@@ -518,8 +416,6 @@ static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
-static ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
- bool recurse, bool recursing);
static ObjectAddress ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
Node *newExpr, LOCKMODE lockmode);
static void ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode);
@@ -565,37 +461,6 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
static int validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, int16 *fksetcolsattnums,
List *fksetcols);
-static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
- char *constraintname,
- Constraint *fkconstraint, Relation rel,
- Relation pkrel, Oid indexOid,
- Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators,
- Oid *ffeqoperators, int numfkdelsetcols,
- int16 *fkdelsetcols, bool is_internal,
- bool with_period);
-static void addFkRecurseReferenced(Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger,
- bool with_period);
-static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
- Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
- int numfks, int16 *pkattnum, int16 *fkattnum,
- Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok, LOCKMODE lockmode,
- Oid parentInsTrigger, Oid parentUpdTrigger,
- bool with_period);
-static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
- Relation partitionRel);
-static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
-static void CloneFkReferencing(List **wqueue, Relation parentRel,
- Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
Oid indexOid,
@@ -606,31 +471,8 @@ static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid,
Oid indexOid,
Oid parentDelTrigger, Oid parentUpdTrigger,
Oid *deleteTrigOid, Oid *updateTrigOid);
-static bool tryAttachPartitionForeignKey(List **wqueue,
- ForeignKeyCacheInfo *fk,
- Relation partition,
- Oid parentConstrOid, int numfks,
- AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop,
- Oid parentInsTrigger,
- Oid parentUpdTrigger,
- Relation trigrel);
-static void AttachPartitionForeignKey(List **wqueue, Relation partition,
- Oid partConstrOid, Oid parentConstrOid,
- Oid parentInsTrigger, Oid parentUpdTrigger,
- Relation trigrel);
-static void RemoveInheritedConstraint(Relation conrel, Relation trigrel,
- Oid conoid, Oid conrelid);
static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
Oid confrelid, Oid conrelid);
-static void GetForeignKeyActionTriggers(Relation trigrel,
- Oid conoid, Oid confrelid, Oid conrelid,
- Oid *deleteTriggerOid,
- Oid *updateTriggerOid);
-static void GetForeignKeyCheckTriggers(Relation trigrel,
- Oid conoid, Oid confrelid, Oid conrelid,
- Oid *insertTriggerOid,
- Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior, bool recurse,
bool missing_ok, LOCKMODE lockmode);
@@ -707,36 +549,6 @@ static void RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid,
Oid oldRelOid, void *arg);
static void RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid,
Oid oldrelid, void *arg);
-static PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec);
-static void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
- List **partexprs, Oid *partopclass, Oid *partcollation,
- PartitionStrategy strategy);
-static void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition);
-static void RemoveInheritance(Relation child_rel, Relation parent_rel,
- bool expect_detached);
-static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
- PartitionCmd *cmd,
- AlterTableUtilityContext *context);
-static void AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel);
-static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
- List *partConstraint,
- bool validate_default);
-static void CloneRowTriggersToPartition(Relation parent, Relation partition);
-static void DropClonedTriggersFromPartition(Oid partitionId);
-static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
- Relation rel, RangeVar *name,
- bool concurrent);
-static void DetachPartitionFinalize(Relation rel, Relation partRel,
- bool concurrent, Oid defaultPartOid);
-static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
-static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
- RangeVar *name);
-static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
-static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
- Relation partitionTbl);
-static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition);
-static List *GetParentedForeignKeyRefs(Relation partition);
-static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
@@ -6550,7 +6362,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
/*
* ATGetQueueEntry: find or create an entry in the ALTER TABLE work queue
*/
-static AlteredTableInfo *
+AlteredTableInfo *
ATGetQueueEntry(List **wqueue, Relation rel)
{
Oid relid = RelationGetRelid(rel);
@@ -6727,7 +6539,7 @@ alter_table_type_to_string(AlterTableType cmdtype)
* - Ensure this user is the owner
* - Ensure that it is not a system table
*/
-static void
+void
ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets)
{
int actual_target;
@@ -8476,7 +8288,7 @@ ATExecSetIdentity(Relation rel, const char *colName, Node *def,
*
* Return the address of the affected column.
*/
-static ObjectAddress
+ObjectAddress
ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
bool recurse, bool recursing)
{
@@ -10710,7 +10522,7 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* NULL/DEFAULT clause
* with_period: true if this is a temporal FK
*/
-static ObjectAddress
+ObjectAddress
addFkConstraint(addFkConstraintSides fkside,
char *constraintname, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
@@ -10888,7 +10700,7 @@ addFkConstraint(addFkConstraintSides fkside,
* UPDATE respectively.
* with_period: true if this is a temporal FK
*/
-static void
+void
addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
@@ -11026,7 +10838,7 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel,
* UPDATE respectively.
* with_period: true if this is a temporal FK
*/
-static void
+void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -11192,795 +11004,6 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
}
}
-/*
- * CloneForeignKeyConstraints
- * Clone foreign keys from a partitioned table to a newly acquired
- * partition.
- *
- * partitionRel is a partition of parentRel, so we can be certain that it has
- * the same columns with the same datatypes. The columns may be in different
- * order, though.
- *
- * wqueue must be passed to set up phase 3 constraint checking, unless the
- * referencing-side partition is known to be empty (such as in CREATE TABLE /
- * PARTITION OF).
- */
-static void
-CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
- Relation partitionRel)
-{
- /* This only works for declarative partitioning */
- Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
-
- /*
- * First, clone constraints where the parent is on the referencing side.
- */
- CloneFkReferencing(wqueue, parentRel, partitionRel);
-
- /*
- * Clone constraints for which the parent is on the referenced side.
- */
- CloneFkReferenced(parentRel, partitionRel);
-}
-
-/*
- * CloneFkReferenced
- * Subroutine for CloneForeignKeyConstraints
- *
- * Find all the FKs that have the parent relation on the referenced side;
- * clone those constraints to the given partition. This is to be called
- * when the partition is being created or attached.
- *
- * This recurses to partitions, if the relation being attached is partitioned.
- * Recursion is done by calling addFkRecurseReferenced.
- */
-static void
-CloneFkReferenced(Relation parentRel, Relation partitionRel)
-{
- Relation pg_constraint;
- AttrMap *attmap;
- ListCell *cell;
- SysScanDesc scan;
- ScanKeyData key[2];
- HeapTuple tuple;
- List *clone = NIL;
- Relation trigrel;
-
- /*
- * Search for any constraints where this partition's parent is in the
- * referenced side. However, we must not clone any constraint whose
- * parent constraint is also going to be cloned, to avoid duplicates. So
- * do it in two steps: first construct the list of constraints to clone,
- * then go over that list cloning those whose parents are not in the list.
- * (We must not rely on the parent being seen first, since the catalog
- * scan could return children first.)
- */
- pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ScanKeyInit(&key[0],
- Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
- F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel)));
- ScanKeyInit(&key[1],
- Anum_pg_constraint_contype, BTEqualStrategyNumber,
- F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
- /* This is a seqscan, as we don't have a usable index ... */
- scan = systable_beginscan(pg_constraint, InvalidOid, true,
- NULL, 2, key);
- while ((tuple = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
-
- clone = lappend_oid(clone, constrForm->oid);
- }
- systable_endscan(scan);
- table_close(pg_constraint, RowShareLock);
-
- /*
- * Triggers of the foreign keys will be manipulated a bunch of times in
- * the loop below. To avoid repeatedly opening/closing the trigger
- * catalog relation, we open it here and pass it to the subroutines called
- * below.
- */
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
- RelationGetDescr(parentRel),
- false);
- foreach(cell, clone)
- {
- Oid constrOid = lfirst_oid(cell);
- Form_pg_constraint constrForm;
- Relation fkRel;
- Oid indexOid;
- Oid partIndexId;
- int numfks;
- AttrNumber conkey[INDEX_MAX_KEYS];
- AttrNumber mapped_confkey[INDEX_MAX_KEYS];
- AttrNumber confkey[INDEX_MAX_KEYS];
- Oid conpfeqop[INDEX_MAX_KEYS];
- Oid conppeqop[INDEX_MAX_KEYS];
- Oid conffeqop[INDEX_MAX_KEYS];
- int numfkdelsetcols;
- AttrNumber confdelsetcols[INDEX_MAX_KEYS];
- Constraint *fkconstraint;
- ObjectAddress address;
- Oid deleteTriggerOid = InvalidOid,
- updateTriggerOid = InvalidOid;
-
- tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for constraint %u", constrOid);
- constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
-
- /*
- * As explained above: don't try to clone a constraint for which we're
- * going to clone the parent.
- */
- if (list_member_oid(clone, constrForm->conparentid))
- {
- ReleaseSysCache(tuple);
- continue;
- }
-
- /* We need the same lock level that CreateTrigger will acquire */
- fkRel = table_open(constrForm->conrelid, ShareRowExclusiveLock);
-
- indexOid = constrForm->conindid;
- DeconstructFkConstraintRow(tuple,
- &numfks,
- conkey,
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- &numfkdelsetcols,
- confdelsetcols);
-
- for (int i = 0; i < numfks; i++)
- mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
-
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = NameStr(constrForm->conname);
- fkconstraint->deferrable = constrForm->condeferrable;
- fkconstraint->initdeferred = constrForm->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- /* ->fk_attrs determined below */
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = constrForm->confmatchtype;
- fkconstraint->fk_upd_action = constrForm->confupdtype;
- fkconstraint->fk_del_action = constrForm->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->is_enforced = constrForm->conenforced;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = constrForm->convalidated;
-
- /* set up colnames that are used to generate the constraint name */
- for (int i = 0; i < numfks; i++)
- {
- Form_pg_attribute att;
-
- att = TupleDescAttr(RelationGetDescr(fkRel),
- conkey[i] - 1);
- fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
- makeString(NameStr(att->attname)));
- }
-
- /*
- * Add the new foreign key constraint pointing to the new partition.
- * Because this new partition appears in the referenced side of the
- * constraint, we don't need to set up for Phase 3 check.
- */
- partIndexId = index_get_partition(partitionRel, indexOid);
- if (!OidIsValid(partIndexId))
- elog(ERROR, "index for %u not found in partition %s",
- indexOid, RelationGetRelationName(partitionRel));
-
- /*
- * Get the "action" triggers belonging to the constraint to pass as
- * parent OIDs for similar triggers that will be created on the
- * partition in addFkRecurseReferenced().
- */
- if (constrForm->conenforced)
- GetForeignKeyActionTriggers(trigrel, constrOid,
- constrForm->confrelid, constrForm->conrelid,
- &deleteTriggerOid, &updateTriggerOid);
-
- /* Add this constraint ... */
- address = addFkConstraint(addFkReferencedSide,
- fkconstraint->conname, fkconstraint, fkRel,
- partitionRel, partIndexId, constrOid,
- numfks, mapped_confkey,
- conkey, conpfeqop, conppeqop, conffeqop,
- numfkdelsetcols, confdelsetcols, false,
- constrForm->conperiod);
- /* ... and recurse */
- addFkRecurseReferenced(fkconstraint,
- fkRel,
- partitionRel,
- partIndexId,
- address.objectId,
- numfks,
- mapped_confkey,
- conkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfkdelsetcols,
- confdelsetcols,
- true,
- deleteTriggerOid,
- updateTriggerOid,
- constrForm->conperiod);
-
- table_close(fkRel, NoLock);
- ReleaseSysCache(tuple);
- }
-
- table_close(trigrel, RowExclusiveLock);
-}
-
-/*
- * CloneFkReferencing
- * Subroutine for CloneForeignKeyConstraints
- *
- * For each FK constraint of the parent relation in the given list, find an
- * equivalent constraint in its partition relation that can be reparented;
- * if one cannot be found, create a new constraint in the partition as its
- * child.
- *
- * If wqueue is given, it is used to set up phase-3 verification for each
- * cloned constraint; omit it if such verification is not needed
- * (example: the partition is being created anew).
- */
-static void
-CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
-{
- AttrMap *attmap;
- List *partFKs;
- List *clone = NIL;
- ListCell *cell;
- Relation trigrel;
-
- /* obtain a list of constraints that we need to clone */
- foreach(cell, RelationGetFKeyList(parentRel))
- {
- ForeignKeyCacheInfo *fk = lfirst(cell);
-
- /*
- * Refuse to attach a table as partition that this partitioned table
- * already has a foreign key to. This isn't useful schema, which is
- * proven by the fact that there have been no user complaints that
- * it's already impossible to achieve this in the opposite direction,
- * i.e., creating a foreign key that references a partition. This
- * restriction allows us to dodge some complexities around
- * pg_constraint and pg_trigger row creations that would be needed
- * during ATTACH/DETACH for this kind of relationship.
- */
- if (fk->confrelid == RelationGetRelid(partRel))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot attach table \"%s\" as a partition because it is referenced by foreign key \"%s\"",
- RelationGetRelationName(partRel),
- get_constraint_name(fk->conoid))));
-
- clone = lappend_oid(clone, fk->conoid);
- }
-
- /*
- * Silently do nothing if there's nothing to do. In particular, this
- * avoids throwing a spurious error for foreign tables.
- */
- if (clone == NIL)
- return;
-
- if (partRel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("foreign key constraints are not supported on foreign tables")));
-
- /*
- * Triggers of the foreign keys will be manipulated a bunch of times in
- * the loop below. To avoid repeatedly opening/closing the trigger
- * catalog relation, we open it here and pass it to the subroutines called
- * below.
- */
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
- * The constraint key may differ, if the columns in the partition are
- * different. This map is used to convert them.
- */
- attmap = build_attrmap_by_name(RelationGetDescr(partRel),
- RelationGetDescr(parentRel),
- false);
-
- partFKs = copyObject(RelationGetFKeyList(partRel));
-
- foreach(cell, clone)
- {
- Oid parentConstrOid = lfirst_oid(cell);
- Form_pg_constraint constrForm;
- Relation pkrel;
- HeapTuple tuple;
- int numfks;
- AttrNumber conkey[INDEX_MAX_KEYS];
- AttrNumber mapped_conkey[INDEX_MAX_KEYS];
- AttrNumber confkey[INDEX_MAX_KEYS];
- Oid conpfeqop[INDEX_MAX_KEYS];
- Oid conppeqop[INDEX_MAX_KEYS];
- Oid conffeqop[INDEX_MAX_KEYS];
- int numfkdelsetcols;
- AttrNumber confdelsetcols[INDEX_MAX_KEYS];
- Constraint *fkconstraint;
- bool attached;
- Oid indexOid;
- ObjectAddress address;
- ListCell *lc;
- Oid insertTriggerOid = InvalidOid,
- updateTriggerOid = InvalidOid;
- bool with_period;
-
- tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for constraint %u",
- parentConstrOid);
- constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
-
- /* Don't clone constraints whose parents are being cloned */
- if (list_member_oid(clone, constrForm->conparentid))
- {
- ReleaseSysCache(tuple);
- continue;
- }
-
- /*
- * Need to prevent concurrent deletions. If pkrel is a partitioned
- * relation, that means to lock all partitions.
- */
- pkrel = table_open(constrForm->confrelid, ShareRowExclusiveLock);
- if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- (void) find_all_inheritors(RelationGetRelid(pkrel),
- ShareRowExclusiveLock, NULL);
-
- DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey,
- conpfeqop, conppeqop, conffeqop,
- &numfkdelsetcols, confdelsetcols);
- for (int i = 0; i < numfks; i++)
- mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
-
- /*
- * Get the "check" triggers belonging to the constraint, if it is
- * ENFORCED, to pass as parent OIDs for similar triggers that will be
- * created on the partition in addFkRecurseReferencing(). They are
- * also passed to tryAttachPartitionForeignKey() below to simply
- * assign as parents to the partition's existing "check" triggers,
- * that is, if the corresponding constraints is deemed attachable to
- * the parent constraint.
- */
- if (constrForm->conenforced)
- GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
- constrForm->confrelid, constrForm->conrelid,
- &insertTriggerOid, &updateTriggerOid);
-
- /*
- * Before creating a new constraint, see whether any existing FKs are
- * fit for the purpose. If one is, attach the parent constraint to
- * it, and don't clone anything. This way we avoid the expensive
- * verification step and don't end up with a duplicate FK, and we
- * don't need to recurse to partitions for this constraint.
- */
- attached = false;
- foreach(lc, partFKs)
- {
- ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc);
-
- if (tryAttachPartitionForeignKey(wqueue,
- fk,
- partRel,
- parentConstrOid,
- numfks,
- mapped_conkey,
- confkey,
- conpfeqop,
- insertTriggerOid,
- updateTriggerOid,
- trigrel))
- {
- attached = true;
- table_close(pkrel, NoLock);
- break;
- }
- }
- if (attached)
- {
- ReleaseSysCache(tuple);
- continue;
- }
-
- /* No dice. Set up to create our own constraint */
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- /* ->conname determined below */
- fkconstraint->deferrable = constrForm->condeferrable;
- fkconstraint->initdeferred = constrForm->condeferred;
- fkconstraint->location = -1;
- fkconstraint->pktable = NULL;
- /* ->fk_attrs determined below */
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = constrForm->confmatchtype;
- fkconstraint->fk_upd_action = constrForm->confupdtype;
- fkconstraint->fk_del_action = constrForm->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->is_enforced = constrForm->conenforced;
- fkconstraint->skip_validation = false;
- fkconstraint->initially_valid = constrForm->convalidated;
- for (int i = 0; i < numfks; i++)
- {
- Form_pg_attribute att;
-
- att = TupleDescAttr(RelationGetDescr(partRel),
- mapped_conkey[i] - 1);
- fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
- makeString(NameStr(att->attname)));
- }
-
- indexOid = constrForm->conindid;
- with_period = constrForm->conperiod;
-
- /* Create the pg_constraint entry at this level */
- address = addFkConstraint(addFkReferencingSide,
- NameStr(constrForm->conname), fkconstraint,
- partRel, pkrel, indexOid, parentConstrOid,
- numfks, confkey,
- mapped_conkey, conpfeqop,
- conppeqop, conffeqop,
- numfkdelsetcols, confdelsetcols,
- false, with_period);
-
- /* Done with the cloned constraint's tuple */
- ReleaseSysCache(tuple);
-
- /* Create the check triggers, and recurse to partitions, if any */
- addFkRecurseReferencing(wqueue,
- fkconstraint,
- partRel,
- pkrel,
- indexOid,
- address.objectId,
- numfks,
- confkey,
- mapped_conkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfkdelsetcols,
- confdelsetcols,
- false, /* no old check exists */
- AccessExclusiveLock,
- insertTriggerOid,
- updateTriggerOid,
- with_period);
- table_close(pkrel, NoLock);
- }
-
- table_close(trigrel, RowExclusiveLock);
-}
-
-/*
- * When the parent of a partition receives [the referencing side of] a foreign
- * key, we must propagate that foreign key to the partition. However, the
- * partition might already have an equivalent foreign key; this routine
- * compares the given ForeignKeyCacheInfo (in the partition) to the FK defined
- * by the other parameters. If they are equivalent, create the link between
- * the two constraints and return true.
- *
- * If the given FK does not match the one defined by rest of the params,
- * return false.
- */
-static bool
-tryAttachPartitionForeignKey(List **wqueue,
- ForeignKeyCacheInfo *fk,
- Relation partition,
- Oid parentConstrOid,
- int numfks,
- AttrNumber *mapped_conkey,
- AttrNumber *confkey,
- Oid *conpfeqop,
- Oid parentInsTrigger,
- Oid parentUpdTrigger,
- Relation trigrel)
-{
- HeapTuple parentConstrTup;
- Form_pg_constraint parentConstr;
- HeapTuple partcontup;
- Form_pg_constraint partConstr;
-
- parentConstrTup = SearchSysCache1(CONSTROID,
- ObjectIdGetDatum(parentConstrOid));
- if (!HeapTupleIsValid(parentConstrTup))
- elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
- parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
-
- /*
- * Do some quick & easy initial checks. If any of these fail, we cannot
- * use this constraint.
- */
- if (fk->confrelid != parentConstr->confrelid || fk->nkeys != numfks)
- {
- ReleaseSysCache(parentConstrTup);
- return false;
- }
- for (int i = 0; i < numfks; i++)
- {
- if (fk->conkey[i] != mapped_conkey[i] ||
- fk->confkey[i] != confkey[i] ||
- fk->conpfeqop[i] != conpfeqop[i])
- {
- ReleaseSysCache(parentConstrTup);
- return false;
- }
- }
-
- /* Looks good so far; perform more extensive checks. */
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
- if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
- partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
-
- /*
- * An error should be raised if the constraint enforceability is
- * different. Returning false without raising an error, as we do for other
- * attributes, could lead to a duplicate constraint with the same
- * enforceability as the parent. While this may be acceptable, it may not
- * be ideal. Therefore, it's better to raise an error and allow the user
- * to correct the enforceability before proceeding.
- */
- if (partConstr->conenforced != parentConstr->conenforced)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"",
- NameStr(parentConstr->conname),
- NameStr(partConstr->conname),
- RelationGetRelationName(partition))));
-
- if (OidIsValid(partConstr->conparentid) ||
- partConstr->condeferrable != parentConstr->condeferrable ||
- partConstr->condeferred != parentConstr->condeferred ||
- partConstr->confupdtype != parentConstr->confupdtype ||
- partConstr->confdeltype != parentConstr->confdeltype ||
- partConstr->confmatchtype != parentConstr->confmatchtype)
- {
- ReleaseSysCache(parentConstrTup);
- ReleaseSysCache(partcontup);
- return false;
- }
-
- ReleaseSysCache(parentConstrTup);
- ReleaseSysCache(partcontup);
-
- /* Looks good! Attach this constraint. */
- AttachPartitionForeignKey(wqueue, partition, fk->conoid,
- parentConstrOid, parentInsTrigger,
- parentUpdTrigger, trigrel);
-
- return true;
-}
-
-/*
- * AttachPartitionForeignKey
- *
- * The subroutine for tryAttachPartitionForeignKey performs the final tasks of
- * attaching the constraint, removing redundant triggers and entries from
- * pg_constraint, and setting the constraint's parent.
- */
-static void
-AttachPartitionForeignKey(List **wqueue,
- Relation partition,
- Oid partConstrOid,
- Oid parentConstrOid,
- Oid parentInsTrigger,
- Oid parentUpdTrigger,
- Relation trigrel)
-{
- HeapTuple parentConstrTup;
- Form_pg_constraint parentConstr;
- HeapTuple partcontup;
- Form_pg_constraint partConstr;
- bool queueValidation;
- Oid partConstrFrelid;
- Oid partConstrRelid;
- bool parentConstrIsEnforced;
-
- /* Fetch the parent constraint tuple */
- parentConstrTup = SearchSysCache1(CONSTROID,
- ObjectIdGetDatum(parentConstrOid));
- if (!HeapTupleIsValid(parentConstrTup))
- elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid);
- parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup);
- parentConstrIsEnforced = parentConstr->conenforced;
-
- /* Fetch the child constraint tuple */
- partcontup = SearchSysCache1(CONSTROID,
- ObjectIdGetDatum(partConstrOid));
- if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
- partConstr = (Form_pg_constraint) GETSTRUCT(partcontup);
- partConstrFrelid = partConstr->confrelid;
- partConstrRelid = partConstr->conrelid;
-
- /*
- * If the referenced table is partitioned, then the partition we're
- * attaching now has extra pg_constraint rows and action triggers that are
- * no longer needed. Remove those.
- */
- if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE)
- {
- Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock);
-
- RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid,
- partConstrRelid);
-
- table_close(pg_constraint, RowShareLock);
- }
-
- /*
- * Will we need to validate this constraint? A valid parent constraint
- * implies that all child constraints have been validated, so if this one
- * isn't, we must trigger phase 3 validation.
- */
- queueValidation = parentConstr->convalidated && !partConstr->convalidated;
-
- ReleaseSysCache(partcontup);
- ReleaseSysCache(parentConstrTup);
-
- /*
- * The action triggers in the new partition become redundant -- the parent
- * table already has equivalent ones, and those will be able to reach the
- * partition. Remove the ones in the partition. We identify them because
- * they have our constraint OID, as well as being on the referenced rel.
- */
- DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid,
- partConstrRelid);
-
- ConstraintSetParentConstraint(partConstrOid, parentConstrOid,
- RelationGetRelid(partition));
-
- /*
- * Like the constraint, attach partition's "check" triggers to the
- * corresponding parent triggers if the constraint is ENFORCED. NOT
- * ENFORCED constraints do not have these triggers.
- */
- if (parentConstrIsEnforced)
- {
- Oid insertTriggerOid,
- updateTriggerOid;
-
- GetForeignKeyCheckTriggers(trigrel,
- partConstrOid, partConstrFrelid, partConstrRelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
- RelationGetRelid(partition));
- Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
- RelationGetRelid(partition));
- }
-
- /*
- * We updated this pg_constraint row above to set its parent; validating
- * it will cause its convalidated flag to change, so we need CCI here. In
- * addition, we need it unconditionally for the rare case where the parent
- * table has *two* identical constraints; when reaching this function for
- * the second one, we must have made our changes visible, otherwise we
- * would try to attach both to this one.
- */
- CommandCounterIncrement();
-
- /* If validation is needed, put it in the queue now. */
- if (queueValidation)
- {
- Relation conrel;
- Oid confrelid;
-
- conrel = table_open(ConstraintRelationId, RowExclusiveLock);
-
- partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid));
- if (!HeapTupleIsValid(partcontup))
- elog(ERROR, "cache lookup failed for constraint %u", partConstrOid);
-
- confrelid = ((Form_pg_constraint) GETSTRUCT(partcontup))->confrelid;
-
- /* Use the same lock as for AT_ValidateConstraint */
- QueueFKConstraintValidation(wqueue, conrel, partition, confrelid,
- partcontup, ShareUpdateExclusiveLock);
- ReleaseSysCache(partcontup);
- table_close(conrel, RowExclusiveLock);
- }
-}
-
-/*
- * RemoveInheritedConstraint
- *
- * Removes the constraint and its associated trigger from the specified
- * relation, which inherited the given constraint.
- */
-static void
-RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
- Oid conrelid)
-{
- ObjectAddresses *objs;
- HeapTuple consttup;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
-
- ScanKeyInit(&key,
- Anum_pg_constraint_conrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conrelid));
-
- scan = systable_beginscan(conrel,
- ConstraintRelidTypidNameIndexId,
- true, NULL, 1, &key);
- objs = new_object_addresses();
- while ((consttup = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup);
-
- if (conform->conparentid != conoid)
- continue;
- else
- {
- ObjectAddress addr;
- SysScanDesc scan2;
- ScanKeyData key2;
- int n PG_USED_FOR_ASSERTS_ONLY;
-
- ObjectAddressSet(addr, ConstraintRelationId, conform->oid);
- add_exact_object_address(&addr, objs);
-
- /*
- * First we must delete the dependency record that binds the
- * constraint records together.
- */
- n = deleteDependencyRecordsForSpecific(ConstraintRelationId,
- conform->oid,
- DEPENDENCY_INTERNAL,
- ConstraintRelationId,
- conoid);
- Assert(n == 1); /* actually only one is expected */
-
- /*
- * Now search for the triggers for this constraint and set them up
- * for deletion too
- */
- ScanKeyInit(&key2,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conform->oid));
- scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId,
- true, NULL, 1, &key2);
- while ((trigtup = systable_getnext(scan2)) != NULL)
- {
- ObjectAddressSet(addr, TriggerRelationId,
- ((Form_pg_trigger) GETSTRUCT(trigtup))->oid);
- add_exact_object_address(&addr, objs);
- }
- systable_endscan(scan2);
- }
- }
- /* make the dependency deletions visible */
- CommandCounterIncrement();
- performMultipleDeletions(objs, DROP_RESTRICT,
- PERFORM_DELETION_INTERNAL);
- systable_endscan(scan);
-}
-
/*
* DropForeignKeyConstraintTriggers
*
@@ -12054,128 +11077,6 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
systable_endscan(scan);
}
-/*
- * GetForeignKeyActionTriggers
- * Returns delete and update "action" triggers of the given relation
- * belonging to the given constraint
- */
-static void
-GetForeignKeyActionTriggers(Relation trigrel,
- Oid conoid, Oid confrelid, Oid conrelid,
- Oid *deleteTriggerOid,
- Oid *updateTriggerOid)
-{
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
-
- *deleteTriggerOid = *updateTriggerOid = InvalidOid;
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
-
- if (trgform->tgconstrrelid != conrelid)
- continue;
- if (trgform->tgrelid != confrelid)
- continue;
- /* Only ever look at "action" triggers on the PK side. */
- if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_PK)
- continue;
- if (TRIGGER_FOR_DELETE(trgform->tgtype))
- {
- Assert(*deleteTriggerOid == InvalidOid);
- *deleteTriggerOid = trgform->oid;
- }
- else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
- {
- Assert(*updateTriggerOid == InvalidOid);
- *updateTriggerOid = trgform->oid;
- }
-#ifndef USE_ASSERT_CHECKING
- /* In an assert-enabled build, continue looking to find duplicates */
- if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
- break;
-#endif
- }
-
- if (!OidIsValid(*deleteTriggerOid))
- elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
- conoid);
- if (!OidIsValid(*updateTriggerOid))
- elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
- conoid);
-
- systable_endscan(scan);
-}
-
-/*
- * GetForeignKeyCheckTriggers
- * Returns insert and update "check" triggers of the given relation
- * belonging to the given constraint
- */
-static void
-GetForeignKeyCheckTriggers(Relation trigrel,
- Oid conoid, Oid confrelid, Oid conrelid,
- Oid *insertTriggerOid,
- Oid *updateTriggerOid)
-{
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple trigtup;
-
- *insertTriggerOid = *updateTriggerOid = InvalidOid;
- ScanKeyInit(&key,
- Anum_pg_trigger_tgconstraint,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(conoid));
-
- scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
- NULL, 1, &key);
- while ((trigtup = systable_getnext(scan)) != NULL)
- {
- Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
-
- if (trgform->tgconstrrelid != confrelid)
- continue;
- if (trgform->tgrelid != conrelid)
- continue;
- /* Only ever look at "check" triggers on the FK side. */
- if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_FK)
- continue;
- if (TRIGGER_FOR_INSERT(trgform->tgtype))
- {
- Assert(*insertTriggerOid == InvalidOid);
- *insertTriggerOid = trgform->oid;
- }
- else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
- {
- Assert(*updateTriggerOid == InvalidOid);
- *updateTriggerOid = trgform->oid;
- }
-#ifndef USE_ASSERT_CHECKING
- /* In an assert-enabled build, continue looking to find duplicates. */
- if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
- break;
-#endif
- }
-
- if (!OidIsValid(*insertTriggerOid))
- elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
- conoid);
- if (!OidIsValid(*updateTriggerOid))
- elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
- conoid);
-
- systable_endscan(scan);
-}
-
/*
* ALTER TABLE ALTER CONSTRAINT
*
@@ -12986,7 +11887,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
* Phase 3 and update the convalidated field in the pg_constraint catalog
* for the specified relation and all its children.
*/
-static void
+void
QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel,
Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode)
{
@@ -17360,7 +16261,7 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
*
* Common to ATExecAddInherit() and ATExecAttachPartition().
*/
-static void
+void
CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition)
{
Relation catalogRelation;
@@ -17846,78 +16747,6 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
return address;
}
-/*
- * MarkInheritDetached
- *
- * Set inhdetachpending for a partition, for ATExecDetachPartition
- * in concurrent mode. While at it, verify that no other partition is
- * already pending detach.
- */
-static void
-MarkInheritDetached(Relation child_rel, Relation parent_rel)
-{
- Relation catalogRelation;
- SysScanDesc scan;
- ScanKeyData key;
- HeapTuple inheritsTuple;
- bool found = false;
-
- Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
-
- /*
- * Find pg_inherits entries by inhparent. (We need to scan them all in
- * order to verify that no other partition is pending detach.)
- */
- catalogRelation = table_open(InheritsRelationId, RowExclusiveLock);
- ScanKeyInit(&key,
- Anum_pg_inherits_inhparent,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(RelationGetRelid(parent_rel)));
- scan = systable_beginscan(catalogRelation, InheritsParentIndexId,
- true, NULL, 1, &key);
-
- while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan)))
- {
- Form_pg_inherits inhForm;
-
- inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple);
- if (inhForm->inhdetachpending)
- ereport(ERROR,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"",
- get_rel_name(inhForm->inhrelid),
- get_namespace_name(parent_rel->rd_rel->relnamespace),
- RelationGetRelationName(parent_rel)),
- errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation."));
-
- if (inhForm->inhrelid == RelationGetRelid(child_rel))
- {
- HeapTuple newtup;
-
- newtup = heap_copytuple(inheritsTuple);
- ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true;
-
- CatalogTupleUpdate(catalogRelation,
- &inheritsTuple->t_self,
- newtup);
- found = true;
- heap_freetuple(newtup);
- /* keep looking, to ensure we catch others pending detach */
- }
- }
-
- /* Done */
- systable_endscan(scan);
- table_close(catalogRelation, RowExclusiveLock);
-
- if (!found)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_TABLE),
- errmsg("relation \"%s\" is not a partition of relation \"%s\"",
- RelationGetRelationName(child_rel),
- RelationGetRelationName(parent_rel))));
-}
-
/*
* RemoveInheritance
*
@@ -17936,7 +16765,7 @@ MarkInheritDetached(Relation child_rel, Relation parent_rel)
*
* Common to ATExecDropInherit() and ATExecDetachPartition().
*/
-static void
+void
RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
{
Relation catalogRelation;
@@ -19708,2271 +18537,6 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid,
ReleaseSysCache(tuple);
}
-/*
- * Transform any expressions present in the partition key
- *
- * Returns a transformed PartitionSpec.
- */
-static PartitionSpec *
-transformPartitionSpec(Relation rel, PartitionSpec *partspec)
-{
- PartitionSpec *newspec;
- ParseState *pstate;
- ParseNamespaceItem *nsitem;
- ListCell *l;
-
- newspec = makeNode(PartitionSpec);
-
- newspec->strategy = partspec->strategy;
- newspec->partParams = NIL;
- newspec->location = partspec->location;
-
- /* Check valid number of columns for strategy */
- if (partspec->strategy == PARTITION_STRATEGY_LIST &&
- list_length(partspec->partParams) != 1)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot use \"list\" partition strategy with more than one column")));
-
- /*
- * Create a dummy ParseState and insert the target relation as its sole
- * rangetable entry. We need a ParseState for transformExpr.
- */
- pstate = make_parsestate(NULL);
- nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock,
- NULL, false, true);
- addNSItemToQuery(pstate, nsitem, true, true, true);
-
- /* take care of any partition expressions */
- foreach(l, partspec->partParams)
- {
- PartitionElem *pelem = lfirst_node(PartitionElem, l);
-
- if (pelem->expr)
- {
- /* Copy, to avoid scribbling on the input */
- pelem = copyObject(pelem);
-
- /* Now do parse transformation of the expression */
- pelem->expr = transformExpr(pstate, pelem->expr,
- EXPR_KIND_PARTITION_EXPRESSION);
-
- /* we have to fix its collations too */
- assign_expr_collations(pstate, pelem->expr);
- }
-
- newspec->partParams = lappend(newspec->partParams, pelem);
- }
-
- return newspec;
-}
-
-/*
- * Compute per-partition-column information from a list of PartitionElems.
- * Expressions in the PartitionElems must be parse-analyzed already.
- */
-static void
-ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
- List **partexprs, Oid *partopclass, Oid *partcollation,
- PartitionStrategy strategy)
-{
- int attn;
- ListCell *lc;
- Oid am_oid;
-
- attn = 0;
- foreach(lc, partParams)
- {
- PartitionElem *pelem = lfirst_node(PartitionElem, lc);
- Oid atttype;
- Oid attcollation;
-
- if (pelem->name != NULL)
- {
- /* Simple attribute reference */
- HeapTuple atttuple;
- Form_pg_attribute attform;
-
- atttuple = SearchSysCacheAttName(RelationGetRelid(rel),
- pelem->name);
- if (!HeapTupleIsValid(atttuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("column \"%s\" named in partition key does not exist",
- pelem->name),
- parser_errposition(pstate, pelem->location)));
- attform = (Form_pg_attribute) GETSTRUCT(atttuple);
-
- if (attform->attnum <= 0)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot use system column \"%s\" in partition key",
- pelem->name),
- parser_errposition(pstate, pelem->location)));
-
- /*
- * Stored generated columns cannot work: They are computed after
- * BEFORE triggers, but partition routing is done before all
- * triggers. Maybe virtual generated columns could be made to
- * work, but then they would need to be handled as an expression
- * below.
- */
- if (attform->attgenerated)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot use generated column in partition key"),
- errdetail("Column \"%s\" is a generated column.",
- pelem->name),
- parser_errposition(pstate, pelem->location)));
-
- partattrs[attn] = attform->attnum;
- atttype = attform->atttypid;
- attcollation = attform->attcollation;
- ReleaseSysCache(atttuple);
- }
- else
- {
- /* Expression */
- Node *expr = pelem->expr;
- char partattname[16];
- Bitmapset *expr_attrs = NULL;
- int i;
-
- Assert(expr != NULL);
- atttype = exprType(expr);
- attcollation = exprCollation(expr);
-
- /*
- * The expression must be of a storable type (e.g., not RECORD).
- * The test is the same as for whether a table column is of a safe
- * type (which is why we needn't check for the non-expression
- * case).
- */
- snprintf(partattname, sizeof(partattname), "%d", attn + 1);
- CheckAttributeType(partattname,
- atttype, attcollation,
- NIL, CHKATYPE_IS_PARTKEY);
-
- /*
- * Strip any top-level COLLATE clause. This ensures that we treat
- * "x COLLATE y" and "(x COLLATE y)" alike.
- */
- while (IsA(expr, CollateExpr))
- expr = (Node *) ((CollateExpr *) expr)->arg;
-
- /*
- * Examine all the columns in the partition key expression. When
- * the whole-row reference is present, examine all the columns of
- * the partitioned table.
- */
- pull_varattnos(expr, 1, &expr_attrs);
- if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs))
- {
- expr_attrs = bms_add_range(expr_attrs,
- 1 - FirstLowInvalidHeapAttributeNumber,
- RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber);
- expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber);
- }
-
- i = -1;
- while ((i = bms_next_member(expr_attrs, i)) >= 0)
- {
- AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber;
-
- Assert(attno != 0);
-
- /*
- * Cannot allow system column references, since that would
- * make partition routing impossible: their values won't be
- * known yet when we need to do that.
- */
- if (attno < 0)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("partition key expressions cannot contain system column references")));
-
- /*
- * Stored generated columns cannot work: They are computed
- * after BEFORE triggers, but partition routing is done before
- * all triggers. Virtual generated columns could probably
- * work, but it would require more work elsewhere (for example
- * SET EXPRESSION would need to check whether the column is
- * used in partition keys). Seems safer to prohibit for now.
- */
- if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot use generated column in partition key"),
- errdetail("Column \"%s\" is a generated column.",
- get_attname(RelationGetRelid(rel), attno, false)),
- parser_errposition(pstate, pelem->location)));
- }
-
- if (IsA(expr, Var) &&
- ((Var *) expr)->varattno > 0)
- {
-
- /*
- * User wrote "(column)" or "(column COLLATE something)".
- * Treat it like simple attribute anyway.
- */
- partattrs[attn] = ((Var *) expr)->varattno;
- }
- else
- {
- partattrs[attn] = 0; /* marks the column as expression */
- *partexprs = lappend(*partexprs, expr);
-
- /*
- * transformPartitionSpec() should have already rejected
- * subqueries, aggregates, window functions, and SRFs, based
- * on the EXPR_KIND_ for partition expressions.
- */
-
- /*
- * Preprocess the expression before checking for mutability.
- * This is essential for the reasons described in
- * contain_mutable_functions_after_planning. However, we call
- * expression_planner for ourselves rather than using that
- * function, because if constant-folding reduces the
- * expression to a constant, we'd like to know that so we can
- * complain below.
- *
- * Like contain_mutable_functions_after_planning, assume that
- * expression_planner won't scribble on its input, so this
- * won't affect the partexprs entry we saved above.
- */
- expr = (Node *) expression_planner((Expr *) expr);
-
- /*
- * Partition expressions cannot contain mutable functions,
- * because a given row must always map to the same partition
- * as long as there is no change in the partition boundary
- * structure.
- */
- if (contain_mutable_functions(expr))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("functions in partition key expression must be marked IMMUTABLE")));
-
- /*
- * While it is not exactly *wrong* for a partition expression
- * to be a constant, it seems better to reject such keys.
- */
- if (IsA(expr, Const))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot use constant expression as partition key")));
- }
- }
-
- /*
- * Apply collation override if any
- */
- if (pelem->collation)
- attcollation = get_collation_oid(pelem->collation, false);
-
- /*
- * Check we have a collation iff it's a collatable type. The only
- * expected failures here are (1) COLLATE applied to a noncollatable
- * type, or (2) partition expression had an unresolved collation. But
- * we might as well code this to be a complete consistency check.
- */
- if (type_is_collatable(atttype))
- {
- if (!OidIsValid(attcollation))
- ereport(ERROR,
- (errcode(ERRCODE_INDETERMINATE_COLLATION),
- errmsg("could not determine which collation to use for partition expression"),
- errhint("Use the COLLATE clause to set the collation explicitly.")));
- }
- else
- {
- if (OidIsValid(attcollation))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("collations are not supported by type %s",
- format_type_be(atttype))));
- }
-
- partcollation[attn] = attcollation;
-
- /*
- * Identify the appropriate operator class. For list and range
- * partitioning, we use a btree operator class; hash partitioning uses
- * a hash operator class.
- */
- if (strategy == PARTITION_STRATEGY_HASH)
- am_oid = HASH_AM_OID;
- else
- am_oid = BTREE_AM_OID;
-
- if (!pelem->opclass)
- {
- partopclass[attn] = GetDefaultOpClass(atttype, am_oid);
-
- if (!OidIsValid(partopclass[attn]))
- {
- if (strategy == PARTITION_STRATEGY_HASH)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("data type %s has no default operator class for access method \"%s\"",
- format_type_be(atttype), "hash"),
- errhint("You must specify a hash operator class or define a default hash operator class for the data type.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("data type %s has no default operator class for access method \"%s\"",
- format_type_be(atttype), "btree"),
- errhint("You must specify a btree operator class or define a default btree operator class for the data type.")));
- }
- }
- else
- partopclass[attn] = ResolveOpClass(pelem->opclass,
- atttype,
- am_oid == HASH_AM_OID ? "hash" : "btree",
- am_oid);
-
- attn++;
- }
-}
-
-/*
- * PartConstraintImpliedByRelConstraint
- * Do scanrel's existing constraints imply the partition constraint?
- *
- * "Existing constraints" include its check constraints and column-level
- * not-null constraints. partConstraint describes the partition constraint,
- * in implicit-AND form.
- */
-bool
-PartConstraintImpliedByRelConstraint(Relation scanrel,
- List *partConstraint)
-{
- List *existConstraint = NIL;
- TupleConstr *constr = RelationGetDescr(scanrel)->constr;
- int i;
-
- if (constr && constr->has_not_null)
- {
- int natts = scanrel->rd_att->natts;
-
- for (i = 1; i <= natts; i++)
- {
- CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1);
-
- /* invalid not-null constraint must be ignored here */
- if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped)
- {
- Form_pg_attribute wholeatt = TupleDescAttr(scanrel->rd_att, i - 1);
- NullTest *ntest = makeNode(NullTest);
-
- ntest->arg = (Expr *) makeVar(1,
- i,
- wholeatt->atttypid,
- wholeatt->atttypmod,
- wholeatt->attcollation,
- 0);
- ntest->nulltesttype = IS_NOT_NULL;
-
- /*
- * argisrow=false is correct even for a composite column,
- * because attnotnull does not represent a SQL-spec IS NOT
- * NULL test in such a case, just IS DISTINCT FROM NULL.
- */
- ntest->argisrow = false;
- ntest->location = -1;
- existConstraint = lappend(existConstraint, ntest);
- }
- }
- }
-
- return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint);
-}
-
-/*
- * ConstraintImpliedByRelConstraint
- * Do scanrel's existing constraints imply the given constraint?
- *
- * testConstraint is the constraint to validate. provenConstraint is a
- * caller-provided list of conditions which this function may assume
- * to be true. Both provenConstraint and testConstraint must be in
- * implicit-AND form, must only contain immutable clauses, and must
- * contain only Vars with varno = 1.
- */
-bool
-ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint)
-{
- List *existConstraint = list_copy(provenConstraint);
- TupleConstr *constr = RelationGetDescr(scanrel)->constr;
- int num_check,
- i;
-
- num_check = (constr != NULL) ? constr->num_check : 0;
- for (i = 0; i < num_check; i++)
- {
- Node *cexpr;
-
- /*
- * If this constraint hasn't been fully validated yet, we must ignore
- * it here.
- */
- if (!constr->check[i].ccvalid)
- continue;
-
- /*
- * NOT ENFORCED constraints are always marked as invalid, which should
- * have been ignored.
- */
- Assert(constr->check[i].ccenforced);
-
- cexpr = stringToNode(constr->check[i].ccbin);
-
- /*
- * Run each expression through const-simplification and
- * canonicalization. It is necessary, because we will be comparing it
- * to similarly-processed partition constraint expressions, and may
- * fail to detect valid matches without this.
- */
- cexpr = eval_const_expressions(NULL, cexpr);
- cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true);
-
- existConstraint = list_concat(existConstraint,
- make_ands_implicit((Expr *) cexpr));
- }
-
- /*
- * Try to make the proof. Since we are comparing CHECK constraints, we
- * need to use weak implication, i.e., we assume existConstraint is
- * not-false and try to prove the same for testConstraint.
- *
- * Note that predicate_implied_by assumes its first argument is known
- * immutable. That should always be true for both NOT NULL and partition
- * constraints, so we don't test it here.
- */
- return predicate_implied_by(testConstraint, existConstraint, true);
-}
-
-/*
- * QueuePartitionConstraintValidation
- *
- * Add an entry to wqueue to have the given partition constraint validated by
- * Phase 3, for the given relation, and all its children.
- *
- * We first verify whether the given constraint is implied by pre-existing
- * relation constraints; if it is, there's no need to scan the table to
- * validate, so don't queue in that case.
- */
-static void
-QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
- List *partConstraint,
- bool validate_default)
-{
- /*
- * Based on the table's existing constraints, determine whether or not we
- * may skip scanning the table.
- */
- if (PartConstraintImpliedByRelConstraint(scanrel, partConstraint))
- {
- if (!validate_default)
- ereport(DEBUG1,
- (errmsg_internal("partition constraint for table \"%s\" is implied by existing constraints",
- RelationGetRelationName(scanrel))));
- else
- ereport(DEBUG1,
- (errmsg_internal("updated partition constraint for default partition \"%s\" is implied by existing constraints",
- RelationGetRelationName(scanrel))));
- return;
- }
-
- /*
- * Constraints proved insufficient. For plain relations, queue a
- * validation item now; for partitioned tables, recurse to process each
- * partition.
- */
- if (scanrel->rd_rel->relkind == RELKIND_RELATION)
- {
- AlteredTableInfo *tab;
-
- /* Grab a work queue entry. */
- tab = ATGetQueueEntry(wqueue, scanrel);
- Assert(tab->partition_constraint == NULL);
- tab->partition_constraint = (Expr *) linitial(partConstraint);
- tab->validate_default = validate_default;
- }
- else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, true);
- int i;
-
- for (i = 0; i < partdesc->nparts; i++)
- {
- Relation part_rel;
- List *thisPartConstraint;
-
- /*
- * This is the minimum lock we need to prevent deadlocks.
- */
- part_rel = table_open(partdesc->oids[i], AccessExclusiveLock);
-
- /*
- * Adjust the constraint for scanrel so that it matches this
- * partition's attribute numbers.
- */
- thisPartConstraint =
- map_partition_varattnos(partConstraint, 1,
- part_rel, scanrel);
-
- QueuePartitionConstraintValidation(wqueue, part_rel,
- thisPartConstraint,
- validate_default);
- table_close(part_rel, NoLock); /* keep lock till commit */
- }
- }
-}
-
-/*
- * ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
- *
- * Return the address of the newly attached partition.
- */
-static ObjectAddress
-ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
- AlterTableUtilityContext *context)
-{
- Relation attachrel,
- catalog;
- List *attachrel_children;
- List *partConstraint;
- SysScanDesc scan;
- ScanKeyData skey;
- AttrNumber attno;
- int natts;
- TupleDesc tupleDesc;
- ObjectAddress address;
- const char *trigger_name;
- Oid defaultPartOid;
- List *partBoundConstraint;
- ParseState *pstate = make_parsestate(NULL);
-
- pstate->p_sourcetext = context->queryString;
-
- /*
- * We must lock the default partition if one exists, because attaching a
- * new partition will change its partition constraint.
- */
- defaultPartOid =
- get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
- if (OidIsValid(defaultPartOid))
- LockRelationOid(defaultPartOid, AccessExclusiveLock);
-
- attachrel = table_openrv(cmd->name, AccessExclusiveLock);
-
- /*
- * XXX I think it'd be a good idea to grab locks on all tables referenced
- * by FKs at this point also.
- */
-
- /*
- * Must be owner of both parent and source table -- parent was checked by
- * ATSimplePermissions call in ATPrepCmd
- */
- ATSimplePermissions(AT_AttachPartition, attachrel,
- ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
-
- /* A partition can only have one parent */
- if (attachrel->rd_rel->relispartition)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is already a partition",
- RelationGetRelationName(attachrel))));
-
- if (OidIsValid(attachrel->rd_rel->reloftype))
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a typed table as partition")));
-
- /*
- * Table being attached should not already be part of inheritance; either
- * as a child table...
- */
- catalog = table_open(InheritsRelationId, AccessShareLock);
- ScanKeyInit(&skey,
- Anum_pg_inherits_inhrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(RelationGetRelid(attachrel)));
- scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true,
- NULL, 1, &skey);
- if (HeapTupleIsValid(systable_getnext(scan)))
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach inheritance child as partition")));
- systable_endscan(scan);
-
- /* ...or as a parent table (except the case when it is partitioned) */
- ScanKeyInit(&skey,
- Anum_pg_inherits_inhparent,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(RelationGetRelid(attachrel)));
- scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL,
- 1, &skey);
- if (HeapTupleIsValid(systable_getnext(scan)) &&
- attachrel->rd_rel->relkind == RELKIND_RELATION)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach inheritance parent as partition")));
- systable_endscan(scan);
- table_close(catalog, AccessShareLock);
-
- /*
- * Prevent circularity by seeing if rel is a partition of attachrel. (In
- * particular, this disallows making a rel a partition of itself.)
- *
- * We do that by checking if rel is a member of the list of attachrel's
- * partitions provided the latter is partitioned at all. We want to avoid
- * having to construct this list again, so we request the strongest lock
- * on all partitions. We need the strongest lock, because we may decide
- * to scan them if we find out that the table being attached (or its leaf
- * partitions) may contain rows that violate the partition constraint. If
- * the table has a constraint that would prevent such rows, which by
- * definition is present in all the partitions, we need not scan the
- * table, nor its partitions. But we cannot risk a deadlock by taking a
- * weaker lock now and the stronger one only when needed.
- */
- attachrel_children = find_all_inheritors(RelationGetRelid(attachrel),
- AccessExclusiveLock, NULL);
- if (list_member_oid(attachrel_children, RelationGetRelid(rel)))
- ereport(ERROR,
- (errcode(ERRCODE_DUPLICATE_TABLE),
- errmsg("circular inheritance not allowed"),
- errdetail("\"%s\" is already a child of \"%s\".",
- RelationGetRelationName(rel),
- RelationGetRelationName(attachrel))));
-
- /* If the parent is permanent, so must be all of its partitions. */
- if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
- attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
- RelationGetRelationName(rel))));
-
- /* Temp parent cannot have a partition that is itself not a temp */
- if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
- attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
- RelationGetRelationName(rel))));
-
- /* If the parent is temp, it must belong to this session */
- if (RELATION_IS_OTHER_TEMP(rel))
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach as partition of temporary relation of another session")));
-
- /* Ditto for the partition */
- if (RELATION_IS_OTHER_TEMP(attachrel))
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach temporary relation of another session as partition")));
-
- /*
- * Check if attachrel has any identity columns or any columns that aren't
- * in the parent.
- */
- tupleDesc = RelationGetDescr(attachrel);
- natts = tupleDesc->natts;
- for (attno = 1; attno <= natts; attno++)
- {
- Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1);
- char *attributeName = NameStr(attribute->attname);
-
- /* Ignore dropped */
- if (attribute->attisdropped)
- continue;
-
- if (attribute->attidentity)
- ereport(ERROR,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("table \"%s\" being attached contains an identity column \"%s\"",
- RelationGetRelationName(attachrel), attributeName),
- errdetail("The new partition may not contain an identity column."));
-
- /* Try to find the column in parent (matching on column name) */
- if (!SearchSysCacheExists2(ATTNAME,
- ObjectIdGetDatum(RelationGetRelid(rel)),
- CStringGetDatum(attributeName)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"",
- RelationGetRelationName(attachrel), attributeName,
- RelationGetRelationName(rel)),
- errdetail("The new partition may contain only the columns present in parent.")));
- }
-
- /*
- * If child_rel has row-level triggers with transition tables, we
- * currently don't allow it to become a partition. See also prohibitions
- * in ATExecAddInherit() and CreateTrigger().
- */
- trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc);
- if (trigger_name != NULL)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition",
- trigger_name, RelationGetRelationName(attachrel)),
- errdetail("ROW triggers with transition tables are not supported on partitions.")));
-
- /*
- * Check that the new partition's bound is valid and does not overlap any
- * of existing partitions of the parent - note that it does not return on
- * error.
- */
- check_new_partition_bound(RelationGetRelationName(attachrel), rel,
- cmd->bound, pstate);
-
- /* OK to create inheritance. Rest of the checks performed there */
- CreateInheritance(attachrel, rel, true);
-
- /* Update the pg_class entry. */
- StorePartitionBound(attachrel, rel, cmd->bound);
-
- /* Ensure there exists a correct set of indexes in the partition. */
- AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
-
- /* and triggers */
- CloneRowTriggersToPartition(rel, attachrel);
-
- /*
- * Clone foreign key constraints. Callee is responsible for setting up
- * for phase 3 constraint verification.
- */
- CloneForeignKeyConstraints(wqueue, rel, attachrel);
-
- /*
- * Generate partition constraint from the partition bound specification.
- * If the parent itself is a partition, make sure to include its
- * constraint as well.
- */
- partBoundConstraint = get_qual_from_partbound(rel, cmd->bound);
-
- /*
- * Use list_concat_copy() to avoid modifying partBoundConstraint in place,
- * since it's needed later to construct the constraint expression for
- * validating against the default partition, if any.
- */
- partConstraint = list_concat_copy(partBoundConstraint,
- RelationGetPartitionQual(rel));
-
- /* Skip validation if there are no constraints to validate. */
- if (partConstraint)
- {
- /*
- * Run the partition quals through const-simplification similar to
- * check constraints. We skip canonicalize_qual, though, because
- * partition quals should be in canonical form already.
- */
- partConstraint =
- (List *) eval_const_expressions(NULL,
- (Node *) partConstraint);
-
- /* XXX this sure looks wrong */
- partConstraint = list_make1(make_ands_explicit(partConstraint));
-
- /*
- * Adjust the generated constraint to match this partition's attribute
- * numbers.
- */
- partConstraint = map_partition_varattnos(partConstraint, 1, attachrel,
- rel);
-
- /* Validate partition constraints against the table being attached. */
- QueuePartitionConstraintValidation(wqueue, attachrel, partConstraint,
- false);
- }
-
- /*
- * If we're attaching a partition other than the default partition and a
- * default one exists, then that partition's partition constraint changes,
- * so add an entry to the work queue to validate it, too. (We must not do
- * this when the partition being attached is the default one; we already
- * did it above!)
- */
- if (OidIsValid(defaultPartOid))
- {
- Relation defaultrel;
- List *defPartConstraint;
-
- Assert(!cmd->bound->is_default);
-
- /* we already hold a lock on the default partition */
- defaultrel = table_open(defaultPartOid, NoLock);
- defPartConstraint =
- get_proposed_default_constraint(partBoundConstraint);
-
- /*
- * Map the Vars in the constraint expression from rel's attnos to
- * defaultrel's.
- */
- defPartConstraint =
- map_partition_varattnos(defPartConstraint,
- 1, defaultrel, rel);
- QueuePartitionConstraintValidation(wqueue, defaultrel,
- defPartConstraint, true);
-
- /* keep our lock until commit. */
- table_close(defaultrel, NoLock);
- }
-
- ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel));
-
- /*
- * If the partition we just attached is partitioned itself, invalidate
- * relcache for all descendent partitions too to ensure that their
- * rd_partcheck expression trees are rebuilt; partitions already locked at
- * the beginning of this function.
- */
- if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- ListCell *l;
-
- foreach(l, attachrel_children)
- {
- CacheInvalidateRelcacheByRelid(lfirst_oid(l));
- }
- }
-
- /* keep our lock until commit */
- table_close(attachrel, NoLock);
-
- return address;
-}
-
-/*
- * AttachPartitionEnsureIndexes
- * subroutine for ATExecAttachPartition to create/match indexes
- *
- * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH
- * PARTITION: every partition must have an index attached to each index on the
- * partitioned table.
- */
-static void
-AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel)
-{
- List *idxes;
- List *attachRelIdxs;
- Relation *attachrelIdxRels;
- IndexInfo **attachInfos;
- ListCell *cell;
- MemoryContext cxt;
- MemoryContext oldcxt;
-
- cxt = AllocSetContextCreate(CurrentMemoryContext,
- "AttachPartitionEnsureIndexes",
- ALLOCSET_DEFAULT_SIZES);
- oldcxt = MemoryContextSwitchTo(cxt);
-
- idxes = RelationGetIndexList(rel);
- attachRelIdxs = RelationGetIndexList(attachrel);
- attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs));
- attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs));
-
- /* Build arrays of all existing indexes and their IndexInfos */
- foreach_oid(cldIdxId, attachRelIdxs)
- {
- int i = foreach_current_index(cldIdxId);
-
- attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock);
- attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]);
- }
-
- /*
- * If we're attaching a foreign table, we must fail if any of the indexes
- * is a constraint index; otherwise, there's nothing to do here. Do this
- * before starting work, to avoid wasting the effort of building a few
- * non-unique indexes before coming across a unique one.
- */
- if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
- {
- foreach(cell, idxes)
- {
- Oid idx = lfirst_oid(cell);
- Relation idxRel = index_open(idx, AccessShareLock);
-
- if (idxRel->rd_index->indisunique ||
- idxRel->rd_index->indisprimary)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach foreign table \"%s\" as partition of partitioned table \"%s\"",
- RelationGetRelationName(attachrel),
- RelationGetRelationName(rel)),
- errdetail("Partitioned table \"%s\" contains unique indexes.",
- RelationGetRelationName(rel))));
- index_close(idxRel, AccessShareLock);
- }
-
- goto out;
- }
-
- /*
- * For each index on the partitioned table, find a matching one in the
- * partition-to-be; if one is not found, create one.
- */
- foreach(cell, idxes)
- {
- Oid idx = lfirst_oid(cell);
- Relation idxRel = index_open(idx, AccessShareLock);
- IndexInfo *info;
- AttrMap *attmap;
- bool found = false;
- Oid constraintOid;
-
- /*
- * Ignore indexes in the partitioned table other than partitioned
- * indexes.
- */
- if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
- {
- index_close(idxRel, AccessShareLock);
- continue;
- }
-
- /* construct an indexinfo to compare existing indexes against */
- info = BuildIndexInfo(idxRel);
- attmap = build_attrmap_by_name(RelationGetDescr(attachrel),
- RelationGetDescr(rel),
- false);
- constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), idx);
-
- /*
- * Scan the list of existing indexes in the partition-to-be, and mark
- * the first matching, valid, unattached one we find, if any, as
- * partition of the parent index. If we find one, we're done.
- */
- for (int i = 0; i < list_length(attachRelIdxs); i++)
- {
- Oid cldIdxId = RelationGetRelid(attachrelIdxRels[i]);
- Oid cldConstrOid = InvalidOid;
-
- /* does this index have a parent? if so, can't use it */
- if (attachrelIdxRels[i]->rd_rel->relispartition)
- continue;
-
- /* If this index is invalid, can't use it */
- if (!attachrelIdxRels[i]->rd_index->indisvalid)
- continue;
-
- if (CompareIndexInfo(attachInfos[i], info,
- attachrelIdxRels[i]->rd_indcollation,
- idxRel->rd_indcollation,
- attachrelIdxRels[i]->rd_opfamily,
- idxRel->rd_opfamily,
- attmap))
- {
- /*
- * If this index is being created in the parent because of a
- * constraint, then the child needs to have a constraint also,
- * so look for one. If there is no such constraint, this
- * index is no good, so keep looking.
- */
- if (OidIsValid(constraintOid))
- {
- cldConstrOid =
- get_relation_idx_constraint_oid(RelationGetRelid(attachrel),
- cldIdxId);
- /* no dice */
- if (!OidIsValid(cldConstrOid))
- continue;
-
- /* Ensure they're both the same type of constraint */
- if (get_constraint_type(constraintOid) !=
- get_constraint_type(cldConstrOid))
- continue;
- }
-
- /* bingo. */
- IndexSetParentIndex(attachrelIdxRels[i], idx);
- if (OidIsValid(constraintOid))
- ConstraintSetParentConstraint(cldConstrOid, constraintOid,
- RelationGetRelid(attachrel));
- found = true;
-
- CommandCounterIncrement();
- break;
- }
- }
-
- /*
- * If no suitable index was found in the partition-to-be, create one
- * now. Note that if this is a PK, not-null constraints must already
- * exist.
- */
- if (!found)
- {
- IndexStmt *stmt;
- Oid conOid;
-
- stmt = generateClonedIndexStmt(NULL,
- idxRel, attmap,
- &conOid);
- DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid,
- RelationGetRelid(idxRel),
- conOid,
- -1,
- true, false, false, false, false);
- }
-
- index_close(idxRel, AccessShareLock);
- }
-
-out:
- /* Clean up. */
- for (int i = 0; i < list_length(attachRelIdxs); i++)
- index_close(attachrelIdxRels[i], AccessShareLock);
- MemoryContextSwitchTo(oldcxt);
- MemoryContextDelete(cxt);
-}
-
-/*
- * CloneRowTriggersToPartition
- * subroutine for ATExecAttachPartition/DefineRelation to create row
- * triggers on partitions
- */
-static void
-CloneRowTriggersToPartition(Relation parent, Relation partition)
-{
- Relation pg_trigger;
- ScanKeyData key;
- SysScanDesc scan;
- HeapTuple tuple;
- MemoryContext perTupCxt;
-
- ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
- F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent)));
- pg_trigger = table_open(TriggerRelationId, RowExclusiveLock);
- scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId,
- true, NULL, 1, &key);
-
- perTupCxt = AllocSetContextCreate(CurrentMemoryContext,
- "clone trig", ALLOCSET_SMALL_SIZES);
-
- while (HeapTupleIsValid(tuple = systable_getnext(scan)))
- {
- Form_pg_trigger trigForm = (Form_pg_trigger) GETSTRUCT(tuple);
- CreateTrigStmt *trigStmt;
- Node *qual = NULL;
- Datum value;
- bool isnull;
- List *cols = NIL;
- List *trigargs = NIL;
- MemoryContext oldcxt;
-
- /*
- * Ignore statement-level triggers; those are not cloned.
- */
- if (!TRIGGER_FOR_ROW(trigForm->tgtype))
- continue;
-
- /*
- * Don't clone internal triggers, because the constraint cloning code
- * will.
- */
- if (trigForm->tgisinternal)
- continue;
-
- /*
- * Complain if we find an unexpected trigger type.
- */
- if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) &&
- !TRIGGER_FOR_AFTER(trigForm->tgtype))
- elog(ERROR, "unexpected trigger \"%s\" found",
- NameStr(trigForm->tgname));
-
- /* Use short-lived context for CREATE TRIGGER */
- oldcxt = MemoryContextSwitchTo(perTupCxt);
-
- /*
- * If there is a WHEN clause, generate a 'cooked' version of it that's
- * appropriate for the partition.
- */
- value = heap_getattr(tuple, Anum_pg_trigger_tgqual,
- RelationGetDescr(pg_trigger), &isnull);
- if (!isnull)
- {
- qual = stringToNode(TextDatumGetCString(value));
- qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO,
- partition, parent);
- qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
- partition, parent);
- }
-
- /*
- * If there is a column list, transform it to a list of column names.
- * Note we don't need to map this list in any way ...
- */
- if (trigForm->tgattr.dim1 > 0)
- {
- int i;
-
- for (i = 0; i < trigForm->tgattr.dim1; i++)
- {
- Form_pg_attribute col;
-
- col = TupleDescAttr(parent->rd_att,
- trigForm->tgattr.values[i] - 1);
- cols = lappend(cols,
- makeString(pstrdup(NameStr(col->attname))));
- }
- }
-
- /* Reconstruct trigger arguments list. */
- if (trigForm->tgnargs > 0)
- {
- char *p;
-
- value = heap_getattr(tuple, Anum_pg_trigger_tgargs,
- RelationGetDescr(pg_trigger), &isnull);
- if (isnull)
- elog(ERROR, "tgargs is null for trigger \"%s\" in partition \"%s\"",
- NameStr(trigForm->tgname), RelationGetRelationName(partition));
-
- p = (char *) VARDATA_ANY(DatumGetByteaPP(value));
-
- for (int i = 0; i < trigForm->tgnargs; i++)
- {
- trigargs = lappend(trigargs, makeString(pstrdup(p)));
- p += strlen(p) + 1;
- }
- }
-
- trigStmt = makeNode(CreateTrigStmt);
- trigStmt->replace = false;
- trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
- trigStmt->trigname = NameStr(trigForm->tgname);
- trigStmt->relation = NULL;
- trigStmt->funcname = NULL; /* passed separately */
- trigStmt->args = trigargs;
- trigStmt->row = true;
- trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
- trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
- trigStmt->columns = cols;
- trigStmt->whenClause = NULL; /* passed separately */
- trigStmt->transitionRels = NIL; /* not supported at present */
- trigStmt->deferrable = trigForm->tgdeferrable;
- trigStmt->initdeferred = trigForm->tginitdeferred;
- trigStmt->constrrel = NULL; /* passed separately */
-
- CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
- trigForm->tgconstrrelid, InvalidOid, InvalidOid,
- trigForm->tgfoid, trigForm->oid, qual,
- false, true, trigForm->tgenabled);
-
- MemoryContextSwitchTo(oldcxt);
- MemoryContextReset(perTupCxt);
- }
-
- MemoryContextDelete(perTupCxt);
-
- systable_endscan(scan);
- table_close(pg_trigger, RowExclusiveLock);
-}
-
-/*
- * ALTER TABLE DETACH PARTITION
- *
- * Return the address of the relation that is no longer a partition of rel.
- *
- * If concurrent mode is requested, we run in two transactions. A side-
- * effect is that this command cannot run in a multi-part ALTER TABLE.
- * Currently, that's enforced by the grammar.
- *
- * The strategy for concurrency is to first modify the partition's
- * pg_inherit catalog row to make it visible to everyone that the
- * partition is detached, lock the partition against writes, and commit
- * the transaction; anyone who requests the partition descriptor from
- * that point onwards has to ignore such a partition. In a second
- * transaction, we wait until all transactions that could have seen the
- * partition as attached are gone, then we remove the rest of partition
- * metadata (pg_inherits and pg_class.relpartbounds).
- */
-static ObjectAddress
-ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
- RangeVar *name, bool concurrent)
-{
- Relation partRel;
- ObjectAddress address;
- Oid defaultPartOid;
-
- /*
- * We must lock the default partition, because detaching this partition
- * will change its partition constraint.
- */
- defaultPartOid =
- get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
- if (OidIsValid(defaultPartOid))
- {
- /*
- * Concurrent detaching when a default partition exists is not
- * supported. The main problem is that the default partition
- * constraint would change. And there's a definitional problem: what
- * should happen to the tuples that are being inserted that belong to
- * the partition being detached? Putting them on the partition being
- * detached would be wrong, since they'd become "lost" after the
- * detaching completes but we cannot put them in the default partition
- * either until we alter its partition constraint.
- *
- * I think we could solve this problem if we effected the constraint
- * change before committing the first transaction. But the lock would
- * have to remain AEL and it would cause concurrent query planning to
- * be blocked, so changing it that way would be even worse.
- */
- if (concurrent)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot detach partitions concurrently when a default partition exists")));
- LockRelationOid(defaultPartOid, AccessExclusiveLock);
- }
-
- /*
- * In concurrent mode, the partition is locked with share-update-exclusive
- * in the first transaction. This allows concurrent transactions to be
- * doing DML to the partition.
- */
- partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock :
- AccessExclusiveLock);
-
- /*
- * Check inheritance conditions and either delete the pg_inherits row (in
- * non-concurrent mode) or just set the inhdetachpending flag.
- */
- if (!concurrent)
- RemoveInheritance(partRel, rel, false);
- else
- MarkInheritDetached(partRel, rel);
-
- /*
- * Ensure that foreign keys still hold after this detach. This keeps
- * locks on the referencing tables, which prevents concurrent transactions
- * from adding rows that we wouldn't see. For this to work in concurrent
- * mode, it is critical that the partition appears as no longer attached
- * for the RI queries as soon as the first transaction commits.
- */
- ATDetachCheckNoForeignKeyRefs(partRel);
-
- /*
- * Concurrent mode has to work harder; first we add a new constraint to
- * the partition that matches the partition constraint. Then we close our
- * existing transaction, and in a new one wait for all processes to catch
- * up on the catalog updates we've done so far; at that point we can
- * complete the operation.
- */
- if (concurrent)
- {
- Oid partrelid,
- parentrelid;
- LOCKTAG tag;
- char *parentrelname;
- char *partrelname;
-
- /*
- * We're almost done now; the only traces that remain are the
- * pg_inherits tuple and the partition's relpartbounds. Before we can
- * remove those, we need to wait until all transactions that know that
- * this is a partition are gone.
- */
-
- /*
- * Remember relation OIDs to re-acquire them later; and relation names
- * too, for error messages if something is dropped in between.
- */
- partrelid = RelationGetRelid(partRel);
- parentrelid = RelationGetRelid(rel);
- parentrelname = MemoryContextStrdup(PortalContext,
- RelationGetRelationName(rel));
- partrelname = MemoryContextStrdup(PortalContext,
- RelationGetRelationName(partRel));
-
- /* Invalidate relcache entries for the parent -- must be before close */
- CacheInvalidateRelcache(rel);
-
- table_close(partRel, NoLock);
- table_close(rel, NoLock);
- tab->rel = NULL;
-
- /* Make updated catalog entry visible */
- PopActiveSnapshot();
- CommitTransactionCommand();
-
- StartTransactionCommand();
-
- /*
- * Now wait. This ensures that all queries that were planned
- * including the partition are finished before we remove the rest of
- * catalog entries. We don't need or indeed want to acquire this
- * lock, though -- that would block later queries.
- *
- * We don't need to concern ourselves with waiting for a lock on the
- * partition itself, since we will acquire AccessExclusiveLock below.
- */
- SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
- WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
-
- /*
- * Now acquire locks in both relations again. Note they may have been
- * removed in the meantime, so care is required.
- */
- rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock);
- partRel = try_relation_open(partrelid, AccessExclusiveLock);
-
- /* If the relations aren't there, something bad happened; bail out */
- if (rel == NULL)
- {
- if (partRel != NULL) /* shouldn't happen */
- elog(WARNING, "dangling partition \"%s\" remains, can't fix",
- partrelname);
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("partitioned table \"%s\" was removed concurrently",
- parentrelname)));
- }
- if (partRel == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("partition \"%s\" was removed concurrently", partrelname)));
-
- tab->rel = rel;
- }
-
- /*
- * Detaching the partition might involve TOAST table access, so ensure we
- * have a valid snapshot.
- */
- PushActiveSnapshot(GetTransactionSnapshot());
-
- /* Do the final part of detaching */
- DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid);
-
- PopActiveSnapshot();
-
- ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
-
- /* keep our lock until commit */
- table_close(partRel, NoLock);
-
- return address;
-}
-
-/*
- * Second part of ALTER TABLE .. DETACH.
- *
- * This is separate so that it can be run independently when the second
- * transaction of the concurrent algorithm fails (crash or abort).
- */
-static void
-DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
- Oid defaultPartOid)
-{
- Relation classRel;
- List *fks;
- ListCell *cell;
- List *indexes;
- Datum new_val[Natts_pg_class];
- bool new_null[Natts_pg_class],
- new_repl[Natts_pg_class];
- HeapTuple tuple,
- newtuple;
- Relation trigrel = NULL;
- List *fkoids = NIL;
-
- if (concurrent)
- {
- /*
- * We can remove the pg_inherits row now. (In the non-concurrent case,
- * this was already done).
- */
- RemoveInheritance(partRel, rel, true);
- }
-
- /* Drop any triggers that were cloned on creation/attach. */
- DropClonedTriggersFromPartition(RelationGetRelid(partRel));
-
- /*
- * Detach any foreign keys that are inherited. This includes creating
- * additional action triggers.
- */
- fks = copyObject(RelationGetFKeyList(partRel));
- if (fks != NIL)
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
- * It's possible that the partition being detached has a foreign key that
- * references a partitioned table. When that happens, there are multiple
- * pg_constraint rows for the partition: one points to the partitioned
- * table itself, while the others point to each of its partitions. Only
- * the topmost one is to be considered here; the child constraints must be
- * left alone, because conceptually those aren't coming from our parent
- * partitioned table, but from this partition itself.
- *
- * We implement this by collecting all the constraint OIDs in a first scan
- * of the FK array, and skipping in the loop below those constraints whose
- * parents are listed here.
- */
- foreach_node(ForeignKeyCacheInfo, fk, fks)
- fkoids = lappend_oid(fkoids, fk->conoid);
-
- foreach(cell, fks)
- {
- ForeignKeyCacheInfo *fk = lfirst(cell);
- HeapTuple contup;
- Form_pg_constraint conform;
-
- contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
- if (!HeapTupleIsValid(contup))
- elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
- conform = (Form_pg_constraint) GETSTRUCT(contup);
-
- /*
- * Consider only inherited foreign keys, and only if their parents
- * aren't in the list.
- */
- if (conform->contype != CONSTRAINT_FOREIGN ||
- !OidIsValid(conform->conparentid) ||
- list_member_oid(fkoids, conform->conparentid))
- {
- ReleaseSysCache(contup);
- continue;
- }
-
- /*
- * The constraint on this table must be marked no longer a child of
- * the parent's constraint, as do its check triggers.
- */
- ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
-
- /*
- * Also, look up the partition's "check" triggers corresponding to the
- * ENFORCED constraint being detached and detach them from the parent
- * triggers. NOT ENFORCED constraints do not have these triggers;
- * therefore, this step is not needed.
- */
- if (fk->conenforced)
- {
- Oid insertTriggerOid,
- updateTriggerOid;
-
- GetForeignKeyCheckTriggers(trigrel,
- fk->conoid, fk->confrelid, fk->conrelid,
- &insertTriggerOid, &updateTriggerOid);
- Assert(OidIsValid(insertTriggerOid));
- TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- Assert(OidIsValid(updateTriggerOid));
- TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
- RelationGetRelid(partRel));
- }
-
- /*
- * Lastly, create the action triggers on the referenced table, using
- * addFkRecurseReferenced, which requires some elaborate setup (so put
- * it in a separate block). While at it, if the table is partitioned,
- * that function will recurse to create the pg_constraint rows and
- * action triggers for each partition.
- *
- * Note there's no need to do addFkConstraint() here, because the
- * pg_constraint row already exists.
- */
- {
- Constraint *fkconstraint;
- int numfks;
- AttrNumber conkey[INDEX_MAX_KEYS];
- AttrNumber confkey[INDEX_MAX_KEYS];
- Oid conpfeqop[INDEX_MAX_KEYS];
- Oid conppeqop[INDEX_MAX_KEYS];
- Oid conffeqop[INDEX_MAX_KEYS];
- int numfkdelsetcols;
- AttrNumber confdelsetcols[INDEX_MAX_KEYS];
- Relation refdRel;
-
- DeconstructFkConstraintRow(contup,
- &numfks,
- conkey,
- confkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- &numfkdelsetcols,
- confdelsetcols);
-
- /* Create a synthetic node we'll use throughout */
- fkconstraint = makeNode(Constraint);
- fkconstraint->contype = CONSTRAINT_FOREIGN;
- fkconstraint->conname = pstrdup(NameStr(conform->conname));
- fkconstraint->deferrable = conform->condeferrable;
- fkconstraint->initdeferred = conform->condeferred;
- fkconstraint->is_enforced = conform->conenforced;
- fkconstraint->skip_validation = true;
- fkconstraint->initially_valid = conform->convalidated;
- /* a few irrelevant fields omitted here */
- fkconstraint->pktable = NULL;
- fkconstraint->fk_attrs = NIL;
- fkconstraint->pk_attrs = NIL;
- fkconstraint->fk_matchtype = conform->confmatchtype;
- fkconstraint->fk_upd_action = conform->confupdtype;
- fkconstraint->fk_del_action = conform->confdeltype;
- fkconstraint->fk_del_set_cols = NIL;
- fkconstraint->old_conpfeqop = NIL;
- fkconstraint->old_pktable_oid = InvalidOid;
- fkconstraint->location = -1;
-
- /* set up colnames, used to generate the constraint name */
- for (int i = 0; i < numfks; i++)
- {
- Form_pg_attribute att;
-
- att = TupleDescAttr(RelationGetDescr(partRel),
- conkey[i] - 1);
-
- fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs,
- makeString(NameStr(att->attname)));
- }
-
- refdRel = table_open(fk->confrelid, ShareRowExclusiveLock);
-
- addFkRecurseReferenced(fkconstraint, partRel,
- refdRel,
- conform->conindid,
- fk->conoid,
- numfks,
- confkey,
- conkey,
- conpfeqop,
- conppeqop,
- conffeqop,
- numfkdelsetcols,
- confdelsetcols,
- true,
- InvalidOid, InvalidOid,
- conform->conperiod);
- table_close(refdRel, NoLock); /* keep lock till end of xact */
- }
-
- ReleaseSysCache(contup);
- }
- list_free_deep(fks);
- if (trigrel)
- table_close(trigrel, RowExclusiveLock);
-
- /*
- * Any sub-constraints that are in the referenced-side of a larger
- * constraint have to be removed. This partition is no longer part of the
- * key space of the constraint.
- */
- foreach(cell, GetParentedForeignKeyRefs(partRel))
- {
- Oid constrOid = lfirst_oid(cell);
- ObjectAddress constraint;
-
- ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
- deleteDependencyRecordsForClass(ConstraintRelationId,
- constrOid,
- ConstraintRelationId,
- DEPENDENCY_INTERNAL);
- CommandCounterIncrement();
-
- ObjectAddressSet(constraint, ConstraintRelationId, constrOid);
- performDeletion(&constraint, DROP_RESTRICT, 0);
- }
-
- /* Now we can detach indexes */
- indexes = RelationGetIndexList(partRel);
- foreach(cell, indexes)
- {
- Oid idxid = lfirst_oid(cell);
- Oid parentidx;
- Relation idx;
- Oid constrOid;
- Oid parentConstrOid;
-
- if (!has_superclass(idxid))
- continue;
-
- parentidx = get_partition_parent(idxid, false);
- Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel)));
-
- idx = index_open(idxid, AccessExclusiveLock);
- IndexSetParentIndex(idx, InvalidOid);
-
- /*
- * If there's a constraint associated with the index, detach it too.
- * Careful: it is possible for a constraint index in a partition to be
- * the child of a non-constraint index, so verify whether the parent
- * index does actually have a constraint.
- */
- constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel),
- idxid);
- parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel),
- parentidx);
- if (OidIsValid(parentConstrOid) && OidIsValid(constrOid))
- ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid);
-
- index_close(idx, NoLock);
- }
-
- /* Update pg_class tuple */
- classRel = table_open(RelationRelationId, RowExclusiveLock);
- tuple = SearchSysCacheCopy1(RELOID,
- ObjectIdGetDatum(RelationGetRelid(partRel)));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u",
- RelationGetRelid(partRel));
- Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition);
-
- /* Clear relpartbound and reset relispartition */
- memset(new_val, 0, sizeof(new_val));
- memset(new_null, false, sizeof(new_null));
- memset(new_repl, false, sizeof(new_repl));
- new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0;
- new_null[Anum_pg_class_relpartbound - 1] = true;
- new_repl[Anum_pg_class_relpartbound - 1] = true;
- newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel),
- new_val, new_null, new_repl);
-
- ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false;
- CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple);
- heap_freetuple(newtuple);
- table_close(classRel, RowExclusiveLock);
-
- /*
- * Drop identity property from all identity columns of partition.
- */
- for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++)
- {
- Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno);
-
- if (!attr->attisdropped && attr->attidentity)
- ATExecDropIdentity(partRel, NameStr(attr->attname), false,
- AccessExclusiveLock, true, true);
- }
-
- if (OidIsValid(defaultPartOid))
- {
- /*
- * If the relation being detached is the default partition itself,
- * remove it from the parent's pg_partitioned_table entry.
- *
- * If not, we must invalidate default partition's relcache entry, as
- * in StorePartitionBound: its partition constraint depends on every
- * other partition's partition constraint.
- */
- if (RelationGetRelid(partRel) == defaultPartOid)
- update_default_partition_oid(RelationGetRelid(rel), InvalidOid);
- else
- CacheInvalidateRelcacheByRelid(defaultPartOid);
- }
-
- /*
- * Invalidate the parent's relcache so that the partition is no longer
- * included in its partition descriptor.
- */
- CacheInvalidateRelcache(rel);
-
- /*
- * If the partition we just detached is partitioned itself, invalidate
- * relcache for all descendent partitions too to ensure that their
- * rd_partcheck expression trees are rebuilt; must lock partitions before
- * doing so, using the same lockmode as what partRel has been locked with
- * by the caller.
- */
- if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- List *children;
-
- children = find_all_inheritors(RelationGetRelid(partRel),
- AccessExclusiveLock, NULL);
- foreach(cell, children)
- {
- CacheInvalidateRelcacheByRelid(lfirst_oid(cell));
- }
- }
-}
-
-/*
- * ALTER TABLE ... DETACH PARTITION ... FINALIZE
- *
- * To use when a DETACH PARTITION command previously did not run to
- * completion; this completes the detaching process.
- */
-static ObjectAddress
-ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
-{
- Relation partRel;
- ObjectAddress address;
- Snapshot snap = GetActiveSnapshot();
-
- partRel = table_openrv(name, AccessExclusiveLock);
-
- /*
- * Wait until existing snapshots are gone. This is important if the
- * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the
- * user could immediately run DETACH FINALIZE without actually waiting for
- * existing transactions. We must not complete the detach action until
- * all such queries are complete (otherwise we would present them with an
- * inconsistent view of catalogs).
- */
- WaitForOlderSnapshots(snap->xmin, false);
-
- DetachPartitionFinalize(rel, partRel, true, InvalidOid);
-
- ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel));
-
- table_close(partRel, NoLock);
-
- return address;
-}
-
-/*
- * DropClonedTriggersFromPartition
- * subroutine for ATExecDetachPartition to remove any triggers that were
- * cloned to the partition when it was created-as-partition or attached.
- * This undoes what CloneRowTriggersToPartition did.
- */
-static void
-DropClonedTriggersFromPartition(Oid partitionId)
-{
- ScanKeyData skey;
- SysScanDesc scan;
- HeapTuple trigtup;
- Relation tgrel;
- ObjectAddresses *objects;
-
- objects = new_object_addresses();
-
- /*
- * Scan pg_trigger to search for all triggers on this rel.
- */
- ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
- F_OIDEQ, ObjectIdGetDatum(partitionId));
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
- scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
- true, NULL, 1, &skey);
- while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
- {
- Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
- ObjectAddress trig;
-
- /* Ignore triggers that weren't cloned */
- if (!OidIsValid(pg_trigger->tgparentid))
- continue;
-
- /*
- * Ignore internal triggers that are implementation objects of foreign
- * keys, because these will be detached when the foreign keys
- * themselves are.
- */
- if (OidIsValid(pg_trigger->tgconstrrelid))
- continue;
-
- /*
- * This is ugly, but necessary: remove the dependency markings on the
- * trigger so that it can be removed.
- */
- deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
- TriggerRelationId,
- DEPENDENCY_PARTITION_PRI);
- deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
- RelationRelationId,
- DEPENDENCY_PARTITION_SEC);
-
- /* remember this trigger to remove it below */
- ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid);
- add_exact_object_address(&trig, objects);
- }
-
- /* make the dependency removal visible to the deletion below */
- CommandCounterIncrement();
- performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
-
- /* done */
- free_object_addresses(objects);
- systable_endscan(scan);
- table_close(tgrel, RowExclusiveLock);
-}
-
-/*
- * Before acquiring lock on an index, acquire the same lock on the owning
- * table.
- */
-struct AttachIndexCallbackState
-{
- Oid partitionOid;
- Oid parentTblOid;
- bool lockedParentTbl;
-};
-
-static void
-RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid,
- void *arg)
-{
- struct AttachIndexCallbackState *state;
- Form_pg_class classform;
- HeapTuple tuple;
-
- state = (struct AttachIndexCallbackState *) arg;
-
- if (!state->lockedParentTbl)
- {
- LockRelationOid(state->parentTblOid, AccessShareLock);
- state->lockedParentTbl = true;
- }
-
- /*
- * If we previously locked some other heap, and the name we're looking up
- * no longer refers to an index on that relation, release the now-useless
- * lock. XXX maybe we should do *after* we verify whether the index does
- * not actually belong to the same relation ...
- */
- if (relOid != oldRelOid && OidIsValid(state->partitionOid))
- {
- UnlockRelationOid(state->partitionOid, AccessShareLock);
- state->partitionOid = InvalidOid;
- }
-
- /* Didn't find a relation, so no need for locking or permission checks. */
- if (!OidIsValid(relOid))
- return;
-
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
- if (!HeapTupleIsValid(tuple))
- return; /* concurrently dropped, so nothing to do */
- classform = (Form_pg_class) GETSTRUCT(tuple);
- if (classform->relkind != RELKIND_PARTITIONED_INDEX &&
- classform->relkind != RELKIND_INDEX)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("\"%s\" is not an index", rv->relname)));
- ReleaseSysCache(tuple);
-
- /*
- * Since we need only examine the heap's tupledesc, an access share lock
- * on it (preventing any DDL) is sufficient.
- */
- state->partitionOid = IndexGetRelation(relOid, false);
- LockRelationOid(state->partitionOid, AccessShareLock);
-}
-
-/*
- * ALTER INDEX i1 ATTACH PARTITION i2
- */
-static ObjectAddress
-ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
-{
- Relation partIdx;
- Relation partTbl;
- Relation parentTbl;
- ObjectAddress address;
- Oid partIdxId;
- Oid currParent;
- struct AttachIndexCallbackState state;
-
- /*
- * We need to obtain lock on the index 'name' to modify it, but we also
- * need to read its owning table's tuple descriptor -- so we need to lock
- * both. To avoid deadlocks, obtain lock on the table before doing so on
- * the index. Furthermore, we need to examine the parent table of the
- * partition, so lock that one too.
- */
- state.partitionOid = InvalidOid;
- state.parentTblOid = parentIdx->rd_index->indrelid;
- state.lockedParentTbl = false;
- partIdxId =
- RangeVarGetRelidExtended(name, AccessExclusiveLock, 0,
- RangeVarCallbackForAttachIndex,
- &state);
- /* Not there? */
- if (!OidIsValid(partIdxId))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("index \"%s\" does not exist", name->relname)));
-
- /* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */
- partIdx = relation_open(partIdxId, AccessExclusiveLock);
-
- /* we already hold locks on both tables, so this is safe: */
- parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock);
- partTbl = relation_open(partIdx->rd_index->indrelid, NoLock);
-
- ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx));
-
- /* Silently do nothing if already in the right state */
- currParent = partIdx->rd_rel->relispartition ?
- get_partition_parent(partIdxId, false) : InvalidOid;
- if (currParent != RelationGetRelid(parentIdx))
- {
- IndexInfo *childInfo;
- IndexInfo *parentInfo;
- AttrMap *attmap;
- bool found;
- int i;
- PartitionDesc partDesc;
- Oid constraintOid,
- cldConstrId = InvalidOid;
-
- /*
- * If this partition already has an index attached, refuse the
- * operation.
- */
- refuseDupeIndexAttach(parentIdx, partIdx, partTbl);
-
- if (OidIsValid(currParent))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentIdx)),
- errdetail("Index \"%s\" is already attached to another index.",
- RelationGetRelationName(partIdx))));
-
- /* Make sure it indexes a partition of the other index's table */
- partDesc = RelationGetPartitionDesc(parentTbl, true);
- found = false;
- for (i = 0; i < partDesc->nparts; i++)
- {
- if (partDesc->oids[i] == state.partitionOid)
- {
- found = true;
- break;
- }
- }
- if (!found)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentIdx)),
- errdetail("Index \"%s\" is not an index on any partition of table \"%s\".",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentTbl))));
-
- /* Ensure the indexes are compatible */
- childInfo = BuildIndexInfo(partIdx);
- parentInfo = BuildIndexInfo(parentIdx);
- attmap = build_attrmap_by_name(RelationGetDescr(partTbl),
- RelationGetDescr(parentTbl),
- false);
- if (!CompareIndexInfo(childInfo, parentInfo,
- partIdx->rd_indcollation,
- parentIdx->rd_indcollation,
- partIdx->rd_opfamily,
- parentIdx->rd_opfamily,
- attmap))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentIdx)),
- errdetail("The index definitions do not match.")));
-
- /*
- * If there is a constraint in the parent, make sure there is one in
- * the child too.
- */
- constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(parentTbl),
- RelationGetRelid(parentIdx));
-
- if (OidIsValid(constraintOid))
- {
- cldConstrId = get_relation_idx_constraint_oid(RelationGetRelid(partTbl),
- partIdxId);
- if (!OidIsValid(cldConstrId))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentIdx)),
- errdetail("The index \"%s\" belongs to a constraint in table \"%s\" but no constraint exists for index \"%s\".",
- RelationGetRelationName(parentIdx),
- RelationGetRelationName(parentTbl),
- RelationGetRelationName(partIdx))));
- }
-
- /*
- * If it's a primary key, make sure the columns in the partition are
- * NOT NULL.
- */
- if (parentIdx->rd_index->indisprimary)
- verifyPartitionIndexNotNull(childInfo, partTbl);
-
- /* All good -- do it */
- IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx));
- if (OidIsValid(constraintOid))
- ConstraintSetParentConstraint(cldConstrId, constraintOid,
- RelationGetRelid(partTbl));
-
- free_attrmap(attmap);
-
- validatePartitionedIndex(parentIdx, parentTbl);
- }
-
- relation_close(parentTbl, AccessShareLock);
- /* keep these locks till commit */
- relation_close(partTbl, NoLock);
- relation_close(partIdx, NoLock);
-
- return address;
-}
-
-/*
- * Verify whether the given partition already contains an index attached
- * to the given partitioned index. If so, raise an error.
- */
-static void
-refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl)
-{
- Oid existingIdx;
-
- existingIdx = index_get_partition(partitionTbl,
- RelationGetRelid(parentIdx));
- if (OidIsValid(existingIdx))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot attach index \"%s\" as a partition of index \"%s\"",
- RelationGetRelationName(partIdx),
- RelationGetRelationName(parentIdx)),
- errdetail("Another index \"%s\" is already attached for partition \"%s\".",
- get_rel_name(existingIdx),
- RelationGetRelationName(partitionTbl))));
-}
-
-/*
- * Verify whether the set of attached partition indexes to a parent index on
- * a partitioned table is complete. If it is, mark the parent index valid.
- *
- * This should be called each time a partition index is attached.
- */
-static void
-validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
-{
- Relation inheritsRel;
- SysScanDesc scan;
- ScanKeyData key;
- int tuples = 0;
- HeapTuple inhTup;
- bool updated = false;
-
- Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX);
-
- /*
- * Scan pg_inherits for this parent index. Count each valid index we find
- * (verifying the pg_index entry for each), and if we reach the total
- * amount we expect, we can mark this parent index as valid.
- */
- inheritsRel = table_open(InheritsRelationId, AccessShareLock);
- ScanKeyInit(&key, Anum_pg_inherits_inhparent,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(RelationGetRelid(partedIdx)));
- scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true,
- NULL, 1, &key);
- while ((inhTup = systable_getnext(scan)) != NULL)
- {
- Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup);
- HeapTuple indTup;
- Form_pg_index indexForm;
-
- indTup = SearchSysCache1(INDEXRELID,
- ObjectIdGetDatum(inhForm->inhrelid));
- if (!HeapTupleIsValid(indTup))
- elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid);
- indexForm = (Form_pg_index) GETSTRUCT(indTup);
- if (indexForm->indisvalid)
- tuples += 1;
- ReleaseSysCache(indTup);
- }
-
- /* Done with pg_inherits */
- systable_endscan(scan);
- table_close(inheritsRel, AccessShareLock);
-
- /*
- * If we found as many inherited indexes as the partitioned table has
- * partitions, we're good; update pg_index to set indisvalid.
- */
- if (tuples == RelationGetPartitionDesc(partedTbl, true)->nparts)
- {
- Relation idxRel;
- HeapTuple indTup;
- Form_pg_index indexForm;
-
- idxRel = table_open(IndexRelationId, RowExclusiveLock);
- indTup = SearchSysCacheCopy1(INDEXRELID,
- ObjectIdGetDatum(RelationGetRelid(partedIdx)));
- if (!HeapTupleIsValid(indTup))
- elog(ERROR, "cache lookup failed for index %u",
- RelationGetRelid(partedIdx));
- indexForm = (Form_pg_index) GETSTRUCT(indTup);
-
- indexForm->indisvalid = true;
- updated = true;
-
- CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
-
- table_close(idxRel, RowExclusiveLock);
- heap_freetuple(indTup);
- }
-
- /*
- * If this index is in turn a partition of a larger index, validating it
- * might cause the parent to become valid also. Try that.
- */
- if (updated && partedIdx->rd_rel->relispartition)
- {
- Oid parentIdxId,
- parentTblId;
- Relation parentIdx,
- parentTbl;
-
- /* make sure we see the validation we just did */
- CommandCounterIncrement();
-
- parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false);
- parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false);
- parentIdx = relation_open(parentIdxId, AccessExclusiveLock);
- parentTbl = relation_open(parentTblId, AccessExclusiveLock);
- Assert(!parentIdx->rd_index->indisvalid);
-
- validatePartitionedIndex(parentIdx, parentTbl);
-
- relation_close(parentIdx, AccessExclusiveLock);
- relation_close(parentTbl, AccessExclusiveLock);
- }
-}
-
-/*
- * When attaching an index as a partition of a partitioned index which is a
- * primary key, verify that all the columns in the partition are marked NOT
- * NULL.
- */
-static void
-verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
-{
- for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++)
- {
- Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
- iinfo->ii_IndexAttrNumbers[i] - 1);
-
- if (!att->attnotnull)
- ereport(ERROR,
- errcode(ERRCODE_INVALID_TABLE_DEFINITION),
- errmsg("invalid primary key definition"),
- errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.",
- NameStr(att->attname),
- RelationGetRelationName(partition)));
- }
-}
-
-/*
- * Return an OID list of constraints that reference the given relation
- * that are marked as having a parent constraints.
- */
-static List *
-GetParentedForeignKeyRefs(Relation partition)
-{
- Relation pg_constraint;
- HeapTuple tuple;
- SysScanDesc scan;
- ScanKeyData key[2];
- List *constraints = NIL;
-
- /*
- * If no indexes, or no columns are referenceable by FKs, we can avoid the
- * scan.
- */
- if (RelationGetIndexList(partition) == NIL ||
- bms_is_empty(RelationGetIndexAttrBitmap(partition,
- INDEX_ATTR_BITMAP_KEY)))
- return NIL;
-
- /* Search for constraints referencing this table */
- pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
- ScanKeyInit(&key[0],
- Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
- F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition)));
- ScanKeyInit(&key[1],
- Anum_pg_constraint_contype, BTEqualStrategyNumber,
- F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
-
- /* XXX This is a seqscan, as we don't have a usable index */
- scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key);
- while ((tuple = systable_getnext(scan)) != NULL)
- {
- Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
-
- /*
- * We only need to process constraints that are part of larger ones.
- */
- if (!OidIsValid(constrForm->conparentid))
- continue;
-
- constraints = lappend_oid(constraints, constrForm->oid);
- }
-
- systable_endscan(scan);
- table_close(pg_constraint, AccessShareLock);
-
- return constraints;
-}
-
-/*
- * During DETACH PARTITION, verify that any foreign keys pointing to the
- * partitioned table would not become invalid. An error is raised if any
- * referenced values exist.
- */
-static void
-ATDetachCheckNoForeignKeyRefs(Relation partition)
-{
- List *constraints;
- ListCell *cell;
-
- constraints = GetParentedForeignKeyRefs(partition);
-
- foreach(cell, constraints)
- {
- Oid constrOid = lfirst_oid(cell);
- HeapTuple tuple;
- Form_pg_constraint constrForm;
- Relation rel;
- Trigger trig = {0};
-
- tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for constraint %u", constrOid);
- constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
-
- Assert(OidIsValid(constrForm->conparentid));
- Assert(constrForm->confrelid == RelationGetRelid(partition));
-
- /* prevent data changes into the referencing table until commit */
- rel = table_open(constrForm->conrelid, ShareLock);
-
- trig.tgoid = InvalidOid;
- trig.tgname = NameStr(constrForm->conname);
- trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN;
- trig.tgisinternal = true;
- trig.tgconstrrelid = RelationGetRelid(partition);
- trig.tgconstrindid = constrForm->conindid;
- trig.tgconstraint = constrForm->oid;
- trig.tgdeferrable = false;
- trig.tginitdeferred = false;
- /* we needn't fill in remaining fields */
-
- RI_PartitionRemove_Check(&trig, rel, partition);
-
- ReleaseSysCache(tuple);
-
- table_close(rel, NoLock);
- }
-}
-
/*
* resolve column compression specification to compression method.
*/
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 8ba038c5ef4..453bbf46cef 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -20,6 +20,7 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_type.h"
+#include "commands/partcmds.h"
#include "commands/tablecmds.h"
#include "common/hashfn.h"
#include "executor/executor.h"
diff --git a/src/include/commands/partcmds.h b/src/include/commands/partcmds.h
new file mode 100644
index 00000000000..743431909fb
--- /dev/null
+++ b/src/include/commands/partcmds.h
@@ -0,0 +1,53 @@
+/*-------------------------------------------------------------------------
+ *
+ * partcmds.h
+ * prototypes for partcmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/partcmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARTCMDS_H
+#define PARTCMDS_H
+
+/* to avoid including other headers */
+typedef struct AlteredTableInfo AlteredTableInfo;
+typedef struct ParseState ParseState;
+typedef struct AlterTableUtilityContext AlterTableUtilityContext;
+typedef struct ForeignKeyCacheInfo ForeignKeyCacheInfo;
+
+extern bool tryAttachPartitionForeignKey(List **wqueue,
+ ForeignKeyCacheInfo *fk,
+ Relation partition,
+ Oid parentConstrOid, int numfks,
+ AttrNumber *mapped_conkey, AttrNumber *confkey,
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+extern PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec);
+extern void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs,
+ List **partexprs, Oid *partopclass, Oid *partcollation,
+ PartitionStrategy strategy);
+extern void CloneRowTriggersToPartition(Relation parent, Relation partition);
+extern void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
+ Relation partitionRel);
+extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
+ List *partConstraint);
+extern bool ConstraintImpliedByRelConstraint(Relation scanrel,
+ List *testConstraint, List *provenConstraint);
+extern ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
+ PartitionCmd *cmd,
+ AlterTableUtilityContext *context);
+extern ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab,
+ Relation rel, RangeVar *name,
+ bool concurrent);
+extern ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name);
+extern ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
+ RangeVar *name);
+
+#endif /* PARTCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e9b0fab0767..35d33fa9339 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -24,6 +24,138 @@
typedef struct AlterTableUtilityContext AlterTableUtilityContext; /* avoid including
* tcop/utility.h here */
+/*
+ * State information for ALTER TABLE
+ *
+ * The pending-work queue for an ALTER TABLE is a List of AlteredTableInfo
+ * structs, one for each table modified by the operation (the named table
+ * plus any child tables that are affected). We save lists of subcommands
+ * to apply to this table (possibly modified by parse transformation steps);
+ * these lists will be executed in Phase 2. If a Phase 3 step is needed,
+ * necessary information is stored in the constraints and newvals lists.
+ *
+ * Phase 2 is divided into multiple passes; subcommands are executed in
+ * a pass determined by subcommand type.
+ */
+
+typedef enum AlterTablePass
+{
+ AT_PASS_UNSET = -1, /* UNSET will cause ERROR */
+ AT_PASS_DROP, /* DROP (all flavors) */
+ AT_PASS_ALTER_TYPE, /* ALTER COLUMN TYPE */
+ AT_PASS_ADD_COL, /* ADD COLUMN */
+ AT_PASS_SET_EXPRESSION, /* ALTER SET EXPRESSION */
+ AT_PASS_OLD_INDEX, /* re-add existing indexes */
+ AT_PASS_OLD_CONSTR, /* re-add existing constraints */
+ /* We could support a RENAME COLUMN pass here, but not currently used */
+ AT_PASS_ADD_CONSTR, /* ADD constraints (initial examination) */
+ AT_PASS_COL_ATTRS, /* set column attributes, eg NOT NULL */
+ AT_PASS_ADD_INDEXCONSTR, /* ADD index-based constraints */
+ AT_PASS_ADD_INDEX, /* ADD indexes */
+ AT_PASS_ADD_OTHERCONSTR, /* ADD other constraints, defaults */
+ AT_PASS_MISC, /* other stuff */
+} AlterTablePass;
+
+#define AT_NUM_PASSES (AT_PASS_MISC + 1)
+
+typedef struct AlteredTableInfo
+{
+ /* Information saved before any work commences: */
+ Oid relid; /* Relation to work on */
+ char relkind; /* Its relkind */
+ TupleDesc oldDesc; /* Pre-modification tuple descriptor */
+
+ /*
+ * Transiently set during Phase 2, normally set to NULL.
+ *
+ * ATRewriteCatalogs sets this when it starts, and closes when ATExecCmd
+ * returns control. This can be exploited by ATExecCmd subroutines to
+ * close/reopen across transaction boundaries.
+ */
+ Relation rel;
+
+ /* Information saved by Phase 1 for Phase 2: */
+ List *subcmds[AT_NUM_PASSES]; /* Lists of AlterTableCmd */
+ /* Information saved by Phases 1/2 for Phase 3: */
+ List *constraints; /* List of NewConstraint */
+ List *newvals; /* List of NewColumnValue */
+ List *afterStmts; /* List of utility command parsetrees */
+ bool verify_new_notnull; /* T if we should recheck NOT NULL */
+ int rewrite; /* Reason for forced rewrite, if any */
+ bool chgAccessMethod; /* T if SET ACCESS METHOD is used */
+ Oid newAccessMethod; /* new access method; 0 means no change,
+ * if above is true */
+ Oid newTableSpace; /* new tablespace; 0 means no change */
+ bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
+ char newrelpersistence; /* if above is true */
+ Expr *partition_constraint; /* for attach partition validation */
+ /* true, if validating default due to some other attach/detach */
+ bool validate_default;
+ /* Objects to rebuild after completing ALTER TYPE operations */
+ List *changedConstraintOids; /* OIDs of constraints to rebuild */
+ List *changedConstraintDefs; /* string definitions of same */
+ List *changedIndexOids; /* OIDs of indexes to rebuild */
+ List *changedIndexDefs; /* string definitions of same */
+ char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */
+ char *clusterOnIndex; /* index to use for CLUSTER */
+ List *changedStatisticsOids; /* OIDs of statistics to rebuild */
+ List *changedStatisticsDefs; /* string definitions of same */
+} AlteredTableInfo;
+
+/* Alter table target-type flags for ATSimplePermissions */
+#define ATT_TABLE 0x0001
+#define ATT_VIEW 0x0002
+#define ATT_MATVIEW 0x0004
+#define ATT_INDEX 0x0008
+#define ATT_COMPOSITE_TYPE 0x0010
+#define ATT_FOREIGN_TABLE 0x0020
+#define ATT_PARTITIONED_INDEX 0x0040
+#define ATT_SEQUENCE 0x0080
+#define ATT_PARTITIONED_TABLE 0x0100
+
+/* Partial or complete FK creation in addFkConstraint() */
+typedef enum addFkConstraintSides
+{
+ addFkReferencedSide,
+ addFkReferencingSide,
+ addFkBothSides,
+} addFkConstraintSides;
+
+extern AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel);
+extern void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets);
+extern void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition);
+extern void RemoveInheritance(Relation child_rel, Relation parent_rel,
+ bool expect_detached);
+extern void addFkRecurseReferenced(Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool with_period);
+extern ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
+ bool recurse, bool recursing);
+extern void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel,
+ Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode);
+extern ObjectAddress addFkConstraint(addFkConstraintSides fkside,
+ char *constraintname,
+ Constraint *fkconstraint, Relation rel,
+ Relation pkrel, Oid indexOid,
+ Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators,
+ Oid *ffeqoperators, int numfkdelsetcols,
+ int16 *fkdelsetcols, bool is_internal,
+ bool with_period);
+extern void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
+ Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
+ int numfks, int16 *pkattnum, int16 *fkattnum,
+ Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
+ int numfkdelsetcols, int16 *fkdelsetcols,
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ bool with_period);
extern ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ObjectAddress *typaddress, const char *queryString);
@@ -105,7 +237,5 @@ extern void RangeVarCallbackMaintainsTable(const RangeVar *relation,
extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
-extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
- List *partConstraint);
#endif /* TABLECMDS_H */
--
2.39.5 (Apple Git-154)
On Dec 2, 2025, at 06:43, Nathan Bossart <nathandbossart@gmail.com> wrote:
On Mon, Dec 01, 2025 at 01:59:01PM -0500, Tom Lane wrote:
I didn't do any math about it, but that's got to be a far faster rate
of expansion than the overall PG code base. Maybe partitioning is
largely to blame? Perhaps analyzing what functionality got added
here in the past dozen or so years would yield some ideas for how to
split it.+1 for a split, if we can figure out a good plan.
I tried to move the partitioning-related code to a new file, and it wasn't
too bad. Note that there are a couple of internal-to-tablecmds.c things
that need to be exported. Besides that, the attached patch is still pretty
rough, and I'm not sure I correctly placed the line in the sand when
determining what stays and what goes, but this at least shows the general
shape of what's needed. (BTW git was generating an atrocious diff for
tablecmds.c. You might need to set the diff algorithm to "minimal" if you
are similarly affected.)src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 3456 +--------------------------------------------------------------------------------------------
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/partcmds.h | 53 ++
src/include/commands/tablecmds.h | 134 +++-
7 files changed, 3575 insertions(+), 3448 deletions(-)--
nathan
<v1-0001-move-partition-code-in-tablecmds.c-to-new-file.patch>
I am just thinking if tablecmds deserves a subfolder given the large code size. If we just split the file into multiple and still place them under src/backend/commands, then it would be not easy to identify alter-table related code.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On Mon, Dec 01, 2025 at 04:43:37PM -0600, Nathan Bossart wrote:
I tried to move the partitioning-related code to a new file, and it wasn't
too bad. Note that there are a couple of internal-to-tablecmds.c things
that need to be exported. Besides that, the attached patch is still pretty
rough, and I'm not sure I correctly placed the line in the sand when
determining what stays and what goes, but this at least shows the general
shape of what's needed. (BTW git was generating an atrocious diff for
tablecmds.c. You might need to set the diff algorithm to "minimal" if you
are similarly affected.)
Moving all the partition-specific code into a different file makes
sense here. Is partcmds.c as name the best fit though? Perhaps a
tablecmds_partition.c, with other files named tablecmds_popo.c to
indicate the sub-systems formerly in tablecmds.c?
src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 3456 +--------------------------------------------------------------------------------------------
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/partcmds.h | 53 ++
src/include/commands/tablecmds.h | 134 +++-
7 files changed, 3575 insertions(+), 3448 deletions(-)
The new contents of tablecmds.h don't have any strong dependency with
tablecmds.h, so perhaps having the "internal" structures like the ones
you are moving here into a new tablecmds_internal.h would be cleaner?
Another sub-area of tablecmds.c that could be split is I think the
rewrite logic. It has a lot of its own perks that become harder to
figure out the more tablecmds.c gets bloated.
--
Michael
Hi,
On Tue, Dec 2, 2025 at 9:04 AM Michael Paquier <michael@paquier.xyz> wrote:
On Mon, Dec 01, 2025 at 04:43:37PM -0600, Nathan Bossart wrote:
I tried to move the partitioning-related code to a new file, and it wasn't
too bad. Note that there are a couple of internal-to-tablecmds.c things
that need to be exported. Besides that, the attached patch is still pretty
rough, and I'm not sure I correctly placed the line in the sand when
determining what stays and what goes, but this at least shows the general
shape of what's needed. (BTW git was generating an atrocious diff for
tablecmds.c. You might need to set the diff algorithm to "minimal" if you
are similarly affected.)
+1 to splitting tablecmds.c at long last.
(I suppose I or someone could’ve proposed that back in Dec 2016 :-).
We did create src/backend/partitioning in v11 to move code from then
big catalog/partition.c, but this one has stayed untouched since then.
Better late than never.)
Moving all the partition-specific code into a different file makes
sense here. Is partcmds.c as name the best fit though? Perhaps a
tablecmds_partition.c, with other files named tablecmds_popo.c to
indicate the sub-systems formerly in tablecmds.c?
As Andres mentioned, it’s good to avoid slicing too granularly, but I
also thought of the name tablecmds_partition.c as soon as I saw “split
tablecmds.c” and “partitioning code.” That seems a reasonable first
cut.
src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 3456 +--------------------------------------------------------------------------------------------
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/partcmds.h | 53 ++
src/include/commands/tablecmds.h | 134 +++-
7 files changed, 3575 insertions(+), 3448 deletions(-)The new contents of tablecmds.h don't have any strong dependency with
tablecmds.h, so perhaps having the "internal" structures like the ones
you are moving here into a new tablecmds_internal.h would be cleaner?
+1 to introducing tablecmds_internal.h as well.
Another sub-area of tablecmds.c that could be split is I think the
rewrite logic. It has a lot of its own perks that become harder to
figure out the more tablecmds.c gets bloated.
+1 on splitting out the rewrite logic separately too.
--
Thanks, Amit Langote