diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 1435f05..99af4b4 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -3308,7 +3308,13 @@ eval_const_expressions_mutator(Node *node,
 						newntest = makeNode(NullTest);
 						newntest->arg = (Expr *) relem;
 						newntest->nulltesttype = ntest->nulltesttype;
-						newntest->argisrow = type_is_rowtype(exprType(relem));
+						/*
+						 * We must unconditionally set argisrow false here.
+						 * Otherwise, we'd be treating a nested composite
+						 * structure as though it were at top level, which
+						 * would change the semantics of the test.
+						 */
+						newntest->argisrow = false;
 						newntest->location = ntest->location;
 						newargs = lappend(newargs, newntest);
 					}
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 3630ef4..17cbb4c 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -657,3 +657,93 @@ select row_to_json(r) from (select q2,q1 from tt1 offset 0) r;
  {"q2":0,"q1":0}
 (3 rows)
 
+--
+-- test IS [NOT] NULL on nested cases, to ensure consistency
+-- (bug #14235)
+--
+create type ct1 as (a integer, b integer);
+create type ct2 as (a integer, b ct1);
+create type ct3 as (a integer, b ct2);
+create function make_ct1(integer,integer) returns ct1
+  language sql immutable
+  as $f$ select row($1,$2*$2)::ct1; $f$;
+create function make_ct2(integer,integer) returns ct2
+  language sql immutable
+  as $f$ select row($1,row($2,$2)::ct1)::ct2; $f$;
+create function make_ct3(integer,integer) returns ct3
+  language sql immutable
+  as $f$ select row($1,row($2,row($2,$2)::ct1)::ct2)::ct3; $f$;
+create function stable_null() returns integer
+  language plpgsql stable cost 1
+  as $f$ begin return null; end; $f$;
+create function volatile_null() returns integer
+  language plpgsql volatile cost 1000
+  as $f$ begin return null; end; $f$;
+select row(null,null)::ct1 is null as ct1,
+       row(null,row(null,null)::ct1)::ct2 is null as ct2,
+       row(null,row(null,row(null,null)::ct1)::ct2)::ct3 is null as ct3;
+ ct1 | ct2 | ct3 
+-----+-----+-----
+ t   | f   | f
+(1 row)
+
+select row(1,null)::ct1 is not null as row_cons,
+       '(1,)'::ct1 is not null as row_literal,
+       make_ct1(1,null) is not null as inlined_const,
+       make_ct1(1,stable_null()) is not null as inlined_func,
+       make_ct1(1,volatile_null()) is not null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ f        | f           | f             | f            | f
+(1 row)
+
+select row(1,row(null,null)::ct1)::ct2 is not null as row_cons,
+       '(1,"(,)")'::ct2 is not null as row_literal,
+       make_ct2(1,null) is not null as inlined_const,
+       make_ct2(1,stable_null()) is not null as inlined_func,
+       make_ct2(1,volatile_null()) is not null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ t        | t           | t             | t            | t
+(1 row)
+
+select row(1,row(null,row(null,null)::ct1)::ct2)::ct3 is not null as row_cons,
+       '(1,"(,""(,)"")")'::ct3 is not null as row_literal,
+       make_ct3(1,null) is not null as inlined_const,
+       make_ct3(1,stable_null()) is not null as inlined_func,
+       make_ct3(1,volatile_null()) is not null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ t        | t           | t             | t            | t
+(1 row)
+
+select row(null,null)::ct1 is null as row_cons,
+       '(,)'::ct1 is null as row_literal,
+       make_ct1(null,null) is null as inlined_const,
+       make_ct1(null,stable_null()) is null as inlined_func,
+       make_ct1(null,volatile_null()) is null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ t        | t           | t             | t            | t
+(1 row)
+
+select row(null,row(null,null)::ct1)::ct2 is null as row_cons,
+       '(,"(,)")'::ct2 is null as row_literal,
+       make_ct2(null,null) is null as inlined_const,
+       make_ct2(null,stable_null()) is null as inlined_func,
+       make_ct2(null,volatile_null()) is null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ f        | f           | f             | f            | f
+(1 row)
+
+select row(null,row(null,row(null,null)::ct1)::ct2)::ct3 is null as row_cons,
+       '(,"(,""(,)"")")'::ct3 is null as row_literal,
+       make_ct3(null,null) is null as inlined_const,
+       make_ct3(null,stable_null()) is null as inlined_func,
+       make_ct3(null,volatile_null()) is null as called_func;
+ row_cons | row_literal | inlined_const | inlined_func | called_func 
+----------+-------------+---------------+--------------+-------------
+ f        | f           | f             | f            | f
+(1 row)
+
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index 677d34a..5faa7e6 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -286,3 +286,72 @@ create temp table tt1 as select * from int8_tbl limit 2;
 create temp table tt2 () inherits(tt1);
 insert into tt2 values(0,0);
 select row_to_json(r) from (select q2,q1 from tt1 offset 0) r;
+
+--
+-- test IS [NOT] NULL on nested cases, to ensure consistency
+-- (bug #14235)
+--
+
+create type ct1 as (a integer, b integer);
+create type ct2 as (a integer, b ct1);
+create type ct3 as (a integer, b ct2);
+
+create function make_ct1(integer,integer) returns ct1
+  language sql immutable
+  as $f$ select row($1,$2*$2)::ct1; $f$;
+
+create function make_ct2(integer,integer) returns ct2
+  language sql immutable
+  as $f$ select row($1,row($2,$2)::ct1)::ct2; $f$;
+
+create function make_ct3(integer,integer) returns ct3
+  language sql immutable
+  as $f$ select row($1,row($2,row($2,$2)::ct1)::ct2)::ct3; $f$;
+
+create function stable_null() returns integer
+  language plpgsql stable cost 1
+  as $f$ begin return null; end; $f$;
+
+create function volatile_null() returns integer
+  language plpgsql volatile cost 1000
+  as $f$ begin return null; end; $f$;
+
+select row(null,null)::ct1 is null as ct1,
+       row(null,row(null,null)::ct1)::ct2 is null as ct2,
+       row(null,row(null,row(null,null)::ct1)::ct2)::ct3 is null as ct3;
+
+select row(1,null)::ct1 is not null as row_cons,
+       '(1,)'::ct1 is not null as row_literal,
+       make_ct1(1,null) is not null as inlined_const,
+       make_ct1(1,stable_null()) is not null as inlined_func,
+       make_ct1(1,volatile_null()) is not null as called_func;
+
+select row(1,row(null,null)::ct1)::ct2 is not null as row_cons,
+       '(1,"(,)")'::ct2 is not null as row_literal,
+       make_ct2(1,null) is not null as inlined_const,
+       make_ct2(1,stable_null()) is not null as inlined_func,
+       make_ct2(1,volatile_null()) is not null as called_func;
+
+select row(1,row(null,row(null,null)::ct1)::ct2)::ct3 is not null as row_cons,
+       '(1,"(,""(,)"")")'::ct3 is not null as row_literal,
+       make_ct3(1,null) is not null as inlined_const,
+       make_ct3(1,stable_null()) is not null as inlined_func,
+       make_ct3(1,volatile_null()) is not null as called_func;
+
+select row(null,null)::ct1 is null as row_cons,
+       '(,)'::ct1 is null as row_literal,
+       make_ct1(null,null) is null as inlined_const,
+       make_ct1(null,stable_null()) is null as inlined_func,
+       make_ct1(null,volatile_null()) is null as called_func;
+
+select row(null,row(null,null)::ct1)::ct2 is null as row_cons,
+       '(,"(,)")'::ct2 is null as row_literal,
+       make_ct2(null,null) is null as inlined_const,
+       make_ct2(null,stable_null()) is null as inlined_func,
+       make_ct2(null,volatile_null()) is null as called_func;
+
+select row(null,row(null,row(null,null)::ct1)::ct2)::ct3 is null as row_cons,
+       '(,"(,""(,)"")")'::ct3 is null as row_literal,
+       make_ct3(null,null) is null as inlined_const,
+       make_ct3(null,stable_null()) is null as inlined_func,
+       make_ct3(null,volatile_null()) is null as called_func;
