From fb1daf83b73d357714a204840b2ed8ec7a3203ce Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Fri, 13 Nov 2026 21:22:09 +0900 Subject: [PATCH v5 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 or record(ROW(1, 2)), 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, written as a cast or as a function call, 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 | 19 +++++++++++++- src/test/regress/expected/rowtypes.out | 34 ++++++++++++-------------- src/test/regress/sql/rowtypes.sql | 4 +-- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index d3240f4b265..e90ead6095b 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -166,7 +166,24 @@ coerce_type(ParseState *pstate, Node *node, if (targetTypeId == inputTypeId || node == NULL) { - /* no conversion needed */ + /* + * No conversion needed. But an explicit cast of a ROW(), also one + * written as a function call, 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 || + cformat == COERCE_EXPLICIT_CALL) && + ((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 9d43e75d584..f05638af33e 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -1585,18 +1585,18 @@ DROP TYPE expanded_comp; 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() @@ -1642,12 +1642,12 @@ 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)); + pg_get_viewdef +-------------------------------------------------------------------------- + SELECT x, + + (ROW(x, NULL::integer)::record = ROW(1, NULL::integer)::record) AS r+ + FROM rowtypes_cmp_a + + WHERE (ROW(x, NULL::integer)::record = ROW(1, NULL::integer)::record); (1 row) -- A generated column reaches the same code through pg_get_expr() @@ -1656,13 +1656,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, - rowtypes_cmp_v5; -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 8662e15f1b9..38e2bdef844 100644 --- a/src/test/regress/sql/rowtypes.sql +++ b/src/test/regress/sql/rowtypes.sql @@ -764,6 +764,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, - rowtypes_cmp_v5; -DROP TABLE rowtypes_cmp_g; +DROP VIEW rowtypes_cmp_v3, rowtypes_cmp_v4; -- 2.54.0 (Apple Git-157)