Thanks for the review.

On 2017/07/03 20:13, Ashutosh Bapat wrote:
> Thanks for working on the previous comments. The code really looks good now.
> On Fri, Jun 23, 2017 at 2:29 PM, Amit Langote
> <langote_amit...@lab.ntt.co.jp> wrote:
>>
>>> Don't we need an exclusive lock to
>>> make sure that the constraints are not changed while we are validating 
>>> those?
>>
>> If I understand your question correctly, you meant to ask if we don't need
>> the strongest lock on individual partitions while looking at their
>> constraints to prove that we don't need to scan them.  We do and we do
>> take the strongest lock on individual partitions even today in the second
>> call to find_all_inheritors().  We're trying to eliminate the second call
>> here.
> 
> The comment seems to imply that we need strongest lock only when we
> "scan" the table/s.
> 
> Some more comments on 0001
> -     * Prevent circularity by seeing if rel is a partition of attachRel. (In
> +     * Prevent circularity by seeing if rel is a partition of attachRel, (In
>       * particular, this disallows making a rel a partition of itself.)
> The sentence outside () doesn't have a full-stop. I think the original
> construct was better.

Yep, fixed.

> +     * We want to avoid having to construct this list again, so we request 
> the
>
> "this list" is confusing here since the earlier sentence doesn't mention any
> list at all. Instead we may reword it as "We will need the list of children
> later to check whether any of those have a row which would not fit the
> partition constraints. So, take the strongest lock ..."

It was confusing for sure, so rewrote.

>      * XXX - Do we need to lock the partitions here if we already have the
>      * strongest lock on attachRel?  The information we need here to check
>      * for circularity cannot change without taking a lock on attachRel.
>
> I wondered about this. Do we really need an exclusive lock to check whether
> partition constraint is valid? May be we can compare this condition with ALTER
> TABLE ... ADD CONSTRAINT since the children will all get a new constraint
> effectively. So, exclusive lock it is.

Actually, the XXX comment is about whether we need to lock the children at
all when checking the circularity of inheritance, that is, not about
whether we need lock to check the partition constraint is valid.

>         Assert(linitial_oid(attachRel_children) ==
>                                             RelationGetRelid(attachRel));
>         if (attachRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
>             attachRel_children = list_delete_first(attachRel_children);
>
> Is it necessary for this code to have OID of the relation being attached as 
> the
> first one? You could simply call list_delete_oid() instead of
> list_delete_first(). If for any reason find_all_inheritors() changes the 
> output
> order, this assertion and code would need a change.\

I concluded that removing attachRel's OID from attachRel_children is
pointless, considering that we have to check for attachRel in the loop
below anyway.  The step of removing the OID wasn't helping much.

>>> The name skipPartConstraintValidation() looks very specific to the case at
>>> hand. The function is really checking whether existing constraints on the 
>>> table
>>> can imply the given constraints (which happen to be partition constraints). 
>>> How
>>> about PartConstraintImpliedByRelConstraint()? The idea is to pick a general
>>> name so that the function can be used for purposes other than skipping
>>> validation scan in future.
>>
>> I liked this idea, so done.
> 
> + * skipPartConstraintValidation
> +PartConstraintImpliedByRelConstraint(Relation partrel, List *partConstraint)
> Different function names in prologue and the definition.

Oops, fixed.

> +    if (predicate_implied_by(partConstraint, existConstraint, true))
> +        return true;
> +
> +    /* Tough luck. */
> +    return false;
>
> why not to just return the return value of predicate_implied_by() as is?

Sigh.  Done. :)

> With this we can actually handle constr == NULL a bit differently.
> +    if (constr == NULL)
> +        return false;
>
> To be on safer side, return false when partConstraint is not NULL. If both the
> relation constraint and partConstraint are both NULL, the first does imply the
> other. Or may be leave that decision to predicate_implied_by(), which takes
> care of it right at the beginning of the function.

Rearranged code considering this comment.  Let's let
predicate_implied_by() make ultimate decisions about logical implication. :)

> 
> +         * For each leaf partition, check if it we can skip the validation
>
> An extra "it".

Fixed.

> 
> +         * Note that attachRel's OID is in this list.  Since we already
> +         * determined above that its validation scan cannot be skipped, we
> +         * need not check that again in the loop below.  If it's partitioned,
>
> I don't see code to skip checking whether scan can be skipped for relation
> being attached. The loop below this comments executes for every unpartitioned
> table in the list of OIDs returned. Thus for an unpartitioned relation being
> attached, it will try to compare the constraints again. Am I correct?

