From 5557794efd95bbd9ff5a21f364ea3646cdbb35b8 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Wed, 21 Oct 2026 10:57:26 +0900
Subject: [PATCH v3 1/3] Add tests for deparsing record comparisons of ROW()s

The parser reads "ROW(..) op ROW(..)" as a column-by-column row
comparison and "ROW(..) op (SELECT ..)" as a row comparison sublink.
ruleutils.c prints an operator on record values between such operands
in that form, so it is read back as something else:

    row(1, null::int)::record = row(1, null::int)::record
        printed as ROW(1, NULL::integer) = ROW(1, NULL::integer),
        which gives NULL instead of true
    row(1, null::int)::record = (select row(1, null::int)::record)
        ERROR:  subquery has too few columns
    j.* = (select row(1)), for a JOIN USING alias j
        ERROR:  operator does not exist: integer = record
    s1 =#= s2, for an operator on a named composite type
        ERROR:  operator does not exist: integer =#= integer

Add a view for each case and a generated column, and show what
pg_get_viewdef() and pg_get_expr() print for them.  The expected
output shows the current behavior.  The following commits fix it and
leave the objects in place, so that the pg_upgrade test dumps and
restores them.
---
 src/test/regress/expected/rowtypes.out | 70 ++++++++++++++++++++++++++
 src/test/regress/sql/rowtypes.sql      | 44 ++++++++++++++++
 2 files changed, 114 insertions(+)

diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 956bc2d02fc..16a735df0ba 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1408,3 +1408,73 @@ ERROR:  column "oid" not found in data type compositetable
 LINE 1: SELECT (NULL::compositetable).oid;
                 ^
 DROP TABLE compositetable;
+-- A record operator between ROW()s cast to record must not be read back as a
+-- row comparison, which gives NULL here
+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;
+(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;
+(1 row)
+
+-- The parser expands a whole-row reference to a JOIN USING alias into a ROW()
+CREATE TABLE rowtypes_cmp_a (x int);
+CREATE TABLE rowtypes_cmp_b (x int);
+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                                     +
+      JOIN rowtypes_cmp_b USING (x) AS j);
+(1 row)
+
+-- So does coercing a whole-row reference to an operator's composite input
+-- type
+CREATE TYPE rowtypes_cmp_t AS (a int, b int);
+CREATE FUNCTION rowtypes_cmp_eq(rowtypes_cmp_t, rowtypes_cmp_t) RETURNS bool
+  LANGUAGE sql IMMUTABLE
+  AS 'SELECT $1.a IS NOT DISTINCT FROM $2.a AND $1.b IS NOT DISTINCT FROM $2.b';
+CREATE OPERATOR =#= (LEFTARG = rowtypes_cmp_t, RIGHTARG = rowtypes_cmp_t,
+  FUNCTION = rowtypes_cmp_eq);
+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?",                                 +
+             NULL::integer AS int4) s2;
+(1 row)
+
+-- A generated column reaches the same code through pg_get_expr()
+CREATE TABLE rowtypes_cmp_g (a int,
+  r bool GENERATED ALWAYS AS
+    (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))
+(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;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index 174b062144a..f09b7211987 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -562,3 +562,47 @@ SELECT (NULL::compositetable).a;
 SELECT (NULL::compositetable).oid;
 
 DROP TABLE compositetable;
+
+-- A record operator between ROW()s cast to record must not be read back as a
+-- row comparison, which gives NULL here
+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);
+
+-- 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);
+
+-- The parser expands a whole-row reference to a JOIN USING alias into a ROW()
+CREATE TABLE rowtypes_cmp_a (x int);
+CREATE TABLE rowtypes_cmp_b (x int);
+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);
+
+-- So does coercing a whole-row reference to an operator's composite input
+-- type
+CREATE TYPE rowtypes_cmp_t AS (a int, b int);
+CREATE FUNCTION rowtypes_cmp_eq(rowtypes_cmp_t, rowtypes_cmp_t) RETURNS bool
+  LANGUAGE sql IMMUTABLE
+  AS 'SELECT $1.a IS NOT DISTINCT FROM $2.a AND $1.b IS NOT DISTINCT FROM $2.b';
+CREATE OPERATOR =#= (LEFTARG = rowtypes_cmp_t, RIGHTARG = rowtypes_cmp_t,
+  FUNCTION = rowtypes_cmp_eq);
+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);
+
+-- A generated column reaches the same code through pg_get_expr()
+CREATE TABLE rowtypes_cmp_g (a int,
+  r bool GENERATED ALWAYS AS
+    (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;
+
+-- 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;
-- 
2.54.0 (Apple Git-157)

