From 3badebaadca7979fe9e69093779a28c005c6a65f Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Wed, 21 Oct 2026 10:59:32 +0900
Subject: [PATCH v3 3/3] Show casts of ROW() constructors made by the parser

The parser makes a ROW() of its own for a whole-row reference to a
JOIN USING alias, which lists only some columns of the join, and when
it coerces a whole-row reference of type record to an operator's
composite input type.  Both were marked as implicit, so ruleutils.c
printed a bare ROW(), and an operator on it was read back as a row
comparison, which failed:

    j.* = (select row(1)), for a JOIN USING alias j
        printed as ROW(a.x) = ( SELECT ROW(1) AS "row")
        ERROR:  operator does not exist: integer = record
    s1 =#= s2, for an operator on a named composite type and
    subqueries s1 and s2 with columns a and b
        printed as ROW(s1.a, s1.b) =#= ROW(s2.a, s2.b)
        ERROR:  operator does not exist: integer =#= integer

Mark the ROW() for a JOIN USING alias as COERCE_EXPLICIT_CAST, so that
it is printed with ::record wherever it is used.  In make_op(), when
the operands would take the form that transformAExprOp reads as a row
comparison, coerce the ROW() operands with COERCE_EXPLICIT_CAST, so
that they are printed with a cast to the operator's input type; other
coercions of records to composite types, as for function arguments,
are not affected.  This covers objects created after this change only.
---
 src/backend/parser/parse_expr.c        |  3 +-
 src/backend/parser/parse_oper.c        | 54 ++++++++++++++++++++++++++
 src/test/regress/expected/rowtypes.out | 26 ++++++-------
 src/test/regress/sql/rowtypes.sql      |  4 +-
 4 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 05a6b72d4c9..6f76bb4a99b 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2695,7 +2695,8 @@ transformWholeRowRef(ParseState *pstate, ParseNamespaceItem *nsitem,
 		rowexpr->args = list_truncate(fields,
 									  list_length(nsitem->p_names->colnames));
 		rowexpr->row_typeid = RECORDOID;
-		rowexpr->row_format = COERCE_IMPLICIT_CAST;
+		/* show a cast, as a bare ROW() reads back as a row comparison */
+		rowexpr->row_format = COERCE_EXPLICIT_CAST;
 		rowexpr->colnames = copyObject(nsitem->p_names->colnames);
 		rowexpr->location = location;
 
diff --git a/src/backend/parser/parse_oper.c b/src/backend/parser/parse_oper.c
index dc0f047ca25..baadf82cb14 100644
--- a/src/backend/parser/parse_oper.c
+++ b/src/backend/parser/parse_oper.c
@@ -809,6 +809,60 @@ make_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree,
 						"internal"),
 				 parser_errposition(pstate, location)));
 
