Access-method APIs — operational guide
This skill is for code that implements an AM (or significantly extends one).
Read-only "how does btree work" questions don't need it — go straight to the
per-AM README.
Two completely separate plug points
|
Index AM |
Table AM |
| Struct |
IndexAmRoutine in src/include/access/amapi.h |
TableAmRoutine in src/include/access/tableam.h |
| Resolver |
GetIndexAmRoutine(amhandler) in src/backend/access/index/amapi.c |
GetTableAmRoutine(amhandler) in src/backend/access/table/tableamapi.c |
| Catalog |
pg_am.amtype = 'i' |
pg_am.amtype = 't' |
| Executor wrapper |
genam.c / indexam.c |
inline wrappers in tableam.h (table_beginscan, table_tuple_insert, …) |
| Canonical impl |
nbtree.c, also brin.c, gin.c, gist.c, hash.c, spgist.c |
heapam_handler.c (the only in-tree one) |
| Minimal stub |
src/test/modules/dummy_index_am/dummy_index_am.c |
none in tree |
| Added |
Index AM API since 9.6 (amapi.h); table AM since v12 |
|
Both APIs work the same way at the dispatch level: a pg_am row's amhandler
column names a SQL-callable C function (PG_FUNCTION_INFO_V1) that returns a
pointer to a statically allocated IndexAmRoutine/TableAmRoutine. The
core never copies or frees the struct.
Datum
myhandler(PG_FUNCTION_ARGS)
{
static const IndexAmRoutine amroutine = { .type = T_IndexAmRoutine, ... };
PG_RETURN_POINTER(&amroutine);
}
Index AM (IndexAmRoutine)
Flag/property fields (~22 booleans + counts)
Declarative facts the planner needs before it ever calls a function pointer:
amstrategies — count of operator strategies (e.g. btree has 5: <, <=, =,
>=, >). 0 if the AM doesn't use a fixed strategy set (gin/gist).
amsupport — count of mandatory support functions per opclass.
amoptsprocnum — opclass-options procedure number, or 0.
amcanorder, amcanorderbyop, amcanhash, amcanbackward, amcanunique,
amcanmulticol, amoptionalkey, amsearcharray, amsearchnulls,
amstorage, amclusterable, ampredlocks, amcanparallel,
amcanbuildparallel, amcaninclude, amusemaintenanceworkmem,
amsummarizing, amconsistentequality, amconsistentordering.
amparallelvacuumoptions — bitmask of VACUUM_OPTION_* (PARALLEL_BULKDEL,
PARALLEL_COND_CLEANUP, PARALLEL_CLEANUP). Set to VACUUM_OPTION_NO_PARALLEL
(= 0) to opt out of parallel vacuum entirely — the right default for a brand-new AM.
amkeytype — fixed key type OID, or InvalidOid if variable.
Function pointers — mandatory
Must be non-NULL (planner/executor will dereference unconditionally):
| Callback |
Purpose |
ambuild |
Build a new index from scratch over heapRelation. Drives parallel build internally. |
ambuildempty |
Build the init fork for unlogged indexes. |
aminsert |
Insert one tuple. Called inside ExecInsertIndexTuples. |
ambulkdelete |
Vacuum pass that drops index entries whose heap TID matches a callback (returns IndexBulkDeleteResult *). |
amvacuumcleanup |
Final vacuum pass (returns IndexBulkDeleteResult *); may return stats and reclaim empty pages. |
amcostestimate |
Planner cost callback. Fill in start/total/selectivity/correlation/pages. |
amoptions |
Parse WITH (...) reloptions via build_reloptions. May return NULL. |
amvalidate |
Sanity-check an opclass at CREATE OPERATOR CLASS time. |
ambeginscan |
Must call RelationGetIndexScan() and return its result. |
amrescan |
(Re)bind scan keys. |
amendscan |
Tear down scan-private state (don't free the IndexScanDesc itself). |
GOTCHA — ambeginscan return identity.
ambeginscan MUST return the exact IndexScanDesc that RelationGetIndexScan()
gave it — not a copy, not a wrapper. index_endscan (in genam.c) reaches
inside that struct after the AM's amendscan has already run, so any
reallocation breaks teardown. The comment at the top of genam.c calls this
"kinda ugly". Allocate your private state separately and hang it off
scan->opaque.
Function pointers — optional (may be NULL)
aminsertcleanup, amcanreturn (for index-only scans), amgettreeheight,
amproperty, ambuildphasename, amadjustmembers, amgettuple (NULL ⇒ no
plain indexscan, only bitmap), amgetbitmap (NULL ⇒ no bitmap scan),
ammarkpos/amrestrpos (NULL ⇒ no mark/restore — fine if amcanbackward=false
or no merge-join support is needed), the three amestimate/init/parallelrescan
parallel-scan hooks (required iff amcanparallel=true), amtranslatestrategy/
amtranslatecmptype.
You need either amgettuple or amgetbitmap (typically both — gin is
bitmap-only).
Lifecycle — build / insert / scan / vacuum
CREATE INDEX → ambuild
SELECT ... → ambeginscan → amrescan → (loop) amgettuple|amgetbitmap → amendscan
INSERT/UPDATE → (per row) aminsert → (once at end of statement) aminsertcleanup
VACUUM → ambulkdelete (may be called many times) → amvacuumcleanup
DROP INDEX → catalog work only; storage smgr handles file
amvalidate runs at CREATE OPERATOR CLASS / ALTER OPERATOR FAMILY ADD. It
should check that all required strategy numbers and support function numbers
are present and have sane signatures. See amvalidate.c for the shared
helpers (identify_opfamily_groups, check_amop_signature, etc.).
Opclass / strategy / support function
pg_amop rows declare operators (<, =, &&, …) and tag each with a
strategy number that's AM-private (btree: 1=less, 5=greater; gist:
1..n varies per opclass). pg_amproc rows declare support functions, also
numbered 1..amsupport per AM. The AM code looks them up via
index_getprocinfo() (cached FmgrInfo) inside its callbacks.
amtranslatestrategy/amtranslatecmptype is the bridge to generic
CompareType enum values (COMPARE_LT etc.) so the planner can reason about
btree-compatible opclasses on other AMs.
Table AM (TableAmRoutine)
Pluggable since v12. There is exactly one in-tree implementation: heap.
The struct surface is much larger than the index-AM one (~45-callback struct;
tableamapi.c::GetTableAmRoutine asserts 37 of them non-NULL, the rest have
soft "may be NULL" contracts inside specific call sites) because table AMs own
MVCC, storage layout, vacuum, sampling, and the per-tuple slot type.
What "heap is just a table-AM" means in practice
- The
Relation cache stores rd_tableam (a TableAmRoutine *); every
heap_* style access in the executor went through a table_* inline wrapper
in tableam.h since v12.
- The TID-addressed visibility map, FSM, and toast machinery are not part
of the API — they're heap implementation details. A non-heap AM has to
reinvent or skip them.
- WAL, buffer manager, smgr, snapshots are still core — table AMs live above
bufmgr.
Slot interface
slot_callbacks(rel) returns a TupleTableSlotOps * (e.g. TTSOpsHeapTuple,
TTSOpsBufferHeapTuple, TTSOpsMinimalTuple, TTSOpsVirtual). All tuple
movement in/out of the AM is through TupleTableSlot; raw HeapTuple only
appears inside the heap AM. A new AM defines its own TupleTableSlotOps.
Scan family
| Group |
Callbacks |
| Plain |
scan_begin, scan_end, scan_rescan, scan_getnextslot |
| TID range |
scan_set_tidrange, scan_getnextslot_tidrange (both or neither) |
| Parallel |
parallelscan_estimate, parallelscan_initialize, parallelscan_reinitialize |
| Index fetch |
index_fetch_begin, index_fetch_reset, index_fetch_end, index_fetch_tuple |
| Analyze |
scan_analyze_next_block, scan_analyze_next_tuple |
| Sample |
scan_sample_next_block, scan_sample_next_tuple |
Tuple ops
tuple_insert, tuple_insert_speculative, tuple_complete_speculative,
multi_insert, tuple_delete, tuple_update, tuple_lock,
tuple_fetch_row_version, tuple_tid_valid, tuple_get_latest_tid,
tuple_satisfies_snapshot, index_delete_tuples.
Return type TM_Result (TM_Ok, TM_Invisible, TM_SelfModified, TM_Updated,
TM_Deleted, TM_BeingModified, TM_WouldBlock) carries MVCC outcomes.
TU_UpdateIndexes tells the executor which indexes still need re-insert after
update (TU_None / TU_All / TU_Summarizing enables HOT-like
optimizations for non-heap AMs).
DDL / storage
relation_set_new_filelocator, relation_nontransactional_truncate,
relation_copy_data, relation_copy_for_cluster, relation_vacuum,
relation_size, relation_needs_toast_table, relation_estimate_size,
index_build_range_scan, index_validate_scan.
All mandatory
tableamapi.c::GetTableAmRoutine runs 37 Assert(routine->X != NULL) lines.
Only finish_bulk_insert and the TID-range pair are truly optional.
The hard part: TID semantics
ItemPointer is a 6-byte (block, offset) pair, baked into the index AM
interface, WAL, syscaches, and pg_class rowcount estimation. A table AM that
doesn't store tuples in (block, offset) pages (columnar, LSM, external) has to
fabricate stable, 48-bit, monotone-ish TIDs for every row and route
index_fetch_tuple and tuple_fetch_row_version against them. This is the
single biggest reason most experimental table AMs never become production
ready. See MaxHeapTuplesPerPage and the warnings in tableam.sgml.
Stats-leakage corollary: autovacuum's per-table scheduling is driven by the
n_dead_tup / n_live_tup counters in pg_stat_all_tables, which heap
maintains via pgstat_count_heap_*. A non-heap AM that doesn't fake equivalent
counters will simply never be visited by autovacuum — plan to call
pgstat_count_heap_insert/_update/_delete (or the lower-level
pgstat_report_vacuum) from inside your own tuple ops from day one.
Registering a new AM
pg_am row via SQL: CREATE ACCESS METHOD myam TYPE INDEX HANDLER myam_handler;
(or TYPE TABLE). The handler function must already exist and have
signature myam_handler(internal) RETURNS index_am_handler (or
table_am_handler).
Handler function: PG_FUNCTION_INFO_V1(myam_handler); returning a
pointer to a static IndexAmRoutine/TableAmRoutine. Done in an extension's
shared library or in core.
Opclass(es) (index AM only): CREATE OPERATOR CLASS … DEFAULT FOR TYPE foo USING myam AS OPERATOR 1 …, FUNCTION 1 …;. Without at least one
opclass, the AM is useless — CREATE INDEX … USING myam (col) will fail
to find an opclass for col's type.
Catalog vs SQL: in-tree AMs (btree, brin, …) get a hard-coded pg_am
row via src/include/catalog/pg_am.dat, plus opclasses via
src/include/catalog/pg_opclass.dat, pg_amop.dat, pg_amproc.dat. See
the catalog-conventions skill.
default_table_access_method GUC controls which table AM CREATE TABLE
uses when no USING clause is given. check_default_table_access_method in
tableamapi.c validates it.
Things you almost certainly need an existing AM as reference for
- Parallel index build — see
brin.c for the modern pattern (BrinShared,
BrinLeader, tuplesort integration), nbtindex.c for the canonical one.
- Predicate locking (
ampredlocks=true) — see nbtree/gist. You must
call PredicateLock* in the right spots for SSI to work.
- Index-only scans (
amcanreturn) — btree, gist.
- WAL — every real AM uses a custom rmgr (see
wal-and-xlog skill).
Dummy AMs in src/test/modules skip WAL and so are useless past a crash.
- Vacuum two-pass with cycle id — nbtree (
BTCycleId) is the reference.
- Opclass validation —
amvalidate.c helpers (check_amop_signature,
identify_opfamily_groups).
- Bottom-up index deletion (heap's
index_delete_tuples + the
TM_IndexDeleteOp struct) — table AM side; nbtree drives it.
Files to read before touching this
src/include/access/amapi.h — entire file, ~336 lines, ~30 function pointers.
src/include/access/tableam.h — first ~900 lines is the struct; rest is
inline wrappers and helpers.
src/backend/access/index/{amapi,genam,indexam,amvalidate}.c — dispatch and
shared helpers.
src/backend/access/table/tableamapi.c — dispatch + required-callback Asserts.
src/test/modules/dummy_index_am/dummy_index_am.c — minimal valid handler.
src/backend/access/brin/brin.c (top ~200 lines, brinhandler function) —
modern-style handler with parallel build.
src/backend/access/nbtree/nbtree.c (top, bthandler) — canonical handler.
src/backend/access/heap/heapam_handler.c (heap_tableam_handler,
heapam_methods near line 2665) — the only table AM.
doc/src/sgml/indexam.sgml, doc/src/sgml/tableam.sgml — user-facing chapters
with extra discussion of locking and semantic requirements.
Cross-references
.claude/skills/wal-and-xlog/SKILL.md — durability for the AM: rmgr design, custom rmgr vs Generic WAL.
.claude/skills/catalog-conventions/SKILL.md — pg_am.dat, opclass / strategy / support-function registration via pg_opclass.dat / pg_amop.dat / pg_amproc.dat.
.claude/skills/executor-and-planner/SKILL.md — amcostestimate interaction with the planner; bitmap-scan plumbing.
.claude/skills/locking/SKILL.md — AM-specific lock-ordering rules (e.g. nbtree left-to-right buffer coupling).
.claude/skills/extension-development/SKILL.md — CREATE ACCESS METHOD from an extension; PGXS / meson packaging.
.claude/skills/testing/SKILL.md — amcheck integration; isolation specs for AM concurrency.
knowledge/subsystems/access-nbtree.md, knowledge/subsystems/access-heap.md — canonical AM deep-dives.
1---2name: access-method-apis3description: Implement or modify a PostgreSQL pluggable index AM or table AM — covers IndexAmRoutine callbacks (ambuild, aminsert, amgettuple, amgetbitmap, ambulkdelete, amvacuumcleanup, amparallelrescan), TableAmRoutine callbacks (scan_begin, scan_getnextslot, tuple_insert, tuple_insert_speculative, slot_callbacks, index_fetch_*), opclass / strategy numbers / support functions, TID semantics for non-heap stores, genam.c and tableam.h wrappers, plus CREATE ACCESS METHOD + pg_am / pg_opclass / pg_amproc / pg_amop catalog registration. Use whenever a PG patch implements or modifies an index AM (btree variant, hash, gin, gist, spgist, brin, custom) or a table AM (heap, columnar, in-memory, custom store), or designs TID semantics for a non-heap storage. Skip user-facing "which index type should I use" / "should this be a btree or hash index" advice, EXPLAIN tuning questions, GIN / GIST / btree CONCURRENTLY operational guidance, application-side ORM index hints, and non-PG storage engines (MySQL InnoDB / MyRocks, RocksDB, Leve4---56# Access-method APIs — operational guide78This skill is for code that **implements** an AM (or significantly extends one).9Read-only "how does btree work" questions don't need it — go straight to the10per-AM README.1112## Two completely separate plug points1314| | Index AM | Table AM |15|---|---|---|16| Struct | `IndexAmRoutine` in `src/include/access/amapi.h` | `TableAmRoutine` in `src/include/access/tableam.h` |17| Resolver | `GetIndexAmRoutine(amhandler)` in `src/backend/access/index/amapi.c` | `GetTableAmRoutine(amhandler)` in `src/backend/access/table/tableamapi.c` |18| Catalog | `pg_am.amtype = 'i'` | `pg_am.amtype = 't'` |19| Executor wrapper | `genam.c` / `indexam.c` | inline wrappers in `tableam.h` (`table_beginscan`, `table_tuple_insert`, …) |20| Canonical impl | `nbtree.c`, also `brin.c`, `gin.c`, `gist.c`, `hash.c`, `spgist.c` | `heapam_handler.c` (the only in-tree one) |21| Minimal stub | `src/test/modules/dummy_index_am/dummy_index_am.c` | none in tree |22| Added | Index AM API since 9.6 (`amapi.h`); table AM since v12 | |2324Both APIs work the same way at the dispatch level: a `pg_am` row's `amhandler`25column names a SQL-callable C function (`PG_FUNCTION_INFO_V1`) that returns a26pointer to a **statically allocated** `IndexAmRoutine`/`TableAmRoutine`. The27core never copies or frees the struct.2829```c30Datum31myhandler(PG_FUNCTION_ARGS)32{33 static const IndexAmRoutine amroutine = { .type = T_IndexAmRoutine, ... };34 PG_RETURN_POINTER(&amroutine);35}36```3738## Index AM (IndexAmRoutine)3940### Flag/property fields (~22 booleans + counts)41Declarative facts the planner needs *before* it ever calls a function pointer:4243- `amstrategies` — count of operator strategies (e.g. btree has 5: `<`, `<=`, `=`,44 `>=`, `>`). 0 if the AM doesn't use a fixed strategy set (gin/gist).45- `amsupport` — count of mandatory support functions per opclass.46- `amoptsprocnum` — opclass-options procedure number, or 0.47- `amcanorder`, `amcanorderbyop`, `amcanhash`, `amcanbackward`, `amcanunique`,48 `amcanmulticol`, `amoptionalkey`, `amsearcharray`, `amsearchnulls`,49 `amstorage`, `amclusterable`, `ampredlocks`, `amcanparallel`,50 `amcanbuildparallel`, `amcaninclude`, `amusemaintenanceworkmem`,51 `amsummarizing`, `amconsistentequality`, `amconsistentordering`.52- `amparallelvacuumoptions` — bitmask of `VACUUM_OPTION_*` (`PARALLEL_BULKDEL`,53 `PARALLEL_COND_CLEANUP`, `PARALLEL_CLEANUP`). Set to `VACUUM_OPTION_NO_PARALLEL`54 (= 0) to opt out of parallel vacuum entirely — the right default for a brand-new AM.55- `amkeytype` — fixed key type OID, or `InvalidOid` if variable.5657### Function pointers — mandatory58Must be non-NULL (planner/executor will dereference unconditionally):5960| Callback | Purpose |61|---|---|62| `ambuild` | Build a new index from scratch over `heapRelation`. Drives parallel build internally. |63| `ambuildempty` | Build the **init fork** for unlogged indexes. |64| `aminsert` | Insert one tuple. Called inside `ExecInsertIndexTuples`. |65| `ambulkdelete` | Vacuum pass that drops index entries whose heap TID matches a callback (returns `IndexBulkDeleteResult *`). |66| `amvacuumcleanup` | Final vacuum pass (returns `IndexBulkDeleteResult *`); may return stats and reclaim empty pages. |67| `amcostestimate` | Planner cost callback. Fill in start/total/selectivity/correlation/pages. |68| `amoptions` | Parse `WITH (...)` reloptions via `build_reloptions`. May return NULL. |69| `amvalidate` | Sanity-check an opclass at `CREATE OPERATOR CLASS` time. |70| `ambeginscan` | Must call `RelationGetIndexScan()` and return its result. |71| `amrescan` | (Re)bind scan keys. |72| `amendscan` | Tear down scan-private state (don't free the IndexScanDesc itself). |7374> **GOTCHA — `ambeginscan` return identity.**75> `ambeginscan` **MUST return the exact `IndexScanDesc` that `RelationGetIndexScan()`76> gave it** — not a copy, not a wrapper. `index_endscan` (in `genam.c`) reaches77> inside that struct *after* the AM's `amendscan` has already run, so any78> reallocation breaks teardown. The comment at the top of `genam.c` calls this79> "kinda ugly". Allocate your private state separately and hang it off80> `scan->opaque`.8182### Function pointers — optional (may be NULL)83`aminsertcleanup`, `amcanreturn` (for index-only scans), `amgettreeheight`,84`amproperty`, `ambuildphasename`, `amadjustmembers`, `amgettuple` (NULL ⇒ no85plain indexscan, only bitmap), `amgetbitmap` (NULL ⇒ no bitmap scan),86`ammarkpos`/`amrestrpos` (NULL ⇒ no mark/restore — fine if `amcanbackward=false`87or no merge-join support is needed), the three `amestimate/init/parallelrescan`88parallel-scan hooks (required iff `amcanparallel=true`), `amtranslatestrategy`/89`amtranslatecmptype`.9091You need **either** `amgettuple` or `amgetbitmap` (typically both — gin is92bitmap-only).9394### Lifecycle — build / insert / scan / vacuum9596```97CREATE INDEX → ambuild98SELECT ... → ambeginscan → amrescan → (loop) amgettuple|amgetbitmap → amendscan99INSERT/UPDATE → (per row) aminsert → (once at end of statement) aminsertcleanup100VACUUM → ambulkdelete (may be called many times) → amvacuumcleanup101DROP INDEX → catalog work only; storage smgr handles file102```103104`amvalidate` runs at `CREATE OPERATOR CLASS` / `ALTER OPERATOR FAMILY ADD`. It105should check that all required strategy numbers and support function numbers106are present and have sane signatures. See `amvalidate.c` for the shared107helpers (`identify_opfamily_groups`, `check_amop_signature`, etc.).108109### Opclass / strategy / support function110111`pg_amop` rows declare operators (`<`, `=`, `&&`, …) and tag each with a112**strategy number** that's AM-private (btree: 1=less, 5=greater; gist:1131..n varies per opclass). `pg_amproc` rows declare **support functions**, also114numbered 1..`amsupport` per AM. The AM code looks them up via115`index_getprocinfo()` (cached FmgrInfo) inside its callbacks.116117`amtranslatestrategy`/`amtranslatecmptype` is the bridge to generic118`CompareType` enum values (`COMPARE_LT` etc.) so the planner can reason about119btree-compatible opclasses on other AMs.120121## Table AM (TableAmRoutine)122123Pluggable since v12. There is exactly **one in-tree implementation: heap.**124The struct surface is much larger than the index-AM one (~45-callback struct;125`tableamapi.c::GetTableAmRoutine` asserts 37 of them non-NULL, the rest have126soft "may be NULL" contracts inside specific call sites) because table AMs own127MVCC, storage layout, vacuum, sampling, and the per-tuple slot type.128129### What "heap is just a table-AM" means in practice130131- The `Relation` cache stores `rd_tableam` (a `TableAmRoutine *`); every132 `heap_*` style access in the executor went through a `table_*` inline wrapper133 in `tableam.h` since v12.134- The TID-addressed visibility map, FSM, and toast machinery are **not** part135 of the API — they're heap implementation details. A non-heap AM has to136 reinvent or skip them.137- WAL, buffer manager, smgr, snapshots are still core — table AMs live above138 bufmgr.139140### Slot interface141`slot_callbacks(rel)` returns a `TupleTableSlotOps *` (e.g. `TTSOpsHeapTuple`,142`TTSOpsBufferHeapTuple`, `TTSOpsMinimalTuple`, `TTSOpsVirtual`). All tuple143movement in/out of the AM is through `TupleTableSlot`; raw `HeapTuple` only144appears inside the heap AM. A new AM defines its own `TupleTableSlotOps`.145146### Scan family147148| Group | Callbacks |149|---|---|150| Plain | `scan_begin`, `scan_end`, `scan_rescan`, `scan_getnextslot` |151| TID range | `scan_set_tidrange`, `scan_getnextslot_tidrange` (both or neither) |152| Parallel | `parallelscan_estimate`, `parallelscan_initialize`, `parallelscan_reinitialize` |153| Index fetch | `index_fetch_begin`, `index_fetch_reset`, `index_fetch_end`, `index_fetch_tuple` |154| Analyze | `scan_analyze_next_block`, `scan_analyze_next_tuple` |155| Sample | `scan_sample_next_block`, `scan_sample_next_tuple` |156157### Tuple ops158`tuple_insert`, `tuple_insert_speculative`, `tuple_complete_speculative`,159`multi_insert`, `tuple_delete`, `tuple_update`, `tuple_lock`,160`tuple_fetch_row_version`, `tuple_tid_valid`, `tuple_get_latest_tid`,161`tuple_satisfies_snapshot`, `index_delete_tuples`.162163Return type `TM_Result` (`TM_Ok`, `TM_Invisible`, `TM_SelfModified`, `TM_Updated`,164`TM_Deleted`, `TM_BeingModified`, `TM_WouldBlock`) carries MVCC outcomes.165`TU_UpdateIndexes` tells the executor which indexes still need re-insert after166update (`TU_None` / `TU_All` / `TU_Summarizing` enables HOT-like167optimizations for non-heap AMs).168169### DDL / storage170`relation_set_new_filelocator`, `relation_nontransactional_truncate`,171`relation_copy_data`, `relation_copy_for_cluster`, `relation_vacuum`,172`relation_size`, `relation_needs_toast_table`, `relation_estimate_size`,173`index_build_range_scan`, `index_validate_scan`.174175### All mandatory176`tableamapi.c::GetTableAmRoutine` runs 37 `Assert(routine->X != NULL)` lines.177Only `finish_bulk_insert` and the TID-range pair are truly optional.178179### The hard part: TID semantics180181`ItemPointer` is a 6-byte (block, offset) pair, baked into the index AM182interface, WAL, syscaches, and `pg_class` rowcount estimation. A table AM that183doesn't store tuples in (block, offset) pages (columnar, LSM, external) has to184**fabricate stable, 48-bit, monotone-ish TIDs** for every row and route185`index_fetch_tuple` and `tuple_fetch_row_version` against them. This is the186single biggest reason most experimental table AMs never become production187ready. See `MaxHeapTuplesPerPage` and the warnings in `tableam.sgml`.188189Stats-leakage corollary: autovacuum's per-table scheduling is driven by the190`n_dead_tup` / `n_live_tup` counters in `pg_stat_all_tables`, which heap191maintains via `pgstat_count_heap_*`. A non-heap AM that doesn't fake equivalent192counters will simply never be visited by autovacuum — plan to call193`pgstat_count_heap_insert`/`_update`/`_delete` (or the lower-level194`pgstat_report_vacuum`) from inside your own tuple ops from day one.195196## Registering a new AM1971981. **`pg_am` row** via SQL: `CREATE ACCESS METHOD myam TYPE INDEX HANDLER myam_handler;`199 (or `TYPE TABLE`). The handler function must already exist and have200 signature `myam_handler(internal) RETURNS index_am_handler` (or201 `table_am_handler`).2022032. **Handler function**: `PG_FUNCTION_INFO_V1(myam_handler);` returning a204 pointer to a static `IndexAmRoutine`/`TableAmRoutine`. Done in an extension's205 shared library or in core.2062073. **Opclass(es)** (index AM only): `CREATE OPERATOR CLASS … DEFAULT FOR TYPE208 foo USING myam AS OPERATOR 1 …, FUNCTION 1 …;`. Without at least one209 opclass, the AM is useless — `CREATE INDEX … USING myam (col)` will fail210 to find an opclass for `col`'s type.2112124. **Catalog vs SQL**: in-tree AMs (btree, brin, …) get a hard-coded `pg_am`213 row via `src/include/catalog/pg_am.dat`, plus opclasses via214 `src/include/catalog/pg_opclass.dat`, `pg_amop.dat`, `pg_amproc.dat`. See215 the `catalog-conventions` skill.2162175. **`default_table_access_method` GUC** controls which table AM `CREATE TABLE`218 uses when no `USING` clause is given. `check_default_table_access_method` in219 `tableamapi.c` validates it.220221## Things you almost certainly need an existing AM as reference for222223- **Parallel index build** — see `brin.c` for the modern pattern (`BrinShared`,224 `BrinLeader`, `tuplesort` integration), `nbtindex.c` for the canonical one.225- **Predicate locking** (`ampredlocks=true`) — see `nbtree`/`gist`. You must226 call `PredicateLock*` in the right spots for SSI to work.227- **Index-only scans** (`amcanreturn`) — btree, gist.228- **WAL** — every real AM uses a custom rmgr (see `wal-and-xlog` skill).229 Dummy AMs in `src/test/modules` skip WAL and so are useless past a crash.230- **Vacuum two-pass with cycle id** — nbtree (`BTCycleId`) is the reference.231- **Opclass validation** — `amvalidate.c` helpers (`check_amop_signature`,232 `identify_opfamily_groups`).233- **Bottom-up index deletion** (heap's `index_delete_tuples` + the234 `TM_IndexDeleteOp` struct) — table AM side; nbtree drives it.235236## Files to read before touching this237238- `src/include/access/amapi.h` — entire file, ~336 lines, ~30 function pointers.239- `src/include/access/tableam.h` — first ~900 lines is the struct; rest is240 inline wrappers and helpers.241- `src/backend/access/index/{amapi,genam,indexam,amvalidate}.c` — dispatch and242 shared helpers.243- `src/backend/access/table/tableamapi.c` — dispatch + required-callback Asserts.244- `src/test/modules/dummy_index_am/dummy_index_am.c` — minimal valid handler.245- `src/backend/access/brin/brin.c` (top ~200 lines, `brinhandler` function) —246 modern-style handler with parallel build.247- `src/backend/access/nbtree/nbtree.c` (top, `bthandler`) — canonical handler.248- `src/backend/access/heap/heapam_handler.c` (`heap_tableam_handler`,249 `heapam_methods` near line 2665) — the only table AM.250- `doc/src/sgml/indexam.sgml`, `doc/src/sgml/tableam.sgml` — user-facing chapters251 with extra discussion of locking and semantic requirements.252253## Cross-references254255- `.claude/skills/wal-and-xlog/SKILL.md` — durability for the AM: rmgr design, custom rmgr vs Generic WAL.256- `.claude/skills/catalog-conventions/SKILL.md` — `pg_am.dat`, opclass / strategy / support-function registration via `pg_opclass.dat` / `pg_amop.dat` / `pg_amproc.dat`.257- `.claude/skills/executor-and-planner/SKILL.md` — `amcostestimate` interaction with the planner; bitmap-scan plumbing.258- `.claude/skills/locking/SKILL.md` — AM-specific lock-ordering rules (e.g. nbtree left-to-right buffer coupling).259- `.claude/skills/extension-development/SKILL.md` — `CREATE ACCESS METHOD` from an extension; PGXS / meson packaging.260- `.claude/skills/testing/SKILL.md` — amcheck integration; isolation specs for AM concurrency.261- `knowledge/subsystems/access-nbtree.md`, `knowledge/subsystems/access-heap.md` — canonical AM deep-dives.