Re: Anonymous record member access

From: Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>
To: "David G(dot) Johnston" <david(dot)g(dot)johnston(at)gmail(dot)com>
Cc: "kengruven(at)gmail(dot)com" <kengruven(at)gmail(dot)com>, "pgsql-docs(at)lists(dot)postgresql(dot)org" <pgsql-docs(at)lists(dot)postgresql(dot)org>
Subject: Re: Anonymous record member access
Date: 2026-09-15 14:19:30
Message-ID: 274668.1789481970@sss.pgh.pa.us
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-docs

"David G. Johnston" <david(dot)g(dot)johnston(at)gmail(dot)com> writes:
> On Monday, September 14, 2026, PG Doc comments form <noreply(at)postgresql(dot)org>
> wrote:
>> To summarize, things I don't understand from the docs:
>> - where exactly the .f1 notation is documented

> It probably isn’t documented behavior and thus should not be relied upon.

Yeah, it's not documented in any user-facing place, AFAICS. That's
because it's not a feature so much as a collection of legacy behaviors.

>> - where/why it's not allowed to be used
>> - how/why a ROW changes behavior when returned from a FUNCTION

> I suspect these are just boundary issues that would be considered bugs if
> this entire thing were considered documented behavior.

The key thing to understand is static versus dynamic typing.
If foo() is declared to return a named composite type, then
when you write

SELECT (foo(...)).x

the parser can look up the composite type and identify that yes,
x is a column of that type, it has position n and data type so-and-so,
and then it knows the result type of that expression and can continue
parsing. But if foo() is declared to return record then no such
information is available, and the parser must throw up its hands.
The function might return one thing today and something entirely
different tomorrow.

ROW() constructors do need to assign column names in the
anonymous record type they construct, and what they use is
indeed f1,f2,etc. But for most purposes in SQL you can't
see that because SQL is mostly a statically-typed language,
so it can't do much with an anonymous record value other
than pass it around. There are functions like to_json()
that are declared to accept type record, which means that
they can work on any composite type whatever. They use
below-SQL-level implementation details to find out what are
the column names and datatypes inside whatever they're handed.

You can do some of this stuff in plpgsql or other PLs, which
are less resolute about being statically typed than the main
SQL grammar. There are also behaviors that are just plain
warts, such as your example

SELECT (ROW(3,4,5)).f1; -- returns 3

On what I've been telling you, that ought to fail. It does
work, because the parser logic that looks up the composite
type of the left-hand side of a field accessor dot has a
special case for when that left-hand side is exactly a RowExpr.
(I think yours truly might be responsible for that, but it's
still a wart.)

So it's all pretty messy and no one has cared to try to make it
coherent enough to be document-able. I join with David in
recommending that you avoid relying on this. If we ever did
try to make it coherent, we'd likely elect to break some
behaviors that happen to work today.

regards, tom lane

In response to

Browse pgsql-docs by date

  From Date Subject
Next Message Bruce Momjian 2026-09-15 21:28:31 Re: 4.2.7. Aggregate Expressions
Previous Message David G. Johnston 2026-09-15 12:29:28 Re: Anonymous record member access