From c28ecbe5ac850fb77c9f0b859c7d6a2b867cc4b9 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Tue, 26 May 2026 11:03:15 +0800 Subject: [PATCH v2 1/2] Fix CHECK constraint enforceability recursion ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED skipped recursion when the constraint on the target table was already enforced. That assumed descendant CHECK constraints could not have different enforceability, but both regular inheritance and some partition cases can have descendants with different conenforced states. Recurse for inheritable CHECK constraints regardless of whether the target constraint row itself changed, while still skipping NO INHERIT constraints. This ensures descendant constraints are updated and validated when needed. Add regression tests for both regular inheritance and partitioning: change a descendant CHECK constraint to NOT ENFORCED while the parent remains ENFORCED, then re-enforce the parent and verify that the operation recurses to the descendant. Author: Chao Li Reviewed-by: Discussion: https://postgr.es/m/E74C57FA-1DD0-4C8E-8FB1-538034752592@gmail.com --- src/backend/commands/tablecmds.c | 15 ++++++------- src/test/regress/expected/constraints.out | 26 +++++++++++++++++++++++ src/test/regress/expected/inherit.out | 24 +++++++++++++++++++++ src/test/regress/sql/constraints.sql | 18 ++++++++++++++++ src/test/regress/sql/inherit.sql | 15 +++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 1e0bacf85fc..05289207305 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -12700,14 +12700,13 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, } /* - * Note that we must recurse even when trying to change a check constraint - * to not enforced if it is already not enforced, in case descendant - * constraints might be enforced and need to be changed to not enforced. - * Conversely, we should do nothing if a constraint is being set to - * enforced and is already enforced, as descendant constraints cannot be - * different in that case. - */ - if (!cmdcon->is_enforced || changed) + * Recurse for inheritable constraints even when this constraint already + * has the requested enforceability. For both partitioning and regular + * inheritance, descendant constraints can have different enforceability + * and still need to be updated. Only NO INHERIT constraints do not + * recurse. + */ + if (!currcon->connoinherit) { /* * If we're recursing, the parent has already done this, so skip it. diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index e54fec7fb57..579882e75f6 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -468,6 +468,32 @@ select * from check_constraint_status; drop table parted_ch; drop view check_constraint_status; +-- an already enforced partitioned parent must still recurse to partitions +create table parted_ch_recurse( + a int, + constraint cc check (a > 0) enforced +) partition by range(a); +create table parted_ch_recurse_1 partition of parted_ch_recurse for values from (-100) to (100); +alter table parted_ch_recurse_1 alter constraint cc not enforced; +insert into parted_ch_recurse_1 values(-1); +alter table parted_ch_recurse alter constraint cc enforced; --error +ERROR: check constraint "cc" of relation "parted_ch_recurse_1" is violated by some row +delete from parted_ch_recurse_1 where a = -1; +alter table parted_ch_recurse alter constraint cc enforced; +select conrelid::regclass, conenforced, convalidated +from pg_constraint +where conname = 'cc' and conrelid::regclass::text like 'parted_ch_recurse%' +order by conrelid::regclass::text; + conrelid | conenforced | convalidated +---------------------+-------------+-------------- + parted_ch_recurse | t | t + parted_ch_recurse_1 | t | t +(2 rows) + +insert into parted_ch_recurse_1 values(-1); --error +ERROR: new row for relation "parted_ch_recurse_1" violates check constraint "cc" +DETAIL: Failing row contains (-1). +drop table parted_ch_recurse; -- -- Primary keys -- diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 3d8e8d8afd2..05a1604d609 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -1479,6 +1479,30 @@ NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table p1_c1 drop cascades to table p1_c2 drop cascades to table p1_c3 +-- an already enforced parent must still recurse to regular inheritance children +create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); +create table p1_c1() inherits(p1); +alter table p1_c1 alter constraint p1_a_check not enforced; +insert into p1_c1 values(-1); +alter table p1 alter constraint p1_a_check enforced; --error +ERROR: check constraint "p1_a_check" of relation "p1_c1" is violated by some row +delete from p1_c1; +alter table p1 alter constraint p1_a_check enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +------------+-------------+--------------+---------- + p1_a_check | t | t | p1 + p1_a_check | t | t | p1_c1 +(2 rows) + +insert into p1_c1 values(-1); --error +ERROR: new row for relation "p1_c1" violates check constraint "p1_a_check" +DETAIL: Failing row contains (-1). +drop table p1 cascade; +NOTICE: drop cascades to table p1_c1 --for "no inherit" check constraint, it will not recurse to child table create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index dc133b124bb..c84eaa44b9c 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -318,6 +318,24 @@ select * from check_constraint_status; drop table parted_ch; drop view check_constraint_status; +-- an already enforced partitioned parent must still recurse to partitions +create table parted_ch_recurse( + a int, + constraint cc check (a > 0) enforced +) partition by range(a); +create table parted_ch_recurse_1 partition of parted_ch_recurse for values from (-100) to (100); +alter table parted_ch_recurse_1 alter constraint cc not enforced; +insert into parted_ch_recurse_1 values(-1); +alter table parted_ch_recurse alter constraint cc enforced; --error +delete from parted_ch_recurse_1 where a = -1; +alter table parted_ch_recurse alter constraint cc enforced; +select conrelid::regclass, conenforced, convalidated +from pg_constraint +where conname = 'cc' and conrelid::regclass::text like 'parted_ch_recurse%' +order by conrelid::regclass::text; +insert into parted_ch_recurse_1 values(-1); --error +drop table parted_ch_recurse; + -- -- Primary keys -- diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 8f986904389..8a7417765aa 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -535,6 +535,21 @@ where conname = 'inh_check_constraint3' and contype = 'c' order by conrelid::regclass::text collate "C"; drop table p1 cascade; +-- an already enforced parent must still recurse to regular inheritance children +create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); +create table p1_c1() inherits(p1); +alter table p1_c1 alter constraint p1_a_check not enforced; +insert into p1_c1 values(-1); +alter table p1 alter constraint p1_a_check enforced; --error +delete from p1_c1; +alter table p1 alter constraint p1_a_check enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; +insert into p1_c1 values(-1); --error +drop table p1 cascade; + --for "no inherit" check constraint, it will not recurse to child table create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); -- 2.50.1 (Apple Git-155)