From 8f43852c685597cc83b5778576dc40f5e5cb01e7 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Wed, 21 Oct 2026 10:58:16 +0900
Subject: [PATCH v3 2/3] Record explicit casts of ROW() constructors in the
 parse tree

A cast of a ROW() to its own type, as in ROW(1, 2)::record, does
nothing, so coerce_type() returned the RowExpr unchanged and the cast
left no trace in the tree.  The cast matters to the parser, though: it
keeps an operator on the ROW() from being read as a row comparison.
Since ruleutils.c printed the ROW() without it,

    row(1, null::int)::record = row(1, null::int)::record

was printed as ROW(1, NULL::integer) = ROW(1, NULL::integer), which
gives NULL instead of true, and a comparison with a scalar subquery
did not read back, failing with "subquery has too few columns".

When coerce_type() is asked for an explicit cast of a RowExpr to its
own type, mark a copy of the RowExpr as COERCE_EXPLICIT_CAST, so that
the cast is printed.  This covers objects created after this change
only.
---
 src/backend/parser/parse_coerce.c      | 17 ++++++++++++++++-
 src/test/regress/expected/rowtypes.out | 21 ++++++++++-----------
 src/test/regress/sql/rowtypes.sql      |  3 +--
 3 files changed, 27 insertions(+), 14 deletions(-)

diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index d3240f4b265..22cb4be8cac 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -166,7 +166,22 @@ coerce_type(ParseState *pstate, Node *node,
 	if (targetTypeId == inputTypeId ||
 		node == NULL)
 	{
-		/* no conversion needed */
+		/*
+		 * No conversion needed.  But an explicit cast of a ROW() changes how
+		 * an operator on it is parsed, so record it in the RowExpr, else
+		 * ruleutils.c prints a bare ROW() that reads back as a row
+		 * comparison.  Copy the node, since we must not modify the input.
+		 */
+		if (node != NULL && IsA(node, RowExpr) &&
+			cformat == COERCE_EXPLICIT_CAST &&
+			((RowExpr *) node)->row_format != COERCE_EXPLICIT_CAST)
+		{
+			RowExpr    *rowexpr = makeNode(RowExpr);
+
+			memcpy(rowexpr, node, sizeof(RowExpr));
+			rowexpr->row_format = COERCE_EXPLICIT_CAST;
+			return (Node *) rowexpr;
+		}
 		return node;
 	}
 	if (targetTypeId == ANYOID ||
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 16a735df0ba..6f8b53a6ea3 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1413,18 +1413,18 @@ DROP TABLE compositetable;
 CREATE VIEW rowtypes_cmp_v1 AS
 SELECT (ROW(1, NULL::int)::record = ROW(1, NULL::int)::record) AS r;
 SELECT pg_get_viewdef('rowtypes_cmp_v1'::regclass);
-                        pg_get_viewdef                         
----------------------------------------------------------------
-  SELECT (ROW(1, NULL::integer) = ROW(1, NULL::integer)) AS r;
+                                pg_get_viewdef                                 
+-------------------------------------------------------------------------------
+  SELECT (ROW(1, NULL::integer)::record = ROW(1, NULL::integer)::record) AS r;
 (1 row)
 
 -- Nor as a row comparison with the columns of a scalar subquery
 CREATE VIEW rowtypes_cmp_v2 AS
 SELECT (ROW(1, NULL::int)::record = (SELECT ROW(1, NULL::int)::record)) AS r;
 SELECT pg_get_viewdef('rowtypes_cmp_v2'::regclass);
-                                  pg_get_viewdef                                  
-----------------------------------------------------------------------------------
-  SELECT (ROW(1, NULL::integer) = ( SELECT ROW(1, NULL::integer) AS "row")) AS r;
+                                          pg_get_viewdef                                          
+--------------------------------------------------------------------------------------------------
+  SELECT (ROW(1, NULL::integer)::record = ( SELECT ROW(1, NULL::integer)::record AS "row")) AS r;
 (1 row)
 
 -- The parser expands a whole-row reference to a JOIN USING alias into a ROW()
@@ -1469,12 +1469,11 @@ CREATE TABLE rowtypes_cmp_g (a int,
     (ROW(a, NULL::int)::record = ROW(a, NULL::int)::record) STORED);
 SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
   WHERE adrelid = 'rowtypes_cmp_g'::regclass;
-                   pg_get_expr                   
--------------------------------------------------
- (ROW(a, NULL::integer) = ROW(a, NULL::integer))
+                           pg_get_expr                           
+-----------------------------------------------------------------
+ (ROW(a, NULL::integer)::record = ROW(a, NULL::integer)::record)
 (1 row)
 
 -- leave the objects around for the pg_upgrade test, except the ones that do
 -- not survive dump and restore yet
-DROP VIEW rowtypes_cmp_v1, rowtypes_cmp_v2, rowtypes_cmp_v3, rowtypes_cmp_v4;
-DROP TABLE rowtypes_cmp_g;
+DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index f09b7211987..bb4f3d124e0 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -604,5 +604,4 @@ SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
 
 -- leave the objects around for the pg_upgrade test, except the ones that do
 -- not survive dump and restore yet
-DROP VIEW rowtypes_cmp_v1, rowtypes_cmp_v2, rowtypes_cmp_v3, rowtypes_cmp_v4;
-DROP TABLE rowtypes_cmp_g;
+DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4;
-- 
2.54.0 (Apple Git-157)

