agora inbox for [email protected]
help / color / mirror / Atom feed[PATCH v5 07/12] consistent language: to which
16+ messages / 7 participants
[nested] [flat]
* [PATCH v5 07/12] consistent language: to which
@ 2019-05-21 00:02 Justin Pryzby <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Justin Pryzby @ 2019-05-21 00:02 UTC (permalink / raw)
XXX: should have trailing period in tables?
---
doc/src/sgml/monitoring.sgml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index cba36c0..df5df62 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -602,12 +602,12 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
<row>
<entry><structfield>datid</structfield></entry>
<entry><type>oid</type></entry>
- <entry>OID of the database this backend is connected to</entry>
+ <entry>OID of the database to which this backend is connected</entry>
</row>
<row>
<entry><structfield>datname</structfield></entry>
<entry><type>name</type></entry>
- <entry>Name of the database this backend is connected to</entry>
+ <entry>Name of the database to which this backend is connected</entry>
</row>
<row>
<entry><structfield>pid</structfield></entry>
@@ -2099,7 +2099,7 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
<entry><type>text</type></entry>
<entry>
Host of the <productname>PostgreSQL</productname> instance
- this WAL receiver is connected to. This can be a host name,
+ to which this WAL receiver is connected. This can be a host name,
an IP address, or a directory path if the connection is via
Unix socket. (The path case can be distinguished because it
will always be an absolute path, beginning with <literal>/</literal>.)
@@ -2110,7 +2110,7 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
<entry><type>integer</type></entry>
<entry>
Port number of the <productname>PostgreSQL</productname> instance
- this WAL receiver is connected to.
+ to which this WAL receiver is connected.
</entry>
</row>
<row>
@@ -3375,7 +3375,7 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
<row>
<entry><literal><function>pg_stat_get_backend_dbid(integer)</function></literal></entry>
<entry><type>oid</type></entry>
- <entry>OID of the database this backend is connected to</entry>
+ <entry>OID of the database to which this backend is connected</entry>
</row>
<row>
--
2.7.4
--FkmkrVfFsRoUs1wW
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v5-0008-Consistent-language-contains.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 1/2] Determine operator semantics using btree opclasses
@ 2019-07-12 20:26 Tomas Vondra <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Tomas Vondra @ 2019-07-12 20:26 UTC (permalink / raw)
---
src/backend/statistics/dependencies.c | 15 +++---
src/backend/statistics/extended_stats.c | 69 +++++++++++++++++++------
src/backend/statistics/mcv.c | 60 ++++++++++++---------
src/include/statistics/statistics.h | 1 +
4 files changed, 99 insertions(+), 46 deletions(-)
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 66c38ce2bc..2723ca33df 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "access/stratnum.h"
#include "access/sysattr.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
@@ -788,15 +789,13 @@ dependency_is_compatible_clause(Node *clause, Index relid, AttrNumber *attnum)
* If it's not an "=" operator, just ignore the clause, as it's not
* compatible with functional dependencies.
*
- * This uses the function for estimating selectivity, not the operator
- * directly (a bit awkward, but well ...).
- *
- * XXX this is pretty dubious; probably it'd be better to check btree
- * or hash opclass membership, so as not to be fooled by custom
- * selectivity functions, and to be more consistent with decisions
- * elsewhere in the planner.
+ * To decide this, we need to decide the semantics of the operator.
+ * We do that by matching it to btree opclasses, and checking the
+ * strategy numbers. When there's no btree opclass, or when there
+ * are multiple strategy numbers, we consider the semantics to be
+ * unclear (i.e. not an equality operator).
*/
- if (get_oprrest(expr->opno) != F_EQSEL)
+ if (determine_operator_strategy(expr->opno) != BTEqualStrategyNumber)
return false;
/* OK to proceed with checking "var" */
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index c56ed48270..2ed28c054f 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -20,6 +20,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "access/tuptoaster.h"
+#include "access/stratnum.h"
#include "catalog/indexing.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_statistic_ext.h"
@@ -819,22 +820,14 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
*
* This uses the function for estimating selectivity, not the operator
* directly (a bit awkward, but well ...).
+ *
+ * XXX We can't check this against strategies defined in stratnum.h,
+ * because get_op_btree_interpretation() introduces a new strategy
+ * number for operator '<>' and we'd miss that. So we simply check
+ * if the result is (-1) which means strategy is unknown.
*/
- switch (get_oprrest(expr->opno))
- {
- case F_EQSEL:
- case F_NEQSEL:
- case F_SCALARLTSEL:
- case F_SCALARLESEL:
- case F_SCALARGTSEL:
- case F_SCALARGESEL:
- /* supported, will continue with inspection of the Var */
- break;
-
- default:
- /* other estimators are considered unknown/unsupported */
- return false;
- }
+ if (determine_operator_strategy(expr->opno) == -1)
+ return false;
/*
* If there are any securityQuals on the RTE from security barrier
@@ -1196,3 +1189,49 @@ statext_clauselist_selectivity(PlannerInfo *root, List *clauses, int varRelid,
return sel;
}
+
+/*
+ * determine_operator_strategy
+ * Determine operator semantics by matching it to btree opclasses.
+ *
+ * We look at all the btree opclasses referencing the supplied operator, and
+ * check if the strategy number is the same in all of them. If yes, we consider
+ * that to be the semantics of the operator (equality, less-than, ...) and
+ * return that strategy number.
+ *
+ * If there are no matching btree opclasses, or when the operator has multiple
+ * strategy numbers in various opclasses, we consider the operator semantics to
+ * be unclear/undefined and return -1.
+ *
+ * Note: This only looks at btree opclasses, so for data types with only a hash
+ * opclass, this always returns (-1). That ultimately means we won't be able
+ * to use extended statistics for them.
+ *
+ * Note: The result strategy number may not be defined in stratnum.h, so you
+ * need to be careful when checking the result. This is because this calls
+ * get_op_btree_interpretation which may introduce new stragies that are not
+ * actually part of btree definition (e.g. '<>')
+ */
+int
+determine_operator_strategy(Oid opno)
+{
+ ListCell *lc;
+ int strat;
+ List *opinfos;
+ Bitmapset *strats = NULL;
+
+ opinfos = get_op_btree_interpretation(opno);
+
+ foreach(lc, opinfos)
+ {
+ OpBtreeInterpretation *opinfo = lfirst(lc);
+
+ strats = bms_add_member(strats, opinfo->strategy);
+ }
+
+ /* multiple (or multiple) strategies means unclear semantics */
+ if (!bms_get_singleton_member(strats, &strat))
+ return -1;
+
+ return strat;
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 913a72ff67..1e919c82f9 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -1565,9 +1565,6 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses,
bool ok;
FmgrInfo opproc;
- /* get procedure computing operator selectivity */
- RegProcedure oprrest = get_oprrest(expr->opno);
-
fmgr_info(get_opcode(expr->opno), &opproc);
ok = (NumRelids(clause) == 1) &&
@@ -1583,6 +1580,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses,
Const *cst;
bool isgt;
int idx;
+ int strategy;
/* extract the var and const from the expression */
var = (varonleft) ? linitial(expr->args) : lsecond(expr->args);
@@ -1600,6 +1598,16 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses,
typecache = lookup_type_cache(var->vartype, TYPECACHE_GT_OPR);
fmgr_info(get_opcode(typecache->gt_opr), >proc);
+ /* determine operator semantics from btree opclasses */
+ strategy = determine_operator_strategy(expr->opno);
+
+ /*
+ * At this point all the expressions must have a meaningful
+ * btree opclass interpretation (thanks to passing through
+ * statext_is_compatible_clause, which enforces that).
+ */
+ Assert(strategy > 0);
+
/*
* Walk through the MCV items and evaluate the current clause.
* We can skip items that were already ruled out, and
@@ -1625,27 +1633,12 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses,
else if (is_or && (matches[i] == true))
continue;
- switch (oprrest)
+ switch (strategy)
{
- case F_EQSEL:
- case F_NEQSEL:
-
- /*
- * We don't care about isgt in equality, because
- * it does not matter whether it's (var op const)
- * or (const op var).
- */
- mismatch = !DatumGetBool(FunctionCall2Coll(&opproc,
- DEFAULT_COLLATION_OID,
- cst->constvalue,
- item->values[idx]));
-
- break;
-
- case F_SCALARLTSEL: /* column < constant */
- case F_SCALARLESEL: /* column <= constant */
- case F_SCALARGTSEL: /* column > constant */
- case F_SCALARGESEL: /* column >= constant */
+ case BTLessStrategyNumber: /* column < constant */
+ case BTLessEqualStrategyNumber: /* column <= constant */
+ case BTGreaterEqualStrategyNumber: /* column > constant */
+ case BTGreaterStrategyNumber: /* column >= constant */
/*
* First check whether the constant is below the
@@ -1664,6 +1657,27 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses,
cst->constvalue));
break;
+
+ default:
+
+ /*
+ * This does handle equality and inequality, but we handle
+ * that as a default case because inequality operator '<>'
+ * does not actually have a btree opclass strategy, it's
+ * added by get_op_btree_interpretation.
+ */
+
+ /*
+ * We don't care about isgt in equality, because
+ * it does not matter whether it's (var op const)
+ * or (const op var).
+ */
+ mismatch = !DatumGetBool(FunctionCall2Coll(&opproc,
+ DEFAULT_COLLATION_OID,
+ cst->constvalue,
+ item->values[idx]));
+
+ break;
}
/*
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index cb7bc630e9..82e2d5c619 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -118,5 +118,6 @@ extern Selectivity statext_clauselist_selectivity(PlannerInfo *root,
extern bool has_stats_of_kind(List *stats, char requiredkind);
extern StatisticExtInfo *choose_best_statistics(List *stats,
Bitmapset *attnums, char requiredkind);
+extern int determine_operator_strategy(Oid opno);
#endif /* STATISTICS_H */
--
2.20.1
--omnnplrhcrdpixl2
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
filename="0002-Fix-processing-of-OpExpr-in-extended-statistics.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v15 1/2] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e28335915..7c3bfde571 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--icqk4nrzalktuh5g
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v15-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e08ca1c68..7234cb3da6 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--u5bqxbslpyv5rwih
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v17-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e08ca1c68..7234cb3da6 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--u5bqxbslpyv5rwih
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v17-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v13 1/2] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8a08463c2b..b5a99b4edc 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4788,19 +4788,28 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid. XXX does this cause other problems?
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--57gotikuhuh4be5c
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v13-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v14 1/2] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e28335915..7c3bfde571 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--6opfqulj2xnxqhkn
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v14-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v15 1/2] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e28335915..7c3bfde571 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--icqk4nrzalktuh5g
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v15-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v16 1/3] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e28335915..7c3bfde571 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--arayebouu3vhq6bx
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v16-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid
@ 2023-07-12 16:57 Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Alvaro Herrera @ 2023-07-12 16:57 UTC (permalink / raw)
---
src/backend/utils/cache/relcache.c | 34 ++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8e08ca1c68..7234cb3da6 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4789,19 +4789,41 @@ RelationGetIndexList(Relation relation)
result = lappend_oid(result, index->indexrelid);
/*
- * Invalid, non-unique, non-immediate or predicate indexes aren't
- * interesting for either oid indexes or replication identity indexes,
- * so don't check them.
+ * Non-unique, non-immediate or predicate indexes aren't interesting
+ * for either oid indexes or replication identity indexes, so don't
+ * check them.
*/
- if (!index->indisvalid || !index->indisunique ||
+ if (!index->indisunique ||
!index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
- /* remember primary key index if any */
- if (index->indisprimary)
+ /*
+ * Remember primary key index, if any. We do this only if the index
+ * is valid; but if the table is partitioned, then we do it even if
+ * it's invalid.
+ *
+ * The reason for returning invalid primary keys for foreign tables is
+ * because of pg_dump of NOT NULL constraints, and the fact that PKs
+ * remain marked invalid until the partitions' PKs are attached to it.
+ * If we make rd_pkindex invalid, then the attnotnull flag is reset
+ * after the PK is created, which causes the ALTER INDEX ATTACH
+ * PARTITION to fail with 'column ... is not marked NOT NULL'. With
+ * this, dropconstraint_internal() will believe that the columns must
+ * not have attnotnull reset, so the PKs-on-partitions can be attached
+ * correctly, until finally the PK-on-parent is marked valid.
+ *
+ * Also, this doesn't harm anything, because rd_pkindex is not a
+ * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+ */
+ if (index->indisprimary &&
+ (index->indisvalid ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
pkeyIndex = index->indexrelid;
+ if (!index->indisvalid)
+ continue;
+
/* remember explicitly chosen replica index */
if (index->indisreplident)
candidateIndex = index->indexrelid;
--
2.39.2
--u5bqxbslpyv5rwih
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v17-0002-Add-pg_constraint-rows-for-NOT-NULL-constraints.patch"
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v3a] edit comments and stuff
@ 2026-01-05 14:26 Álvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Álvaro Herrera @ 2026-01-05 14:26 UTC (permalink / raw)
---
src/backend/utils/sort/tuplesort.c | 2 +-
src/include/executor/instrument_node.h | 146 ++++++++++++-------------
src/include/utils/tuplesort.h | 15 ---
3 files changed, 73 insertions(+), 90 deletions(-)
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c
index 473d31188b8..9e4cd816137 100644
--- a/src/backend/utils/sort/tuplesort.c
+++ b/src/backend/utils/sort/tuplesort.c
@@ -200,7 +200,7 @@ struct Tuplesortstate
int64 maxSpace; /* maximum amount of space occupied among sort
* of groups, either in-memory or on-disk */
bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk
- * space, false when its value for in-memory
+ * space, false when it's value for in-memory
* space */
TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */
LogicalTapeSet *tapeset; /* logtape.c object for tapes in a temp file */
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index 75520008c36..49627eb4a27 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -1,8 +1,12 @@
/*-------------------------------------------------------------------------
*
* instrument_node.h
- * Definitions for node-specific instrumentation
+ * Definitions for node-specific support for parallel query instrumentation
*
+ * These structs purposely contain no pointers because they are copied
+ * across processes during parallel query execution. Each worker copies its
+ * individual information into the container struct at executor shutdown time,
+ * to allow the leader to display the information in EXPLAIN ANALYZE.
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -15,32 +19,30 @@
#define INSTRUMENT_NODE_H
-
/* ---------------------
- * per-worker aggregate information
+ * Instrumentation information for aggregate function execution
* ---------------------
*/
typedef struct AggregateInstrumentation
{
- Size hash_mem_peak; /* peak hash table memory usage */
- uint64 hash_disk_used; /*kB of disk space used */
- int hash_batches_used; /* batches used during entire execution */
+ Size hash_mem_peak; /* peak hash table memory usage */
+ uint64 hash_disk_used; /* kB of disk space used */
+ int hash_batches_used; /* batches used during entire execution */
} AggregateInstrumentation;
-/* ----------------
- * Shared memory container for per-worker aggregate information
- * ----------------
+/*
+ * Shared memory container for per-worker aggregate information
*/
typedef struct SharedAggInfo
{
- int num_workers;
+ int num_workers;
AggregateInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedAggInfo;
-/*
- * Struct for statistics maintained by amgettuple and amgetbitmap
- *
- * Note: IndexScanInstrumentation can't contain any pointers, since it is
- * copied into a SharedIndexScanInstrumentation during parallel scans
+
+
+/* ---------------------
+ * Instrumentation information for indexscans (amgettuple and amgetbitmap)
+ * ---------------------
*/
typedef struct IndexScanInstrumentation
{
@@ -49,7 +51,7 @@ typedef struct IndexScanInstrumentation
} IndexScanInstrumentation;
/*
- * Struct for every worker's IndexScanInstrumentation, stored in shared memory
+ * Shared memory container for per-worker information
*/
typedef struct SharedIndexScanInstrumentation
{
@@ -57,11 +59,13 @@ typedef struct SharedIndexScanInstrumentation
IndexScanInstrumentation winstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedIndexScanInstrumentation;
-/*
- * BitmapHeapScanInstrumentation information
+
+/* ---------------------
+ * Instrumentation information for bitmap heap scans
*
* exact_pages total number of exact pages retrieved
* lossy_pages total number of lossy pages retrieved
+ * ---------------------
*/
typedef struct BitmapHeapScanInstrumentation
{
@@ -70,11 +74,7 @@ typedef struct BitmapHeapScanInstrumentation
} BitmapHeapScanInstrumentation;
/*
- * Instrumentation data for a parallel bitmap heap scan.
- *
- * A shared memory struct that each parallel worker copies its
- * BitmapHeapScanInstrumentation information into at executor shutdown to
- * allow the leader to display the information in EXPLAIN ANALYZE.
+ * Shared memory container for per-worker information
*/
typedef struct SharedBitmapHeapInstrumentation
{
@@ -83,25 +83,27 @@ typedef struct SharedBitmapHeapInstrumentation
} SharedBitmapHeapInstrumentation;
+/* ---------------------
+ * Instrumentation information for Memoize
+ * ---------------------
+ */
typedef struct MemoizeInstrumentation
{
- uint64 cache_hits; /* number of rescans where we've found the
- * scan parameters values to be cached */
- uint64 cache_misses; /* number of rescans where we've not found the
- * scan parameters values to be cached */
- uint64 cache_evictions; /* number of cache entries removed due to
- * the need to free memory */
- uint64 cache_overflows; /* number of times we've had to bypass the
- * cache when filling it due to not being
- * able to free enough space to store the
- * current scan's tuples */
- uint64 mem_peak; /* peak memory usage in bytes */
-
+ uint64 cache_hits; /* number of rescans where we've found the
+ * scan parameters values to be cached */
+ uint64 cache_misses; /* number of rescans where we've not found the
+ * scan parameters values to be cached */
+ uint64 cache_evictions; /* number of cache entries removed due to
+ * the need to free memory */
+ uint64 cache_overflows; /* number of times we've had to bypass the
+ * cache when filling it due to not being
+ * able to free enough space to store the
+ * current scan's tuples */
+ uint64 mem_peak; /* peak memory usage in bytes */
} MemoizeInstrumentation;
-/* ----------------
- * Shared memory container for per-worker memoize information
- * ----------------
+/*
+ * Shared memory container for per-worker memoize information
*/
typedef struct SharedMemoizeInfo
{
@@ -110,27 +112,25 @@ typedef struct SharedMemoizeInfo
} SharedMemoizeInfo;
-/*
- * Data structures for reporting sort statistics. Note that
- * TuplesortInstrumentation can't contain any pointers because we
- * sometimes put it in shared memory.
- *
- * The parallel-sort infrastructure relies on having a zero TuplesortMethod
- * to indicate that a worker never did anything, so we assign zero to
- * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
- * OR'ed together to represent a situation where different workers used
- * different methods, so we need a separate bit for each one. Keep the
- * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
+/* ---------------------
+ * Instrumentation information for Sorts.
+ * ---------------------
*/
-#define NUM_TUPLESORTMETHODS 4
-
typedef enum
{
SORT_SPACE_TYPE_DISK,
SORT_SPACE_TYPE_MEMORY,
} TuplesortSpaceType;
+/*
+ * The parallel-sort infrastructure relies on having a zero TuplesortMethod
+ * to indicate that a worker never did anything, so we assign zero to
+ * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
+ * OR'ed together to represent a situation where different workers used
+ * different methods, so we need a separate bit for each one. Keep the
+ * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
+ */
typedef enum
{
SORT_TYPE_STILL_IN_PROGRESS = 0,
@@ -139,40 +139,40 @@ typedef enum
SORT_TYPE_EXTERNAL_SORT = 1 << 2,
SORT_TYPE_EXTERNAL_MERGE = 1 << 3
} TuplesortMethod;
+#define NUM_TUPLESORTMETHODS 4
typedef struct TuplesortInstrumentation
{
- TuplesortMethod sortMethod; /* sort algorithm used */
+ TuplesortMethod sortMethod; /* sort algorithm used */
TuplesortSpaceType spaceType; /* type of space spaceUsed represents */
- int64 spaceUsed; /* space consumption, in kB */
+ int64 spaceUsed; /* space consumption, in kB */
} TuplesortInstrumentation;
-/* ----------------
- * Shared memory container for per-worker sort information
- * ----------------
+/*
+ * Shared memory container for per-worker sort information
*/
typedef struct SharedSortInfo
{
- int num_workers;
+ int num_workers;
TuplesortInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedSortInfo;
-/* ----------------
- * Values displayed by EXPLAIN ANALYZE
- * ----------------
+
+/* ---------------------
+ * Instrumentation information for nodeHash.c
+ * ---------------------
*/
typedef struct HashInstrumentation
{
- int nbuckets; /* number of buckets at end of execution */
- int nbuckets_original; /* planned number of buckets */
- int nbatch; /* number of batches at end of execution */
- int nbatch_original; /* planned number of batches */
- Size space_peak; /* peak memory usage in bytes */
+ int nbuckets; /* number of buckets at end of execution */
+ int nbuckets_original; /* planned number of buckets */
+ int nbatch; /* number of batches at end of execution */
+ int nbatch_original; /* planned number of batches */
+ Size space_peak; /* peak memory usage in bytes */
} HashInstrumentation;
-/* ----------------
- * Shared memory container for per-worker hash information
- * ----------------
+/*
+ * Shared memory container for per-worker information
*/
typedef struct SharedHashInfo
{
@@ -180,9 +180,10 @@ typedef struct SharedHashInfo
HashInstrumentation hinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedHashInfo;
-/* ----------------
+
+/* ---------------------
* Instrumentation information for IncrementalSort
- * ----------------
+ * ---------------------
*/
typedef struct IncrementalSortGroupInfo
{
@@ -200,10 +201,7 @@ typedef struct IncrementalSortInfo
IncrementalSortGroupInfo prefixsortGroupInfo;
} IncrementalSortInfo;
-/* ----------------
- * Shared memory container for per-worker incremental sort information
- * ----------------
- */
+/* Shared memory container for per-worker incremental sort information */
typedef struct SharedIncrementalSortInfo
{
int num_workers;
diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.h
index a7e867f36b8..943a2b7dc93 100644
--- a/src/include/utils/tuplesort.h
+++ b/src/include/utils/tuplesort.h
@@ -62,21 +62,6 @@ typedef struct SortCoordinateData
typedef struct SortCoordinateData *SortCoordinate;
-/*
- * Data structures for reporting sort statistics. Note that
- * TuplesortInstrumentation can't contain any pointers because we
- * sometimes put it in shared memory.
- *
- * The parallel-sort infrastructure relies on having a zero TuplesortMethod
- * to indicate that a worker never did anything, so we assign zero to
- * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
- * OR'ed together to represent a situation where different workers used
- * different methods, so we need a separate bit for each one. Keep the
- * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
- */
-
-#define NUM_TUPLESORTMETHODS 4
-
/* Bitwise option flags for tuple sorts */
#define TUPLESORT_NONE 0
--
2.47.3
--xs5il3t73pzvzpq7--
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v3a] edit comments and stuff
@ 2026-01-05 14:26 Álvaro Herrera <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Álvaro Herrera @ 2026-01-05 14:26 UTC (permalink / raw)
---
src/backend/utils/sort/tuplesort.c | 2 +-
src/include/executor/instrument_node.h | 146 ++++++++++++-------------
src/include/utils/tuplesort.h | 15 ---
3 files changed, 73 insertions(+), 90 deletions(-)
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c
index 473d31188b8..9e4cd816137 100644
--- a/src/backend/utils/sort/tuplesort.c
+++ b/src/backend/utils/sort/tuplesort.c
@@ -200,7 +200,7 @@ struct Tuplesortstate
int64 maxSpace; /* maximum amount of space occupied among sort
* of groups, either in-memory or on-disk */
bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk
- * space, false when its value for in-memory
+ * space, false when it's value for in-memory
* space */
TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */
LogicalTapeSet *tapeset; /* logtape.c object for tapes in a temp file */
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index 75520008c36..49627eb4a27 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -1,8 +1,12 @@
/*-------------------------------------------------------------------------
*
* instrument_node.h
- * Definitions for node-specific instrumentation
+ * Definitions for node-specific support for parallel query instrumentation
*
+ * These structs purposely contain no pointers because they are copied
+ * across processes during parallel query execution. Each worker copies its
+ * individual information into the container struct at executor shutdown time,
+ * to allow the leader to display the information in EXPLAIN ANALYZE.
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -15,32 +19,30 @@
#define INSTRUMENT_NODE_H
-
/* ---------------------
- * per-worker aggregate information
+ * Instrumentation information for aggregate function execution
* ---------------------
*/
typedef struct AggregateInstrumentation
{
- Size hash_mem_peak; /* peak hash table memory usage */
- uint64 hash_disk_used; /*kB of disk space used */
- int hash_batches_used; /* batches used during entire execution */
+ Size hash_mem_peak; /* peak hash table memory usage */
+ uint64 hash_disk_used; /* kB of disk space used */
+ int hash_batches_used; /* batches used during entire execution */
} AggregateInstrumentation;
-/* ----------------
- * Shared memory container for per-worker aggregate information
- * ----------------
+/*
+ * Shared memory container for per-worker aggregate information
*/
typedef struct SharedAggInfo
{
- int num_workers;
+ int num_workers;
AggregateInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedAggInfo;
-/*
- * Struct for statistics maintained by amgettuple and amgetbitmap
- *
- * Note: IndexScanInstrumentation can't contain any pointers, since it is
- * copied into a SharedIndexScanInstrumentation during parallel scans
+
+
+/* ---------------------
+ * Instrumentation information for indexscans (amgettuple and amgetbitmap)
+ * ---------------------
*/
typedef struct IndexScanInstrumentation
{
@@ -49,7 +51,7 @@ typedef struct IndexScanInstrumentation
} IndexScanInstrumentation;
/*
- * Struct for every worker's IndexScanInstrumentation, stored in shared memory
+ * Shared memory container for per-worker information
*/
typedef struct SharedIndexScanInstrumentation
{
@@ -57,11 +59,13 @@ typedef struct SharedIndexScanInstrumentation
IndexScanInstrumentation winstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedIndexScanInstrumentation;
-/*
- * BitmapHeapScanInstrumentation information
+
+/* ---------------------
+ * Instrumentation information for bitmap heap scans
*
* exact_pages total number of exact pages retrieved
* lossy_pages total number of lossy pages retrieved
+ * ---------------------
*/
typedef struct BitmapHeapScanInstrumentation
{
@@ -70,11 +74,7 @@ typedef struct BitmapHeapScanInstrumentation
} BitmapHeapScanInstrumentation;
/*
- * Instrumentation data for a parallel bitmap heap scan.
- *
- * A shared memory struct that each parallel worker copies its
- * BitmapHeapScanInstrumentation information into at executor shutdown to
- * allow the leader to display the information in EXPLAIN ANALYZE.
+ * Shared memory container for per-worker information
*/
typedef struct SharedBitmapHeapInstrumentation
{
@@ -83,25 +83,27 @@ typedef struct SharedBitmapHeapInstrumentation
} SharedBitmapHeapInstrumentation;
+/* ---------------------
+ * Instrumentation information for Memoize
+ * ---------------------
+ */
typedef struct MemoizeInstrumentation
{
- uint64 cache_hits; /* number of rescans where we've found the
- * scan parameters values to be cached */
- uint64 cache_misses; /* number of rescans where we've not found the
- * scan parameters values to be cached */
- uint64 cache_evictions; /* number of cache entries removed due to
- * the need to free memory */
- uint64 cache_overflows; /* number of times we've had to bypass the
- * cache when filling it due to not being
- * able to free enough space to store the
- * current scan's tuples */
- uint64 mem_peak; /* peak memory usage in bytes */
-
+ uint64 cache_hits; /* number of rescans where we've found the
+ * scan parameters values to be cached */
+ uint64 cache_misses; /* number of rescans where we've not found the
+ * scan parameters values to be cached */
+ uint64 cache_evictions; /* number of cache entries removed due to
+ * the need to free memory */
+ uint64 cache_overflows; /* number of times we've had to bypass the
+ * cache when filling it due to not being
+ * able to free enough space to store the
+ * current scan's tuples */
+ uint64 mem_peak; /* peak memory usage in bytes */
} MemoizeInstrumentation;
-/* ----------------
- * Shared memory container for per-worker memoize information
- * ----------------
+/*
+ * Shared memory container for per-worker memoize information
*/
typedef struct SharedMemoizeInfo
{
@@ -110,27 +112,25 @@ typedef struct SharedMemoizeInfo
} SharedMemoizeInfo;
-/*
- * Data structures for reporting sort statistics. Note that
- * TuplesortInstrumentation can't contain any pointers because we
- * sometimes put it in shared memory.
- *
- * The parallel-sort infrastructure relies on having a zero TuplesortMethod
- * to indicate that a worker never did anything, so we assign zero to
- * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
- * OR'ed together to represent a situation where different workers used
- * different methods, so we need a separate bit for each one. Keep the
- * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
+/* ---------------------
+ * Instrumentation information for Sorts.
+ * ---------------------
*/
-#define NUM_TUPLESORTMETHODS 4
-
typedef enum
{
SORT_SPACE_TYPE_DISK,
SORT_SPACE_TYPE_MEMORY,
} TuplesortSpaceType;
+/*
+ * The parallel-sort infrastructure relies on having a zero TuplesortMethod
+ * to indicate that a worker never did anything, so we assign zero to
+ * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
+ * OR'ed together to represent a situation where different workers used
+ * different methods, so we need a separate bit for each one. Keep the
+ * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
+ */
typedef enum
{
SORT_TYPE_STILL_IN_PROGRESS = 0,
@@ -139,40 +139,40 @@ typedef enum
SORT_TYPE_EXTERNAL_SORT = 1 << 2,
SORT_TYPE_EXTERNAL_MERGE = 1 << 3
} TuplesortMethod;
+#define NUM_TUPLESORTMETHODS 4
typedef struct TuplesortInstrumentation
{
- TuplesortMethod sortMethod; /* sort algorithm used */
+ TuplesortMethod sortMethod; /* sort algorithm used */
TuplesortSpaceType spaceType; /* type of space spaceUsed represents */
- int64 spaceUsed; /* space consumption, in kB */
+ int64 spaceUsed; /* space consumption, in kB */
} TuplesortInstrumentation;
-/* ----------------
- * Shared memory container for per-worker sort information
- * ----------------
+/*
+ * Shared memory container for per-worker sort information
*/
typedef struct SharedSortInfo
{
- int num_workers;
+ int num_workers;
TuplesortInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedSortInfo;
-/* ----------------
- * Values displayed by EXPLAIN ANALYZE
- * ----------------
+
+/* ---------------------
+ * Instrumentation information for nodeHash.c
+ * ---------------------
*/
typedef struct HashInstrumentation
{
- int nbuckets; /* number of buckets at end of execution */
- int nbuckets_original; /* planned number of buckets */
- int nbatch; /* number of batches at end of execution */
- int nbatch_original; /* planned number of batches */
- Size space_peak; /* peak memory usage in bytes */
+ int nbuckets; /* number of buckets at end of execution */
+ int nbuckets_original; /* planned number of buckets */
+ int nbatch; /* number of batches at end of execution */
+ int nbatch_original; /* planned number of batches */
+ Size space_peak; /* peak memory usage in bytes */
} HashInstrumentation;
-/* ----------------
- * Shared memory container for per-worker hash information
- * ----------------
+/*
+ * Shared memory container for per-worker information
*/
typedef struct SharedHashInfo
{
@@ -180,9 +180,10 @@ typedef struct SharedHashInfo
HashInstrumentation hinstrument[FLEXIBLE_ARRAY_MEMBER];
} SharedHashInfo;
-/* ----------------
+
+/* ---------------------
* Instrumentation information for IncrementalSort
- * ----------------
+ * ---------------------
*/
typedef struct IncrementalSortGroupInfo
{
@@ -200,10 +201,7 @@ typedef struct IncrementalSortInfo
IncrementalSortGroupInfo prefixsortGroupInfo;
} IncrementalSortInfo;
-/* ----------------
- * Shared memory container for per-worker incremental sort information
- * ----------------
- */
+/* Shared memory container for per-worker incremental sort information */
typedef struct SharedIncrementalSortInfo
{
int num_workers;
diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.h
index a7e867f36b8..943a2b7dc93 100644
--- a/src/include/utils/tuplesort.h
+++ b/src/include/utils/tuplesort.h
@@ -62,21 +62,6 @@ typedef struct SortCoordinateData
typedef struct SortCoordinateData *SortCoordinate;
-/*
- * Data structures for reporting sort statistics. Note that
- * TuplesortInstrumentation can't contain any pointers because we
- * sometimes put it in shared memory.
- *
- * The parallel-sort infrastructure relies on having a zero TuplesortMethod
- * to indicate that a worker never did anything, so we assign zero to
- * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be
- * OR'ed together to represent a situation where different workers used
- * different methods, so we need a separate bit for each one. Keep the
- * NUM_TUPLESORTMETHODS constant in sync with the number of bits!
- */
-
-#define NUM_TUPLESORTMETHODS 4
-
/* Bitwise option flags for tuple sorts */
#define TUPLESORT_NONE 0
--
2.47.3
--xs5il3t73pzvzpq7--
^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v3 1/1] move partitioning code out of tablecmds.c
@ 2026-01-29 22:09 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Nathan Bossart @ 2026-01-29 22:09 UTC (permalink / raw)
---
src/backend/commands/Makefile | 1 +
src/backend/commands/meson.build | 1 +
src/backend/commands/tablecmds.c | 4787 +-------------------
src/backend/commands/tablecmds_partition.c | 4627 +++++++++++++++++++
src/backend/partitioning/partbounds.c | 1 +
src/include/commands/tablecmds.h | 2 -
src/include/commands/tablecmds_internal.h | 194 +
src/include/commands/tablecmds_partition.h | 66 +
8 files changed, 4903 insertions(+), 4776 deletions(-)
create mode 100644 src/backend/commands/tablecmds_partition.c
create mode 100644 src/include/commands/tablecmds_internal.h
create mode 100644 src/include/commands/tablecmds_partition.h
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 64cb6278409..764f75f6b52 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -57,6 +57,7 @@ OBJS = \
statscmds.o \
subscriptioncmds.o \
tablecmds.o \
+ tablecmds_partition.o \
tablespace.o \
trigger.o \
tsearchcmds.o \
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index ca3f53c6213..86b9d8780fd 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -45,6 +45,7 @@ backend_sources += files(
'statscmds.c',
'subscriptioncmds.c',
'tablecmds.c',
+ 'tablecmds_partition.c',
'tablespace.c',
'trigger.c',
'tsearchcmds.c',
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f976c0e5c7e..4abba13a5b8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -62,6 +62,8 @@
#include "commands/event_trigger.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
+#include "commands/tablecmds_internal.h"
+#include "commands/tablecmds_partition.h"
#include "commands/tablespace.h"
#include "commands/trigger.h"
#include "commands/typecmds.h"
@@ -131,115 +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
-{
- char *name; /* Constraint name, or NULL if none */
- ConstrType contype; /* CHECK or FOREIGN */
- Oid refrelid; /* PK rel, if FOREIGN */
- Oid refindid; /* OID of PK's index, if FOREIGN */
- bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */
- Oid conid; /* OID of pg_constraint entry, if FOREIGN */
- Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
- ExprState *qualstate; /* Execution state for CHECK expr */
-} NewConstraint;
-
-/*
- * Struct describing one new column value that needs to be computed during
- * Phase 3 copy (this could be either a new column with a non-null default, or
- * a column that we're changing the type of). Columns without such an entry
- * are just copied from the old table during ATRewriteTable. Note that the
- * expr is an expression over *old* table values, except when is_generated
- * is true; then it is an expression over columns of the *new* tuple.
- */
-typedef struct NewColumnValue
-{
- AttrNumber attnum; /* which column */
- Expr *expr; /* expression to compute */
- ExprState *exprstate; /* execution state */
- bool is_generated; /* is it a GENERATED expression? */
-} NewColumnValue;
-
/*
* Error-reporting support for RemoveRelations
*/
@@ -325,17 +218,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 +232,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 +305,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);
@@ -455,7 +327,6 @@ static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
Oid pkindOid, Oid constraintOid, bool hasperiod);
-static void CheckAlterTableIsSafe(Relation rel);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -476,12 +347,9 @@ 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);
-static void ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode);
static void ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd,
LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -508,8 +376,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 +384,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 +429,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 +439,6 @@ 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,45 +515,9 @@ 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);
-static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
- PartitionCmd *cmd, AlterTableUtilityContext *context);
-static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
- Relation rel, PartitionCmd *cmd,
- AlterTableUtilityContext *context);
-
/* ----------------------------------------------------------------
* DefineRelation
* Creates a new relation.
@@ -4443,7 +4215,7 @@ CheckTableNotInUse(Relation rel, const char *stmt)
* that, notably DROP TABLE. (We must allow DROP or we couldn't clean out
* an orphaned temp schema.) Compare truncate_check_activity().
*/
-static void
+void
CheckAlterTableIsSafe(Relation rel)
{
/*
@@ -6583,7 +6355,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);
@@ -6764,7 +6536,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;
@@ -6879,36 +6651,6 @@ ATSimpleRecursion(List **wqueue, Relation rel,
}
}
-/*
- * Obtain list of partitions of the given table, locking them all at the given
- * lockmode and ensuring that they all pass CheckAlterTableIsSafe.
- *
- * This function is a no-op if the given relation is not a partitioned table;
- * in particular, nothing is done if it's a legacy inheritance parent.
- */
-static void
-ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode)
-{
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- List *inh;
- ListCell *cell;
-
- inh = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
- /* first element is the parent rel; must ignore it */
- for_each_from(cell, inh, 1)
- {
- Relation childrel;
-
- /* find_all_inheritors already got lock */
- childrel = table_open(lfirst_oid(cell), NoLock);
- CheckAlterTableIsSafe(childrel);
- table_close(childrel, NoLock);
- }
- list_free(inh);
- }
-}
-
/*
* ATTypedTableRecursion
*
@@ -8513,7 +8255,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)
{
@@ -10748,7 +10490,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,
@@ -10926,7 +10668,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,
@@ -11064,7 +10806,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,
@@ -11230,795 +10972,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
*
@@ -12029,7 +10982,7 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid,
* trigger check will be skipped, and the trigger will be considered for
* removal.
*/
-static void
+void
DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid,
Oid conrelid)
{
@@ -12092,128 +11045,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
*
@@ -13024,7 +11855,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)
{
@@ -17398,7 +16229,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;
@@ -17884,78 +16715,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
*
@@ -17974,7 +16733,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;
@@ -19746,2290 +18505,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 */
- }
- }
-}
-
-/*
- * attachPartitionTable: attach a new partition to the partitioned table
- *
- * wqueue: the ALTER TABLE work queue; can be NULL when not running as part
- * of an ALTER TABLE sequence.
- * rel: partitioned relation;
- * attachrel: relation of attached partition;
- * bound: bounds of attached relation.
- */
-static void
-attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound)
-{
- /*
- * Create an inheritance; the relevant checks are performed inside the
- * function.
- */
- CreateInheritance(attachrel, rel, true);
-
- /* Update the pg_class entry. */
- StorePartitionBound(attachrel, rel, 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);
-}
-
-/*
- * 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);
-
- attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
-
- /*
- * Generate a 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_array(Relation, list_length(attachRelIdxs));
- attachInfos = palloc_array(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(NULL,
- 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.
*/
@@ -22104,1239 +18579,3 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
return cstorage;
}
-
-/*
- * buildExpressionExecutionStates: build the needed expression execution states
- * for new partition (newPartRel) checks and initialize expressions for
- * generated columns. All expressions should be created in "tab"
- * (AlteredTableInfo structure).
- */
-static void
-buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate)
-{
- /*
- * Build the needed expression execution states. Here, we expect only NOT
- * NULL and CHECK constraint.
- */
- foreach_ptr(NewConstraint, con, tab->constraints)
- {
- switch (con->contype)
- {
- case CONSTR_CHECK:
-
- /*
- * We already expanded virtual expression in
- * createTableConstraints.
- */
- con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
- break;
- case CONSTR_NOTNULL:
- /* Nothing to do here. */
- break;
- default:
- elog(ERROR, "unrecognized constraint type: %d",
- (int) con->contype);
- }
- }
-
- /* Expression already planned in createTableConstraints */
- foreach_ptr(NewColumnValue, ex, tab->newvals)
- ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
-}
-
-/*
- * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated
- * expressions for "tab" (AlteredTableInfo structure) whose inputs come from
- * the new tuple (insertslot) of the new partition (newPartRel).
- */
-static void
-evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab,
- Relation newPartRel,
- TupleTableSlot *insertslot,
- ExprContext *econtext)
-{
- econtext->ecxt_scantuple = insertslot;
-
- foreach_ptr(NewColumnValue, ex, tab->newvals)
- {
- if (!ex->is_generated)
- continue;
-
- insertslot->tts_values[ex->attnum - 1]
- = ExecEvalExpr(ex->exprstate,
- econtext,
- &insertslot->tts_isnull[ex->attnum - 1]);
- }
-
- foreach_ptr(NewConstraint, con, tab->constraints)
- {
- switch (con->contype)
- {
- case CONSTR_CHECK:
- if (!ExecCheck(con->qualstate, econtext))
- ereport(ERROR,
- errcode(ERRCODE_CHECK_VIOLATION),
- errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row",
- con->name, RelationGetRelationName(newPartRel)),
- errtableconstraint(newPartRel, con->name));
- break;
- case CONSTR_NOTNULL:
- case CONSTR_FOREIGN:
- /* Nothing to do here */
- break;
- default:
- elog(ERROR, "unrecognized constraint type: %d",
- (int) con->contype);
- }
- }
-}
-
-/*
- * getAttributesList: build a list of columns (ColumnDef) based on parent_rel
- */
-static List *
-getAttributesList(Relation parent_rel)
-{
- AttrNumber parent_attno;
- TupleDesc modelDesc;
- List *colList = NIL;
-
- modelDesc = RelationGetDescr(parent_rel);
-
- for (parent_attno = 1; parent_attno <= modelDesc->natts;
- parent_attno++)
- {
- Form_pg_attribute attribute = TupleDescAttr(modelDesc,
- parent_attno - 1);
- ColumnDef *def;
-
- /* Ignore dropped columns in the parent. */
- if (attribute->attisdropped)
- continue;
-
- def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid,
- attribute->atttypmod, attribute->attcollation);
-
- def->is_not_null = attribute->attnotnull;
-
- /* Copy identity. */
- def->identity = attribute->attidentity;
-
- /* Copy attgenerated. */
- def->generated = attribute->attgenerated;
-
- def->storage = attribute->attstorage;
-
- /* Likewise, copy compression. */
- if (CompressionMethodIsValid(attribute->attcompression))
- def->compression =
- pstrdup(GetCompressionMethodName(attribute->attcompression));
- else
- def->compression = NULL;
-
- /* Add to column list. */
- colList = lappend(colList, def);
- }
-
- return colList;
-}
-
-/*
- * createTableConstraints:
- * create check constraints, default values, and generated values for newRel
- * based on parent_rel. tab is pending-work queue for newRel, we may need it in
- * MergePartitionsMoveRows.
- */
-static void
-createTableConstraints(List **wqueue, AlteredTableInfo *tab,
- Relation parent_rel, Relation newRel)
-{
- TupleDesc tupleDesc;
- TupleConstr *constr;
- AttrMap *attmap;
- AttrNumber parent_attno;
- int ccnum;
- List *constraints = NIL;
- List *cookedConstraints = NIL;
-
- tupleDesc = RelationGetDescr(parent_rel);
- constr = tupleDesc->constr;
-
- if (!constr)
- return;
-
- /*
- * Construct a map from the parent relation's attnos to the child rel's.
- * This re-checks type match, etc, although it shouldn't be possible to
- * have a failure since both tables are locked.
- */
- attmap = build_attrmap_by_name(RelationGetDescr(newRel),
- tupleDesc,
- false);
-
- /* Cycle for default values. */
- for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++)
- {
- Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
- parent_attno - 1);
-
- /* Ignore dropped columns in the parent. */
- if (attribute->attisdropped)
- continue;
-
- /* Copy the default, if present, and it should be copied. */
- if (attribute->atthasdef)
- {
- Node *this_default = NULL;
- bool found_whole_row;
- AttrNumber num;
- Node *def;
- NewColumnValue *newval;
-
- if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
- this_default = build_generation_expression(parent_rel, attribute->attnum);
- else
- {
- this_default = TupleDescGetDefault(tupleDesc, attribute->attnum);
- if (this_default == NULL)
- elog(ERROR, "default expression not found for attribute %d of relation \"%s\"",
- attribute->attnum, RelationGetRelationName(parent_rel));
- }
-
- num = attmap->attnums[parent_attno - 1];
- def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row);
-
- if (found_whole_row && attribute->attgenerated != '\0')
- elog(ERROR, "cannot convert whole-row table reference");
-
- /* Add a pre-cooked default expression. */
- StoreAttrDefault(newRel, num, def, true);
-
- /*
- * Stored generated column expressions in parent_rel might
- * reference the tableoid. newRel, parent_rel tableoid clear is
- * not the same. If so, these stored generated columns require
- * recomputation for newRel within MergePartitionsMoveRows.
- */
- if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED)
- {
- newval = palloc0_object(NewColumnValue);
- newval->attnum = num;
- newval->expr = expression_planner((Expr *) def);
- newval->is_generated = (attribute->attgenerated != '\0');
- tab->newvals = lappend(tab->newvals, newval);
- }
- }
- }
-
- /* Cycle for CHECK constraints. */
- for (ccnum = 0; ccnum < constr->num_check; ccnum++)
- {
- char *ccname = constr->check[ccnum].ccname;
- char *ccbin = constr->check[ccnum].ccbin;
- bool ccenforced = constr->check[ccnum].ccenforced;
- bool ccnoinherit = constr->check[ccnum].ccnoinherit;
- bool ccvalid = constr->check[ccnum].ccvalid;
- Node *ccbin_node;
- bool found_whole_row;
- Constraint *constr;
-
- /*
- * The partitioned table can not have a NO INHERIT check constraint
- * (see StoreRelCheck function for details).
- */
- Assert(!ccnoinherit);
-
- ccbin_node = map_variable_attnos(stringToNode(ccbin),
- 1, 0,
- attmap,
- InvalidOid, &found_whole_row);
-
- /*
- * For the moment we have to reject whole-row variables (as for CREATE
- * TABLE LIKE and inheritances).
- */
- if (found_whole_row)
- elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".",
- ccname,
- RelationGetRelationName(parent_rel));
-
- constr = makeNode(Constraint);
- constr->contype = CONSTR_CHECK;
- constr->conname = pstrdup(ccname);
- constr->deferrable = false;
- constr->initdeferred = false;
- constr->is_enforced = ccenforced;
- constr->skip_validation = !ccvalid;
- constr->initially_valid = ccvalid;
- constr->is_no_inherit = ccnoinherit;
- constr->raw_expr = NULL;
- constr->cooked_expr = nodeToString(ccbin_node);
- constr->location = -1;
- constraints = lappend(constraints, constr);
- }
-
- /* Install all CHECK constraints. */
- cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints,
- false, true, true, NULL);
-
- /* Make the additional catalog changes visible. */
- CommandCounterIncrement();
-
- /*
- * parent_rel check constraint expression may reference tableoid, so later
- * in MergePartitionsMoveRows, we need to evaluate the check constraint
- * again for the newRel. We can check whether the check constraint
- * contains a tableoid reference via pull_varattnos.
- */
- foreach_ptr(CookedConstraint, ccon, cookedConstraints)
- {
- if (!ccon->skip_validation)
- {
- Node *qual;
- Bitmapset *attnums = NULL;
-
- Assert(ccon->contype == CONSTR_CHECK);
- qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1);
- pull_varattnos(qual, 1, &attnums);
-
- /*
- * Add a check only if it contains a tableoid
- * (TableOidAttributeNumber).
- */
- if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber,
- attnums))
- {
- NewConstraint *newcon;
-
- newcon = palloc0_object(NewConstraint);
- newcon->name = ccon->name;
- newcon->contype = CONSTR_CHECK;
- newcon->qual = qual;
-
- tab->constraints = lappend(tab->constraints, newcon);
- }
- }
- }
-
- /* Don't need the cookedConstraints anymore. */
- list_free_deep(cookedConstraints);
-
- /* Reproduce not-null constraints. */
- if (constr->has_not_null)
- {
- List *nnconstraints;
-
- /*
- * The "include_noinh" argument is false because a partitioned table
- * can't have NO INHERIT constraint.
- */
- nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel),
- false, false);
-
- Assert(list_length(nnconstraints) > 0);
-
- /*
- * We already set pg_attribute.attnotnull in createPartitionTable. No
- * need call set_attnotnull again.
- */
- AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL);
- }
-}
-
-/*
- * createPartitionTable:
- *
- * Create a new partition (newPartName) for the partitioned table (parent_rel).
- * ownerId is determined by the partition on which the operation is performed,
- * so it is passed separately. The new partition will inherit the access method
- * and persistence type from the parent table.
- *
- * Returns the created relation (locked in AccessExclusiveLock mode).
- */
-static Relation
-createPartitionTable(List **wqueue, RangeVar *newPartName,
- Relation parent_rel, Oid ownerId)
-{
- Relation newRel;
- Oid newRelId;
- Oid existingRelid;
- TupleDesc descriptor;
- List *colList = NIL;
- Oid relamId;
- Oid namespaceId;
- AlteredTableInfo *new_partrel_tab;
- Form_pg_class parent_relform = parent_rel->rd_rel;
-
- /* If the existing rel is temp, it must belong to this session. */
- if (RELATION_IS_OTHER_TEMP(parent_rel))
- ereport(ERROR,
- errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create as partition of temporary relation of another session"));
-
- /* Look up inheritance ancestors and generate the relation schema. */
- colList = getAttributesList(parent_rel);
-
- /* Create a tuple descriptor from the relation schema. */
- descriptor = BuildDescForRelation(colList);
-
- /* Look up the access method for the new relation. */
- relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID;
-
- /* Look up the namespace in which we are supposed to create the relation. */
- namespaceId =
- RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid);
- if (OidIsValid(existingRelid))
- ereport(ERROR,
- errcode(ERRCODE_DUPLICATE_TABLE),
- errmsg("relation \"%s\" already exists", newPartName->relname));
-
- /*
- * We intended to create the partition with the same persistence as the
- * parent table, but we still need to recheck because that might be
- * affected by the search_path. If the parent is permanent, so must be
- * all of its partitions.
- */
- if (parent_relform->relpersistence != RELPERSISTENCE_TEMP &&
- newPartName->relpersistence == RELPERSISTENCE_TEMP)
- ereport(ERROR,
- errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
- RelationGetRelationName(parent_rel)));
-
- /* Permanent rels cannot be partitions belonging to a temporary parent. */
- if (newPartName->relpersistence != RELPERSISTENCE_TEMP &&
- parent_relform->relpersistence == RELPERSISTENCE_TEMP)
- ereport(ERROR,
- errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
- RelationGetRelationName(parent_rel)));
-
- /* Create the relation. */
- newRelId = heap_create_with_catalog(newPartName->relname,
- namespaceId,
- parent_relform->reltablespace,
- InvalidOid,
- InvalidOid,
- InvalidOid,
- ownerId,
- relamId,
- descriptor,
- NIL,
- RELKIND_RELATION,
- newPartName->relpersistence,
- false,
- false,
- ONCOMMIT_NOOP,
- (Datum) 0,
- true,
- allowSystemTableMods,
- true,
- InvalidOid,
- NULL);
-
- /*
- * We must bump the command counter to make the newly-created relation
- * tuple visible for opening.
- */
- CommandCounterIncrement();
-
- /*
- * Open the new partition with no lock, because we already have an
- * AccessExclusiveLock placed there after creation.
- */
- newRel = table_open(newRelId, NoLock);
-
- /* Find or create a work queue entry for the newly created table. */
- new_partrel_tab = ATGetQueueEntry(wqueue, newRel);
-
- /* Create constraints, default values, and generated values. */
- createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel);
-
- /*
- * Need to call CommandCounterIncrement, so a fresh relcache entry has
- * newly installed constraint info.
- */
- CommandCounterIncrement();
-
- return newRel;
-}
-
-/*
- * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions)
- * of the partitioned table and move rows into the new partition
- * (newPartRel). We also verify check constraints against these rows.
- */
-static void
-MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel)
-{
- CommandId mycid;
- EState *estate;
- AlteredTableInfo *tab;
- ListCell *ltab;
-
- /* The FSM is empty, so don't bother using it. */
- int ti_options = TABLE_INSERT_SKIP_FSM;
- BulkInsertState bistate; /* state of bulk inserts for partition */
- TupleTableSlot *dstslot;
-
- /* Find the work queue entry for the new partition table: newPartRel. */
- tab = ATGetQueueEntry(wqueue, newPartRel);
-
- /* Generate the constraint and default execution states. */
- estate = CreateExecutorState();
-
- buildExpressionExecutionStates(tab, newPartRel, estate);
-
- mycid = GetCurrentCommandId(true);
-
- /* Prepare a BulkInsertState for table_tuple_insert. */
- bistate = GetBulkInsertState();
-
- /* Create the necessary tuple slot. */
- dstslot = table_slot_create(newPartRel, NULL);
-
- foreach_oid(merging_oid, mergingPartitions)
- {
- ExprContext *econtext;
- TupleTableSlot *srcslot;
- TupleConversionMap *tuple_map;
- TableScanDesc scan;
- MemoryContext oldCxt;
- Snapshot snapshot;
- Relation mergingPartition;
-
- econtext = GetPerTupleExprContext(estate);
-
- /*
- * Partition is already locked in the transformPartitionCmdForMerge
- * function.
- */
- mergingPartition = table_open(merging_oid, NoLock);
-
- /* Create a source tuple slot for the partition being merged. */
- srcslot = table_slot_create(mergingPartition, NULL);
-
- /*
- * Map computing for moving attributes of the merged partition to the
- * new partition.
- */
- tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition),
- RelationGetDescr(newPartRel));
-
- /* Scan through the rows. */
- snapshot = RegisterSnapshot(GetLatestSnapshot());
- scan = table_beginscan(mergingPartition, snapshot, 0, NULL);
-
- /*
- * Switch to per-tuple memory context and reset it for each tuple
- * produced, so we don't leak memory.
- */
- oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-
- while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
- {
- TupleTableSlot *insertslot;
-
- CHECK_FOR_INTERRUPTS();
-
- if (tuple_map)
- {
- /* Need to use a map to copy attributes. */
- insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot);
- }
- else
- {
- slot_getallattrs(srcslot);
-
- /* Copy attributes directly. */
- insertslot = dstslot;
-
- ExecClearTuple(insertslot);
-
- memcpy(insertslot->tts_values, srcslot->tts_values,
- sizeof(Datum) * srcslot->tts_nvalid);
- memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
- sizeof(bool) * srcslot->tts_nvalid);
-
- ExecStoreVirtualTuple(insertslot);
- }
-
- /*
- * Constraints and GENERATED expressions might reference the
- * tableoid column, so fill tts_tableOid with the desired value.
- * (We must do this each time, because it gets overwritten with
- * newrel's OID during storing.)
- */
- insertslot->tts_tableOid = RelationGetRelid(newPartRel);
-
- /*
- * Now, evaluate any generated expressions whose inputs come from
- * the new tuple. We assume these columns won't reference each
- * other, so that there's no ordering dependency.
- */
- evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel,
- insertslot, econtext);
-
- /* Write the tuple out to the new relation. */
- table_tuple_insert(newPartRel, insertslot, mycid,
- ti_options, bistate);
-
- ResetExprContext(econtext);
- }
-
- MemoryContextSwitchTo(oldCxt);
- table_endscan(scan);
- UnregisterSnapshot(snapshot);
-
- if (tuple_map)
- free_conversion_map(tuple_map);
-
- ExecDropSingleTupleTableSlot(srcslot);
- table_close(mergingPartition, NoLock);
- }
-
- FreeExecutorState(estate);
- ExecDropSingleTupleTableSlot(dstslot);
- FreeBulkInsertState(bistate);
-
- table_finish_bulk_insert(newPartRel, ti_options);
-
- /*
- * We don't need to process this newPartRel since we already processed it
- * here, so delete the ALTER TABLE queue for it.
- */
- foreach(ltab, *wqueue)
- {
- tab = (AlteredTableInfo *) lfirst(ltab);
- if (tab->relid == RelationGetRelid(newPartRel))
- {
- *wqueue = list_delete_cell(*wqueue, ltab);
- break;
- }
- }
-}
-
-/*
- * detachPartitionTable: detach partition "child_rel" from partitioned table
- * "parent_rel" with default partition identifier "defaultPartOid"
- */
-static void
-detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid)
-{
- /* Remove the pg_inherits row first. */
- RemoveInheritance(child_rel, parent_rel, false);
-
- /*
- * Detaching the partition might involve TOAST table access, so ensure we
- * have a valid snapshot.
- */
- PushActiveSnapshot(GetTransactionSnapshot());
-
- /* Do the final part of detaching. */
- DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid);
-
- PopActiveSnapshot();
-}
-
-/*
- * ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
- */
-static void
-ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
- PartitionCmd *cmd, AlterTableUtilityContext *context)
-{
- Relation newPartRel;
- List *mergingPartitions = NIL;
- Oid defaultPartOid;
- Oid existingRelid;
- Oid ownerId = InvalidOid;
- Oid save_userid;
- int save_sec_context;
- int save_nestlevel;
-
- /*
- * Check ownership of merged partitions - partitions with different owners
- * cannot be merged. Also, collect the OIDs of these partitions during the
- * check.
- */
- foreach_node(RangeVar, name, cmd->partlist)
- {
- Relation mergingPartition;
-
- /*
- * We are going to detach and remove this partition. We already took
- * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here,
- * NoLock is fine.
- */
- mergingPartition = table_openrv_extended(name, NoLock, false);
- Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false));
-
- if (OidIsValid(ownerId))
- {
- /* Do the partitions being merged have different owners? */
- if (ownerId != mergingPartition->rd_rel->relowner)
- ereport(ERROR,
- errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("partitions being merged have different owners"));
- }
- else
- ownerId = mergingPartition->rd_rel->relowner;
-
- /* Store the next merging partition into the list. */
- mergingPartitions = lappend_oid(mergingPartitions,
- RelationGetRelid(mergingPartition));
-
- table_close(mergingPartition, NoLock);
- }
-
- /* Look up the existing relation by the new partition name. */
- RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid);
-
- /*
- * Check if this name is already taken. This helps us to detect the
- * situation when one of the merging partitions has the same name as the
- * new partition. Otherwise, this would fail later on anyway, but
- * catching this here allows us to emit a nicer error message.
- */
- if (OidIsValid(existingRelid))
- {
- if (list_member_oid(mergingPartitions, existingRelid))
- {
- /*
- * The new partition has the same name as one of the merging
- * partitions.
- */
- char tmpRelName[NAMEDATALEN];
-
- /* Generate a temporary name. */
- sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
-
- /*
- * Rename the existing partition with a temporary name, leaving it
- * free for the new partition. We don't need to care about this
- * in the future because we're going to eventually drop the
- * existing partition anyway.
- */
- RenameRelationInternal(existingRelid, tmpRelName, true, false);
-
- /*
- * We must bump the command counter to make the new partition
- * tuple visible for rename.
- */
- CommandCounterIncrement();
- }
- else
- {
- ereport(ERROR,
- errcode(ERRCODE_DUPLICATE_TABLE),
- errmsg("relation \"%s\" already exists", cmd->name->relname));
- }
- }
-
- defaultPartOid =
- get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
-
- /* Detach all merging partitions. */
- foreach_oid(mergingPartitionOid, mergingPartitions)
- {
- Relation child_rel;
-
- child_rel = table_open(mergingPartitionOid, NoLock);
-
- detachPartitionTable(rel, child_rel, defaultPartOid);
-
- table_close(child_rel, NoLock);
- }
-
- /*
- * Perform a preliminary check to determine whether it's safe to drop all
- * merging partitions before we actually do so later. After merging rows
- * into the new partitions via MergePartitionsMoveRows, all old partitions
- * need to be dropped. However, since the drop behavior is DROP_RESTRICT
- * and the merge process (MergePartitionsMoveRows) can be time-consuming,
- * performing an early check on the drop eligibility of old partitions is
- * preferable.
- */
- foreach_oid(mergingPartitionOid, mergingPartitions)
- {
- ObjectAddress object;
-
- /* Get oid of the later to be dropped relation. */
- object.objectId = mergingPartitionOid;
- object.classId = RelationRelationId;
- object.objectSubId = 0;
-
- performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
- }
-
- /*
- * Create a table for the new partition, using the partitioned table as a
- * model.
- */
- Assert(OidIsValid(ownerId));
- newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId);
-
- /*
- * Switch to the table owner's userid, so that any index functions are run
- * as that user. Also, lockdown security-restricted operations and
- * arrange to make GUC variable changes local to this command.
- *
- * Need to do it after determining the namespace in the
- * createPartitionTable() call.
- */
- GetUserIdAndSecContext(&save_userid, &save_sec_context);
- SetUserIdAndSecContext(ownerId,
- save_sec_context | SECURITY_RESTRICTED_OPERATION);
- save_nestlevel = NewGUCNestLevel();
- RestrictSearchPath();
-
- /* Copy data from merged partitions to the new partition. */
- MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel);
-
- /* Drop the current partitions before attaching the new one. */
- foreach_oid(mergingPartitionOid, mergingPartitions)
- {
- ObjectAddress object;
-
- object.objectId = mergingPartitionOid;
- object.classId = RelationRelationId;
- object.objectSubId = 0;
-
- performDeletion(&object, DROP_RESTRICT, 0);
- }
-
- list_free(mergingPartitions);
-
- /*
- * Attach a new partition to the partitioned table. wqueue = NULL:
- * verification for each cloned constraint is not needed.
- */
- attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
-
- /* Keep the lock until commit. */
- table_close(newPartRel, NoLock);
-
- /* Roll back any GUC changes executed by index functions. */
- AtEOXact_GUC(false, save_nestlevel);
-
- /* Restore the userid and security context. */
- SetUserIdAndSecContext(save_userid, save_sec_context);
-}
-
-/*
- * Struct with the context of the new partition for inserting rows from the
- * split partition.
- */
-typedef struct SplitPartitionContext
-{
- ExprState *partqualstate; /* expression for checking a slot for a
- * partition (NULL for DEFAULT partition) */
- BulkInsertState bistate; /* state of bulk inserts for partition */
- TupleTableSlot *dstslot; /* slot for inserting row into partition */
- AlteredTableInfo *tab; /* structure with generated column expressions
- * and check constraint expressions. */
- Relation partRel; /* relation for partition */
-} SplitPartitionContext;
-
-/*
- * createSplitPartitionContext: create context for partition and fill it
- */
-static SplitPartitionContext *
-createSplitPartitionContext(Relation partRel)
-{
- SplitPartitionContext *pc;
-
- pc = palloc0_object(SplitPartitionContext);
- pc->partRel = partRel;
-
- /*
- * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
- * don't bother using it.
- */
- pc->bistate = GetBulkInsertState();
-
- /* Create a destination tuple slot for the new partition. */
- pc->dstslot = table_slot_create(pc->partRel, NULL);
-
- return pc;
-}
-
-/*
- * deleteSplitPartitionContext: delete context for partition
- */
-static void
-deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options)
-{
- ListCell *ltab;
-
- ExecDropSingleTupleTableSlot(pc->dstslot);
- FreeBulkInsertState(pc->bistate);
-
- table_finish_bulk_insert(pc->partRel, ti_options);
-
- /*
- * We don't need to process this pc->partRel so delete the ALTER TABLE
- * queue of it.
- */
- foreach(ltab, *wqueue)
- {
- AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
-
- if (tab->relid == RelationGetRelid(pc->partRel))
- {
- *wqueue = list_delete_cell(*wqueue, ltab);
- break;
- }
- }
-
- pfree(pc);
-}
-
-/*
- * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table
- * (rel) and move rows into new partitions.
- *
- * New partitions description:
- * partlist: list of pointers to SinglePartitionSpec structures. It contains
- * the partition specification details for all new partitions.
- * newPartRels: list of Relations, new partitions created in
- * ATExecSplitPartition.
- */
-static void
-SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel,
- List *partlist, List *newPartRels)
-{
- /* The FSM is empty, so don't bother using it. */
- int ti_options = TABLE_INSERT_SKIP_FSM;
- CommandId mycid;
- EState *estate;
- ListCell *listptr,
- *listptr2;
- TupleTableSlot *srcslot;
- ExprContext *econtext;
- TableScanDesc scan;
- Snapshot snapshot;
- MemoryContext oldCxt;
- List *partContexts = NIL;
- TupleConversionMap *tuple_map;
- SplitPartitionContext *defaultPartCtx = NULL,
- *pc;
-
- mycid = GetCurrentCommandId(true);
-
- estate = CreateExecutorState();
-
- forboth(listptr, partlist, listptr2, newPartRels)
- {
- SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
-
- pc = createSplitPartitionContext((Relation) lfirst(listptr2));
-
- /* Find the work queue entry for the new partition table: newPartRel. */
- pc->tab = ATGetQueueEntry(wqueue, pc->partRel);
-
- buildExpressionExecutionStates(pc->tab, pc->partRel, estate);
-
- if (sps->bound->is_default)
- {
- /*
- * We should not create a structure to check the partition
- * constraint for the new DEFAULT partition.
- */
- defaultPartCtx = pc;
- }
- else
- {
- List *partConstraint;
-
- /* Build expression execution states for partition check quals. */
- partConstraint = get_qual_from_partbound(rel, sps->bound);
- partConstraint =
- (List *) eval_const_expressions(NULL,
- (Node *) partConstraint);
- /* Make a boolean expression for ExecCheck(). */
- partConstraint = list_make1(make_ands_explicit(partConstraint));
-
- /*
- * Map the vars in the constraint expression from rel's attnos to
- * splitRel's.
- */
- partConstraint = map_partition_varattnos(partConstraint,
- 1, splitRel, rel);
-
- pc->partqualstate =
- ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
- Assert(pc->partqualstate != NULL);
- }
-
- /* Store partition context into a list. */
- partContexts = lappend(partContexts, pc);
- }
-
- econtext = GetPerTupleExprContext(estate);
-
- /* Create the necessary tuple slot. */
- srcslot = table_slot_create(splitRel, NULL);
-
- /*
- * Map computing for moving attributes of the split partition to the new
- * partition (for the first new partition, but other new partitions can
- * use the same map).
- */
- pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
- tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel),
- RelationGetDescr(pc->partRel));
-
- /* Scan through the rows. */
- snapshot = RegisterSnapshot(GetLatestSnapshot());
- scan = table_beginscan(splitRel, snapshot, 0, NULL);
-
- /*
- * Switch to per-tuple memory context and reset it for each tuple
- * produced, so we don't leak memory.
- */
- oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-
- while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
- {
- bool found = false;
- TupleTableSlot *insertslot;
-
- CHECK_FOR_INTERRUPTS();
-
- econtext->ecxt_scantuple = srcslot;
-
- /* Search partition for the current slot, srcslot. */
- foreach(listptr, partContexts)
- {
- pc = (SplitPartitionContext *) lfirst(listptr);
-
- /* skip DEFAULT partition */
- if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext))
- {
- found = true;
- break;
- }
- }
- if (!found)
- {
- /* Use the DEFAULT partition if it exists. */
- if (defaultPartCtx)
- pc = defaultPartCtx;
- else
- ereport(ERROR,
- errcode(ERRCODE_CHECK_VIOLATION),
- errmsg("can not find partition for split partition row"),
- errtable(splitRel));
- }
-
- if (tuple_map)
- {
- /* Need to use a map to copy attributes. */
- insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot);
- }
- else
- {
- /* Extract data from the old tuple. */
- slot_getallattrs(srcslot);
-
- /* Copy attributes directly. */
- insertslot = pc->dstslot;
-
- ExecClearTuple(insertslot);
-
- memcpy(insertslot->tts_values, srcslot->tts_values,
- sizeof(Datum) * srcslot->tts_nvalid);
- memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
- sizeof(bool) * srcslot->tts_nvalid);
-
- ExecStoreVirtualTuple(insertslot);
- }
-
- /*
- * Constraints and GENERATED expressions might reference the tableoid
- * column, so fill tts_tableOid with the desired value. (We must do
- * this each time, because it gets overwritten with newrel's OID
- * during storing.)
- */
- insertslot->tts_tableOid = RelationGetRelid(pc->partRel);
-
- /*
- * Now, evaluate any generated expressions whose inputs come from the
- * new tuple. We assume these columns won't reference each other, so
- * that there's no ordering dependency.
- */
- evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel,
- insertslot, econtext);
-
- /* Write the tuple out to the new relation. */
- table_tuple_insert(pc->partRel, insertslot, mycid,
- ti_options, pc->bistate);
-
- ResetExprContext(econtext);
- }
-
- MemoryContextSwitchTo(oldCxt);
-
- table_endscan(scan);
- UnregisterSnapshot(snapshot);
-
- if (tuple_map)
- free_conversion_map(tuple_map);
-
- ExecDropSingleTupleTableSlot(srcslot);
-
- FreeExecutorState(estate);
-
- foreach_ptr(SplitPartitionContext, spc, partContexts)
- deleteSplitPartitionContext(spc, wqueue, ti_options);
-}
-
-/*
- * ALTER TABLE <name> SPLIT PARTITION <partition-name> INTO <partition-list>
- */
-static void
-ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
- PartitionCmd *cmd, AlterTableUtilityContext *context)
-{
- Relation splitRel;
- Oid splitRelOid;
- ListCell *listptr,
- *listptr2;
- bool isSameName = false;
- char tmpRelName[NAMEDATALEN];
- List *newPartRels = NIL;
- ObjectAddress object;
- Oid defaultPartOid;
- Oid save_userid;
- int save_sec_context;
- int save_nestlevel;
-
- defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
-
- /*
- * Partition is already locked in the transformPartitionCmdForSplit
- * function.
- */
- splitRel = table_openrv(cmd->name, NoLock);
-
- splitRelOid = RelationGetRelid(splitRel);
-
- /* Check descriptions of new partitions. */
- foreach_node(SinglePartitionSpec, sps, cmd->partlist)
- {
- Oid existingRelid;
-
- /* Look up the existing relation by the new partition name. */
- RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid);
-
- /*
- * This would fail later on anyway if the relation already exists. But
- * by catching it here, we can emit a nicer error message.
- */
- if (existingRelid == splitRelOid && !isSameName)
- /* One new partition can have the same name as a split partition. */
- isSameName = true;
- else if (OidIsValid(existingRelid))
- ereport(ERROR,
- errcode(ERRCODE_DUPLICATE_TABLE),
- errmsg("relation \"%s\" already exists", sps->name->relname));
- }
-
- /* Detach the split partition. */
- detachPartitionTable(rel, splitRel, defaultPartOid);
-
- /*
- * Perform a preliminary check to determine whether it's safe to drop the
- * split partition before we actually do so later. After merging rows into
- * the new partitions via SplitPartitionMoveRows, all old partitions need
- * to be dropped. However, since the drop behavior is DROP_RESTRICT and
- * the merge process (SplitPartitionMoveRows) can be time-consuming,
- * performing an early check on the drop eligibility of old partitions is
- * preferable.
- */
- object.objectId = splitRelOid;
- object.classId = RelationRelationId;
- object.objectSubId = 0;
- performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
-
- /*
- * If a new partition has the same name as the split partition, then we
- * should rename the split partition to reuse its name.
- */
- if (isSameName)
- {
- /*
- * We must bump the command counter to make the split partition tuple
- * visible for renaming.
- */
- CommandCounterIncrement();
- /* Rename partition. */
- sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
- RenameRelationInternal(splitRelOid, tmpRelName, true, false);
-
- /*
- * We must bump the command counter to make the split partition tuple
- * visible after renaming.
- */
- CommandCounterIncrement();
- }
-
- /* Create new partitions (like a split partition), without indexes. */
- foreach_node(SinglePartitionSpec, sps, cmd->partlist)
- {
- Relation newPartRel;
-
- newPartRel = createPartitionTable(wqueue, sps->name, rel,
- splitRel->rd_rel->relowner);
- newPartRels = lappend(newPartRels, newPartRel);
- }
-
- /*
- * Switch to the table owner's userid, so that any index functions are run
- * as that user. Also, lockdown security-restricted operations and
- * arrange to make GUC variable changes local to this command.
- *
- * Need to do it after determining the namespace in the
- * createPartitionTable() call.
- */
- GetUserIdAndSecContext(&save_userid, &save_sec_context);
- SetUserIdAndSecContext(splitRel->rd_rel->relowner,
- save_sec_context | SECURITY_RESTRICTED_OPERATION);
- save_nestlevel = NewGUCNestLevel();
- RestrictSearchPath();
-
- /* Copy data from the split partition to the new partitions. */
- SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels);
- /* Keep the lock until commit. */
- table_close(splitRel, NoLock);
-
- /* Attach new partitions to the partitioned table. */
- forboth(listptr, cmd->partlist, listptr2, newPartRels)
- {
- SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
- Relation newPartRel = (Relation) lfirst(listptr2);
-
- /*
- * wqueue = NULL: verification for each cloned constraint is not
- * needed.
- */
- attachPartitionTable(NULL, rel, newPartRel, sps->bound);
- /* Keep the lock until commit. */
- table_close(newPartRel, NoLock);
- }
-
- /* Drop the split partition. */
- object.classId = RelationRelationId;
- object.objectId = splitRelOid;
- object.objectSubId = 0;
- /* Probably DROP_CASCADE is not needed. */
- performDeletion(&object, DROP_RESTRICT, 0);
-
- /* Roll back any GUC changes executed by index functions. */
- AtEOXact_GUC(false, save_nestlevel);
-
- /* Restore the userid and security context. */
- SetUserIdAndSecContext(save_userid, save_sec_context);
-}
diff --git a/src/backend/commands/tablecmds_partition.c b/src/backend/commands/tablecmds_partition.c
new file mode 100644
index 00000000000..1e8e7f5b669
--- /dev/null
+++ b/src/backend/commands/tablecmds_partition.c
@@ -0,0 +1,4627 @@
+/*-------------------------------------------------------------------------
+ *
+ * tablecmds_partition.c
+ * TODO
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/commands/tablecmds_partition.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/toast_compression.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.h"
+#include "catalog/pg_attrdef.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "commands/tablecmds_internal.h"
+#include "commands/tablecmds_partition.h"
+#include "commands/trigger.h"
+#include "executor/executor.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 "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.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"
+
+static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
+static void CloneFkReferencing(List **wqueue, Relation parentRel,
+ Relation partRel);
+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 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 AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel);
+static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
+ List *partConstraint,
+ bool validate_default);
+static void DropClonedTriggersFromPartition(Oid partitionId);
+static void DetachPartitionFinalize(Relation rel, Relation partRel,
+ bool concurrent, Oid defaultPartOid);
+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);
+
+/*
+ * Obtain list of partitions of the given table, locking them all at the given
+ * lockmode and ensuring that they all pass CheckAlterTableIsSafe.
+ *
+ * This function is a no-op if the given relation is not a partitioned table;
+ * in particular, nothing is done if it's a legacy inheritance parent.
+ */
+void
+ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode)
+{
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *inh;
+ ListCell *cell;
+
+ inh = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+ /* first element is the parent rel; must ignore it */
+ for_each_from(cell, inh, 1)
+ {
+ Relation childrel;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(lfirst_oid(cell), NoLock);
+ CheckAlterTableIsSafe(childrel);
+ table_close(childrel, NoLock);
+ }
+ list_free(inh);
+ }
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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.
+ */
+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);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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 */
+ }
+ }
+}
+
+/*
+ * attachPartitionTable: attach a new partition to the partitioned table
+ *
+ * wqueue: the ALTER TABLE work queue; can be NULL when not running as part
+ * of an ALTER TABLE sequence.
+ * rel: partitioned relation;
+ * attachrel: relation of attached partition;
+ * bound: bounds of attached relation.
+ */
+static void
+attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound)
+{
+ /*
+ * Create an inheritance; the relevant checks are performed inside the
+ * function.
+ */
+ CreateInheritance(attachrel, rel, true);
+
+ /* Update the pg_class entry. */
+ StorePartitionBound(attachrel, rel, 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);
+}
+
+/*
+ * 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);
+
+ attachPartitionTable(wqueue, rel, attachrel, cmd->bound);
+
+ /*
+ * Generate a 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_array(Relation, list_length(attachRelIdxs));
+ attachInfos = palloc_array(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(NULL,
+ 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
+ */
+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).
+ */
+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.
+ */
+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
+ */
+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);
+ }
+}
+
+/*
+ * buildExpressionExecutionStates: build the needed expression execution states
+ * for new partition (newPartRel) checks and initialize expressions for
+ * generated columns. All expressions should be created in "tab"
+ * (AlteredTableInfo structure).
+ */
+static void
+buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate)
+{
+ /*
+ * Build the needed expression execution states. Here, we expect only NOT
+ * NULL and CHECK constraint.
+ */
+ foreach_ptr(NewConstraint, con, tab->constraints)
+ {
+ switch (con->contype)
+ {
+ case CONSTR_CHECK:
+
+ /*
+ * We already expanded virtual expression in
+ * createTableConstraints.
+ */
+ con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
+ break;
+ case CONSTR_NOTNULL:
+ /* Nothing to do here. */
+ break;
+ default:
+ elog(ERROR, "unrecognized constraint type: %d",
+ (int) con->contype);
+ }
+ }
+
+ /* Expression already planned in createTableConstraints */
+ foreach_ptr(NewColumnValue, ex, tab->newvals)
+ ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
+}
+
+/*
+ * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated
+ * expressions for "tab" (AlteredTableInfo structure) whose inputs come from
+ * the new tuple (insertslot) of the new partition (newPartRel).
+ */
+static void
+evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab,
+ Relation newPartRel,
+ TupleTableSlot *insertslot,
+ ExprContext *econtext)
+{
+ econtext->ecxt_scantuple = insertslot;
+
+ foreach_ptr(NewColumnValue, ex, tab->newvals)
+ {
+ if (!ex->is_generated)
+ continue;
+
+ insertslot->tts_values[ex->attnum - 1]
+ = ExecEvalExpr(ex->exprstate,
+ econtext,
+ &insertslot->tts_isnull[ex->attnum - 1]);
+ }
+
+ foreach_ptr(NewConstraint, con, tab->constraints)
+ {
+ switch (con->contype)
+ {
+ case CONSTR_CHECK:
+ if (!ExecCheck(con->qualstate, econtext))
+ ereport(ERROR,
+ errcode(ERRCODE_CHECK_VIOLATION),
+ errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row",
+ con->name, RelationGetRelationName(newPartRel)),
+ errtableconstraint(newPartRel, con->name));
+ break;
+ case CONSTR_NOTNULL:
+ case CONSTR_FOREIGN:
+ /* Nothing to do here */
+ break;
+ default:
+ elog(ERROR, "unrecognized constraint type: %d",
+ (int) con->contype);
+ }
+ }
+}
+
+/*
+ * getAttributesList: build a list of columns (ColumnDef) based on parent_rel
+ */
+static List *
+getAttributesList(Relation parent_rel)
+{
+ AttrNumber parent_attno;
+ TupleDesc modelDesc;
+ List *colList = NIL;
+
+ modelDesc = RelationGetDescr(parent_rel);
+
+ for (parent_attno = 1; parent_attno <= modelDesc->natts;
+ parent_attno++)
+ {
+ Form_pg_attribute attribute = TupleDescAttr(modelDesc,
+ parent_attno - 1);
+ ColumnDef *def;
+
+ /* Ignore dropped columns in the parent. */
+ if (attribute->attisdropped)
+ continue;
+
+ def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid,
+ attribute->atttypmod, attribute->attcollation);
+
+ def->is_not_null = attribute->attnotnull;
+
+ /* Copy identity. */
+ def->identity = attribute->attidentity;
+
+ /* Copy attgenerated. */
+ def->generated = attribute->attgenerated;
+
+ def->storage = attribute->attstorage;
+
+ /* Likewise, copy compression. */
+ if (CompressionMethodIsValid(attribute->attcompression))
+ def->compression =
+ pstrdup(GetCompressionMethodName(attribute->attcompression));
+ else
+ def->compression = NULL;
+
+ /* Add to column list. */
+ colList = lappend(colList, def);
+ }
+
+ return colList;
+}
+
+/*
+ * createTableConstraints:
+ * create check constraints, default values, and generated values for newRel
+ * based on parent_rel. tab is pending-work queue for newRel, we may need it in
+ * MergePartitionsMoveRows.
+ */
+static void
+createTableConstraints(List **wqueue, AlteredTableInfo *tab,
+ Relation parent_rel, Relation newRel)
+{
+ TupleDesc tupleDesc;
+ TupleConstr *constr;
+ AttrMap *attmap;
+ AttrNumber parent_attno;
+ int ccnum;
+ List *constraints = NIL;
+ List *cookedConstraints = NIL;
+
+ tupleDesc = RelationGetDescr(parent_rel);
+ constr = tupleDesc->constr;
+
+ if (!constr)
+ return;
+
+ /*
+ * Construct a map from the parent relation's attnos to the child rel's.
+ * This re-checks type match, etc, although it shouldn't be possible to
+ * have a failure since both tables are locked.
+ */
+ attmap = build_attrmap_by_name(RelationGetDescr(newRel),
+ tupleDesc,
+ false);
+
+ /* Cycle for default values. */
+ for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++)
+ {
+ Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
+ parent_attno - 1);
+
+ /* Ignore dropped columns in the parent. */
+ if (attribute->attisdropped)
+ continue;
+
+ /* Copy the default, if present, and it should be copied. */
+ if (attribute->atthasdef)
+ {
+ Node *this_default = NULL;
+ bool found_whole_row;
+ AttrNumber num;
+ Node *def;
+ NewColumnValue *newval;
+
+ if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ this_default = build_generation_expression(parent_rel, attribute->attnum);
+ else
+ {
+ this_default = TupleDescGetDefault(tupleDesc, attribute->attnum);
+ if (this_default == NULL)
+ elog(ERROR, "default expression not found for attribute %d of relation \"%s\"",
+ attribute->attnum, RelationGetRelationName(parent_rel));
+ }
+
+ num = attmap->attnums[parent_attno - 1];
+ def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row);
+
+ if (found_whole_row && attribute->attgenerated != '\0')
+ elog(ERROR, "cannot convert whole-row table reference");
+
+ /* Add a pre-cooked default expression. */
+ StoreAttrDefault(newRel, num, def, true);
+
+ /*
+ * Stored generated column expressions in parent_rel might
+ * reference the tableoid. newRel, parent_rel tableoid clear is
+ * not the same. If so, these stored generated columns require
+ * recomputation for newRel within MergePartitionsMoveRows.
+ */
+ if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED)
+ {
+ newval = palloc0_object(NewColumnValue);
+ newval->attnum = num;
+ newval->expr = expression_planner((Expr *) def);
+ newval->is_generated = (attribute->attgenerated != '\0');
+ tab->newvals = lappend(tab->newvals, newval);
+ }
+ }
+ }
+
+ /* Cycle for CHECK constraints. */
+ for (ccnum = 0; ccnum < constr->num_check; ccnum++)
+ {
+ char *ccname = constr->check[ccnum].ccname;
+ char *ccbin = constr->check[ccnum].ccbin;
+ bool ccenforced = constr->check[ccnum].ccenforced;
+ bool ccnoinherit = constr->check[ccnum].ccnoinherit;
+ bool ccvalid = constr->check[ccnum].ccvalid;
+ Node *ccbin_node;
+ bool found_whole_row;
+ Constraint *constr;
+
+ /*
+ * The partitioned table can not have a NO INHERIT check constraint
+ * (see StoreRelCheck function for details).
+ */
+ Assert(!ccnoinherit);
+
+ ccbin_node = map_variable_attnos(stringToNode(ccbin),
+ 1, 0,
+ attmap,
+ InvalidOid, &found_whole_row);
+
+ /*
+ * For the moment we have to reject whole-row variables (as for CREATE
+ * TABLE LIKE and inheritances).
+ */
+ if (found_whole_row)
+ elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".",
+ ccname,
+ RelationGetRelationName(parent_rel));
+
+ constr = makeNode(Constraint);
+ constr->contype = CONSTR_CHECK;
+ constr->conname = pstrdup(ccname);
+ constr->deferrable = false;
+ constr->initdeferred = false;
+ constr->is_enforced = ccenforced;
+ constr->skip_validation = !ccvalid;
+ constr->initially_valid = ccvalid;
+ constr->is_no_inherit = ccnoinherit;
+ constr->raw_expr = NULL;
+ constr->cooked_expr = nodeToString(ccbin_node);
+ constr->location = -1;
+ constraints = lappend(constraints, constr);
+ }
+
+ /* Install all CHECK constraints. */
+ cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints,
+ false, true, true, NULL);
+
+ /* Make the additional catalog changes visible. */
+ CommandCounterIncrement();
+
+ /*
+ * parent_rel check constraint expression may reference tableoid, so later
+ * in MergePartitionsMoveRows, we need to evaluate the check constraint
+ * again for the newRel. We can check whether the check constraint
+ * contains a tableoid reference via pull_varattnos.
+ */
+ foreach_ptr(CookedConstraint, ccon, cookedConstraints)
+ {
+ if (!ccon->skip_validation)
+ {
+ Node *qual;
+ Bitmapset *attnums = NULL;
+
+ Assert(ccon->contype == CONSTR_CHECK);
+ qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1);
+ pull_varattnos(qual, 1, &attnums);
+
+ /*
+ * Add a check only if it contains a tableoid
+ * (TableOidAttributeNumber).
+ */
+ if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber,
+ attnums))
+ {
+ NewConstraint *newcon;
+
+ newcon = palloc0_object(NewConstraint);
+ newcon->name = ccon->name;
+ newcon->contype = CONSTR_CHECK;
+ newcon->qual = qual;
+
+ tab->constraints = lappend(tab->constraints, newcon);
+ }
+ }
+ }
+
+ /* Don't need the cookedConstraints anymore. */
+ list_free_deep(cookedConstraints);
+
+ /* Reproduce not-null constraints. */
+ if (constr->has_not_null)
+ {
+ List *nnconstraints;
+
+ /*
+ * The "include_noinh" argument is false because a partitioned table
+ * can't have NO INHERIT constraint.
+ */
+ nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel),
+ false, false);
+
+ Assert(list_length(nnconstraints) > 0);
+
+ /*
+ * We already set pg_attribute.attnotnull in createPartitionTable. No
+ * need call set_attnotnull again.
+ */
+ AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL);
+ }
+}
+
+/*
+ * createPartitionTable:
+ *
+ * Create a new partition (newPartName) for the partitioned table (parent_rel).
+ * ownerId is determined by the partition on which the operation is performed,
+ * so it is passed separately. The new partition will inherit the access method
+ * and persistence type from the parent table.
+ *
+ * Returns the created relation (locked in AccessExclusiveLock mode).
+ */
+static Relation
+createPartitionTable(List **wqueue, RangeVar *newPartName,
+ Relation parent_rel, Oid ownerId)
+{
+ Relation newRel;
+ Oid newRelId;
+ Oid existingRelid;
+ TupleDesc descriptor;
+ List *colList = NIL;
+ Oid relamId;
+ Oid namespaceId;
+ AlteredTableInfo *new_partrel_tab;
+ Form_pg_class parent_relform = parent_rel->rd_rel;
+
+ /* If the existing rel is temp, it must belong to this session. */
+ if (RELATION_IS_OTHER_TEMP(parent_rel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create as partition of temporary relation of another session"));
+
+ /* Look up inheritance ancestors and generate the relation schema. */
+ colList = getAttributesList(parent_rel);
+
+ /* Create a tuple descriptor from the relation schema. */
+ descriptor = BuildDescForRelation(colList);
+
+ /* Look up the access method for the new relation. */
+ relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID;
+
+ /* Look up the namespace in which we are supposed to create the relation. */
+ namespaceId =
+ RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid);
+ if (OidIsValid(existingRelid))
+ ereport(ERROR,
+ errcode(ERRCODE_DUPLICATE_TABLE),
+ errmsg("relation \"%s\" already exists", newPartName->relname));
+
+ /*
+ * We intended to create the partition with the same persistence as the
+ * parent table, but we still need to recheck because that might be
+ * affected by the search_path. If the parent is permanent, so must be
+ * all of its partitions.
+ */
+ if (parent_relform->relpersistence != RELPERSISTENCE_TEMP &&
+ newPartName->relpersistence == RELPERSISTENCE_TEMP)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+ RelationGetRelationName(parent_rel)));
+
+ /* Permanent rels cannot be partitions belonging to a temporary parent. */
+ if (newPartName->relpersistence != RELPERSISTENCE_TEMP &&
+ parent_relform->relpersistence == RELPERSISTENCE_TEMP)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
+ RelationGetRelationName(parent_rel)));
+
+ /* Create the relation. */
+ newRelId = heap_create_with_catalog(newPartName->relname,
+ namespaceId,
+ parent_relform->reltablespace,
+ InvalidOid,
+ InvalidOid,
+ InvalidOid,
+ ownerId,
+ relamId,
+ descriptor,
+ NIL,
+ RELKIND_RELATION,
+ newPartName->relpersistence,
+ false,
+ false,
+ ONCOMMIT_NOOP,
+ (Datum) 0,
+ true,
+ allowSystemTableMods,
+ true,
+ InvalidOid,
+ NULL);
+
+ /*
+ * We must bump the command counter to make the newly-created relation
+ * tuple visible for opening.
+ */
+ CommandCounterIncrement();
+
+ /*
+ * Open the new partition with no lock, because we already have an
+ * AccessExclusiveLock placed there after creation.
+ */
+ newRel = table_open(newRelId, NoLock);
+
+ /* Find or create a work queue entry for the newly created table. */
+ new_partrel_tab = ATGetQueueEntry(wqueue, newRel);
+
+ /* Create constraints, default values, and generated values. */
+ createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel);
+
+ /*
+ * Need to call CommandCounterIncrement, so a fresh relcache entry has
+ * newly installed constraint info.
+ */
+ CommandCounterIncrement();
+
+ return newRel;
+}
+
+/*
+ * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions)
+ * of the partitioned table and move rows into the new partition
+ * (newPartRel). We also verify check constraints against these rows.
+ */
+static void
+MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel)
+{
+ CommandId mycid;
+ EState *estate;
+ AlteredTableInfo *tab;
+ ListCell *ltab;
+
+ /* The FSM is empty, so don't bother using it. */
+ int ti_options = TABLE_INSERT_SKIP_FSM;
+ BulkInsertState bistate; /* state of bulk inserts for partition */
+ TupleTableSlot *dstslot;
+
+ /* Find the work queue entry for the new partition table: newPartRel. */
+ tab = ATGetQueueEntry(wqueue, newPartRel);
+
+ /* Generate the constraint and default execution states. */
+ estate = CreateExecutorState();
+
+ buildExpressionExecutionStates(tab, newPartRel, estate);
+
+ mycid = GetCurrentCommandId(true);
+
+ /* Prepare a BulkInsertState for table_tuple_insert. */
+ bistate = GetBulkInsertState();
+
+ /* Create the necessary tuple slot. */
+ dstslot = table_slot_create(newPartRel, NULL);
+
+ foreach_oid(merging_oid, mergingPartitions)
+ {
+ ExprContext *econtext;
+ TupleTableSlot *srcslot;
+ TupleConversionMap *tuple_map;
+ TableScanDesc scan;
+ MemoryContext oldCxt;
+ Snapshot snapshot;
+ Relation mergingPartition;
+
+ econtext = GetPerTupleExprContext(estate);
+
+ /*
+ * Partition is already locked in the transformPartitionCmdForMerge
+ * function.
+ */
+ mergingPartition = table_open(merging_oid, NoLock);
+
+ /* Create a source tuple slot for the partition being merged. */
+ srcslot = table_slot_create(mergingPartition, NULL);
+
+ /*
+ * Map computing for moving attributes of the merged partition to the
+ * new partition.
+ */
+ tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition),
+ RelationGetDescr(newPartRel));
+
+ /* Scan through the rows. */
+ snapshot = RegisterSnapshot(GetLatestSnapshot());
+ scan = table_beginscan(mergingPartition, snapshot, 0, NULL);
+
+ /*
+ * Switch to per-tuple memory context and reset it for each tuple
+ * produced, so we don't leak memory.
+ */
+ oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+ while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
+ {
+ TupleTableSlot *insertslot;
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (tuple_map)
+ {
+ /* Need to use a map to copy attributes. */
+ insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot);
+ }
+ else
+ {
+ slot_getallattrs(srcslot);
+
+ /* Copy attributes directly. */
+ insertslot = dstslot;
+
+ ExecClearTuple(insertslot);
+
+ memcpy(insertslot->tts_values, srcslot->tts_values,
+ sizeof(Datum) * srcslot->tts_nvalid);
+ memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
+ sizeof(bool) * srcslot->tts_nvalid);
+
+ ExecStoreVirtualTuple(insertslot);
+ }
+
+ /*
+ * Constraints and GENERATED expressions might reference the
+ * tableoid column, so fill tts_tableOid with the desired value.
+ * (We must do this each time, because it gets overwritten with
+ * newrel's OID during storing.)
+ */
+ insertslot->tts_tableOid = RelationGetRelid(newPartRel);
+
+ /*
+ * Now, evaluate any generated expressions whose inputs come from
+ * the new tuple. We assume these columns won't reference each
+ * other, so that there's no ordering dependency.
+ */
+ evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel,
+ insertslot, econtext);
+
+ /* Write the tuple out to the new relation. */
+ table_tuple_insert(newPartRel, insertslot, mycid,
+ ti_options, bistate);
+
+ ResetExprContext(econtext);
+ }
+
+ MemoryContextSwitchTo(oldCxt);
+ table_endscan(scan);
+ UnregisterSnapshot(snapshot);
+
+ if (tuple_map)
+ free_conversion_map(tuple_map);
+
+ ExecDropSingleTupleTableSlot(srcslot);
+ table_close(mergingPartition, NoLock);
+ }
+
+ FreeExecutorState(estate);
+ ExecDropSingleTupleTableSlot(dstslot);
+ FreeBulkInsertState(bistate);
+
+ table_finish_bulk_insert(newPartRel, ti_options);
+
+ /*
+ * We don't need to process this newPartRel since we already processed it
+ * here, so delete the ALTER TABLE queue for it.
+ */
+ foreach(ltab, *wqueue)
+ {
+ tab = (AlteredTableInfo *) lfirst(ltab);
+ if (tab->relid == RelationGetRelid(newPartRel))
+ {
+ *wqueue = list_delete_cell(*wqueue, ltab);
+ break;
+ }
+ }
+}
+
+/*
+ * detachPartitionTable: detach partition "child_rel" from partitioned table
+ * "parent_rel" with default partition identifier "defaultPartOid"
+ */
+static void
+detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid)
+{
+ /* Remove the pg_inherits row first. */
+ RemoveInheritance(child_rel, parent_rel, false);
+
+ /*
+ * Detaching the partition might involve TOAST table access, so ensure we
+ * have a valid snapshot.
+ */
+ PushActiveSnapshot(GetTransactionSnapshot());
+
+ /* Do the final part of detaching. */
+ DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid);
+
+ PopActiveSnapshot();
+}
+
+/*
+ * ALTER TABLE <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
+ */
+void
+ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+ PartitionCmd *cmd, AlterTableUtilityContext *context)
+{
+ Relation newPartRel;
+ List *mergingPartitions = NIL;
+ Oid defaultPartOid;
+ Oid existingRelid;
+ Oid ownerId = InvalidOid;
+ Oid save_userid;
+ int save_sec_context;
+ int save_nestlevel;
+
+ /*
+ * Check ownership of merged partitions - partitions with different owners
+ * cannot be merged. Also, collect the OIDs of these partitions during the
+ * check.
+ */
+ foreach_node(RangeVar, name, cmd->partlist)
+ {
+ Relation mergingPartition;
+
+ /*
+ * We are going to detach and remove this partition. We already took
+ * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here,
+ * NoLock is fine.
+ */
+ mergingPartition = table_openrv_extended(name, NoLock, false);
+ Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false));
+
+ if (OidIsValid(ownerId))
+ {
+ /* Do the partitions being merged have different owners? */
+ if (ownerId != mergingPartition->rd_rel->relowner)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("partitions being merged have different owners"));
+ }
+ else
+ ownerId = mergingPartition->rd_rel->relowner;
+
+ /* Store the next merging partition into the list. */
+ mergingPartitions = lappend_oid(mergingPartitions,
+ RelationGetRelid(mergingPartition));
+
+ table_close(mergingPartition, NoLock);
+ }
+
+ /* Look up the existing relation by the new partition name. */
+ RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid);
+
+ /*
+ * Check if this name is already taken. This helps us to detect the
+ * situation when one of the merging partitions has the same name as the
+ * new partition. Otherwise, this would fail later on anyway, but
+ * catching this here allows us to emit a nicer error message.
+ */
+ if (OidIsValid(existingRelid))
+ {
+ if (list_member_oid(mergingPartitions, existingRelid))
+ {
+ /*
+ * The new partition has the same name as one of the merging
+ * partitions.
+ */
+ char tmpRelName[NAMEDATALEN];
+
+ /* Generate a temporary name. */
+ sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+
+ /*
+ * Rename the existing partition with a temporary name, leaving it
+ * free for the new partition. We don't need to care about this
+ * in the future because we're going to eventually drop the
+ * existing partition anyway.
+ */
+ RenameRelationInternal(existingRelid, tmpRelName, true, false);
+
+ /*
+ * We must bump the command counter to make the new partition
+ * tuple visible for rename.
+ */
+ CommandCounterIncrement();
+ }
+ else
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_DUPLICATE_TABLE),
+ errmsg("relation \"%s\" already exists", cmd->name->relname));
+ }
+ }
+
+ defaultPartOid =
+ get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+
+ /* Detach all merging partitions. */
+ foreach_oid(mergingPartitionOid, mergingPartitions)
+ {
+ Relation child_rel;
+
+ child_rel = table_open(mergingPartitionOid, NoLock);
+
+ detachPartitionTable(rel, child_rel, defaultPartOid);
+
+ table_close(child_rel, NoLock);
+ }
+
+ /*
+ * Perform a preliminary check to determine whether it's safe to drop all
+ * merging partitions before we actually do so later. After merging rows
+ * into the new partitions via MergePartitionsMoveRows, all old partitions
+ * need to be dropped. However, since the drop behavior is DROP_RESTRICT
+ * and the merge process (MergePartitionsMoveRows) can be time-consuming,
+ * performing an early check on the drop eligibility of old partitions is
+ * preferable.
+ */
+ foreach_oid(mergingPartitionOid, mergingPartitions)
+ {
+ ObjectAddress object;
+
+ /* Get oid of the later to be dropped relation. */
+ object.objectId = mergingPartitionOid;
+ object.classId = RelationRelationId;
+ object.objectSubId = 0;
+
+ performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+ }
+
+ /*
+ * Create a table for the new partition, using the partitioned table as a
+ * model.
+ */
+ Assert(OidIsValid(ownerId));
+ newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId);
+
+ /*
+ * Switch to the table owner's userid, so that any index functions are run
+ * as that user. Also, lockdown security-restricted operations and
+ * arrange to make GUC variable changes local to this command.
+ *
+ * Need to do it after determining the namespace in the
+ * createPartitionTable() call.
+ */
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(ownerId,
+ save_sec_context | SECURITY_RESTRICTED_OPERATION);
+ save_nestlevel = NewGUCNestLevel();
+ RestrictSearchPath();
+
+ /* Copy data from merged partitions to the new partition. */
+ MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel);
+
+ /* Drop the current partitions before attaching the new one. */
+ foreach_oid(mergingPartitionOid, mergingPartitions)
+ {
+ ObjectAddress object;
+
+ object.objectId = mergingPartitionOid;
+ object.classId = RelationRelationId;
+ object.objectSubId = 0;
+
+ performDeletion(&object, DROP_RESTRICT, 0);
+ }
+
+ list_free(mergingPartitions);
+
+ /*
+ * Attach a new partition to the partitioned table. wqueue = NULL:
+ * verification for each cloned constraint is not needed.
+ */
+ attachPartitionTable(NULL, rel, newPartRel, cmd->bound);
+
+ /* Keep the lock until commit. */
+ table_close(newPartRel, NoLock);
+
+ /* Roll back any GUC changes executed by index functions. */
+ AtEOXact_GUC(false, save_nestlevel);
+
+ /* Restore the userid and security context. */
+ SetUserIdAndSecContext(save_userid, save_sec_context);
+}
+
+/*
+ * Struct with the context of the new partition for inserting rows from the
+ * split partition.
+ */
+typedef struct SplitPartitionContext
+{
+ ExprState *partqualstate; /* expression for checking a slot for a
+ * partition (NULL for DEFAULT partition) */
+ BulkInsertState bistate; /* state of bulk inserts for partition */
+ TupleTableSlot *dstslot; /* slot for inserting row into partition */
+ AlteredTableInfo *tab; /* structure with generated column expressions
+ * and check constraint expressions. */
+ Relation partRel; /* relation for partition */
+} SplitPartitionContext;
+
+/*
+ * createSplitPartitionContext: create context for partition and fill it
+ */
+static SplitPartitionContext *
+createSplitPartitionContext(Relation partRel)
+{
+ SplitPartitionContext *pc;
+
+ pc = palloc0_object(SplitPartitionContext);
+ pc->partRel = partRel;
+
+ /*
+ * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so
+ * don't bother using it.
+ */
+ pc->bistate = GetBulkInsertState();
+
+ /* Create a destination tuple slot for the new partition. */
+ pc->dstslot = table_slot_create(pc->partRel, NULL);
+
+ return pc;
+}
+
+/*
+ * deleteSplitPartitionContext: delete context for partition
+ */
+static void
+deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options)
+{
+ ListCell *ltab;
+
+ ExecDropSingleTupleTableSlot(pc->dstslot);
+ FreeBulkInsertState(pc->bistate);
+
+ table_finish_bulk_insert(pc->partRel, ti_options);
+
+ /*
+ * We don't need to process this pc->partRel so delete the ALTER TABLE
+ * queue of it.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ if (tab->relid == RelationGetRelid(pc->partRel))
+ {
+ *wqueue = list_delete_cell(*wqueue, ltab);
+ break;
+ }
+ }
+
+ pfree(pc);
+}
+
+/*
+ * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table
+ * (rel) and move rows into new partitions.
+ *
+ * New partitions description:
+ * partlist: list of pointers to SinglePartitionSpec structures. It contains
+ * the partition specification details for all new partitions.
+ * newPartRels: list of Relations, new partitions created in
+ * ATExecSplitPartition.
+ */
+static void
+SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel,
+ List *partlist, List *newPartRels)
+{
+ /* The FSM is empty, so don't bother using it. */
+ int ti_options = TABLE_INSERT_SKIP_FSM;
+ CommandId mycid;
+ EState *estate;
+ ListCell *listptr,
+ *listptr2;
+ TupleTableSlot *srcslot;
+ ExprContext *econtext;
+ TableScanDesc scan;
+ Snapshot snapshot;
+ MemoryContext oldCxt;
+ List *partContexts = NIL;
+ TupleConversionMap *tuple_map;
+ SplitPartitionContext *defaultPartCtx = NULL,
+ *pc;
+
+ mycid = GetCurrentCommandId(true);
+
+ estate = CreateExecutorState();
+
+ forboth(listptr, partlist, listptr2, newPartRels)
+ {
+ SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+
+ pc = createSplitPartitionContext((Relation) lfirst(listptr2));
+
+ /* Find the work queue entry for the new partition table: newPartRel. */
+ pc->tab = ATGetQueueEntry(wqueue, pc->partRel);
+
+ buildExpressionExecutionStates(pc->tab, pc->partRel, estate);
+
+ if (sps->bound->is_default)
+ {
+ /*
+ * We should not create a structure to check the partition
+ * constraint for the new DEFAULT partition.
+ */
+ defaultPartCtx = pc;
+ }
+ else
+ {
+ List *partConstraint;
+
+ /* Build expression execution states for partition check quals. */
+ partConstraint = get_qual_from_partbound(rel, sps->bound);
+ partConstraint =
+ (List *) eval_const_expressions(NULL,
+ (Node *) partConstraint);
+ /* Make a boolean expression for ExecCheck(). */
+ partConstraint = list_make1(make_ands_explicit(partConstraint));
+
+ /*
+ * Map the vars in the constraint expression from rel's attnos to
+ * splitRel's.
+ */
+ partConstraint = map_partition_varattnos(partConstraint,
+ 1, splitRel, rel);
+
+ pc->partqualstate =
+ ExecPrepareExpr((Expr *) linitial(partConstraint), estate);
+ Assert(pc->partqualstate != NULL);
+ }
+
+ /* Store partition context into a list. */
+ partContexts = lappend(partContexts, pc);
+ }
+
+ econtext = GetPerTupleExprContext(estate);
+
+ /* Create the necessary tuple slot. */
+ srcslot = table_slot_create(splitRel, NULL);
+
+ /*
+ * Map computing for moving attributes of the split partition to the new
+ * partition (for the first new partition, but other new partitions can
+ * use the same map).
+ */
+ pc = (SplitPartitionContext *) lfirst(list_head(partContexts));
+ tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel),
+ RelationGetDescr(pc->partRel));
+
+ /* Scan through the rows. */
+ snapshot = RegisterSnapshot(GetLatestSnapshot());
+ scan = table_beginscan(splitRel, snapshot, 0, NULL);
+
+ /*
+ * Switch to per-tuple memory context and reset it for each tuple
+ * produced, so we don't leak memory.
+ */
+ oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+ while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot))
+ {
+ bool found = false;
+ TupleTableSlot *insertslot;
+
+ CHECK_FOR_INTERRUPTS();
+
+ econtext->ecxt_scantuple = srcslot;
+
+ /* Search partition for the current slot, srcslot. */
+ foreach(listptr, partContexts)
+ {
+ pc = (SplitPartitionContext *) lfirst(listptr);
+
+ /* skip DEFAULT partition */
+ if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext))
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ /* Use the DEFAULT partition if it exists. */
+ if (defaultPartCtx)
+ pc = defaultPartCtx;
+ else
+ ereport(ERROR,
+ errcode(ERRCODE_CHECK_VIOLATION),
+ errmsg("can not find partition for split partition row"),
+ errtable(splitRel));
+ }
+
+ if (tuple_map)
+ {
+ /* Need to use a map to copy attributes. */
+ insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot);
+ }
+ else
+ {
+ /* Extract data from the old tuple. */
+ slot_getallattrs(srcslot);
+
+ /* Copy attributes directly. */
+ insertslot = pc->dstslot;
+
+ ExecClearTuple(insertslot);
+
+ memcpy(insertslot->tts_values, srcslot->tts_values,
+ sizeof(Datum) * srcslot->tts_nvalid);
+ memcpy(insertslot->tts_isnull, srcslot->tts_isnull,
+ sizeof(bool) * srcslot->tts_nvalid);
+
+ ExecStoreVirtualTuple(insertslot);
+ }
+
+ /*
+ * Constraints and GENERATED expressions might reference the tableoid
+ * column, so fill tts_tableOid with the desired value. (We must do
+ * this each time, because it gets overwritten with newrel's OID
+ * during storing.)
+ */
+ insertslot->tts_tableOid = RelationGetRelid(pc->partRel);
+
+ /*
+ * Now, evaluate any generated expressions whose inputs come from the
+ * new tuple. We assume these columns won't reference each other, so
+ * that there's no ordering dependency.
+ */
+ evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel,
+ insertslot, econtext);
+
+ /* Write the tuple out to the new relation. */
+ table_tuple_insert(pc->partRel, insertslot, mycid,
+ ti_options, pc->bistate);
+
+ ResetExprContext(econtext);
+ }
+
+ MemoryContextSwitchTo(oldCxt);
+
+ table_endscan(scan);
+ UnregisterSnapshot(snapshot);
+
+ if (tuple_map)
+ free_conversion_map(tuple_map);
+
+ ExecDropSingleTupleTableSlot(srcslot);
+
+ FreeExecutorState(estate);
+
+ foreach_ptr(SplitPartitionContext, spc, partContexts)
+ deleteSplitPartitionContext(spc, wqueue, ti_options);
+}
+
+/*
+ * ALTER TABLE <name> SPLIT PARTITION <partition-name> INTO <partition-list>
+ */
+void
+ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
+ PartitionCmd *cmd, AlterTableUtilityContext *context)
+{
+ Relation splitRel;
+ Oid splitRelOid;
+ ListCell *listptr,
+ *listptr2;
+ bool isSameName = false;
+ char tmpRelName[NAMEDATALEN];
+ List *newPartRels = NIL;
+ ObjectAddress object;
+ Oid defaultPartOid;
+ Oid save_userid;
+ int save_sec_context;
+ int save_nestlevel;
+
+ defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true));
+
+ /*
+ * Partition is already locked in the transformPartitionCmdForSplit
+ * function.
+ */
+ splitRel = table_openrv(cmd->name, NoLock);
+
+ splitRelOid = RelationGetRelid(splitRel);
+
+ /* Check descriptions of new partitions. */
+ foreach_node(SinglePartitionSpec, sps, cmd->partlist)
+ {
+ Oid existingRelid;
+
+ /* Look up the existing relation by the new partition name. */
+ RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid);
+
+ /*
+ * This would fail later on anyway if the relation already exists. But
+ * by catching it here, we can emit a nicer error message.
+ */
+ if (existingRelid == splitRelOid && !isSameName)
+ /* One new partition can have the same name as a split partition. */
+ isSameName = true;
+ else if (OidIsValid(existingRelid))
+ ereport(ERROR,
+ errcode(ERRCODE_DUPLICATE_TABLE),
+ errmsg("relation \"%s\" already exists", sps->name->relname));
+ }
+
+ /* Detach the split partition. */
+ detachPartitionTable(rel, splitRel, defaultPartOid);
+
+ /*
+ * Perform a preliminary check to determine whether it's safe to drop the
+ * split partition before we actually do so later. After merging rows into
+ * the new partitions via SplitPartitionMoveRows, all old partitions need
+ * to be dropped. However, since the drop behavior is DROP_RESTRICT and
+ * the merge process (SplitPartitionMoveRows) can be time-consuming,
+ * performing an early check on the drop eligibility of old partitions is
+ * preferable.
+ */
+ object.objectId = splitRelOid;
+ object.classId = RelationRelationId;
+ object.objectSubId = 0;
+ performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+
+ /*
+ * If a new partition has the same name as the split partition, then we
+ * should rename the split partition to reuse its name.
+ */
+ if (isSameName)
+ {
+ /*
+ * We must bump the command counter to make the split partition tuple
+ * visible for renaming.
+ */
+ CommandCounterIncrement();
+ /* Rename partition. */
+ sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid);
+ RenameRelationInternal(splitRelOid, tmpRelName, true, false);
+
+ /*
+ * We must bump the command counter to make the split partition tuple
+ * visible after renaming.
+ */
+ CommandCounterIncrement();
+ }
+
+ /* Create new partitions (like a split partition), without indexes. */
+ foreach_node(SinglePartitionSpec, sps, cmd->partlist)
+ {
+ Relation newPartRel;
+
+ newPartRel = createPartitionTable(wqueue, sps->name, rel,
+ splitRel->rd_rel->relowner);
+ newPartRels = lappend(newPartRels, newPartRel);
+ }
+
+ /*
+ * Switch to the table owner's userid, so that any index functions are run
+ * as that user. Also, lockdown security-restricted operations and
+ * arrange to make GUC variable changes local to this command.
+ *
+ * Need to do it after determining the namespace in the
+ * createPartitionTable() call.
+ */
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(splitRel->rd_rel->relowner,
+ save_sec_context | SECURITY_RESTRICTED_OPERATION);
+ save_nestlevel = NewGUCNestLevel();
+ RestrictSearchPath();
+
+ /* Copy data from the split partition to the new partitions. */
+ SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels);
+ /* Keep the lock until commit. */
+ table_close(splitRel, NoLock);
+
+ /* Attach new partitions to the partitioned table. */
+ forboth(listptr, cmd->partlist, listptr2, newPartRels)
+ {
+ SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr);
+ Relation newPartRel = (Relation) lfirst(listptr2);
+
+ /*
+ * wqueue = NULL: verification for each cloned constraint is not
+ * needed.
+ */
+ attachPartitionTable(NULL, rel, newPartRel, sps->bound);
+ /* Keep the lock until commit. */
+ table_close(newPartRel, NoLock);
+ }
+
+ /* Drop the split partition. */
+ object.classId = RelationRelationId;
+ object.objectId = splitRelOid;
+ object.objectSubId = 0;
+ /* Probably DROP_CASCADE is not needed. */
+ performDeletion(&object, DROP_RESTRICT, 0);
+
+ /* Roll back any GUC changes executed by index functions. */
+ AtEOXact_GUC(false, save_nestlevel);
+
+ /* Restore the userid and security context. */
+ SetUserIdAndSecContext(save_userid, save_sec_context);
+}
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 0ca312ac27d..195c9f30a36 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -22,6 +22,7 @@
#include "catalog/pg_inherits.h"
#include "catalog/pg_type.h"
#include "commands/tablecmds.h"
+#include "commands/tablecmds_partition.h"
#include "common/hashfn.h"
#include "executor/executor.h"
#include "miscadmin.h"
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index ed38b311ddf..eae0f123f54 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,7 +105,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 */
diff --git a/src/include/commands/tablecmds_internal.h b/src/include/commands/tablecmds_internal.h
new file mode 100644
index 00000000000..818da023865
--- /dev/null
+++ b/src/include/commands/tablecmds_internal.h
@@ -0,0 +1,194 @@
+/*-------------------------------------------------------------------------
+ *
+ * tablecmds_internal.h
+ * TODO
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/tablecmds_internal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TABLECMDS_INTERNAL_H
+#define TABLECMDS_INTERNAL_H
+
+#include "access/htup.h"
+#include "access/tupdesc.h"
+#include "nodes/parsenodes.h"
+#include "storage/lockdefs.h"
+
+/* to avoid including other headers */
+typedef struct RelationData *Relation;
+typedef struct List List;
+typedef struct ObjectAddress ObjectAddress;
+typedef struct ExprState ExprState;
+
+/*
+ * 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
+{
+ char *name; /* Constraint name, or NULL if none */
+ ConstrType contype; /* CHECK or FOREIGN */
+ Oid refrelid; /* PK rel, if FOREIGN */
+ Oid refindid; /* OID of PK's index, if FOREIGN */
+ bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */
+ Oid conid; /* OID of pg_constraint entry, if FOREIGN */
+ Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
+ ExprState *qualstate; /* Execution state for CHECK expr */
+} NewConstraint;
+
+/*
+ * Struct describing one new column value that needs to be computed during
+ * Phase 3 copy (this could be either a new column with a non-null default, or
+ * a column that we're changing the type of). Columns without such an entry
+ * are just copied from the old table during ATRewriteTable. Note that the
+ * expr is an expression over *old* table values, except when is_generated
+ * is true; then it is an expression over columns of the *new* tuple.
+ */
+typedef struct NewColumnValue
+{
+ AttrNumber attnum; /* which column */
+ Expr *expr; /* expression to compute */
+ ExprState *exprstate; /* execution state */
+ bool is_generated; /* is it a GENERATED expression? */
+} NewColumnValue;
+
+/* 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 void CheckAlterTableIsSafe(Relation rel);
+extern void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel,
+ Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode);
+extern AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel);
+extern void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets);
+extern ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode,
+ bool recurse, bool recursing);
+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 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 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 void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid,
+ Oid confrelid, Oid conrelid);
+extern void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition);
+extern void RemoveInheritance(Relation child_rel, Relation parent_rel,
+ bool expect_detached);
+
+#endif /* TABLECMDS_INTERNAL_H */
diff --git a/src/include/commands/tablecmds_partition.h b/src/include/commands/tablecmds_partition.h
new file mode 100644
index 00000000000..2da6d7edf7e
--- /dev/null
+++ b/src/include/commands/tablecmds_partition.h
@@ -0,0 +1,66 @@
+/*-------------------------------------------------------------------------
+ *
+ * tablecmds_partition.h
+ * prototypes for tablecmds_partition.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/tablecmds_partition.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TABLECMDS_PARTITION_H
+#define TABLECMDS_PARTITION_H
+
+#include "access/attnum.h"
+#include "nodes/parsenodes.h"
+#include "storage/lockdefs.h"
+
+/* to avoid including other headers */
+typedef struct AlteredTableInfo AlteredTableInfo;
+typedef struct ParseState ParseState;
+typedef struct AlterTableUtilityContext AlterTableUtilityContext;
+typedef struct ForeignKeyCacheInfo ForeignKeyCacheInfo;
+typedef struct RelationData *Relation;
+typedef struct List List;
+typedef struct ObjectAddress ObjectAddress;
+
+extern void ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode);
+extern bool ConstraintImpliedByRelConstraint(Relation scanrel,
+ List *testConstraint, List *provenConstraint);
+extern void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
+ Relation partitionRel);
+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 bool PartConstraintImpliedByRelConstraint(Relation scanrel,
+ List *partConstraint);
+extern ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
+ PartitionCmd *cmd,
+ AlterTableUtilityContext *context);
+extern void CloneRowTriggersToPartition(Relation parent, Relation partition);
+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);
+extern void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
+ PartitionCmd *cmd, AlterTableUtilityContext *context);
+extern void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
+ Relation rel, PartitionCmd *cmd,
+ AlterTableUtilityContext *context);
+
+#endif /* TABLECMDS_PARTITION_H */
--
2.50.1 (Apple Git-155)
--+6zGWtUKuQS8Xqxy--
^ permalink raw reply [nested|flat] 16+ messages in thread
* Re: Improve the performance of Unicode Normalization Forms.
@ 2026-07-01 06:29 Henson Choi <[email protected]>
0 siblings, 1 reply; 16+ messages in thread
From: Henson Choi @ 2026-07-01 06:29 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]; [email protected]; pgsql-hackers
Hi Alexander,
Rebased.
>
> v12:
> * Fixed bug in unicode_normalize() logic around blocked recomposition:
> Correctly updates the starter when a new ccc == 0 character appears.
> Avoids resetting prev_ccc incorrectly after successful recomposition.
> Added an NFC regression test for: x + acute + a + acute -> x + acute + á
> * Adds overflow checks for uint8/uint16 table indexes and sizes.
> * Adds decomposition_sort_length() helper.
> * Makes generated tables match what pgindent expects.
>
While reviewing v12 I found a memory-safety issue: the two-stage lookup
tables are read out of bounds during normalization.
Cause: TwoStageTable::generate() fills Index slots only up to the last
mapped key, leaving the final block short, but the generated lookup
always indexes the full block (index_name[offset + (cp & range_mask)]).
The offset table is pre-allocated to full size; @data is not padded to
$pos. Result:
* decomp_table_index[17438]: U+2FA1E..U+2FA3F -> 17408 + (cp & 63) =
17438..17471 (up to 34 slots past the end)
* inverse_table_index[3754]: U+16D6A..U+16D7F -> 3712 + (cp & 63) =
3754..3775 (up to 22 slots past the end)
Two things kept it hidden: no existing test touches this block (so cfbot
stays green), and even a test that does won't fail without a bounds
check -- the over-read silently returns 0 and happens to give the right
answer.
Reproduction: the attached test-only patch (nocfbot-unicode-norm-oob.txt)
adds bounds assertions to the two lookups plus regression tests that hit
the boundary. On an --enable-cassert build,
SELECT normalize(U&'\00C5\+02FA1E', NFD);
SELECT normalize(U&'\+016D6A\0301', NFC);
abort the backend (TRAP: failed Assert("offset + (cp & 63) <
lengthof(...)")).
Fix: pad the final block after the key loop in generate():
$data[$pos - 1] //= $tst->{dummy};
This extends the tables to 17472/3776, leaves the reader unchanged, and
resolves those codepoints to the dummy 0 entry (NULL), which is correct.
The generator change and the regenerated unicode_norm_table.h go
together. I'd keep the bounds assertion in the lookup as a guard against
future under-padding.
Thanks,
Henson
From 8fc31198057582667738d226cc792c7970f25490 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 1 Jul 2026 15:05:56 +0900
Subject: [PATCH] Add tests exposing out-of-bounds reads in Unicode
normalization lookup
Bounds assertions in normalization_index()/inverse_index() plus regression
tests that hit the last, unpadded block of the decomposition and
recomposition Index tables. Test-only; does not fix table generation.
---
src/include/common/unicode_norm_table.h | 2 ++
src/test/regress/expected/unicode.out | 29 +++++++++++++++++++++++++
src/test/regress/sql/unicode.sql | 10 +++++++++
3 files changed, 41 insertions(+)
diff --git a/src/include/common/unicode_norm_table.h b/src/include/common/unicode_norm_table.h
index 7bd198fac5f..99c42854759 100644
--- a/src/include/common/unicode_norm_table.h
+++ b/src/include/common/unicode_norm_table.h
@@ -6521,6 +6521,7 @@ normalization_index(char32_t cp)
offset = decomp_table_offset[offset_idx];
+ Assert(offset + (cp & 63) < lengthof(decomp_table_index));
return decomp_table_index[offset + (cp & 63)];
}
@@ -6541,6 +6542,7 @@ inverse_index(char32_t cp)
offset = inverse_table_offset[offset_idx];
+ Assert(offset + (cp & 63) < lengthof(inverse_table_index));
return inverse_table_index[offset + (cp & 63)];
}
diff --git a/src/test/regress/expected/unicode.out b/src/test/regress/expected/unicode.out
index 63e48d3a961..41de7a4e12f 100644
--- a/src/test/regress/expected/unicode.out
+++ b/src/test/regress/expected/unicode.out
@@ -183,3 +183,32 @@ SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd
t
(1 row)
+-- Two-stage table boundary: the last block of the decomposition and
+-- recomposition Index tables must be padded so the first out-of-bounds slot
+-- and the block's last slot (cp & 63 == 63) stay in bounds.
+-- NFD forces the decomposition lookup (paired with a decomposing character).
+SELECT normalize(U&'\00C5\+02FA1E', NFD) = U&'\0041\030A\+02FA1E' COLLATE "C" AS decomp_first_oob;
+ decomp_first_oob
+------------------
+ t
+(1 row)
+
+SELECT normalize(U&'\00C5\+02FA3F', NFD) = U&'\0041\030A\+02FA3F' COLLATE "C" AS decomp_last_slot;
+ decomp_last_slot
+------------------
+ t
+(1 row)
+
+-- A trailing combining mark forces the NFC recomposition lookup.
+SELECT normalize(U&'\+016D6A\0301', NFC) = U&'\+016D6A\0301' COLLATE "C" AS recomp_first_oob;
+ recomp_first_oob
+------------------
+ t
+(1 row)
+
+SELECT normalize(U&'\+016D7F\0301', NFC) = U&'\+016D7F\0301' COLLATE "C" AS recomp_last_slot;
+ recomp_last_slot
+------------------
+ t
+(1 row)
+
diff --git a/src/test/regress/sql/unicode.sql b/src/test/regress/sql/unicode.sql
index 951f86a336e..b9fe53144db 100644
--- a/src/test/regress/sql/unicode.sql
+++ b/src/test/regress/sql/unicode.sql
@@ -56,3 +56,13 @@ SELECT normalize(U&'\1100\1161\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangu
SELECT normalize(U&'\AC00', NFD) = U&'\1100\1161' COLLATE "C" AS hangul_nfd_lv;
SELECT normalize(U&'\AC01', NFD) = U&'\1100\1161\11A8' COLLATE "C" AS hangul_nfd_lvt;
SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd_last;
+
+-- Two-stage table boundary: the last block of the decomposition and
+-- recomposition Index tables must be padded so the first out-of-bounds slot
+-- and the block's last slot (cp & 63 == 63) stay in bounds.
+-- NFD forces the decomposition lookup (paired with a decomposing character).
+SELECT normalize(U&'\00C5\+02FA1E', NFD) = U&'\0041\030A\+02FA1E' COLLATE "C" AS decomp_first_oob;
+SELECT normalize(U&'\00C5\+02FA3F', NFD) = U&'\0041\030A\+02FA3F' COLLATE "C" AS decomp_last_slot;
+-- A trailing combining mark forces the NFC recomposition lookup.
+SELECT normalize(U&'\+016D6A\0301', NFC) = U&'\+016D6A\0301' COLLATE "C" AS recomp_first_oob;
+SELECT normalize(U&'\+016D7F\0301', NFC) = U&'\+016D7F\0301' COLLATE "C" AS recomp_last_slot;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] nocfbot-unicode-norm-oob.txt (3.9K, ../../CAAAe_zDpT1FgfXa4L8hVrkcyugrQ=5PtVT_r8DySCn0akfXEKw@mail.gmail.com/3-nocfbot-unicode-norm-oob.txt)
download | inline diff:
From 8fc31198057582667738d226cc792c7970f25490 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 1 Jul 2026 15:05:56 +0900
Subject: [PATCH] Add tests exposing out-of-bounds reads in Unicode
normalization lookup
Bounds assertions in normalization_index()/inverse_index() plus regression
tests that hit the last, unpadded block of the decomposition and
recomposition Index tables. Test-only; does not fix table generation.
---
src/include/common/unicode_norm_table.h | 2 ++
src/test/regress/expected/unicode.out | 29 +++++++++++++++++++++++++
src/test/regress/sql/unicode.sql | 10 +++++++++
3 files changed, 41 insertions(+)
diff --git a/src/include/common/unicode_norm_table.h b/src/include/common/unicode_norm_table.h
index 7bd198fac5f..99c42854759 100644
--- a/src/include/common/unicode_norm_table.h
+++ b/src/include/common/unicode_norm_table.h
@@ -6521,6 +6521,7 @@ normalization_index(char32_t cp)
offset = decomp_table_offset[offset_idx];
+ Assert(offset + (cp & 63) < lengthof(decomp_table_index));
return decomp_table_index[offset + (cp & 63)];
}
@@ -6541,6 +6542,7 @@ inverse_index(char32_t cp)
offset = inverse_table_offset[offset_idx];
+ Assert(offset + (cp & 63) < lengthof(inverse_table_index));
return inverse_table_index[offset + (cp & 63)];
}
diff --git a/src/test/regress/expected/unicode.out b/src/test/regress/expected/unicode.out
index 63e48d3a961..41de7a4e12f 100644
--- a/src/test/regress/expected/unicode.out
+++ b/src/test/regress/expected/unicode.out
@@ -183,3 +183,32 @@ SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd
t
(1 row)
+-- Two-stage table boundary: the last block of the decomposition and
+-- recomposition Index tables must be padded so the first out-of-bounds slot
+-- and the block's last slot (cp & 63 == 63) stay in bounds.
+-- NFD forces the decomposition lookup (paired with a decomposing character).
+SELECT normalize(U&'\00C5\+02FA1E', NFD) = U&'\0041\030A\+02FA1E' COLLATE "C" AS decomp_first_oob;
+ decomp_first_oob
+------------------
+ t
+(1 row)
+
+SELECT normalize(U&'\00C5\+02FA3F', NFD) = U&'\0041\030A\+02FA3F' COLLATE "C" AS decomp_last_slot;
+ decomp_last_slot
+------------------
+ t
+(1 row)
+
+-- A trailing combining mark forces the NFC recomposition lookup.
+SELECT normalize(U&'\+016D6A\0301', NFC) = U&'\+016D6A\0301' COLLATE "C" AS recomp_first_oob;
+ recomp_first_oob
+------------------
+ t
+(1 row)
+
+SELECT normalize(U&'\+016D7F\0301', NFC) = U&'\+016D7F\0301' COLLATE "C" AS recomp_last_slot;
+ recomp_last_slot
+------------------
+ t
+(1 row)
+
diff --git a/src/test/regress/sql/unicode.sql b/src/test/regress/sql/unicode.sql
index 951f86a336e..b9fe53144db 100644
--- a/src/test/regress/sql/unicode.sql
+++ b/src/test/regress/sql/unicode.sql
@@ -56,3 +56,13 @@ SELECT normalize(U&'\1100\1161\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangu
SELECT normalize(U&'\AC00', NFD) = U&'\1100\1161' COLLATE "C" AS hangul_nfd_lv;
SELECT normalize(U&'\AC01', NFD) = U&'\1100\1161\11A8' COLLATE "C" AS hangul_nfd_lvt;
SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd_last;
+
+-- Two-stage table boundary: the last block of the decomposition and
+-- recomposition Index tables must be padded so the first out-of-bounds slot
+-- and the block's last slot (cp & 63 == 63) stay in bounds.
+-- NFD forces the decomposition lookup (paired with a decomposing character).
+SELECT normalize(U&'\00C5\+02FA1E', NFD) = U&'\0041\030A\+02FA1E' COLLATE "C" AS decomp_first_oob;
+SELECT normalize(U&'\00C5\+02FA3F', NFD) = U&'\0041\030A\+02FA3F' COLLATE "C" AS decomp_last_slot;
+-- A trailing combining mark forces the NFC recomposition lookup.
+SELECT normalize(U&'\+016D6A\0301', NFC) = U&'\+016D6A\0301' COLLATE "C" AS recomp_first_oob;
+SELECT normalize(U&'\+016D7F\0301', NFC) = U&'\+016D7F\0301' COLLATE "C" AS recomp_last_slot;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 16+ messages in thread
* Re: Improve the performance of Unicode Normalization Forms.
@ 2026-07-03 07:46 Michael Paquier <[email protected]>
parent: Henson Choi <[email protected]>
0 siblings, 1 reply; 16+ messages in thread
From: Michael Paquier @ 2026-07-03 07:46 UTC (permalink / raw)
To: Henson Choi <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]; pgsql-hackers
On Wed, Jul 01, 2026 at 03:29:25PM +0900, Henson Choi wrote:
> Two things kept it hidden: no existing test touches this block (so cfbot
> stays green), and even a test that does won't fail without a bounds
> check -- the over-read silently returns 0 and happens to give the right
> answer.
>
> Reproduction: the attached test-only patch (nocfbot-unicode-norm-oob.txt)
> adds bounds assertions to the two lookups plus regression tests that hit
> the boundary. On an --enable-cassert build,
>
> SELECT normalize(U&'\00C5\+02FA1E', NFD);
> SELECT normalize(U&'\+016D6A\0301', NFC);
>
> abort the backend (TRAP: failed Assert("offset + (cp & 63) <
> lengthof(...)")).
That's a good find (if you've used some AI tooling here, good use). I
don't see an immediate need for in-core checks. One can also enable
ubsan to get the same effect with out-of-bound lookups reusing your
queries:
../../src/include/common/unicode_norm_table.h:6524:27: runtime error:
index 17438 out of bounds for type 'uint16 [17438]'
The tests would be more important. If we have a gap in these, this
usually points to a first patch where we add more tests. Please note
that you may not actually need more tests. If I run the test module
test_saslprep with PG_TEST_EXTRA=saslprep to make the costly TAP test
run, I get two ubsan reports of this kind. Note that I enable it in
some of my buildfarm machines, so it would not go unnoticed.
> This extends the tables to 17472/3776, leaves the reader unchanged, and
> resolves those codepoints to the dummy 0 entry (NULL), which is correct.
> The generator change and the regenerated unicode_norm_table.h go
> together. I'd keep the bounds assertion in the lookup as a guard against
> future under-padding.
This kind of issue is bad. Normalization is applied on the passwords
for SCRAM with SASLprep, meaning this patch would touch code that's
taken before authentication. This would lead to an immediate CVE, and
most likely one with a bad score. We need to be extremely careful
with what we are dealing here. SCRAM is used as default
authentication method for a lot of cloud providers, AFAIK.
Saying that (and still trying to figure out the whole code), the
numbers you have published in terms of reduction of the size of the
binaries and the runtimes, based on Jeff's or your methods are
impressive, seeing what has been posted. I would not underestimate
the cost of the two-pass method we currently use when applying the
normalization (one to calculate the output size, one to do the
normalization). Hence why don't we just do that first and evaluate
the gain in run time that we get? One thing that I have been
wondering why reading this thread is simply: why don't we use a
BinaryStringInfo and start the normalization with a sensible
preallocated size, avoiding the first pass for the sizing of the
output buffer? It should be a simpler change that rewriting all the
tables, and I suspect that it may improve the performance quite a bit,
the longer the strings to normalize. And I also suspect that we
should do that anyway at the end, and do it first.
I am aware of the fact that the patch does some allocation changes,
but I would suggest to split the patch into more reviewable pieces
rather than drop a big chunk of code changes presented as a single
commit, and present one patch for each relevant piece that aims at
improving performance. Something that shows up as a red flag for me
is the fact that the patch has a long list of bullet points. Each
bullet point, in some cases, can point to independent patches worth
doing on their own. Anyway, considering that the simplest pieces may
be better first, I could think of at least:
1) Elimination of the second allocation for the recomposition
(in-place recomposition for the decomps buffers).
2) Merge the two-pass decomposition into a single pass
3) The suggestion of Jeff to use a UTF8-native path is also something
I'd wonder about, rather than do the uint32 codepoint dance. Not sure
about the complexity, but Jeff has mentioned me in the past for his
work on native UTF-8 collation support that relying on the codepoints
in core was much, much, much faster than ICU (that was for sorting,
but we'd burn less CPU for sure here as well).
4) What is now the largest chunk, with the table recreations. I'd
suggest some more underground work here in terms of tests, identifying
potential gaps in our existing testing that's cheap and good enough
for meaningful corner cases. I am wondering if we don't have more to
extract here. One idea: we could store the combining class during
decomposition, saving one extra lookup?
The NF[K]{C,D} operations have been done as they are as a matter of
simplicity, relying on the introduction of the normalization API done
for SCRAM in v10, where performance was not (and is still not)
critical.
As a whole, this patch is too large as-is to be digested in a review.
Due to the pre-authentication requirement, something not rock-solid is
as good as nothing (one can say that for all features, but this one is
particularly exposed).
(Spoiler: I rely on my brain for this kind of work, gentler for the
planet with less tokens and memory burnt. A patch split as a bunch of
pieces is more edible for me, personally.)
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 16+ messages in thread
* Re: Improve the performance of Unicode Normalization Forms.
@ 2026-07-11 03:16 Henson Choi <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 0 replies; 16+ messages in thread
From: Henson Choi @ 2026-07-11 03:16 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]; pgsql-hackers
Hi hackers,
While reviewing this patch I looked into its effect on frontend
(libpq) size, and ended up laying out four ways the tables could
be arranged -- including options that bring the frontend back to,
or below, baseline size. Even if the current direction is right,
I wanted these on record, so the community isn't later left
wondering about "The Road Not Taken" -- with a nod to Frost. This
is purely an architect's-eye summary of structures and sizes; the
implementation and the decision are the author's and the
committer's.
Whichever road is taken below, two points hold. The OOB fix is a
hard requirement, assumed throughout. And while
the patch applies SHIFT=6 to both two-stage tables, the size-optimal
shift differs per table (decomposition 5, inverse 6); adopting that
mixed shift is a small, construction-independent size win I would
recommend as a baseline either way -- the figures below already use
it.
----------------------------------------------------------------------
Unicode normalization: current tables, a frontend compression
option, and four patch constructions
======================================================================
Premise: the backend keeps the unified two-stage table. This is
about the frontend (libpq) only, where normalization is reached
solely through SCRAM, so size matters more than speed. All figures
are static array bytes at the size-optimal shifts (decomposition
SHIFT=5, inverse SHIFT=6); baseline frontend is 75,576 and the patch
is 104,242.
1. Current data structures
The generated header holds nine arrays and three lookup functions.
Decomposition data:
UnicodeDecompMain[3957] (4B struct): decomposition entries,
each {comb_class, dec_size_flags, dec_index}.
UnicodeDecompCodepoints[4596] uint32: multi-codepoint
decomposition sequences.
UnicodeDecompSizes[4596] uint8: compatibility decomposition
length when it differs from the canonical one.
Recomposition data:
UnicodeDecompInverseCodes[962] uint32: composed results.
UnicodeDecompInverseSecond[6208] uint16: per first-codepoint
records, each [start_hi, start_lo, span, code_idx x (span+1)],
mapping a second codepoint in [start, start+span] to an
InverseCodes index.
Forward two-stage lookup (codepoint -> DecompMain index):
decomp_table_offset[6098] uint16
decomp_table_index[13248] uint16
Inverse two-stage lookup (first codepoint -> InverseSecond index):
inverse_table_offset[1463] uint16
inverse_table_index[3776] uint16
Lookup functions:
normalization_index(cp) -- forward two-stage
inverse_index(cp) -- inverse two-stage
normalization_inverse(first, second) -- recomposition:
inverse_index(first) gives an InverseSecond record, then the
second codepoint indexes into it for an InverseCodes result.
A two-stage lookup splits the key: offset[cp >> SHIFT] is where that
page begins in the index array, and index[offset + (cp & mask)] is
the value. Unmapped pages all share one dummy page, so most offset
entries point there -- which is why these tables are mostly empty
(exploited in section 3).
2. What the patch changes (architecture)
The patch reworks the lookup tables. The decomposition entry drops
its codepoint (8B -> 4B), which also lets entries with identical
contents merge. But once the codepoint is gone from the entry, the
two mappings it used to provide -- "codepoint -> entry" and
"pair -> composed" -- must be rebuilt as the explicit two-stage
tables above. The frontend and backend now share them; before this
patch the frontend used bsearch plus a scan and linked no lookup
table at all.
- Backend: a net win. Total tables -4.2%, and no slower: on the
thread, differences between lookup encodings (table vs branch,
big vs small table) were only 1-7%, so changing the encoding
does not move backend speed much.
- Frontend: +28,666 B (+37.9%). The data shrinks by 36,768 B
(the 4B entry plus dedup), but 65,434 B of forward and inverse
lookup tables plus the recomposition tables are added -- tables
baseline did not link at all, since it kept the codepoint and
used bsearch plus a scan. The two together net +28,666 B.
So the frontend cost is the lookup and recomposition tables that
exist only because the codepoint was removed, linked into a path
where speed is irrelevant. That makes the frontend purely a size
question.
3. A frontend high-compression alternative
The frontend can link a succinct form of the same tables, without
touching the backend or the 4B struct. As noted in section 1 the
tables are mostly empty -- the offset arrays are 93% dummy, and the
InverseSecond code_idx slots are 80% zero -- so store only the
non-empty entries:
- each offset array -> a rank/select bitmap: one present bit per
block plus a per-word rank cache. The non-zero ordinals are
1,2,3,... in order, so ordinal = rank + 1 (no value array).
- each index array and the InverseSecond records -> bitfield +
dense: a mask marks the non-empty slots, and only their values
are stored, each minus a base (per page for index, per record
for recomposition).
The decision needs only the resulting size below; the mechanism is
sketched for the curious and worked in full in Appendix A, so it is
safe to skim past it without stopping here. The struct stays 4B and
shared; only the three lookup functions differ, behind #ifdef
FRONTEND.
- Frontend 64,132 B: -11,444 vs baseline, -40,110 vs the patch.
The frontend surplus turns negative -- libpq ends up below
baseline.
- Lookup is the heaviest (rank/select + several popcounts). That
is fine where speed is irrelevant, but a bsearch-vs-this speed
comparison is worth showing before adopting it.
This is the most aggressive frontend option; it is what makes
"keep the backend two-stage, compress only the frontend" reach a
size below baseline.
4. Four patch constructions
Option 1 -- As-is (the patch)
Frontend and backend share the unified two-stage table.
Frontend 104,242 B. Lookup simple (one branch, two arrays).
Single code path. Current form (plus the OOB fix).
Policy: prioritize a single code path; accept +28.7 KB in libpq.
Option 2 -- Restore the codepoint
Keep the codepoint in the entry (both sides, 8B), as before this
patch. Then the frontend needs no lookup table: bsearch
forward, recomposition by scanning the same table.
Frontend 75,576 B (= baseline). Backend data +~39 KB (8B entry,
dedup lost), but the backend is one binary where size matters
less. One data table, one struct; only the lookup differs.
Policy: keep libpq at baseline on one data table; pay with a
larger backend data table. This is the pre-patch structure.
Option 3 -- Dual table (#ifdef)
Backend as in the patch (4B + two-stage); frontend baseline
(8B + bsearch); struct and lookup split by #ifdef FRONTEND.
Frontend 75,576 B. Backend small. Generator emits both.
Policy: keep libpq AND the backend small, at the cost of a dual
struct/lookup.
Option 4 -- Frontend high compression (section 3)
Backend as in the patch; frontend the succinct form; struct 4B
shared, lookup split by #ifdef.
Frontend 64,132 B (below baseline). Lookup heaviest.
Policy: push libpq below baseline, accepting a complex but
frontend-only lookup.
5. Comparison
Option Frontend Backend Lookup(front) Dual?
1 As-is (patch) 104,242 two-stage simple no
2 Restore codepoint 75,576 +39KB data bsearch no
3 Dual table 75,576 small bsearch yes
4 Frontend compress 64,132 two-stage rank/select lookup
In short: option 1 (the patch) is a net loss for the frontend --
the tables grow +28.7 KB, about 1% of libpq -- while the size gain
is on the backend. Whether even that 1% matters on modern servers,
let alone embedded or mobile, is open to doubt; this much memory is
arguably negligible today, so on size grounds option 1 may well be
enough -- which then turns on re-measuring the backend performance
win. If a frontend size win is wanted anyway, options 2/3/4 provide
it -- 2 and 3 at baseline size with a simple bsearch, 4 below
baseline with a complex lookup -- but the shared price is
complexity: a dual struct/lookup, or a complex lookup.
6. Recommendation
The premise is a backend performance win: common to all four
options (all keep the two-stage table), but it must be established
by a clear re-measurement before commit. The frontend is the SCRAM
authentication path, where size and stability -- not speed -- carry
weight, so the choice there comes in two steps.
The first choice is between options 1 and 3. If the frontend size
cost is deemed negligible today, option 1 (the patch, a single
path) is enough. If returning the frontend to baseline size is
preferred, adopt option 3 (simple bsearch, small backend) first.
If option 3 is taken and the frontend compression is confirmed to
cost no performance, option 4 can follow, pushing size below
baseline -- at the cost of a maintainability trade-off in its
complex lookup. (Option 2 also returns the frontend to baseline
but enlarges the backend data, so it loses to option 3, whose
backend stays small.)
The frontend choice is a balance of size against lookup simplicity
and maintainability; the final choice is the committer's, and the
four constructions above are laid out to make it easier.
----------------------------------------------------------------------
Appendix A. Worked compression examples
Small illustrations of the three transforms in section 3. Bit b of
a mask corresponds to slot/block b (bit 0 is lowest).
A.1 offset array -> rank/select bitmap
Suppose the offset array maps 8 blocks to page ordinals, mostly
dummy:
ordinal = [0, 0, 1, 0, 2, 0, 0, 3] (blocks 2,4,7 -> 1,2,3)
Store instead:
present = bits {2,4,7} set (one bit per block)
rank = per-word prefix count (one word here, rank[0]=0)
Pages are laid out in block order, so the k-th non-zero block gets
ordinal k+1; the ordinals run 1,2,3,... in sequence and need not be
stored -- ordinal = rank + 1.
Lookup of block 4:
present bit 4 is set -> not dummy
rank = rank[word] + popcount(present below bit 4) = 0 + 1 = 1
ordinal = rank + 1 = 2
Block 3: present bit 3 clear -> dummy, return 0.
A.2 index page -> bitfield + dense
Two pages of the index array, mostly zero (values not sorted):
page P = [0, 0, 102, 100, 0, 0, 101, 0]
page Q = [0, 202, 200, 0, 0, 0, 201, 0]
Each page stores a mask and a base; the non-zero values minus base
form a delta sequence. Identical delta sequences are shared, so a
page keeps only a seqoff into one common delta pool:
P: mask {2,3,6}, base 100, delta (2,0,1)
Q: mask {1,2,6}, base 200, delta (2,0,1) <- same sequence
delta pool = [2, 0, 1] (stored once); P.seqoff = Q.seqoff = 0
Lookup of slot 3 in page P:
mask bit 3 is set -> present
rank = popcount(mask below bit 3) = 1
value = base + delta[seqoff + rank] = 100 + delta[1] = 100
Slot 4: mask bit clear -> 0.
A.3 recomposition record -> bitfield + dense
An InverseSecond record for one first-codepoint, second range
[0x300 .. 0x30A] (span 10), code_idx mostly zero:
[hi=0, lo=0x300, span=10,
code_idx = 0,0,5,0,0,0,7,0,0,0,0]
Store:
hi=0, lo=0x300, span=10
mask = bits {2,6} set (span+1 = 11 bits)
base = 5 (min non-zero code_idx)
delta = [0, 2] (code_idx - base)
Lookup of second = 0x302 (offset 2):
mask bit 2 is set -> present
rank = popcount(mask below bit 2) = 0
result = InverseCodes[base + delta[0]] = InverseCodes[5]
second = 0x303 (offset 3): mask bit 3 clear -> 0.
A.4 Further sharing (not pursued)
The delta pool above shares only identical sequences. A sequence
that is a subsequence of another, or that overlaps it, could share
storage too:
pool so far: [2, 0, 1]
delta (0, 1) -> reuse pool at offset 1 (subsequence)
delta (1, 5) -> extend to [2, 0, 1, 5]; (1,5) sits at offset 2
This is the overlap/suffix sharing the generator already uses for
the decomposition-sequence table. Applied to these bitfield deltas
it could shrink them further, but the packing becomes a
shortest-common-superstring-like problem and gets complex, so I
did not pursue it -- noting it only as a possibility.
----------------------------------------------------------------------
Best regards,
Henson
^ permalink raw reply [nested|flat] 16+ messages in thread
end of thread, other threads:[~2026-07-11 03:16 UTC | newest]
Thread overview: 16+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-05-21 00:02 [PATCH v5 07/12] consistent language: to which Justin Pryzby <[email protected]>
2019-07-12 20:26 [PATCH 1/2] Determine operator semantics using btree opclasses Tomas Vondra <[email protected]>
2023-07-12 16:57 [PATCH v15 1/2] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v17 1/3] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v15 1/2] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v16 1/3] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v14 1/2] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2023-07-12 16:57 [PATCH v13 1/2] Remember PK oid for partitioned tables even when it's invalid Alvaro Herrera <[email protected]>
2026-01-05 14:26 [PATCH v3a] edit comments and stuff Álvaro Herrera <[email protected]>
2026-01-05 14:26 [PATCH v3a] edit comments and stuff Álvaro Herrera <[email protected]>
2026-01-29 22:09 [PATCH v3 1/1] move partitioning code out of tablecmds.c Nathan Bossart <[email protected]>
2026-07-01 06:29 Re: Improve the performance of Unicode Normalization Forms. Henson Choi <[email protected]>
2026-07-03 07:46 ` Re: Improve the performance of Unicode Normalization Forms. Michael Paquier <[email protected]>
2026-07-11 03:16 ` Re: Improve the performance of Unicode Normalization Forms. Henson Choi <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox