From 2f1e896e54d19cdf47d4e3a1bc169bf4cbae8a4b Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Tue, 20 Oct 2026 11:37:58 -0300 Subject: [PATCH REL_14_STABLE v2] Ignore dropped columns when simplifying ROW(...) IS [NOT] NULL eval_const_expressions() breaks a NullTest on a RowExpr into separate tests on its fields, and a null constant field refutes IS NOT NULL outright. However, a RowExpr of a named composite type contains a placeholder, typically a null int4 constant, for each dropped column of the rowtype. The planner treated that placeholder as a genuinely null field, so ROW(...)::sometype IS NOT NULL was simplified to constant false whenever the type had a dropped column, even when all real fields were non-null. The executor's ExecEvalRowNullInt() already skips dropped attributes, so the result depended on whether the test was simplified at plan time. This could be reached by casting a row to a named rowtype that has a dropped column, for example ROW(a, b)::tbl IS NOT NULL. Used in a WHERE clause, such a test silently filtered out every row. Fix by consulting the rowtype's tuple descriptor and skipping fields that correspond to dropped columns, matching the executor's behavior. --- src/backend/optimizer/util/clauses.c | 28 ++++++++++- src/test/regress/expected/rowtypes.out | 65 ++++++++++++++++++++++++++ src/test/regress/sql/rowtypes.sql | 30 ++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 575a87b2d7d..73261129aa3 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -3309,11 +3309,28 @@ eval_const_expressions_mutator(Node *node, */ RowExpr *rarg = (RowExpr *) arg; List *newargs = NIL; + TupleDesc tupdesc = NULL; + bool refuted = false; ListCell *l; + /* + * A RowExpr of a named composite type has a placeholder + * for each dropped column. Those must be ignored, as + * ExecEvalRowNullInt() does, so we need the tuple + * descriptor to identify them. + */ + if (rarg->row_typeid != RECORDOID) + tupdesc = lookup_rowtype_tupdesc(rarg->row_typeid, -1); + foreach(l, rarg->args) { Node *relem = (Node *) lfirst(l); + int i = foreach_current_index(l); + + /* Ignore dropped columns */ + if (tupdesc && i < tupdesc->natts && + TupleDescAttr(tupdesc, i)->attisdropped) + continue; /* * A constant field refutes the whole NullTest if it's @@ -3326,7 +3343,10 @@ eval_const_expressions_mutator(Node *node, if (carg->constisnull ? (ntest->nulltesttype == IS_NOT_NULL) : (ntest->nulltesttype == IS_NULL)) - return makeBoolConst(false, false); + { + refuted = true; + break; + } continue; } @@ -3343,6 +3363,12 @@ eval_const_expressions_mutator(Node *node, newntest->location = ntest->location; newargs = lappend(newargs, newntest); } + + if (tupdesc) + ReleaseTupleDesc(tupdesc); + + if (refuted) + return makeBoolConst(false, false); /* If all the inputs were constants, result is TRUE */ if (newargs == NIL) return makeBoolConst(true, false); diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 49a74d78277..e6e120da053 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -507,6 +507,71 @@ select ROW() = ROW(); ERROR: cannot compare rows of zero length LINE 1: select ROW() = ROW(); ^ +-- IS [NOT] NULL on a named rowtype with a dropped column must ignore the +-- dropped column's placeholder, also when simplified by the planner +create table rowdropped (a int, dropme int, b int); +alter table rowdropped drop column dropme; +insert into rowdropped values (1, 2), (1, null); +explain (verbose, costs off) +select row(a, b)::rowdropped is not null from rowdropped; + QUERY PLAN +------------------------------------------------- + Seq Scan on public.rowdropped + Output: ((a IS NOT NULL) AND (b IS NOT NULL)) +(2 rows) + +select row(a, b)::rowdropped is not null as nn, + row(a, b)::rowdropped is null as n +from rowdropped; + nn | n +----+--- + t | f + f | f +(2 rows) + +-- same for a composite type with a dropped attribute +create type rowdroppedtype as (x int, y int, z int); +alter type rowdroppedtype drop attribute y; +explain (verbose, costs off) +select row(a, b)::rowdroppedtype is not null from rowdropped; + QUERY PLAN +------------------------------------------------- + Seq Scan on public.rowdropped + Output: ((a IS NOT NULL) AND (b IS NOT NULL)) +(2 rows) + +select row(a, b)::rowdroppedtype is not null as nn from rowdropped; + nn +---- + t + f +(2 rows) + +-- a stored view keeps the original expression for the dropped attribute +create table rowdroppedsrc (x int, y int, z int); +insert into rowdroppedsrc values (1, null, 3), (1, 2, 3); +create type rowdroppedtype2 as (x int, y int, z int); +create view rowdroppedview as + select row(x, y, z)::rowdroppedtype2 is not null as nn from rowdroppedsrc; +alter type rowdroppedtype2 drop attribute y; +explain (verbose, costs off) +select nn from rowdroppedview; + QUERY PLAN +----------------------------------------------------------------------------- + Seq Scan on public.rowdroppedsrc + Output: ((rowdroppedsrc.x IS NOT NULL) AND (rowdroppedsrc.z IS NOT NULL)) +(2 rows) + +select nn from rowdroppedview; + nn +---- + t + t +(2 rows) + +drop view rowdroppedview; +drop type rowdroppedtype, rowdroppedtype2; +drop table rowdropped, rowdroppedsrc; -- Check ability to create arrays of anonymous rowtypes select array[ row(1,2), row(3,4), row(5,6) ]; array diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql index 0deb5f53993..0cb59f91767 100644 --- a/src/test/regress/sql/rowtypes.sql +++ b/src/test/regress/sql/rowtypes.sql @@ -194,6 +194,36 @@ select ROW(); select ROW() IS NULL; select ROW() = ROW(); +-- IS [NOT] NULL on a named rowtype with a dropped column must ignore the +-- dropped column's placeholder, also when simplified by the planner +create table rowdropped (a int, dropme int, b int); +alter table rowdropped drop column dropme; +insert into rowdropped values (1, 2), (1, null); +explain (verbose, costs off) +select row(a, b)::rowdropped is not null from rowdropped; +select row(a, b)::rowdropped is not null as nn, + row(a, b)::rowdropped is null as n +from rowdropped; +-- same for a composite type with a dropped attribute +create type rowdroppedtype as (x int, y int, z int); +alter type rowdroppedtype drop attribute y; +explain (verbose, costs off) +select row(a, b)::rowdroppedtype is not null from rowdropped; +select row(a, b)::rowdroppedtype is not null as nn from rowdropped; +-- a stored view keeps the original expression for the dropped attribute +create table rowdroppedsrc (x int, y int, z int); +insert into rowdroppedsrc values (1, null, 3), (1, 2, 3); +create type rowdroppedtype2 as (x int, y int, z int); +create view rowdroppedview as + select row(x, y, z)::rowdroppedtype2 is not null as nn from rowdroppedsrc; +alter type rowdroppedtype2 drop attribute y; +explain (verbose, costs off) +select nn from rowdroppedview; +select nn from rowdroppedview; +drop view rowdroppedview; +drop type rowdroppedtype, rowdroppedtype2; +drop table rowdropped, rowdroppedsrc; + -- Check ability to create arrays of anonymous rowtypes select array[ row(1,2), row(3,4), row(5,6) ]; -- 2.50.1 (Apple Git-155)