PostgreSQL Storage — From a Hardwired Heap to the Pluggable Table Access Method API
Contents:
- Why this subsystem had to evolve (the original limitation)
- Timeline
- Era 0 — the hardwired heap (pre-12): one storage model, no seam
- Era 1 — PostgreSQL 12: the Table Access Method API (TableAmRoutine)
- Era 2 — PostgreSQL 12: the slot abstraction generalization (TupleTableSlotOps)
- Era 3 — PostgreSQL 12: surfacing the seam in SQL (CREATE ACCESS METHOD, USING, the GUC)
- Era 4 — PostgreSQL 15: ALTER TABLE … SET ACCESS METHOD
- Era 5 — PostgreSQL 17: SET ACCESS METHOD for matviews and the GUC default
- Era 6 — the alternative-AM ecosystem built on the API
- Where it stands at REL_18
- Sources
Why this subsystem had to evolve (the original limitation)
Section titled “Why this subsystem had to evolve (the original limitation)”PostgreSQL inherited from Berkeley POSTGRES a single, opinionated table
storage model: the no-overwrite heap. A table is a sequence of 8 KB
blocks; each block is a slotted page; each row is a HeapTuple with a
23-byte header carrying the xmin/xmax transaction stamps that drive MVCC
visibility. Updates never overwrite in place — they write a new tuple version
and leave the old one for VACUUM to reclaim. The mechanics of this model are
documented in postgres-heap-am.md; this evolution doc does not re-derive them.
For decades this model was not an access method — it was the storage layer,
and the rest of the backend called into it directly. The executor’s
sequential-scan node called heap_getnext. The index machinery resolved a TID
to a row by calling heap_fetch / heap_hot_search_buffer. COPY, INSERT,
UPDATE, and DELETE reached heap_insert, heap_update, heap_delete.
VACUUM was lazy_vacuum_rel over heap pages. There was no indirection: the
function names in heapam.c were the storage interface, hardcoded at every
call site. The word “heap” was effectively a synonym for “table”.
This was simple and fast, but it conflated two separable concerns:
- What the executor needs — a cursor over visible tuples, a way to insert a row, an outcome code when a concurrent transaction touched the same tuple.
- How those needs are met — a heap of slotted pages with MVCC version chains and a vacuum process to reclaim dead versions.
The cost of conflating them is workload rigidity. The no-overwrite heap is excellent for mixed read-write OLTP but carries structural costs that some workloads do not want to pay:
- Tuple bloat and vacuum overhead. Every update leaves a dead version;
reclaiming it requires
VACUUM, autovacuum scheduling, and freezing to avoid XID wraparound. An append-only or analytic workload pays this cost for nothing. - No native columnar layout. The row-major heap stores all columns of a tuple together. An OLAP scan that touches three of fifty columns still pulls whole rows through the buffer manager — no column projection at the storage level, no column-wise compression.
- No alternative concurrency model. The MVCC version chain is baked into the
heap. An in-place update model with a separate undo log (to eliminate bloat)
could not be expressed without forking
heapam.c.
As long as storage was hardwired, the only way to offer a different model was to
fork the whole engine. The fix is the classic strategy object / vtable
pattern from object-oriented design: define a stable interface the executor
calls, select the concrete implementation when a relation is opened, and wire it
in through a dispatch table. PostgreSQL already had exactly this for indexes —
the Index Access Method API (IndexAmRoutine, see postgres-index-am.md) had
let btree, hash, GiST, GIN, SP-GiST, and BRIN coexist behind one interface for
years. The table side was the missing half. The vision dates back to
Stonebraker & Rowe’s 1986 Design of POSTGRES, which described an “abstract
data manager” separating query processing from storage. PostgreSQL 12 finally
built the table-side seam, modeled on the older index-side one.
Timeline
Section titled “Timeline”timeline
title PostgreSQL Storage — From Hardwired Heap to Pluggable Table AM
section Hardwired era
pre-12 : Heap is the storage layer : Executor calls heap_getnext heap_insert directly : No table-level seam : Index AM (IndexAmRoutine) already pluggable
section The seam is cut (PG 12)
PG 12 Table AM API : TableAmRoutine vtable on rd_tableam : heap becomes heapam_methods, one AM among possible others : GetTableAmRoutine validates mandatory callbacks
PG 12 Slot generalization : TupleTableSlotOps abstraction : TTSOpsBufferHeapTuple / Virtual / HeapTuple / MinimalTuple : executor carries any AM in-memory tuple form
PG 12 SQL surface : CREATE ACCESS METHOD ... TYPE TABLE : CREATE TABLE ... USING : default_table_access_method GUC
section Mutating a table's AM
PG 15 SET ACCESS METHOD : ALTER TABLE ... SET ACCESS METHOD rewrites the table : AT_SetAccessMethod
PG 17 matviews + GUC : ALTER MATERIALIZED VIEW ... SET ACCESS METHOD : default_table_access_method governs matviews : SET ACCESS METHOD DEFAULT, pg_dump emits it
section Ecosystem
2017-2020 zheap : in-place AM with undo : stalled, but proved the API's intent
ongoing columnar AMs : Citus columnar, Hydra, ParadeDB : OLAP storage on the same query pipeline
section Current
REL_18 : heap still the only in-tree AM : tableam.c, heapam_handler.c, tableam.h : the API is the stable extension point
Figure 1 — The evolution arc. The decisive cut happened in PostgreSQL 12, where three coordinated changes (the AM vtable, the slot generalization, and the SQL surface) together turned “heap” from the storage layer into one access method. PG15 and PG17 then made a table’s AM mutable. Heap remains the only in-tree AM at REL_18; the alternatives live as extensions.
Era 0 — the hardwired heap (pre-12): one storage model, no seam
Section titled “Era 0 — the hardwired heap (pre-12): one storage model, no seam”What the design was. Before PostgreSQL 12 there was no TableAmRoutine, no
rd_tableam field on the relcache entry, and no table_* wrapper layer. The
executor, the index layer, COPY, and VACUUM all called heap functions by
name. The call graph was a direct edge from each consumer to heapam.c:
flowchart TB
subgraph PRE["Pre-12 — hardwired heap (no seam)"]
direction TB
SEQ["SeqNext / ExecSeqScan"] --> HGN["heap_getnext"]
IDX["Index scan TID resolution"] --> HF["heap_fetch / heap_hot_search_buffer"]
DML["INSERT / UPDATE / DELETE / COPY"] --> HINS["heap_insert / heap_update / heap_delete"]
VAC["VACUUM"] --> LVR["lazy_vacuum_rel"]
HGN --> HEAP["heapam.c — the storage layer"]
HF --> HEAP
HINS --> HEAP
LVR --> HEAP
end
Figure 2 — Pre-12 structure. Every consumer is a direct caller of a named heap function. “heap” is not a configurable property of a relation; it is the only table storage that exists. There is no relation-carried dispatch pointer and no interface boundary to swap.
Why it was built that way. It was the natural state of an engine that grew up around one storage model. The slotted-page heap with no-overwrite MVCC was the Berkeley POSTGRES design, and for a single-storage system, indirection is pure overhead — a function-pointer chase the compiler can never inline and a maintenance burden with no payoff. The cost only became unacceptable once people wanted more than one storage model in the same server.
What was already pluggable, and why it mattered. The index side had been
pluggable for years through IndexAmRoutine (in amapi.h). The executor and
planner spoke to indexes through a stable index_* wrapper layer that
dispatched through rel->rd_indam, and six structurally distinct index types
(btree, hash, GiST, GIN, SP-GiST, BRIN) coexisted behind it. This was the
existence proof and the template: the table-AM authors deliberately modeled
TableAmRoutine on the shape of IndexAmRoutine — same vtable-on-the-relation
pattern, same mandatory/optional callback split, same handler-function
registration through pg_am. The pg_am catalog already carried an amtype
column; pre-12 it only ever held AMTYPE_INDEX (‘i’). Cross-link:
postgres-index-am.md documents the index-side interface that shaped this one.
Era 1 — PostgreSQL 12: the Table Access Method API (TableAmRoutine)
Section titled “Era 1 — PostgreSQL 12: the Table Access Method API (TableAmRoutine)”What changed. PostgreSQL 12 introduced TableAmRoutine: a struct of ~40
function pointers (declared in src/include/access/tableam.h) that captures
everything the rest of the backend needs from table storage — slot callbacks,
the sequential-scan lifecycle (scan_begin / scan_getnextslot / scan_end /
scan_rescan), index-fetch (index_fetch_begin / index_fetch_tuple), DML
(tuple_insert / tuple_update / tuple_delete / tuple_lock /
multi_insert), and DDL/vacuum/analyze (relation_vacuum,
scan_analyze_next_block, index_build_range_scan, …). A pointer to this
vtable is stored on every relation’s relcache entry in the new rd_tableam
field. The mechanism — how the vtable is resolved at table_open time, how
GetTableAmRoutine validates the mandatory slots, how each table_* inline
wrapper dispatches — is documented in full in postgres-table-am.md; this doc
does not duplicate it.
The structural shift (before → after). Every hardwired edge in Figure 2 was
rerouted through a two-step indirection: consumer → table_* wrapper → vtable
slot → AM implementation. Heap did not disappear; it was demoted to a single
filled-in vtable, heapam_methods (a static const TableAmRoutine in
heapam_handler.c), whose slots point at the very same heap_* functions that
used to be called directly.
flowchart TB
subgraph POST["PG 12+ — pluggable Table AM (seam cut)"]
direction TB
SEQ["SeqNext / ExecSeqScan"] --> W1["table_scan_getnextslot\n(tableam.h inline)"]
IDX["Index scan TID resolution"] --> W2["table_index_fetch_tuple\n(tableam.h inline)"]
DML["INSERT / UPDATE / DELETE / COPY"] --> W3["table_tuple_insert / update / delete\n(tableam.h inline)"]
VAC["VACUUM"] --> W4["table_relation_vacuum\n(tableam.h inline)"]
W1 --> RT["rel->rd_tableam\n(const TableAmRoutine *)"]
W2 --> RT
W3 --> RT
W4 --> RT
RT --> HM["heapam_methods\n(static const TableAmRoutine)"]
RT -.-> OTHER["some_other_methods\n(columnar / zheap / ...)"]
HM --> HEAP["heap_* in heapam.c"]
OTHER -.-> OIMPL["alternative storage impl"]
end
Figure 3 — PG12 structure (before → after of Figure 2). The same four
consumers now call stable table_* wrappers. Each wrapper reads
rel->rd_tableam and dispatches through a function-pointer slot. Heap is one
filled-in vtable (heapam_methods); the dotted edge shows the seam that an
alternative AM plugs into. Note the heap implementation functions
(heap_getnext, heap_insert, …) are unchanged — only the call path to them
moved behind the vtable.
Why it was done. The motivating goal, stated in the PG12 commit and release material, was to let alternative storage engines — in particular the zheap in-place engine then under development — share PostgreSQL’s executor, planner, WAL infrastructure, and SQL surface instead of forking them. Recasting heap as one AM was the proof that the interface was complete: if the existing heap could be expressed entirely through the vtable with no remaining hardwired call sites, then a second AM could be expressed too.
The cost it accepted. Two pointer chases per storage operation
(relation → vtable → function) that the compiler cannot inline across the
indirection. For the hot sequential-scan path this is measurable but small, and
PG12 mitigated it by batching: scan_getnextslot returns one tuple at a time
but the heap implementation reads a whole page under one buffer pin, and the
slot abstraction (Era 2) lets the executor avoid copying each tuple. Cross-link:
postgres-table-am.md is the current-state mechanism doc for everything in this
era — TableAmRoutine, GetTableAmRoutine, TM_Result, ScanOptions, the
dispatch chain.
Era 2 — PostgreSQL 12: the slot abstraction generalization (TupleTableSlotOps)
Section titled “Era 2 — PostgreSQL 12: the slot abstraction generalization (TupleTableSlotOps)”What changed. A pluggable storage layer needs a pluggable in-memory tuple
representation, because not every AM stores rows as HeapTuples. PostgreSQL 12
generalized the executor’s TupleTableSlot from a struct that knew about heap
and minimal tuples into one driven by a vtable of its own: TupleTableSlotOps
(declared in src/include/executor/tuptable.h). A slot now carries a
const TupleTableSlotOps *tts_ops pointer, and operations like “give me this
attribute”, “materialize into a copy”, “clear” dispatch through it.
The release shipped four slot-ops implementations, each a const TupleTableSlotOps:
TTSOpsVirtual— columns held as aDatum/isnullarray, no backing tuple; the form produced by expression evaluation and projection.TTSOpsHeapTuple— a palloc’d standaloneHeapTuple.TTSOpsMinimalTuple— the header-stripped form used by tuplestore, sort, and hash-join spools.TTSOpsBufferHeapTuple— aHeapTuplestill pinned in a shared-buffer page; the zero-copy form the heap scan path returns.
The structural shift (before → after). Before PG12 the slot was effectively
heap-shaped: it could hold a heap tuple or a minimal tuple, and the distinction
was a couple of boolean flags inside one monolithic struct. After PG12 the slot
is a thin container plus a behavior vtable, and the AM chooses which ops a scan
produces. The Table AM’s slot_callbacks slot answers exactly this question —
for heap it returns &TTSOpsBufferHeapTuple, so a sequential scan can hand the
executor a tuple still living in its buffer page without materializing a copy.
A columnar AM returns &TTSOpsVirtual (or a custom ops) so it can hand back a
projected column set instead of a fabricated row.
flowchart LR
subgraph BEFORE["Pre-12 slot"]
direction TB
S0["TupleTableSlot\n(monolithic;\nheap-or-minimal,\nflags decide)"]
end
subgraph AFTER["PG 12+ slot"]
direction TB
S1["TupleTableSlot\n+ tts_ops pointer"]
S1 --> V["TTSOpsVirtual"]
S1 --> H["TTSOpsHeapTuple"]
S1 --> M["TTSOpsMinimalTuple"]
S1 --> B["TTSOpsBufferHeapTuple"]
AM["AM slot_callbacks\nchooses the ops"] --> S1
end
BEFORE -.PG12 generalization.-> AFTER
Figure 4 — Slot generalization. The pre-12 slot baked the heap/minimal choice
into one struct. PG12 split behavior into a TupleTableSlotOps vtable selected
by tts_ops, with the Table AM’s slot_callbacks deciding which ops a scan
produces. This is what lets a non-heap AM return tuples in its own in-memory
form without the executor knowing or caring.
Why it was done. Without this, the Table AM API would have leaked the heap
representation: any AM would have had to fabricate HeapTuples to satisfy the
executor, defeating the point of columnar or in-place storage. The slot
generalization is the half of the PG12 work that made the Table AM API
genuinely storage-neutral on the read path, just as TM_Result made it
storage-neutral on the write path. Cross-link: postgres-table-am.md
documents table_slot_callbacks and how ExecInitSeqScan uses it to pick the
slot type; postgres-heap-am.md documents the buffer-pinned heap tuple form.
Era 3 — PostgreSQL 12: surfacing the seam in SQL (CREATE ACCESS METHOD, USING, the GUC)
Section titled “Era 3 — PostgreSQL 12: surfacing the seam in SQL (CREATE ACCESS METHOD, USING, the GUC)”What changed. An internal vtable is invisible to users; PG12 also exposed the seam at the SQL and catalog level so an AM could actually be installed and selected:
pg_am.amtypegained the value't'(AMTYPE_TABLE, insrc/include/catalog/pg_am.h), alongside the long-standing'i'(AMTYPE_INDEX). The built-inheaprow inpg_amis now a table-type AM whoseamhandlerisheap_tableam_handler.CREATE ACCESS METHOD <name> TYPE TABLE HANDLER <fn>lets an extension register a new table AM. The handler function returns aTableAmRoutine *;GetTableAmRoutinevalidates it on first use.CREATE TABLE ... USING <am>selects a relation’s AM at creation time.default_table_access_methodGUC sets the AM used whenUSINGis omitted; its compile-time default is the string"heap"(DEFAULT_TABLE_ACCESS_METHODintableam.h).
The structural shift (before → after). Pre-12, a table’s storage was not a
property anyone could name — there was nothing to put after a hypothetical
USING. Post-12, “which access method” became a first-class, catalog-recorded
attribute of a relation (pg_class.relam), resolved to a vtable at
table_open time. The relation now remembers its AM the same way it remembers
its tablespace.
Why it was done. The vtable seam (Era 1) is useless if there is no way to
ask for a non-default AM. This era is the user-facing completion of the PG12
work: registration (CREATE ACCESS METHOD), per-table selection (USING), and
a server-wide default (the GUC). Cross-link: postgres-table-am.md covers
heap_tableam_handler, GetTableAmRoutine, and the DEFAULT_TABLE_ACCESS_METHOD
constant; the DDL execution path that records pg_class.relam is in
postgres-ddl-execution.md.
Era 4 — PostgreSQL 15: ALTER TABLE … SET ACCESS METHOD
Section titled “Era 4 — PostgreSQL 15: ALTER TABLE … SET ACCESS METHOD”What changed. PG12 let you create a table with a chosen AM, but there was
no supported way to change an existing table’s AM in place. PostgreSQL 15
added ALTER TABLE ... SET ACCESS METHOD <am> — the parse-tree subcommand
AT_SetAccessMethod (in src/include/nodes/parsenodes.h). Changing the AM is
not a metadata-only flip: the rows have to be physically re-encoded into the new
AM’s on-disk format, so the command performs a full table rewrite (a new
relfilenode, every tuple re-inserted through the target AM’s tuple_insert
path), the same rewrite machinery that ALTER TABLE already used for type
changes that alter on-disk layout.
The structural shift (before → after). Before PG15 the AM of a table was
effectively immutable after creation: to move data to a different AM you created
a new table USING the target AM and copied rows yourself (e.g.,
INSERT ... SELECT), then renamed. After PG15 the migration is a single DDL
statement that drives the rewrite through the same table_* insert path the
executor uses, with all the dependency, index-rebuild, and constraint-recheck
bookkeeping handled by the ALTER TABLE phase machinery.
Why it was done. Pluggable storage is far more useful if you can adopt an
alternative AM for existing data, not only for new tables. A user with a large
append-only table created years ago on heap should be able to convert it to a
columnar AM without a hand-rolled copy dance. SET ACCESS METHOD makes the
conversion a first-class, atomic, dependency-aware operation. Cross-link: the
rewrite/phase machinery is documented in postgres-alter-table.md; the per-row
insert path it drives is table_tuple_insert in postgres-table-am.md.
Era 5 — PostgreSQL 17: SET ACCESS METHOD for matviews and the GUC default
Section titled “Era 5 — PostgreSQL 17: SET ACCESS METHOD for matviews and the GUC default”What changed. PostgreSQL 17 rounded out the SET ACCESS METHOD story in
three ways:
ALTER MATERIALIZED VIEW ... SET ACCESS METHOD— matviews are heap-backed relations too, and PG17 let them be moved to a different AM the same way ordinary tables can. (Cross-link:postgres-matview.md.)default_table_access_methodnow governs matviews andCREATE TABLE AS/SELECT INTOmore consistently, so the server-wide default AM choice is honored for the relations these commands materialize, not only for plainCREATE TABLE.SET ACCESS METHOD DEFAULTspelling, andpg_dump/pg_restorelearning to emitSET ACCESS METHODso a non-default AM survives a dump/restore cycle — without this, a logical dump would silently re-materialize every table on the server default AM, losing the columnar (or other) choice.
The structural shift (before → after). Before PG17, the set of relation kinds whose AM you could control was narrower (plain tables), and a logical dump did not faithfully carry the AM. After PG17, the AM is a fully first-class, dump-preserved property across the relation kinds that have physical storage, and the default-AM GUC is applied uniformly. This is less a new mechanism than the closure of the pluggable-storage feature: the seam now behaves consistently everywhere a table-like relation is created, altered, or dumped.
Why it was done. Feature completeness and operational safety. An AM you
cannot preserve across pg_dump is an AM you cannot rely on in production; an AM
you can set on a table but not on a matview is a surprising gap. PG17 removed
both rough edges. Cross-link: postgres-pg-dump-restore.md for the dump-side
emission; postgres-table-am.md for the GUC.
Era 6 — the alternative-AM ecosystem built on the API
Section titled “Era 6 — the alternative-AM ecosystem built on the API”What the API enabled, even with heap as the only in-tree AM. The whole point of the seam was to let storage models live outside the core tree while sharing the query pipeline. Three categories of out-of-tree AM exercise the interface:
- zheap (Percona / EnterpriseDB, ~2017–2020). An in-place update engine with
a separate undo log, intended to eliminate heap bloat and the vacuum/freeze
burden. zheap was the original motivating consumer of
TableAmRoutine— much of the PG12 interface shape was justified by “what would zheap need?”. The project stalled (the undo subsystem proved very hard to get right), but it remains the clearest demonstration that the API was designed with a real second AM in mind, and its design notes are a catalog of the hardest parts of the AM contract: visibility, freezing, TOAST integration, and WAL. - Columnar / OLAP AMs (Citus columnar, Hydra, ParadeDB). These implement
TableAmRoutinefor column-major, compressed, append-optimized storage so that analytic scans project and compress at the storage level while still running through PostgreSQL’s planner and executor. They lean on the slot generalization (Era 2) to return projected columns rather than fabricated heap rows. - Block-oriented helpers as a shared substrate. Many AMs are still
block-and-buffer based, so
tableam.cships shared helpers (table_block_relation_size, thetable_block_parallelscan_*family) that an AM can call from its own callbacks. These reveal which parts of the contract are genuinely AM-neutral (scan lifecycle,TM_Resultoutcome codes) versus which assume a block-structured relation.
Why this is the payoff of the whole arc. Era 0’s hardwired heap made every
one of these a fork-the-engine project. Eras 1–5 turned them into extensions
that compile against a stable header, register through CREATE ACCESS METHOD,
and inherit the executor, planner, WAL, replication, and SQL surface for free.
The ecosystem is the evidence that the seam is real. Cross-link:
postgres-table-am.md “Beyond PostgreSQL” section enumerates these projects with
more detail; postgres-heap-am.md is the reference implementation an AM author
reads first.
Where it stands at REL_18
Section titled “Where it stands at REL_18”At REL_18 (commit 273fe94, PG 18.x) the pluggable-storage architecture is
mature and stable, and heap is still the only table AM in the core tree. The
current design is exactly the PG12 shape, refined by PG15/PG17:
src/include/access/tableam.hdeclaresTableAmRoutine(the ~40-slot vtable), thetable_*inline dispatch wrappers,TM_Result,ScanOptions, and theDEFAULT_TABLE_ACCESS_METHOD "heap"constant.src/backend/access/table/tableam.c(plustableamapi.c) providesGetTableAmRoutine(handler resolution + mandatory-callback validation), thesimple_table_tuple_*convenience wrappers, and the shared block-oriented parallel-scan helpers.src/backend/access/heap/heapam_handler.cdefinesheapam_methods, the referencestatic const TableAmRoutine, andheap_tableam_handler, the SQL-visibleamhandlerfunction that returns&heapam_methods.- The slot side (
src/include/executor/tuptable.h) carries the fourTupleTableSlotOpsimplementations, withTTSOpsBufferHeapTupleas heap’s zero-copy scan form. CREATE ACCESS METHOD ... TYPE TABLE,CREATE TABLE ... USING,default_table_access_method, andALTER TABLE / MATERIALIZED VIEW ... SET ACCESS METHODare all live, andpg_dumppreserves a non-default AM.
The mechanism for all of this is documented — not re-derived here — in the
current-state module docs: postgres-table-am.md (the dispatch layer and
TableAmRoutine inventory), postgres-heap-am.md (heap as the reference AM:
HeapTupleHeaderData, HOT, pruning, visibility, heap_insert/update/delete),
and postgres-index-am.md (the sibling IndexAmRoutine interface that
shaped the table-side API).
The PG19 next step. As of REL_18 the in-tree AM set is unchanged from PG12; the forward direction is incremental — sanding down remaining block-oriented assumptions in the interface so that genuinely non-block AMs (columnar, in-memory) need fewer workarounds, rather than adding a second built-in AM. Treat any PG19-era refinement here as a just-released forward note, not current REL_18 behavior.
Sources
Section titled “Sources”PostgreSQL release notes (feature attribution)
Section titled “PostgreSQL release notes (feature attribution)”- PostgreSQL 12 release notes — “Allow table access methods” / the Table
Access Method API;
CREATE ACCESS METHOD ... TYPE TABLE,CREATE TABLE ... USING,default_table_access_method. The slot abstraction (TupleTableSlotOps) landed in the same release as part of the executor work enabling pluggable storage. - PostgreSQL 15 release notes —
ALTER TABLE ... SET ACCESS METHOD. - PostgreSQL 17 release notes —
ALTER MATERIALIZED VIEW ... SET ACCESS METHOD,default_table_access_methodapplied to matviews /CREATE TABLE AS,SET ACCESS METHOD DEFAULT, andpg_dumpemission ofSET ACCESS METHOD.
Current-state module docs (mechanism — do not re-derive here)
Section titled “Current-state module docs (mechanism — do not re-derive here)”postgres-table-am.md— the dispatch layer,TableAmRoutine,GetTableAmRoutine,TM_Result,ScanOptions,heapam_methodsbinding, executor call sites.postgres-heap-am.md— heap as the reference implementation.postgres-index-am.md— the sibling Index AM (IndexAmRoutine) interface that the table-AM design was modeled on.
Related module docs (touched by the eras above)
Section titled “Related module docs (touched by the eras above)”postgres-alter-table.md— the rewrite/phase machinery behindSET ACCESS METHOD.postgres-matview.md— materialized views as AM-backed relations (PG17).postgres-pg-dump-restore.md— dump-side emission ofSET ACCESS METHOD.postgres-ddl-execution.md— recordingpg_class.relamat create time.
PostgreSQL source (under /data/hgryoo/references/postgres/, REL_18 273fe94)
Section titled “PostgreSQL source (under /data/hgryoo/references/postgres/, REL_18 273fe94)”src/include/access/tableam.h—TableAmRoutine,table_*wrappers,TM_Result,ScanOptions,DEFAULT_TABLE_ACCESS_METHOD.src/backend/access/table/tableam.c,tableamapi.c— dispatch helpers,GetTableAmRoutine.src/backend/access/heap/heapam_handler.c—heapam_methods,heap_tableam_handler.src/include/executor/tuptable.h—TupleTableSlotOpsand the fourTTSOps*slot implementations.src/include/catalog/pg_am.h—amtype,AMTYPE_TABLE(‘t’).src/include/nodes/parsenodes.h—AT_SetAccessMethod.
Theory background
Section titled “Theory background”- Stonebraker & Rowe, The Design of POSTGRES (1986) — the “abstract data manager” vision separating query processing from storage.
- Database Internals (Petrov), ch. 3 — pluggable storage and vtable dispatch.
- Database System Concepts (Silberschatz et al., 7e), ch. 13 — storage manager abstraction and access-method interfaces.