Good catch, fixed.

> +         * comparing it to similarly-processed qual clauses, and may fail
> There are no "qual clauses" here only constraints :).

Oh, yes.  Text fixed.

> The testcase looks good to me.

Attached updated patches.

Thanks,
Amit
From 8440a7995ac4969107f9c1e8803e8a46c7e4bd35 Mon Sep 17 00:00:00 2001
From: amit <amitlangot...@gmail.com>
Date: Wed, 14 Jun 2017 11:32:01 +0900
Subject: [PATCH 1/2] Cope with differing attnos in ATExecAttachPartition code

If the table being attached has different attnos from the parent for
the partitioning columns which are present in the partition constraint
expressions, then predicate_implied_by() will prematurely return false
due to the structural inequality of the corresponding Var expressions
in the partition constraint and those in the table's check constraint
expressions.  Fix this by mapping the partition constraint's expressions
to bear the partition's attnos.

Further, if the validation scan needs to be performed after all and
the table being attached is a partitioned table, we will need to map
the constraint expression again to change the attnos to the individual
leaf partition's attnos from those of the table being attached.

Another minor fix:

Avoid creating an AT work queue entry for the table being attached if
it's partitioned.  Current coding does not lead to that happening.
---
 src/backend/commands/tablecmds.c          | 71 +++++++++++++++++++++----------
 src/test/regress/expected/alter_table.out | 45 ++++++++++++++++++++
 src/test/regress/sql/alter_table.sql      | 38 +++++++++++++++++
 3 files changed, 131 insertions(+), 23 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bb00858ad1..7f6f85ccb2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13421,7 +13421,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
 {
        Relation        attachRel,
                                catalog;
-       List       *childrels;
+       List       *attachRel_children;
        TupleConstr *attachRel_constr;
        List       *partConstraint,
                           *existConstraint;
@@ -13489,10 +13489,26 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
        /*
         * 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 
due
+        * to lack of a constraint that would have prevented them in the first
+        * place.  If such a constraint is present (which by definition is 
present
+        * in all partitions), we are able to skip the scan.  But we cannot risk
+        * a deadlock by taking a weaker lock now and taking the strongest lock
+        * only when needed.
+        *
+        * XXX - Do we need to lock the partitions here if we already have the
+        * strongest lock on attachRel?  The information we need here to check
+        * for circularity cannot change without taking a lock on attachRel.
         */
-       childrels = find_all_inheritors(RelationGetRelid(attachRel),
-                                                                       
AccessShareLock, NULL);
-       if (list_member_oid(childrels, RelationGetRelid(rel)))
+       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"),
@@ -13603,6 +13619,13 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
        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);
