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].
Not sure if we would call this a bug or just a limitation.
cheers
andrew
[1]
https://www.postgresql.org/message-id/ea1c4d33-0780-473c-96dc-1468cf733a04%40dunslane.net
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
From 4cd9ef0f973600f63d3886674d8afa28574e94cc Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Sat, 15 Aug 2026 16:14:37 -0400
Subject: [PATCH] Fix heapam_relation_toast_am() to return the literal heap AM
oid.
heapam_relation_toast_am() returned rel->rd_rel->relam instead of the
heap AM oid, assuming the two are always equal since only heap
relations were expected to reach it. That breaks for a table AM that
reuses this callback by copying heap's whole TableAmRoutine:
rel->rd_rel->relam is then the AM's own oid, so
NewRelationCreateToastTable() makes the TOAST table that AM
too. 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 table
with a toastable column.
Return the literal HEAP_TABLE_AM_OID instead: TOAST tables are always
plain heap, which is what the function's own comment already said.
Discovered while investigating a report from Zsolt Parragi.
Backpatch-thru: 14
---
src/backend/access/heap/heapam_handler.c | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..6b3d5fc46c0 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -31,6 +31,7 @@
#include "access/xact.h"
#include "catalog/catalog.h"
#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
#include "commands/progress.h"
@@ -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.
*/
static Oid
heapam_relation_toast_am(Relation rel)
{
- return rel->rd_rel->relam;
+ return HEAP_TABLE_AM_OID;
}
--
2.43.0