TableAmRoutine - your way of integrating a custom storage engine into PostgreSQL
Part 1 of 4 in Reading PostgreSQLEver thought about building a custom storage engine for PostgreSQL like columnar, in-memory, compressed without rewriting the planner or executor? That's exactly what TableAmRoutine enables. You just implement ~30 callbacks, tell PostgreSQL where to look, and the rest of the engine works as-is.
Part 1: What is TableAmRoutine?
TableAmRoutine is the Table Access Method (AM) API - a vtable (struct of function pointers) that defines the complete interface between the PostgreSQL executor/planner and a storage engine. This is a core extensibility feature that lets change how PostgreSQL store and retrieve the data without changing the planner and executor layer.
Every table in PostgreSQL has a pg_am catalog entry. When the executor needs to do anything with a table be it scan, insert, vacuum. It goes through rel->rd_tableam , which points to the registered TableAmRoutine for that table.
Part 2: Some understanding with an Example
CREATE TABLE sample (id int primary key, col1 int);
SELECT * FROM pg_am;
oid | amname | amhandler | amtype
------+--------+----------------------+--------
2 | heap | heap_tableam_handler | t
403 | btree | bthandler | i
405 | hash | hashhandler | i
783 | gist | gisthandler | i
2742 | gin | ginhandler | i
4000 | spgist | spghandler | i
3580 | brin | brinhandler | i
(7 rows)
SELECT relname, relam FROM pg_class WHERE oid = 'sample'::regclass;
relname | relam
---------+-------
sample | 2
(1 row)Now if you see the example provided, relam of "sample" table points to 2 i.e, heap in pg_am. Now when we ran a query on the top of the table "sample", now postgresql calls the handler function i.e heap_tableam_handler which returns the TableAmRoutine containing all the necessary function definitions can be used to perform the operations like SELECT, DMLs, VACUUM etc..
Reference:
heapam_methodsis the Heap's TableAmRoutine exists ataccess/heap/heapam_handler.c
Part 3: The Core Design
TableAmRoutine
An API struct for a table AM (Access Methods), which should be allocated in a server-lifetime, typically as a static const struct .
typedef struct TableAmRoutine {
NodeTag type; // must be T_TableAmRoutine
// ~30 function pointers grouped by category
const TupleTableSlotOps *(*slot_callbacks)(Relation rel);
TableScanDesc (*scan_begin)(...);
bool (*tuple_insert)(...);
// ...
} TableAmRoutine;Reference:
tableam.h
Now whatever the custom storage engine we are writing should define the respective function definitions and map to the respective pointers in the TableAmRoutine. To explain this further, we are going to use existing Heap Access Methods.
Heap AM Vtable
The heap AM implements its own TableAmRoutine vtable as a static const struct as explained above in the heapam_handler.c
static const TableAmRoutine heapam_methods = {
.type = T_TableAmRoutine,
.slot_callbacks = heapam_slot_callbacks,
.scan_begin = heap_beginscan,
.tuple_insert = heapam_tuple_insert,
.tuple_update = heapam_tuple_update,
.relation_vacuum = heap_vacuum_rel,
// ...
};The Handler Function
This is the entry point which PostgreSQL calls to get the particular custom AM's TableAmRoutine. In heap case, it is defined in the file heapam_handler.c as:
Datum
heap_tableam_handler(PG_FUNCTION_ARGS)
{
PG_RETURN_POINTER(&heapam_methods);
}If you observe the name of the handler function heap_tableam_handler . It is the same name stored in the pg_am 's amhandler shown in the part 2.
Register the Access Method
CREATE ACCESS METHOD heap TYPE TABLE HANDLER heap_tableam_handler;This triggers CreateAccessMethod() in amcmds.c, which:
1. Resolves heap_tableam_handler and validates it returns TABLE_AM_HANDLEROID
2. Inserts one row into pg_am with amtype = 't' (t for table, i for index)
3. Records a dependency: dropping the handler function also drops the AM
Usage
-- New table:
CREATE TABLE my_table (id int, data text) USING heap; -- default is heap anyway, used just for example
-- Existing table (rewrites data):
ALTER TABLE existing_table SET ACCESS METHOD heap;
-- Make it the default for the session:
SET default_table_access_method = heap;Part 4: How PostgreSQL accesses this when querying?
Every time PostgreSQL tries to open a Relation/Table, it usually loads all the required information into the struct Relation (alias of RelationData ) be it the tuple descriptor, table's OID, PK information etc. In those, there are two properties related to access methods i.e, oid rd_amhandler the OID of the access method (like heap) using by this table and TableAmRoutine *rd_tableam containing the vtable given by the amhandler , in our case methods defined in heapam_methods.
Code flow path in case of relation_open()
Now let's see what is the path of the code which populates the access methods when a relation/table is opened using the function relation_open() .
Note: The nested
=> { }style below is simplified pseudocode showing the call chain - seerelation.c,relcache.cfor the actual C source.
Entry Point: relation_open() in the file common/relation.c
Relation relation_open(oid relationId) {
Relation r;
// .....
r = RelationIdGetRelation(relationId); => {
Relation rd;
// ..... Here looks into cache, if hit return that, else build Relation
// Cache Miss: Building from scratch
rd = RelationBuildDesc(relationId, true); => {
// Here all the necessary information will be populated
Relation relation;
// ......
else if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind) ||
relation->rd_rel->relkind == RELKIND_SEQUENCE) {
// OUR AREA OF INTEREST
RelationInitTableAccessMethod(relation); => {
// Here, the cache lookup will be done to get
// the OID of Access method using by this relation
tuple = SearchSysCache1(AMOID,
ObjectIdGetDatum(relation->rd_rel->relam));
aform = (Form_pg_am) GETSTRUCT(tuple);
relation->rd_amhandler = aform->amhandler; //OID saved here
/*
* Now we can fetch the table AM's API struct
*/
InitTableAmRoutine(relation); => {
relation->rd_tableam = GetTableAmRoutine(relation->rd_amhandler); => {
datum = OidFunctionCall0(amhandler);
routine = (TableAmRoutine *) DatumGetPointer(datum);
// Some checks will be done here
return routine; // THIS IS OUR TABLEAMROUTINE
}
}
}
}
// ......
return relation;
}
// .....
return rd;
}
// .....
return r;
}Part 5: Callback Groups
Below table summarizing the callback pointer functions declared in TableAmRoutine
Group | Key Callbacks | Purpose |
|---|---|---|
Slot |
| What kind of |
Sequential Scan |
| Full table scans |
TID Range Scan |
| Range-restricted scans |
Parallel Scan |
| Parallel query support |
Index Scan |
| Fetching tuples via index TIDs |
DML |
| Row mutations |
Bulk Insert |
| COPY/bulk load path |
DDL |
| CREATE TABLE, TRUNCATE, CLUSTER |
Maintenance |
| VACUUM, ANALYZE, CREATE INDEX |
Planner |
| Cost estimation |
Executor |
| Bitmap scans, TABLESAMPLE |
Part 6: Example - How executor uses it?
Let's see the case of Executor, how it uses the access methods. The executor never calls these function pointers directly. Instead it uses table_* wrapper functions (tableam.h) which dispatch through rd_tableam :
static inline TableScanDesc
table_beginscan(Relation rel, Snapshot snapshot, int nkeys, ScanKeyData *key)
{
uint32 flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE;
return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, NULL, flags);
// ^^^^^^^^^^^^ vtable dispatch which calls "heap_beginscan" internally
}Part 7: Why TableAmRoutine Matters
Pluggable storage - You can write a custom AM (columnar, in-memory, compressed, etc.) without touching the core executor. Just implement the ~30 callbacks and register with
CREATE ACCESS METHOD.Separation of concerns - MVCC visibility, tuple format, physical layout, and vacuum strategy are all encapsulated inside the AM. The executor only sees slots and TIDs.
Real-world uses - Citus's columnar AM, Zedstore (experimental), and third-party AMs like Orioledb all use this interface. The default
heapAM (DEFAULT_TABLE_ACCESS_METHOD = "heap", tableam.h:29) is just one implementation.TOAST delegation -
relation_needs_toast_table,relation_toast_am, andrelation_fetch_toast_slicelet each AM decide its own large-value storage strategy.
Closing notes
This post covered the interface, the wiring, and how PostgreSQL loads your AM at runtime. What it didn't cover is which callbacks are mandatory versus stubbable, TOAST integration, and the visibility/snapshot contract your AM needs to honour, those deserve their own deep dives. For now, heapam_handler.c and the Orioledb source are the best two codebases to have open while you build.
Enjoyed this post?
7 reactions