Re: BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay

From: Andrey Rachitskiy <pl0h0yp1(at)gmail(dot)com>
To: yankairong(at)ruc(dot)edu(dot)cn, pgsql-bugs(at)lists(dot)postgresql(dot)org
Subject: Re: BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay
Date: 2026-08-27 21:20:50
Message-ID: CAB8bMitMZ5KOqMOEzC+DOPc1Th+HQ=3cbG_S-ovLC4P+q3SNRg@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

ср, 27 мая 2026 г. в 18:49, PG Bug reporting form <noreply(at)postgresql(dot)org>:

> The following bug has been logged on the website:
>
> Bug reference: 19498
> Logged by: muyehu
> Email address: yankairong(at)ruc(dot)edu(dot)cn
> PostgreSQL version: 18.4
> Operating system: ubuntu22.04
> Description:
>
> PostgreSQL version: 18.4
> Platform: x86_64-pc-linux-gnu, gcc 11.4.0
> Component: parser / rowtypes / expression analysis
>
> Hi,
>
> I found an inconsistency in field expansion of anonymous ROW() values.
>
> A direct relay of an anonymous composite value allows field extraction:
>
> SELECT (c).f1
> FROM (SELECT row(1, 2) AS c) s;
>
> This returns:
>
> f1
> ----
> 1
>
> However, if the same anonymous composite value is first relayed through a
> scalar subquery and then exposed as an output column, later field expansion
> fails with:
>
> ERROR: record type has not been registered
>
> The failure is triggered by operations that need field-level tuple
> descriptor information, such as:
>
> - (c).f1
> - f1(c)
> - (c).*
> - INSERT ... SELECT (c).*
>
> At the same time, the relayed value itself still appears to be preserved:
> operations such as row_to_json(c), to_jsonb(c), and c::text work on the
> same
> query shape. This suggests that the issue is not that the anonymous record
> value is lost, but that its tuple descriptor cannot be recovered later
> during field expansion.
>
> Minimal reproduction:
>
> BEGIN;
>
> -- Direct relay works.
> SELECT (c).f1
> FROM (SELECT row(1, 2) AS c) s;
>
> -- Scalar subquery relay fails.
> SELECT (c).f1
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> -- Star expansion fails in the same way.
> SELECT (c).*
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> ROLLBACK;
>
> Actual result for the second and third queries:
>
> ERROR: record type has not been registered
>
> Expected behavior:
>
> Since the direct relay of the same anonymous ROW() value allows field
> expansion, I expected the scalar-subquery relay to preserve enough row
> descriptor information for the same field expansion, or at least to behave
> consistently with the direct relay case.
>
> The same behavior can also be reproduced with a CTE relay:
>
> WITH cte(c) AS MATERIALIZED (
> SELECT row(1, 2)
> ),
> pass1(c) AS (
> SELECT (SELECT z.c FROM (SELECT cte.c) z)
> FROM cte
> )
> SELECT (c).f1
> FROM pass1;
>
> This also fails with:
>
> ERROR: record type has not been registered
>
> The same relay shape also fails with functional field access:
>
> SELECT f1(c)
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> and with DML projection:
>
> CREATE TEMP TABLE t(f1 int, f2 int);
>
> INSERT INTO t
> SELECT (c).*
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> Both fail with:
>
> ERROR: record type has not been registered
>
> Adjacent operations that work:
>
> On the same scalar-subquery relay shape, the following operations succeed:
>
> SELECT row_to_json(c)
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> SELECT to_jsonb(c)
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> SELECT c::text
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> For example, c::text returns:
>
> (1,2)
>
> This suggests that the scalar subquery does relay the anonymous record
> value, but later field expansion cannot recover its descriptor.
>
> Possible relation to existing rowtype tests:
>
> This looks possibly related to previous rowtype /
> indirect-composite-reference issues, including bug #18077.
>
> The existing regression tests in src/test/regress/sql/rowtypes.sql cover
> several indirect composite reference paths. For example, this shape works:
>
> WITH cte(c) AS MATERIALIZED (SELECT row(1, 2)),
> cte2(c) AS (SELECT * FROM cte)
> SELECT (c).f1
> FROM cte2;
>
> But replacing the relay column with a scalar subquery returning the same
> anonymous composite value fails:
>
> WITH cte(c) AS MATERIALIZED (SELECT row(1, 2)),
> cte2(c) AS (
> SELECT (SELECT c FROM cte) AS c
> )
> SELECT (c).f1
> FROM cte2;
>
> So the remaining uncovered shape seems to be:
>
> anonymous ROW()
> -> scalar subquery returning record
> -> exposed as an upper query output column
> -> later field expansion using (c).f1 / (c).*
>
> Code inspection note:
>
> From code inspection, this may be related to how expandRecordVariable() in
> src/backend/parser/parse_target.c follows RTE_SUBQUERY / RTE_CTE output
> expressions.
>
> It appears to recurse through some simple Var-based relay paths, but when
> the output expression is a SubLink returning record, it may fall back to
> get_expr_result_tupdesc(). For an anonymous record value, that fallback
> does
> not appear to recover the tuple descriptor and raises:
>
> ERROR: record type has not been registered
>
> A parallel path may exist in ParseComplexProjection() in
> src/backend/parser/parse_func.c, which would explain why f1(c) fails
> similarly to (c).f1.
>
> Suggested regression test:
>
> A small regression test near the existing rowtype / bug #18077 cases might
> be enough:
>
> -- scalar subquery relay of anonymous record followed by field expansion
> WITH cte(c) AS MATERIALIZED (SELECT row(1, 2)),
> cte2(c) AS (
> SELECT (SELECT c FROM cte) AS c
> )
> SELECT (c).f1
> FROM cte2;
>
> -- scalar subquery relay followed by star expansion
> SELECT (c).*
> FROM (
> SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
> ) s;
>
> Best regards,
> muyehu
>
>
>
>
>
Hi,

