From ecba17b43204cb4cfd03185a4e436bbfde54f6a8 Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Fri, 13 Nov 2026 21:22:09 +0900 Subject: [PATCH v5 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 record(row(x, null::int)) = record(row(1, null::int)), the same cast written as a function call, in the target list or in WHERE printed as ROW(x, NULL::integer) = ROW(1, NULL::integer), which in WHERE filters out the row with x = 1 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 | 86 ++++++++++++++++++++++++++ src/test/regress/sql/rowtypes.sql | 53 ++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index c402160d488..9d43e75d584 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -1580,3 +1580,89 @@ SELECT expanded_record_field_text(ROW(5, 'field')::expanded_comp, 1), (1 row) DROP TYPE expanded_comp; +-- 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) + +-- Nor when the cast to record is written as a function call, in the target +-- list or in WHERE +CREATE VIEW rowtypes_cmp_v5 AS +SELECT x, (record(ROW(x, NULL::int)) = record(ROW(1, NULL::int))) AS r + FROM rowtypes_cmp_a + WHERE record(ROW(x, NULL::int)) = record(ROW(1, NULL::int)); +SELECT pg_get_viewdef('rowtypes_cmp_v5'::regclass); + pg_get_viewdef +---------------------------------------------------------- + SELECT x, + + (ROW(x, NULL::integer) = ROW(1, NULL::integer)) AS r+ + FROM rowtypes_cmp_a + + WHERE (ROW(x, NULL::integer) = ROW(1, NULL::integer)); +(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, + rowtypes_cmp_v5; +DROP TABLE rowtypes_cmp_g; diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql index 5aef9f5a3c3..8662e15f1b9 100644 --- a/src/test/regress/sql/rowtypes.sql +++ b/src/test/regress/sql/rowtypes.sql @@ -714,3 +714,56 @@ SELECT expanded_record_field_text(ROW(5, 'field')::expanded_comp, 1), expanded_record_field_text(ROW(5, 'field')::expanded_comp, 2); DROP TYPE expanded_comp; + +-- 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); + +-- Nor when the cast to record is written as a function call, in the target +-- list or in WHERE +CREATE VIEW rowtypes_cmp_v5 AS +SELECT x, (record(ROW(x, NULL::int)) = record(ROW(1, NULL::int))) AS r + FROM rowtypes_cmp_a + WHERE record(ROW(x, NULL::int)) = record(ROW(1, NULL::int)); +SELECT pg_get_viewdef('rowtypes_cmp_v5'::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, + rowtypes_cmp_v5; +DROP TABLE rowtypes_cmp_g; -- 2.54.0 (Apple Git-157)