From 8f43852c685597cc83b5778576dc40f5e5cb01e7 Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Wed, 21 Oct 2026 10:58:16 +0900 Subject: [PATCH v3 2/3] Record explicit casts of ROW() constructors in the parse tree A cast of a ROW() to its own type, as in ROW(1, 2)::record, does nothing, so coerce_type() returned the RowExpr unchanged and the cast left no trace in the tree. The cast matters to the parser, though: it keeps an operator on the ROW() from being read as a row comparison. Since ruleutils.c printed the ROW() without it, row(1, null::int)::record = row(1, null::int)::record was printed as ROW(1, NULL::integer) = ROW(1, NULL::integer), which gives NULL instead of true, and a comparison with a scalar subquery did not read back, failing with "subquery has too few columns". When coerce_type() is asked for an explicit cast of a RowExpr to its own type, mark a copy of the RowExpr as COERCE_EXPLICIT_CAST, so that the cast is printed. This covers objects created after this change only. --- src/backend/parser/parse_coerce.c | 17 ++++++++++++++++- src/test/regress/expected/rowtypes.out | 21 ++++++++++----------- src/test/regress/sql/rowtypes.sql | 3 +-- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index d3240f4b265..22cb4be8cac 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -166,7 +166,22 @@ coerce_type(ParseState *pstate, Node *node, if (targetTypeId == inputTypeId || node == NULL) { - /* no conversion needed */ + /* + * No conversion needed. But an explicit cast of a ROW() changes how + * an operator on it is parsed, so record it in the RowExpr, else + * ruleutils.c prints a bare ROW() that reads back as a row + * comparison. Copy the node, since we must not modify the input. + */ + if (node != NULL && IsA(node, RowExpr) && + cformat == COERCE_EXPLICIT_CAST && + ((RowExpr *) node)->row_format != COERCE_EXPLICIT_CAST) + { + RowExpr *rowexpr = makeNode(RowExpr); + + memcpy(rowexpr, node, sizeof(RowExpr)); + rowexpr->row_format = COERCE_EXPLICIT_CAST; + return (Node *) rowexpr; + } return node; } if (targetTypeId == ANYOID || diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 16a735df0ba..6f8b53a6ea3 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -1413,18 +1413,18 @@ DROP TABLE compositetable; 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; + pg_get_viewdef +------------------------------------------------------------------------------- + SELECT (ROW(1, NULL::integer)::record = ROW(1, NULL::integer)::record) 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; + pg_get_viewdef +-------------------------------------------------------------------------------------------------- + SELECT (ROW(1, NULL::integer)::record = ( SELECT ROW(1, NULL::integer)::record AS "row")) AS r; (1 row) -- The parser expands a whole-row reference to a JOIN USING alias into a ROW() @@ -1469,12 +1469,11 @@ CREATE TABLE rowtypes_cmp_g (a int, (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)) + pg_get_expr +----------------------------------------------------------------- + (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_v1, rowtypes_cmp_v2, rowtypes_cmp_v3, rowtypes_cmp_v4; -DROP TABLE rowtypes_cmp_g; +DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4; diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql index f09b7211987..bb4f3d124e0 100644 --- a/src/test/regress/sql/rowtypes.sql +++ b/src/test/regress/sql/rowtypes.sql @@ -604,5 +604,4 @@ SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef -- 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; +DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4; -- 2.54.0 (Apple Git-157)