Thanks for the report.

I reproduced the issue on the master branch.

Field expansion of an anonymous ROW() value works when the composite is
relayed as a plain subquery column, but fails after a scalar SubLink relay:

```
SELECT (c).f1
FROM (SELECT row(1, 2) AS c) s; -- works

SELECT (c).f1
FROM (
SELECT (SELECT z.c FROM (SELECT row(1, 2) AS c) z) AS c
) s; -- ERROR: record type has not been
registered
```

The same error hits (c).*, f1(c), INSERT ... SELECT (c).*, and a CTE that
re-exports the column as (SELECT c FROM cte). On that shape row_to_json(c),
to_jsonb(c), and c::text still work, so the RECORD value is preserved.
Only parse-time tupdesc recovery for field expansion is lost.

Named composites through the same SubLink shape already work. A plain Var
relay through a CTE (bug #18077) also works. The gap is specifically an
anonymous RECORD whose defining expression is an EXPR_SUBLINK.

Drill-down already followed Var relays through subquery and CTE RTEs.
For a SubLink it fell through to get_expr_result_tupdesc(), which cannot
resolve anonymous RECORD.

The patch generalizes that path as
expandRecordExpr(Node *): peel EXPR_SUBLINK with a nested ParseState, then
continue as for Vars. get_name_for_var_field() peels the same SubLinks for
deparsing.

--
Regards,
Rachitskiy Andrey

Attachment Content-Type Size
0001-Fix-field-expansion-of-anonymous-RECORD-via-scalar-S.patch text/x-patch 21.7 KB

In response to

Browse pgsql-bugs by date

  From Date Subject
Next Message Andrey Rachitskiy 2026-08-27 21:26:52 Re: to_char() on integer with V format silently wraps on overflow
Previous Message Bharath Rupireddy 2026-08-27 21:15:00 Re: autovacuum: automatically propagate updated parameters