alter_table_set_not_null_by_constraints_only_v10_fixes.patch

application/octet-stream

Filename: alter_table_set_not_null_by_constraints_only_v10_fixes.patch
Type: application/octet-stream
Part: 0
Message: Re: using index or check in ALTER TABLE SET NOT NULL

Patch

Format: unified
Series: patch v10
File+
doc/src/sgml/ref/alter_table.sgml 7 5
src/backend/commands/tablecmds.c 25 30
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 84d72fdc75..926b3361ea 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -218,11 +218,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
-      You can only use <literal>SET NOT NULL</literal> when the column 
-      contains no null values. Full table scan is performed to check 
-      this condition unless there are valid <literal>CHECK</literal> 
-      constraints that prohibit NULL values for specified column.
-      In the latter case table scan is skipped.
+      <literal>SET NOT NULL</literal> may only be applied to a column
+      providing none of the records in the table contain a
+      <literal>NULL</literal> value for the column.  Ordinarily this is
+      checked during the <literal>ALTER TABLE</literal> by scanning the
+      entire table, however if a valid <literal>CHECK</literal> constraint is
+      found which proves no <literal>NULL</literal> can exist, then the
+      table scan is skipped.
      </para>
 
      <para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f8427865ee..b03e1bc875 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4507,8 +4507,9 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode)
 		else
 		{
 			/*
-			 * Test the current data within the table against new constraints
-			 * generated by ALTER TABLE commands, but don't rebuild data.
+			 * If required, test the current data within the table against new
+			 * constraints generated by ALTER TABLE commands, but don't rebuild
+			 * data.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
 				tab->partition_constraint != NULL)
@@ -4674,10 +4675,10 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
 	if (newrel || tab->verify_new_notnull)
 	{
 		/*
-		 * If we are rebuilding the tuples OR if we added any new NOT NULL
-		 * constraints, check all not-null constraints.  This is a bit of
-		 * overkill but it minimizes risk of bugs, and heap_attisnull is a
-		 * pretty cheap test anyway.
+		 * If we are rebuilding the tuples OR if we added any new but not
+		 * verified NOT NULL constraints, check all not-null constraints.
+		 * This is a bit of overkill but it minimizes risk of bugs, and
+		 * heap_attisnull is a pretty cheap test anyway.
 		 */
 		for (i = 0; i < newTupDesc->natts; i++)
 		{
@@ -5683,9 +5684,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		{
 			/*
 			 * If the new column is NOT NULL, and there is no missing value,
-			 * tell Phase 3 it needs to test that. (Note we don't do this for
-			 * an OID column.  OID will be marked not null, but since it's
-			 * filled specially, there's no need to test anything.)
+			 * tell Phase 3 it needs to check for NULLs.
 			 */
 			tab->verify_new_notnull |= colDef->is_not_null;
 		}
@@ -6056,14 +6055,16 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
 		CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
 		/*
-		 * Verifying table data can be done in Phase 3 with single table scan
-		 * for all constraint (both existing and new)
-		 * We can skip this check only if every new NOT NULL contraint
-		 * implied by existed table constraints
+		 * Ordinarily phase 3 must ensure that no NULLs exist in columns that
+		 * are set NOT NULL, however, if we can find a constraint which proves
+		 * this then we can skip that.  However, we needn't bother looking if
+		 * we've already found that we must verify some other NOT NULL
+		 * constraint.
 		 */
-		if (!NotNullImpliedByRelConstraints(rel, (Form_pg_attribute) GETSTRUCT(tuple)))
+		if (!tab->verify_new_notnull &&
+			!NotNullImpliedByRelConstraints(rel, (Form_pg_attribute) GETSTRUCT(tuple)))
 		{
-			/* Tell Phase 3 it needs to test the constraints */
+			/* Tell Phase 3 it needs to test the constraint */
 			tab->verify_new_notnull = true;
 		}
 
@@ -6083,12 +6084,11 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
 
 /*
  * NotNullImpliedByRelConstraints
- *		Does rel's existing constraints imply NOT NULL for given attribute
+ *		Does rel's existing constraints imply NOT NULL for the given attribute
  */
 static bool
 NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr)
 {
-	List	   *notNullConstraint = NIL;
 	NullTest   *nnulltest = makeNode(NullTest);
 
 	nnulltest->arg = (Expr *) makeVar(1,
@@ -6100,16 +6100,14 @@ NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr)
 	nnulltest->nulltesttype = IS_NOT_NULL;
 
 	/*
-	 * same thing as in ConstraintImpliedByRelConstraint 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.
+	 * 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.
 	 */
 	nnulltest->argisrow = false;
 	nnulltest->location = -1;
-	notNullConstraint = lappend(notNullConstraint, nnulltest);
 
-	if (ConstraintImpliedByRelConstraint(rel, notNullConstraint))
+	if (ConstraintImpliedByRelConstraint(rel, list_make1(nnulltest)))
 	{
 		ereport(DEBUG1,
 				(errmsg("verifying table \"%s\" NOT NULL constraint "
@@ -14368,11 +14366,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 /*
  * ConstraintImpliedByRelConstraint
- *		Do scanrel's existing constraints imply given constraint?
+ *		Do scanrel's existing constraints imply the given constraint?
  *
  * "Existing constraints" include its check constraints and column-level
- * NOT NULL constraints.  testConstraint describes the checked constraint,
- * in implicit-AND form.
+ * NOT NULL constraints.  testConstraint describes the constraint to validate.
+ * This must be in implicitly-AND form, must only contain immutable clauses
+ * and all Vars must be varno=1.
  */
 bool
 ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint)
@@ -14445,10 +14444,6 @@ ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint)
 	 * 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);
 }