+
+       /*
         * Check if we can do away with having to scan the table being attached 
to
         * validate the partition constraint, by *proving* that the existing
         * constraints of the table *imply* the partition predicate.  We include
@@ -13689,25 +13712,23 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                ereport(INFO,
                                (errmsg("partition constraint for table \"%s\" 
is implied by existing constraints",
                                                
RelationGetRelationName(attachRel))));
-
-       /*
-        * Set up to have the table be scanned to validate the partition
-        * constraint (see partConstraint above).  If it's a partitioned table, 
we
-        * instead schedule its leaf partitions to be scanned.
-        */
-       if (!skip_validate)
+       else
        {
-               List       *all_parts;
                ListCell   *lc;
 
-               /* Take an exclusive lock on the partitions to be checked */
+               /*
+                * Schedule the table (or leaf partitions if partitioned) to be 
scanned
+                * later.
+                *
+                * Note that attachRel's OID is in this list.  If it's 
partitioned, we
+                * we don't need to schedule it to be scanned (would be a noop 
anyway
+                * even if we did), so just remove it from the list.
+                */
                if (attachRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-                       all_parts = 
find_all_inheritors(RelationGetRelid(attachRel),
-                                                                               
        AccessExclusiveLock, NULL);
-               else
-                       all_parts = list_make1_oid(RelationGetRelid(attachRel));
+                       attachRel_children = list_delete_oid(attachRel_children,
+                                                                               
                 RelationGetRelid(attachRel));
 
-               foreach(lc, all_parts)
+               foreach(lc, attachRel_children)
                {
                        AlteredTableInfo *tab;
                        Oid                     part_relid = lfirst_oid(lc);
@@ -13724,21 +13745,25 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                         * Skip if it's a partitioned table.  Only 
RELKIND_RELATION
                         * relations (ie, leaf partitions) need to be scanned.
                         */
-                       if (part_rel != attachRel &&
-                               part_rel->rd_rel->relkind == 
RELKIND_PARTITIONED_TABLE)
+                       if (part_rel->rd_rel->relkind == 
RELKIND_PARTITIONED_TABLE)
                        {
-                               heap_close(part_rel, NoLock);
+                               if (part_rel != attachRel)
+                                       heap_close(part_rel, NoLock);
                                continue;
                        }
 
                        /* Grab a work queue entry */
                        tab = ATGetQueueEntry(wqueue, part_rel);
 
-                       /* Adjust constraint to match this partition */
+                       /*
+                        * Adjust the constraint that we constructed above for 
the table
+                        * being attached so that it matches this partition's 
attribute
+                        * numbers.
+                        */
                        constr = linitial(partConstraint);
                        tab->partition_constraint = (Expr *)
                                map_partition_varattnos((List *) constr, 1,
-                                                                               
part_rel, rel);
+                                                                               
part_rel, attachRel);
                        /* keep our lock until commit */
                        if (part_rel != attachRel)
                                heap_close(part_rel, NoLock);
diff --git a/src/test/regress/expected/alter_table.out 
b/src/test/regress/expected/alter_table.out
index 13d6a4b747..3ec5080fd6 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3347,6 +3347,51 @@ ALTER TABLE part_5 DROP CONSTRAINT check_a;
 ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT 
NULL;
 ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);
 INFO:  partition constraint for table "part_5" is implied by existing 
constraints
+-- Check the case where attnos of the partitioning columns in the table being
+-- attached differs from the parent.  It should not affect the constraint-
+-- checking logic that allows to skip the scan.
+CREATE TABLE part_6 (
+       c int,
+       LIKE list_parted2,
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 6)
+);
+ALTER TABLE part_6 DROP c;
+ALTER TABLE list_parted2 ATTACH PARTITION part_6 FOR VALUES IN (6);
+INFO:  partition constraint for table "part_6" is implied by existing 
constraints
+-- Similar to above, but the table being attached is a partitioned table
+-- whose partition has still different attnos for the root partitioning
+-- columns.
+CREATE TABLE part_7 (
+       LIKE list_parted2,
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+) PARTITION BY LIST (b);
+CREATE TABLE part_7_a_null (
+       c int,
+       d int,
+       e int,
+       LIKE list_parted2,      -- 'a' will have attnum = 4
+       CONSTRAINT check_b CHECK (b IS NULL OR b = 'a'),
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+);
+ALTER TABLE part_7_a_null DROP c, DROP d, DROP e;
+ALTER TABLE part_7 ATTACH PARTITION part_7_a_null FOR VALUES IN ('a', null);
+INFO:  partition constraint for table "part_7_a_null" is implied by existing 
constraints
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+INFO:  partition constraint for table "part_7" is implied by existing 
constraints
+-- Same example, but check this time that the constraint correctly detects
+-- violating rows
+ALTER TABLE list_parted2 DETACH PARTITION part_7;
+ALTER TABLE part_7 DROP CONSTRAINT check_a;    -- thusly, scan won't be skipped
+INSERT INTO part_7 (a, b) VALUES (8, null), (9, 'a');
+SELECT tableoid::regclass, a, b FROM part_7 order by a;
+   tableoid    | a | b 
+---------------+---+---
+ part_7_a_null | 8 | 
+ part_7_a_null | 9 | a
+(2 rows)
+
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+ERROR:  partition constraint is violated by some row
 -- check that the table being attached is not already a partition
 ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2);
 ERROR:  "part_2" is already a partition
diff --git a/src/test/regress/sql/alter_table.sql 
b/src/test/regress/sql/alter_table.sql
index 5dd1402ea6..e0b7b37278 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2178,6 +2178,44 @@ ALTER TABLE part_5 DROP CONSTRAINT check_a;
 ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT 
NULL;
 ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);
 
