Skip to content

PostgreSQL Storage — From a Hardwired Heap to the Pluggable Table Access Method API

Contents:

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
  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 a Datum/isnull array, no backing tuple; the form produced by expression evaluation and projection.
  • TTSOpsHeapTuple — a palloc’d standalone HeapTuple.
  • TTSOpsMinimalTuple — the header-stripped form used by tuplestore, sort, and hash-join spools.
  • TTSOpsBufferHeapTuple — a HeapTuple still 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.amtype gained the value 't' (AMTYPE_TABLE, in src/include/catalog/pg_am.h), alongside the long-standing 'i' (AMTYPE_INDEX). The built-in heap row in pg_am is now a table-type AM whose amhandler is heap_tableam_handler.
  • CREATE ACCESS METHOD <name> TYPE TABLE HANDLER <fn> lets an extension register a new table AM. The handler function returns a TableAmRoutine *; GetTableAmRoutine validates it on first use.
  • CREATE TABLE ... USING <am> selects a relation’s AM at creation time.
  • default_table_access_method GUC sets the AM used when USING is omitted; its compile-time default is the string "heap" (DEFAULT_TABLE_ACCESS_METHOD in tableam.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_method now governs matviews and CREATE TABLE AS / SELECT INTO more consistently, so the server-wide default AM choice is honored for the relations these commands materialize, not only for plain CREATE TABLE.
  • SET ACCESS METHOD DEFAULT spelling, and pg_dump/pg_restore learning to emit SET ACCESS METHOD so 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 TableAmRoutine for 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.c ships shared helpers (table_block_relation_size, the table_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_Result outcome 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.

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.h declares TableAmRoutine (the ~40-slot vtable), the table_* inline dispatch wrappers, TM_Result, ScanOptions, and the DEFAULT_TABLE_ACCESS_METHOD "heap" constant.
  • src/backend/access/table/tableam.c (plus tableamapi.c) provides GetTableAmRoutine (handler resolution + mandatory-callback validation), the simple_table_tuple_* convenience wrappers, and the shared block-oriented parallel-scan helpers.
  • src/backend/access/heap/heapam_handler.c defines heapam_methods, the reference static const TableAmRoutine, and heap_tableam_handler, the SQL-visible amhandler function that returns &heapam_methods.
  • The slot side (src/include/executor/tuptable.h) carries the four TupleTableSlotOps implementations, with TTSOpsBufferHeapTuple as heap’s zero-copy scan form.
  • CREATE ACCESS METHOD ... TYPE TABLE, CREATE TABLE ... USING, default_table_access_method, and ALTER TABLE / MATERIALIZED VIEW ... SET ACCESS METHOD are all live, and pg_dump preserves 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.

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 notesALTER TABLE ... SET ACCESS METHOD.
  • PostgreSQL 17 release notesALTER MATERIALIZED VIEW ... SET ACCESS METHOD, default_table_access_method applied to matviews / CREATE TABLE AS, SET ACCESS METHOD DEFAULT, and pg_dump emission of SET 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_methods binding, 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.
Section titled “Related module docs (touched by the eras above)”
  • postgres-alter-table.md — the rewrite/phase machinery behind SET ACCESS METHOD.
  • postgres-matview.md — materialized views as AM-backed relations (PG17).
  • postgres-pg-dump-restore.md — dump-side emission of SET ACCESS METHOD.
  • postgres-ddl-execution.md — recording pg_class.relam at 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.hTableAmRoutine, 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.cheapam_methods, heap_tableam_handler.
  • src/include/executor/tuptable.hTupleTableSlotOps and the four TTSOps* slot implementations.
  • src/include/catalog/pg_am.hamtype, AMTYPE_TABLE (‘t’).
  • src/include/nodes/parsenodes.hAT_SetAccessMethod.
  • 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.