heapam_relation_toast_am() returns the wrong AM for a wrapped heap AM

Started by Andrew Dunstan2 days ago5 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliestests failedCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253516
psql -h localhost -U postgres

Built from patchset v1 (message #1), August 23, 2026 at 01:38 PM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253516_1 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253516_1 && git checkout t253516_1

Patchset v1 (message #1) is on t253516_1

Jump to latest
#1Andrew Dunstan
andrew@dunslane.net

Hi,

heapam_relation_toast_am() returns rel->rd_rel->relam instead of the
literal heap AM oid, on the assumption the two are always equal since
it's only meant to run for relations that are themselves heap. That
breaks for a table AM that copies heap's whole TableAmRoutine (via
GetHeapamTableAmRoutine()) and overrides only a few callbacks -- a
pattern heap_getnext()'s own identity check explicitly anticipates,
per its comment about allowing "regression tests that create another
AM reusing the heap handler." For such an AM, rel->rd_rel->relam is
its own oid, so NewRelationCreateToastTable() creates the TOAST table
with that AM too, and building its chunk_id/chunk_seq index then
fails in heap_getnext(), which requires rd_tableam to be literally
GetHeapamTableAmRoutine(): "only heap AM is supported" for any such
AM as soon as a table needs a TOAST table.

Fix by returning the literal HEAP_TABLE_AM_OID, which is what the
function's own comment already claims it does ("TOAST tables for
heap relations are just heap relations").

Found while testing an AM that wraps heap's storage callbacks for an
unrelated patch [1]/messages/by-id/ea1c4d33-0780-473c-96dc-1468cf733a04@dunslane.net.

Not sure if we would call this a bug or just a limitation.

cheers

andrew

[1]: /messages/by-id/ea1c4d33-0780-473c-96dc-1468cf733a04@dunslane.net
/messages/by-id/ea1c4d33-0780-473c-96dc-1468cf733a04@dunslane.net

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

Attachments:

t253516_1
0001-Fix-heapam_relation_toast_am-to-return-the-literal-h.patchtext/x-patch; charset=UTF-8; name=0001-Fix-heapam_relation_toast_am-to-return-the-literal-h.patchDownload+13-3
#2Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Andrew Dunstan (#1)
Re: heapam_relation_toast_am() returns the wrong AM for a wrapped heap AM

On 2026-Aug-21, Andrew Dunstan wrote:

heapam_relation_toast_am() returns rel->rd_rel->relam instead of the
literal heap AM oid, on the assumption the two are always equal since
it's only meant to run for relations that are themselves heap. That
breaks for a table AM that copies heap's whole TableAmRoutine (via
GetHeapamTableAmRoutine()) and overrides only a few callbacks -- a
pattern heap_getnext()'s own identity check explicitly anticipates,
per its comment about allowing "regression tests that create another
AM reusing the heap handler." For such an AM, rel->rd_rel->relam is
its own oid, so NewRelationCreateToastTable() creates the TOAST table
with that AM too, and building its chunk_id/chunk_seq index then
fails in heap_getnext(), which requires rd_tableam to be literally
GetHeapamTableAmRoutine(): "only heap AM is supported" for any such
AM as soon as a table needs a TOAST table.

Fix by returning the literal HEAP_TABLE_AM_OID, which is what the
function's own comment already claims it does ("TOAST tables for
heap relations are just heap relations").

I'm not sure that this patch is correct. The heapam_relation_toast_am
function is the implementation for the relation_toast_am callback in the
heapam table AM. So this function is specific to heapam, and other
table AMs should have their own if they want to have different behavior.

The function is really small, so if a table AM that's not heapam can
very easily set up its own callback function that returns
HEAP_TABLE_AM_OID to get a regular heapam TOAST table, right?

So "a table AM that [copies the whole heap TableAmRoutine and overrides
only a handful of callbacks]" is Doing It Wrong by failing to also
override relation_toast_am.

(Whether it makes sense to return rd_rel->relam in a heapam-specific
function is something worth discussing, perhaps, but I don't think what
you propose is the right fix here. I think it may make more sense to
have a pg_class column that says what table AM the toast table uses.)

In any case, this comment:

@@ -2048,12 +2049,22 @@ heapam_relation_needs_toast_table(Relation rel)
}

/*
- * TOAST tables for heap relations are just heap relations.
+ * TOAST tables are always plain heap relations, regardless of the AM of
+ * the table they belong to.  Return the literal heap AM oid rather than
+ * rel->rd_rel->relam: those are only the same value when rel is itself a
+ * genuine heap relation.  A table AM that reuses this callback (e.g. by
+ * copying the whole heap TableAmRoutine and overriding only a handful of
+ * callbacks) is not itself heap, so returning rel->rd_rel->relam would
+ * create its TOAST table using that AM instead -- and storage-layer code
+ * that still calls heap_getnext() directly (see its comment) rejects any
+ * relation whose rd_tableam is not literally GetHeapamTableAmRoutine(),
+ * which fails as soon as anything scans that TOAST table, e.g. to build
+ * its chunk_id/chunk_seq index.
*/

would really need to be pared down to something a human would write.

Your commit message mentions review of a patch by Zsolt, but doesn't
provide a Discussion link to that discussion. I think it should.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/