+-- Check the case where attnos of the partitioning columns in the table being
+-- attached differs from the parent.  It should not affect the constraint-
+-- checking logic that allows to skip the scan.
+CREATE TABLE part_6 (
+       c int,
+       LIKE list_parted2,
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 6)
+);
+ALTER TABLE part_6 DROP c;
+ALTER TABLE list_parted2 ATTACH PARTITION part_6 FOR VALUES IN (6);
+
+-- Similar to above, but the table being attached is a partitioned table
+-- whose partition has still different attnos for the root partitioning
+-- columns.
+CREATE TABLE part_7 (
+       LIKE list_parted2,
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+) PARTITION BY LIST (b);
+CREATE TABLE part_7_a_null (
+       c int,
+       d int,
+       e int,
+       LIKE list_parted2,      -- 'a' will have attnum = 4
+       CONSTRAINT check_b CHECK (b IS NULL OR b = 'a'),
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+);
+ALTER TABLE part_7_a_null DROP c, DROP d, DROP e;
+ALTER TABLE part_7 ATTACH PARTITION part_7_a_null FOR VALUES IN ('a', null);
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+
+-- Same example, but check this time that the constraint correctly detects
+-- violating rows
+ALTER TABLE list_parted2 DETACH PARTITION part_7;
+ALTER TABLE part_7 DROP CONSTRAINT check_a;    -- thusly, scan won't be skipped
+INSERT INTO part_7 (a, b) VALUES (8, null), (9, 'a');
+SELECT tableoid::regclass, a, b FROM part_7 order by a;
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+
 -- check that the table being attached is not already a partition
 ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2);
 
-- 
2.11.0

From 73fdfd639d0e45fb0c0db94f520fd18b94935ad6 Mon Sep 17 00:00:00 2001
From: amit <amitlangot...@gmail.com>
Date: Thu, 15 Jun 2017 19:22:31 +0900
Subject: [PATCH 2/2] Teach ATExecAttachPartition to skip validation in more
 cases

In cases where the table being attached is a partitioned table and
the table itself does not have constraints that would allow validation
on the whole table to be skipped, we can still skip the validations
of individual partitions if they each happen to have the requisite
constraints.

Per an idea of Robert Haas', with code refactoring suggestions from
Ashutosh Bapat.
---
 src/backend/commands/tablecmds.c          | 235 ++++++++++++++++--------------
 src/test/regress/expected/alter_table.out |  12 ++
 src/test/regress/sql/alter_table.sql      |  11 ++
 3 files changed, 150 insertions(+), 108 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7f6f85ccb2..f2cc4ca117 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -473,6 +473,8 @@ static void CreateInheritance(Relation child_rel, Relation 
parent_rel);
 static void RemoveInheritance(Relation child_rel, Relation parent_rel);
 static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
                                          PartitionCmd *cmd);
+static bool PartConstraintImpliedByRelConstraint(Relation partrel,
+                                         List *partConstraint);
 static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
 
 
@@ -13422,15 +13424,12 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
        Relation        attachRel,
                                catalog;
        List       *attachRel_children;
-       TupleConstr *attachRel_constr;
-       List       *partConstraint,
-                          *existConstraint;
+       List       *partConstraint;
        SysScanDesc scan;
        ScanKeyData skey;
        AttrNumber      attno;
        int                     natts;
        TupleDesc       tupleDesc;
-       bool            skip_validate = false;
        ObjectAddress address;
        const char *trigger_name;
 
@@ -13626,89 +13625,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                                                                
         rel);
 
        /*
-        * Check if we can do away with having to scan the table being attached 
to
-        * validate the partition constraint, by *proving* that the existing
-        * constraints of the table *imply* the partition predicate.  We include
-        * the table's check constraints and NOT NULL constraints in the list of
-        * clauses passed to predicate_implied_by().
-        *
-        * There is a case in which we cannot rely on just the result of the
-        * proof.
+        * Based on the table's existing constraints, determine if we can skip 
the
+        * partition constraint validation scan.
         */