+	/*
+	 * transformAExprOp reads "ROW(..) op ROW(..)" and "ROW(..) op (SELECT
+	 * ..)" as a row comparison.  An operand gets into that form here when a
+	 * whole-row value of type record is coerced to the operator's composite
+	 * input type, which expands it into a ROW() (see
+	 * coerce_record_to_complex).  Show that coercion as a cast, else the
+	 * operator is printed in a form that reads back as a row comparison.  The
+	 * operands coerced here get their actual type set to the declared one, so
+	 * that make_fn_arguments does not coerce them again.
+	 */
+	if (nargs == 2)
+	{
+		bool		lrow;
+		bool		rrow;
+
+		if (actual_arg_types[0] == declared_arg_types[0])
+			lrow = IsA(ltree, RowExpr);
+		else
+			lrow = (actual_arg_types[0] == RECORDOID &&
+					ISCOMPLEX(declared_arg_types[0]));
+		if (actual_arg_types[1] == declared_arg_types[1])
+			rrow = IsA(rtree, RowExpr);
+		else
+			rrow = (actual_arg_types[1] == RECORDOID &&
+					ISCOMPLEX(declared_arg_types[1]));
+
+		if (lrow &&
+			(rrow ||
+			 (IsA(rtree, SubLink) &&
+			  ((SubLink *) rtree)->subLinkType == EXPR_SUBLINK)))
+		{
+			if (actual_arg_types[0] != declared_arg_types[0])
+			{
+				linitial(args) = coerce_type(pstate, ltree,
+											 actual_arg_types[0],
+											 declared_arg_types[0], -1,
+											 COERCION_IMPLICIT,
+											 COERCE_EXPLICIT_CAST,
+											 -1);
+				actual_arg_types[0] = declared_arg_types[0];
+			}
+			if (rrow && actual_arg_types[1] != declared_arg_types[1])
+			{
+				lsecond(args) = coerce_type(pstate, rtree,
+											actual_arg_types[1],
+											declared_arg_types[1], -1,
+											COERCION_IMPLICIT,
+											COERCE_EXPLICIT_CAST,
+											-1);
+				actual_arg_types[1] = declared_arg_types[1];
+			}
+		}
+	}
+
 	/* perform the necessary typecasting of arguments */
 	make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types);
 
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 6f8b53a6ea3..d3679bad778 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1434,11 +1434,11 @@ CREATE VIEW rowtypes_cmp_v3 AS
 SELECT x, (j.* = (SELECT ROW(1))) AS r
   FROM rowtypes_cmp_a JOIN rowtypes_cmp_b USING (x) AS j;
 SELECT pg_get_viewdef('rowtypes_cmp_v3'::regclass);
-                        pg_get_viewdef                        
---------------------------------------------------------------
-  SELECT rowtypes_cmp_a.x,                                   +
-     (ROW(rowtypes_cmp_a.x) = ( SELECT ROW(1) AS "row")) AS r+
-    FROM (rowtypes_cmp_a                                     +
+                            pg_get_viewdef                            
+----------------------------------------------------------------------
+  SELECT rowtypes_cmp_a.x,                                           +
+     (ROW(rowtypes_cmp_a.x)::record = ( SELECT ROW(1) AS "row")) AS r+
+    FROM (rowtypes_cmp_a                                             +
       JOIN rowtypes_cmp_b USING (x) AS j);
 (1 row)
 
@@ -1454,12 +1454,12 @@ CREATE VIEW rowtypes_cmp_v4 AS
 SELECT (s1 =#= s2) AS r
   FROM (SELECT 1 AS a, NULL::int AS b) s1, (SELECT 1, NULL::int) s2;
 SELECT pg_get_viewdef('rowtypes_cmp_v4'::regclass);
-                         pg_get_viewdef                         
-----------------------------------------------------------------
-  SELECT (ROW(s1.a, s1.b) =#= ROW(s2."?column?", s2.int4)) AS r+
-    FROM ( SELECT 1 AS a,                                      +
-             NULL::integer AS b) s1,                           +
-     ( SELECT 1 AS "?column?",                                 +
+                                         pg_get_viewdef                                         
+------------------------------------------------------------------------------------------------
+  SELECT (ROW(s1.a, s1.b)::rowtypes_cmp_t =#= ROW(s2."?column?", s2.int4)::rowtypes_cmp_t) AS r+
+    FROM ( SELECT 1 AS a,                                                                      +
+             NULL::integer AS b) s1,                                                           +
+     ( SELECT 1 AS "?column?",                                                                 +
              NULL::integer AS int4) s2;
 (1 row)
 
@@ -1474,6 +1474,4 @@ SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
  (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_v3, rowtypes_cmp_v4;
+-- leave the objects around for the pg_upgrade test
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index bb4f3d124e0..3bf0948d337 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -602,6 +602,4 @@ CREATE TABLE rowtypes_cmp_g (a int,
 SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
   WHERE adrelid = 'rowtypes_cmp_g'::regclass;
 
--- leave the objects around for the pg_upgrade test, except the ones that do
--- not survive dump and restore yet
-DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4;
+-- leave the objects around for the pg_upgrade test
-- 
2.54.0 (Apple Git-157)