#3Andres Freund
andres@anarazel.de
In reply to: Andrew Dunstan (#1)
Re: heapam_relation_toast_am() returns the wrong AM for a wrapped heap AM

Hi,

On 2026-08-21 09:20:07 -0400, Andrew Dunstan wrote:

heapam_relation_toast_am() returns rel->rd_rel->relam instead of the
literal heap AM oid, on the assumption the two are always equal since
it's only meant to run for relations that are themselves heap. That
breaks for a table AM that copies heap's whole TableAmRoutine (via
GetHeapamTableAmRoutine()) and overrides only a few callbacks -- a
pattern heap_getnext()'s own identity check explicitly anticipates,
per its comment about allowing "regression tests that create another
AM reusing the heap handler." For such an AM, rel->rd_rel->relam is
its own oid, so NewRelationCreateToastTable() creates the TOAST table
with that AM too, and building its chunk_id/chunk_seq index then
fails in heap_getnext(), which requires rd_tableam to be literally
GetHeapamTableAmRoutine(): "only heap AM is supported" for any such
AM as soon as a table needs a TOAST table.

Fix by returning the literal HEAP_TABLE_AM_OID, which is what the
function's own comment already claims it does ("TOAST tables for
heap relations are just heap relations").

I'm not following would that would be a good idea? Right now we can have a
separately registered table AM to write tests for non-default AMs without a
separately maintained AM. But with this that won't test the toast paths
anymore, because you make those to be a plain heapam, rather than the
simulacrum of a separate AM?

I certainly don't see how that would be a sane thing to backpatch. It's quite
possible that would break currently working code, no?

Greetings,

Andres Freund

#4Andrew Dunstan
andrew@dunslane.net
In reply to: Alvaro Herrera (#2)
Re: heapam_relation_toast_am() returns the wrong AM for a wrapped heap AM

On 2026-08-21 Fr 12:50 PM, Álvaro Herrera wrote:

On 2026-Aug-21, Andrew Dunstan wrote:

heapam_relation_toast_am() returns rel->rd_rel->relam instead of the
literal heap AM oid, on the assumption the two are always equal since
it's only meant to run for relations that are themselves heap. That
breaks for a table AM that copies heap's whole TableAmRoutine (via
GetHeapamTableAmRoutine()) and overrides only a few callbacks -- a
pattern heap_getnext()'s own identity check explicitly anticipates,
per its comment about allowing "regression tests that create another
AM reusing the heap handler." For such an AM, rel->rd_rel->relam is
its own oid, so NewRelationCreateToastTable() creates the TOAST table
with that AM too, and building its chunk_id/chunk_seq index then
fails in heap_getnext(), which requires rd_tableam to be literally
GetHeapamTableAmRoutine(): "only heap AM is supported" for any such
AM as soon as a table needs a TOAST table.

Fix by returning the literal HEAP_TABLE_AM_OID, which is what the
function's own comment already claims it does ("TOAST tables for
heap relations are just heap relations").

I'm not sure that this patch is correct. The heapam_relation_toast_am
function is the implementation for the relation_toast_am callback in the
heapam table AM. So this function is specific to heapam, and other
table AMs should have their own if they want to have different behavior.

The function is really small, so if a table AM that's not heapam can
very easily set up its own callback function that returns
HEAP_TABLE_AM_OID to get a regular heapam TOAST table, right?

So "a table AM that [copies the whole heap TableAmRoutine and overrides
only a handful of callbacks]" is Doing It Wrong by failing to also
override relation_toast_am.

OK, fair enough, I withdraw the proposal. I guess the dummy AM I set up
for testing the AM options patch Did It Wrong ;-)

Maybe this hazard is worth documenting somewhere. I'll look for a good
place.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#5Andrew Dunstan
andrew@dunslane.net
In reply to: Andres Freund (#3)
Re: heapam_relation_toast_am() returns the wrong AM for a wrapped heap AM

On 2026-08-21 Fr 2:21 PM, Andres Freund wrote:

Hi,

On 2026-08-21 09:20:07 -0400, Andrew Dunstan wrote:

heapam_relation_toast_am() returns rel->rd_rel->relam instead of the
literal heap AM oid, on the assumption the two are always equal since
it's only meant to run for relations that are themselves heap. That
breaks for a table AM that copies heap's whole TableAmRoutine (via
GetHeapamTableAmRoutine()) and overrides only a few callbacks -- a
pattern heap_getnext()'s own identity check explicitly anticipates,
per its comment about allowing "regression tests that create another
AM reusing the heap handler." For such an AM, rel->rd_rel->relam is
its own oid, so NewRelationCreateToastTable() creates the TOAST table
with that AM too, and building its chunk_id/chunk_seq index then
fails in heap_getnext(), which requires rd_tableam to be literally
GetHeapamTableAmRoutine(): "only heap AM is supported" for any such
AM as soon as a table needs a TOAST table.

Fix by returning the literal HEAP_TABLE_AM_OID, which is what the
function's own comment already claims it does ("TOAST tables for
heap relations are just heap relations").

I'm not following would that would be a good idea? Right now we can have a
separately registered table AM to write tests for non-default AMs without a
separately maintained AM. But with this that won't test the toast paths
anymore, because you make those to be a plain heapam, rather than the
simulacrum of a separate AM?

I certainly don't see how that would be a sane thing to backpatch. It's quite
possible that would break currently working code, no?

I had convinced myself that we should not require an AM that wanted to
override just one or two things in heap to have to override this as
well. I have seen the error of my ways.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com