From 5557794efd95bbd9ff5a21f364ea3646cdbb35b8 Mon Sep 17 00:00:00 2001 From: Henson Choi 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)