On 2017/08/04 3:29, Robert Haas wrote:
> On Thu, Aug 3, 2017 at 1:04 AM, Amit Langote
> <langote_amit...@lab.ntt.co.jp> wrote:
>> Alright, attached updated 0001 does that.
> 
> Committed 0001 and 0002.

Thanks.

> 0003 needs a rebase.

Rebased patch attached.

Thanks,
Amit
From f069845c027acc36aab4790d6d6afbf50bba803e Mon Sep 17 00:00:00 2001
From: amit <amitlangot...@gmail.com>
Date: Tue, 1 Aug 2017 10:58:38 +0900
Subject: [PATCH 1/2] Cope with differing attnos in ATExecAttachPartition code

If the table being attached has attnos different 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 structural inequality of the corresponding Var expressions in the
the partition constraint and those in the table's check constraint
expressions.  Fix this by changing 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.

Reported by: Ashutosh Bapat
Report: 
https://postgr.es/m/CAFjFpReT_kq_uwU_B8aWDxR7jNGE%3DP0iELycdq5oupi%3DxSQTOw%40mail.gmail.com
---
 src/backend/commands/tablecmds.c          | 40 ++++++++++++++++++---------
 src/test/regress/expected/alter_table.out | 45 +++++++++++++++++++++++++++++++
 src/test/regress/sql/alter_table.sql      | 38 ++++++++++++++++++++++++++
 3 files changed, 111 insertions(+), 12 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7859ef13ac..1b8d4b3d17 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13433,6 +13433,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
        bool            skip_validate = false;
        ObjectAddress address;
        const char *trigger_name;
+       bool            found_whole_row;
 
        attachrel = heap_openrv(cmd->name, AccessExclusiveLock);
 
@@ -13614,6 +13615,16 @@ 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, &found_whole_row);
+       /* There can never be a whole-row reference here */
+       if (found_whole_row)
+               elog(ERROR, "unexpected whole-row reference found in partition 
key");
+
+       /*
         * 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
@@ -13712,8 +13723,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                        AlteredTableInfo *tab;
                        Oid                     part_relid = lfirst_oid(lc);
                        Relation        part_rel;
-                       Expr       *constr;
-                       bool            found_whole_row;
+                       List       *my_partconstr = partConstraint;
 
                        /* Lock already taken */
                        if (part_relid != RelationGetRelid(attachrel))
@@ -13732,18 +13742,24 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                continue;
                        }
 
+                       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(my_partconstr, 1,
+                                                                               
                                part_rel, attachrel,
+                                                                               
                                &found_whole_row);
+                               /* There can never be a whole-row reference 
here */
+                               if (found_whole_row)
+                                       elog(ERROR, "unexpected whole-row 
reference found in partition key");
+                       }
+
                        /* Grab a work queue entry. */
                        tab = ATGetQueueEntry(wqueue, part_rel);
-
-                       /* Adjust constraint to match this partition */
-                       constr = linitial(partConstraint);
-                       tab->partition_constraint = (Expr *)
-                               map_partition_varattnos((List *) constr, 1,
-                                                                               
part_rel, rel,
-                                                                               
&found_whole_row);
-                       /* There can never be a whole-row reference here */
-                       if (found_whole_row)
-                               elog(ERROR, "unexpected whole-row reference 
found in partition key");
+                       tab->partition_constraint = (Expr *) 
linitial(my_partconstr);
 
                        /* keep our lock until commit */
                        if (part_rel != attachrel)
diff --git a/src/test/regress/expected/alter_table.out 
b/src/test/regress/expected/alter_table.out
index 13d6a4b747..b727f4bcde 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..9a20dd141a 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 202fb661e69b51498a50a20491b39e739522e6c9 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          | 198 +++++++++++++++++-------------
 src/test/regress/expected/alter_table.out |  12 ++
 src/test/regress/sql/alter_table.sql      |  11 ++
 3 files changed, 135 insertions(+), 86 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1b8d4b3d17..80f87a8b5b 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;
        bool            found_whole_row;
@@ -13625,88 +13624,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                elog(ERROR, "unexpected whole-row reference found in partition 
key");
 
        /*
-        * 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
+        * scanning the table to validate the partition constraint.
         */
-       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;
-       }
-
-       if (skip_validate)
+       if (PartConstraintImpliedByRelConstraint(attachrel, partConstraint))
        {
                /* No need to scan the table after all. */
                ereport(INFO,
@@ -13715,9 +13636,18 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
        }
        else
        {
-               /* Constraints proved insufficient, so we need to scan the 
table. */
                ListCell   *lc;
 
+               /*
+                * Constraints proved insufficient, so we need to scan the 
table.
+                * However, if the table is partitioned, validation scans of the
+                * individual leaf partitions may still be skipped if they have
+                * constraints that would make scanning them unnecessary.
+                *
+                * 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.
+                */
                foreach(lc, attachrel_children)
                {
                        AlteredTableInfo *tab;
@@ -13742,6 +13672,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                continue;
                        }
 
+                       /*
+                        * Check if the partition's existing constraints imply 
the
+                        * partition constraint and if so, skip the validation 
scan.
+                        */
                        if (part_rel != attachrel)
                        {
                                /*
@@ -13755,6 +13689,17 @@ ATExecAttachPartition(List **wqueue, Relation rel, 
PartitionCmd *cmd)
                                /* There can never be a whole-row reference 
here */
                                if (found_whole_row)
                                        elog(ERROR, "unexpected whole-row 
reference found in partition key");
+
+                               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;
+                               }
                        }
 
                        /* Grab a work queue entry. */
@@ -13776,6 +13721,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 b727f4bcde..568fb3b2b6 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 9a20dd141a..d7dd3b8984 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