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