-       attachRel_constr = tupleDesc->constr;
-       existConstraint = NIL;
-       if (attachRel_constr != NULL)
-       {
-               int                     num_check = attachRel_constr->num_check;
-               int                     i;
-
-               if (attachRel_constr->has_not_null)
-               {
-                       int                     natts = 
attachRel->rd_att->natts;
-
-                       for (i = 1; i <= natts; i++)
-                       {
-                               Form_pg_attribute att = 
attachRel->rd_att->attrs[i - 1];
-
-                               if (att->attnotnull && !att->attisdropped)
-                               {
-                                       NullTest   *ntest = makeNode(NullTest);
-
-                                       ntest->arg = (Expr *) makeVar(1,
-                                                                               
                  i,
-                                                                               
                  att->atttypid,
-                                                                               
                  att->atttypmod,
-                                                                               
                  att->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);
-                               }
-                       }
-               }
-
-               for (i = 0; i < num_check; i++)
-               {
-                       Node       *cexpr;
-
-                       /*
-                        * If this constraint hasn't been fully validated yet, 
we must
-                        * ignore it here.
-                        */
-                       if (!attachRel_constr->check[i].ccvalid)
-                               continue;
-
-                       cexpr = stringToNode(attachRel_constr->check[i].ccbin);
-
-                       /*
-                        * Run each expression through const-simplification and
-                        * canonicalization.  It is necessary, because we will 
be
-                        * comparing it to similarly-processed qual clauses, 
and may fail
-                        * to detect valid matches without this.
-                        */
-                       cexpr = eval_const_expressions(NULL, cexpr);
-                       cexpr = (Node *) canonicalize_qual((Expr *) cexpr);
-
-                       existConstraint = list_concat(existConstraint,
-                                                                               
  make_ands_implicit((Expr *) cexpr));
-               }
-
-               existConstraint = 
list_make1(make_ands_explicit(existConstraint));
-
-               /* And away we go ... */
-               if (predicate_implied_by(partConstraint, existConstraint, true))
-                       skip_validate = true;
-       }
-
-       /* It's safe to skip the validation scan after all */
-       if (skip_validate)
+       if (PartConstraintImpliedByRelConstraint(attachRel, partConstraint))
                ereport(INFO,
                                (errmsg("partition constraint for table \"%s\" 
is implied by existing constraints",
                                                
RelationGetRelationName(attachRel))));
@@ -13717,23 +13637,22 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                ListCell   *lc;
 
                /*
-                * Schedule the table (or leaf partitions if partitioned) to be 
scanned
-                * later.
+                * Constraints proved insufficient, so we need to scan the 
table to
+                * validate the partition constraint by checking it for 
individual
+                * rows.  However, if the table is partitioned, validation 
scans of
+                * individual leaf partitions may still be skipped if they have
+                * constraints that would make scanning unnecessary.
                 *
-                * Note that attachRel's OID is in this list.  If it's 
partitioned, we
-                * we don't need to schedule it to be scanned (would be a noop 
anyway
-                * even if we did), so just remove it from the list.
+                * Note that attachRel's OID is in the attachRel_children list. 
 Since
+                * we already determined above that its validation scan cannot 
be
+                * skipped, we need not check that again in the loop below.
                 */
