agora inbox for pgsql-bugs@postgresql.org
help / color / mirror / Atom feedBUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay
2+ messages / 2 participants
[nested] [flat]
* BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay
@ 2026-05-27 11:50 PG Bug reporting form <noreply@postgresql.org>
2026-08-27 21:20 ` Re: BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 1 reply; 2+ messages in thread
From: PG Bug reporting form @ 2026-05-27 11:50 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org; +Cc: yankairong@ruc.edu.cn
The following bug has been logged on the website:
Bug reference: 19498
Logged by: muyehu
Email address: yankairong@ruc.edu.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:
^ permalink raw reply [nested|flat] 2+ messages in thread
* Re: BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay
2026-05-27 11:50 BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay PG Bug reporting form <noreply@postgresql.org>
@ 2026-08-27 21:20 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 0 replies; 2+ messages in thread
From: Andrey Rachitskiy @ 2026-08-27 21:20 UTC (permalink / raw)
To: yankairong@ruc.edu.cn; pgsql-bugs@lists.postgresql.org
ср, 27 мая 2026 г. в 18:49, PG Bug reporting form <noreply@postgresql.org>:
> The following bug has been logged on the website:
>
> Bug reference: 19498
> Logged by: muyehu
> Email address: yankairong@ruc.edu.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
^ permalink raw reply [nested|flat] 2+ messages in thread
end of thread, other threads:[~2026-08-27 21:20 UTC | newest]
Thread overview: 2+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-27 11:50 BUG #19498: Anonymous ROW() field expansion fails after scalar subquery relay PG Bug reporting form <noreply@postgresql.org>
2026-08-27 21:20 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox