From 1c2e7c4f15367e4fe586d8e83686a4eab40c0eb8 Mon Sep 17 00:00:00 2001 From: Dilip Kumar Date: Mon, 9 Nov 2026 10:44:56 +0000 Subject: [PATCH v1] Don't trust constraints on virtual generated columns of parent tables Commit ce71993ae46 taught get_relation_constraints() to expand virtual generated columns in constraint expressions so that constraint exclusion can match them against qual clauses. However, that is not safe when scanning a parent table along with its child tables or partitions (!include_noinherit). A child table or partition can have a generation expression different from its parent's, and it enforces CHECK and NOT NULL constraints against its own expression, whereas a query on the parent computes the column using the parent's expression. Consequently, rows read through the parent can violate the parent's CHECK or NOT NULL constraints on a virtual generated column. Expanding those constraints on the parent relation using the parent's generation expression can therefore cause constraint exclusion (when constraint_exclusion = on) to falsely prune the parent relation and return wrong results. To fix, make get_relation_constraints() ignore CHECK constraints that reference virtual generated columns (directly or via a whole-row Var) as well as NOT NULL constraints on virtual generated columns when !include_noinherit. This is similar to the commit d8444163d22, which taught var_is_nonnullable() not to trust NOT NULL constraints on virtual generated columns of partitioned tables. Authored-by: Dilip Kumar --- src/backend/optimizer/util/plancat.c | 48 ++++++++++++++++++- .../regress/expected/generated_virtual.out | 45 +++++++++++++++++ src/test/regress/sql/generated_virtual.sql | 27 +++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 22e0e8b11f8..ccca4587a98 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1558,6 +1558,42 @@ get_relation_constraints(PlannerInfo *root, cexpr = stringToNode(constr->check[i].ccbin); + /* + * If we are scanning child tables too, we cannot trust CHECK + * constraints that reference virtual generated columns. Each + * child table or partition enforces CHECK constraints against its + * own generation expression, which can differ from this + * relation's expression. + */ + if (!include_noinherit && constr->has_generated_virtual) + { + Bitmapset *expr_attrs = NULL; + bool has_virtual = false; + int j = -1; + + pull_varattnos(cexpr, 1, &expr_attrs); + while ((j = bms_next_member(expr_attrs, j)) >= 0) + { + AttrNumber attno = j + FirstLowInvalidHeapAttributeNumber; + + /* + * A whole-row Var also references any virtual generated + * columns of the relation. + */ + if (attno == InvalidAttrNumber || + (attno > 0 && + TupleDescAttr(relation->rd_att, attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)) + { + has_virtual = true; + break; + } + } + bms_free(expr_attrs); + + if (has_virtual) + continue; + } + /* * Fix Vars to have the desired varno. This must be done before * const-simplification because eval_const_expressions reduces @@ -1600,8 +1636,18 @@ get_relation_constraints(PlannerInfo *root, if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped) { Form_pg_attribute wholeatt = TupleDescAttr(relation->rd_att, i - 1); - NullTest *ntest = makeNode(NullTest); + NullTest *ntest; + + /* + * As above, we cannot trust a NOT NULL constraint on a + * virtual generated column when scanning child tables + * too. + */ + if (!include_noinherit && + wholeatt->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + continue; + ntest = makeNode(NullTest); ntest->arg = (Expr *) makeVar(varno, i, wholeatt->atttypid, diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index b2f8e16e63b..99ea9b086ab 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -1907,3 +1907,48 @@ SELECT a FROM gtestnn_parent2 WHERE a NOT IN (SELECT b FROM gtestnn_parent2); (0 rows) DROP TABLE gtestnn_parent2; +-- Constraint exclusion must not trust CHECK or NOT NULL constraints on +-- virtual generated columns of parent/partitioned tables, because a child +-- partition can override the generation expression. +CREATE TABLE gtest33p ( + f2 int, + f3 int GENERATED ALWAYS AS (nullif(f2, 10) * 2) VIRTUAL NOT NULL, + CONSTRAINT gtest33p_ck CHECK (f3 > 30) +) PARTITION BY RANGE (f2); +CREATE TABLE gtest33p1 PARTITION OF gtest33p FOR VALUES FROM (0) TO (8); +CREATE TABLE gtest33p2 PARTITION OF gtest33p + (f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL) + FOR VALUES FROM (8) TO (100); +INSERT INTO gtest33p (f2) VALUES (10), (12); +SET constraint_exclusion TO on; +-- gtest33p1 is excluded by its own CHECK/NOT NULL constraints, while gtest33p2 is scanned +EXPLAIN (COSTS OFF) +SELECT f2, f3 FROM gtest33p WHERE NOT (f3 > 30); + QUERY PLAN +---------------------------------------- + Seq Scan on gtest33p2 gtest33p + Filter: ((NULLIF(f2, 10) * 2) <= 30) +(2 rows) + +SELECT f2, f3 FROM gtest33p WHERE NOT (f3 > 30); + f2 | f3 +----+---- + 12 | 24 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT f2, f3 FROM gtest33p WHERE f3 IS NULL; + QUERY PLAN +------------------------------------------ + Seq Scan on gtest33p2 gtest33p + Filter: ((NULLIF(f2, 10) * 2) IS NULL) +(2 rows) + +SELECT f2, f3 FROM gtest33p WHERE f3 IS NULL; + f2 | f3 +----+---- + 10 | +(1 row) + +RESET constraint_exclusion; +DROP TABLE gtest33p; diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index c4c4f3d5ef1..9579802cdb8 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -1014,3 +1014,30 @@ EXPLAIN (COSTS OFF) SELECT a FROM gtestnn_parent2 WHERE a NOT IN (SELECT b FROM gtestnn_parent2); SELECT a FROM gtestnn_parent2 WHERE a NOT IN (SELECT b FROM gtestnn_parent2); DROP TABLE gtestnn_parent2; + +-- Constraint exclusion must not trust CHECK or NOT NULL constraints on +-- virtual generated columns of parent/partitioned tables, because a child +-- partition can override the generation expression. +CREATE TABLE gtest33p ( + f2 int, + f3 int GENERATED ALWAYS AS (nullif(f2, 10) * 2) VIRTUAL NOT NULL, + CONSTRAINT gtest33p_ck CHECK (f3 > 30) +) PARTITION BY RANGE (f2); +CREATE TABLE gtest33p1 PARTITION OF gtest33p FOR VALUES FROM (0) TO (8); +CREATE TABLE gtest33p2 PARTITION OF gtest33p + (f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL) + FOR VALUES FROM (8) TO (100); +INSERT INTO gtest33p (f2) VALUES (10), (12); + +SET constraint_exclusion TO on; +-- gtest33p1 is excluded by its own CHECK/NOT NULL constraints, while gtest33p2 is scanned +EXPLAIN (COSTS OFF) +SELECT f2, f3 FROM gtest33p WHERE NOT (f3 > 30); +SELECT f2, f3 FROM gtest33p WHERE NOT (f3 > 30); + +EXPLAIN (COSTS OFF) +SELECT f2, f3 FROM gtest33p WHERE f3 IS NULL; +SELECT f2, f3 FROM gtest33p WHERE f3 IS NULL; + +RESET constraint_exclusion; +DROP TABLE gtest33p; -- 2.56.0.455.g6c5a503b38-goog