-               if (attachRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-                       attachRel_children = list_delete_oid(attachRel_children,
-                                                                               
                 RelationGetRelid(attachRel));
-
                foreach(lc, attachRel_children)
                {
                        AlteredTableInfo *tab;
                        Oid                     part_relid = lfirst_oid(lc);
                        Relation        part_rel;
-                       Expr       *constr;
+                       List       *my_partconstr = partConstraint;
 
                        /* Lock already taken */
                        if (part_relid != RelationGetRelid(attachRel))
@@ -13742,8 +13661,35 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                part_rel = attachRel;
 
                        /*
-                        * Skip if it's a partitioned table.  Only 
RELKIND_RELATION
-                        * relations (ie, leaf partitions) need to be scanned.
+                        * Check if the partition's existing constraints imply 
the
+                        * partition constraint and if so, skip the validation 
scan.
+                        */
+                       if (part_rel != attachRel)
+                       {
+                               /*
+                                * Adjust the constraint that we constructed 
above for
+                                * attachRel so that it matches this 
partition's attribute
+                                * numbers.
+                                */
+                               my_partconstr = 
map_partition_varattnos(partConstraint, 1,
+                                                                               
                                part_rel,
+                                                                               
                                attachRel);
+
+                               if 
(PartConstraintImpliedByRelConstraint(part_rel,
+                                                                               
                                 my_partconstr))
+                               {
+                                       ereport(INFO,
+                                                       (errmsg("partition 
constraint for table \"%s\" is implied by existing constraints",
+                                                                       
RelationGetRelationName(part_rel))));
+                                       if (part_rel != attachRel)
+                                               heap_close(part_rel, NoLock);
+                                       continue;
+                               }
+                       }
+
+                       /*
+                        * Skip if the partition is itself a partitioned table. 
 We can
+                        * only ever scan RELKIND_RELATION relations.
                         */
                        if (part_rel->rd_rel->relkind == 
RELKIND_PARTITIONED_TABLE)
                        {
@@ -13752,18 +13698,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                continue;
                        }
 
-                       /* Grab a work queue entry */
+                       /* Grab a work queue entry. */
                        tab = ATGetQueueEntry(wqueue, part_rel);
+                       tab->partition_constraint = (Expr *) 
linitial(my_partconstr);
 
-                       /*
-                        * Adjust the constraint that we constructed above for 
the table
-                        * being attached so that it matches this partition's 
attribute
-                        * numbers.
-                        */
-                       constr = linitial(partConstraint);
-                       tab->partition_constraint = (Expr *)
-                               map_partition_varattnos((List *) constr, 1,
-                                                                               
part_rel, attachRel);
                        /* keep our lock until commit */
                        if (part_rel != attachRel)
                                heap_close(part_rel, NoLock);
@@ -13779,6 +13717,87 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
 }
 
 /*
+ * PartConstraintImpliedByRelConstraint
+ *             Does partrel's existing constraints imply the partition 
constraint?
+ *
+ * Existing constraints includes its check constraints and column-level
+ * NOT NULL constraints and partConstraint describes the partition constraint.
+ */
+static bool
+PartConstraintImpliedByRelConstraint(Relation partrel, List *partConstraint)
+{
+       List *existConstraint = NIL;
+       TupleConstr *constr = RelationGetDescr(partrel)->constr;
+       int             num_check,
+                       i;
+
+       if (constr && constr->has_not_null)
+       {
+               int             natts = partrel->rd_att->natts;
+
+               for (i = 1; i <= natts; i++)
+               {
+                       Form_pg_attribute att = partrel->rd_att->attrs[i - 1];
+
+                       if (att->attnotnull && !att->attisdropped)
+                       {
+                               NullTest   *ntest = makeNode(NullTest);
+
+                               ntest->arg = (Expr *) makeVar(1,
+                                                                               
          i,
+                                                                               
          att->atttypid,
+                                                                               
          att->atttypmod,
+                                                                               
          att->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);
+                       }
+               }
+       }
+
+       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;
+
+               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);
+
+               existConstraint = list_concat(existConstraint,
+                                                                         
make_ands_implicit((Expr *) cexpr));
+       }
+
+       if (existConstraint != NIL)
+               existConstraint = 
list_make1(make_ands_explicit(existConstraint));
+
+       /* And away we go ... */
+       return predicate_implied_by(partConstraint, existConstraint, true);
+}
+
+/*
  * ALTER TABLE DETACH PARTITION
  *
  * Return the address of the relation that is no longer a partition of rel.
diff --git a/src/test/regress/expected/alter_table.out 
b/src/test/regress/expected/alter_table.out
index 3ec5080fd6..03571f0e7c 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3392,6 +3392,18 @@ SELECT tableoid::regclass, a, b FROM part_7 order by a;
 
 ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
 ERROR:  partition constraint is violated by some row
+-- If the partitioned table being attached does not have a constraint that
+-- would allow validation scan to be skipped, but an individual partition
+-- does, then the partition's validation scan is skipped.  Note that the
+-- following leaf partition only allows rows that have a = 7 (and b = 'b' but
+-- that's irrelevant).
+CREATE TABLE part_7_b PARTITION OF part_7 (
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+) FOR VALUES IN ('b');
+-- The faulting row in part_7_a_null will still cause the command to fail
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+INFO:  partition constraint for table "part_7_b" is implied by existing 
constraints
+ERROR:  partition constraint is violated by some row
 -- check that the table being attached is not already a partition
 ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2);
 ERROR:  "part_2" is already a partition
diff --git a/src/test/regress/sql/alter_table.sql 
b/src/test/regress/sql/alter_table.sql
index e0b7b37278..7e270b77ca 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2216,6 +2216,17 @@ INSERT INTO part_7 (a, b) VALUES (8, null), (9, 'a');
 SELECT tableoid::regclass, a, b FROM part_7 order by a;
 ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
 
+-- If the partitioned table being attached does not have a constraint that
+-- would allow validation scan to be skipped, but an individual partition
+-- does, then the partition's validation scan is skipped.  Note that the
+-- following leaf partition only allows rows that have a = 7 (and b = 'b' but
+-- that's irrelevant).
+CREATE TABLE part_7_b PARTITION OF part_7 (
+       CONSTRAINT check_a CHECK (a IS NOT NULL AND a = 7)
+) FOR VALUES IN ('b');
+-- The faulting row in part_7_a_null will still cause the command to fail
+ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);
+
 -- check that the table being attached is not already a partition
 ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2);
 
-- 
2.11.0

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to