Building a Table Access Method, Part 2

In Part 1 the difference between table access methods and foreign data wrappers was covered. This post starts implementing one.

This post starts by looking at what you actually want to get out of an in-memory table, drawing on research into why memory-resident engines are fast, then starts writing a table access method (TAM) named pg_memtable to work towards that goal. It sets up the extension, implements the handler function, and walks through the callbacks that need to be defined to get CREATE TABLE events (...) USING memtable working end to end.

Looking into why in-memory tables are fast

It is tempting to assume the speedup from an in-memory table just comes from skipping disk I/O, but the research on this is more specific than that.

Looking into OLTP Through the Looking Glass, and What We Found There Harizopoulos, Abadi, Madden, and Stonebraker took a real disk-based OLTP engine (Shore), ran a TPC-C workload against it, then progressively stripped out one subsystem at a time and remeasured:

Our initial implementation, running on a modern desktop machine, ran about 640 transactions per second (TPS). We then modified it by removing different features from the engine one at a time… until we were left with a tiny kernel of query processing code that could process 12700 TPS.

That is a 20x improvement, and looking at the breakdown in the paper shows exactly where the time was spent:

Component Share of instructions
Buffer manager 34.6%
Locking 16.3%
Latching 14.2%
Logging 11.9%
Hand-coded optimizations 16.2%
Useful work 6.8%

Only 6.8% of the CPU time was the actual query logic. The rest was overhead from architecture built around the assumption that data might not be in memory. In particular, the share of the buffer manager is specifically discussed in the paper:

Buffer management. A main memory database system does not need to access pages through a buffer pool, eliminating a level of indirection on every record access.

Diaconu et al. draw similar conclusions in Hekaton: SQL Server’s Memory-Optimized OLTP Engine, the production engine behind SQL Server’s In-Memory OLTP, making the same case in terms of orders of magnitude rather than a percentage breakdown:

To go 10X faster, the engine must execute 90% fewer instructions and yet still get the work done. To go 100X faster, it must execute 99% fewer instructions.

It also explains why simply putting data in memory is not enough on its own:

A simple key lookup in a B-tree index may require thousands of instructions even when all pages are in memory.

It also names the specific resources that suffer under contention:

Scalability suffers when the system has shared memory locations that are updated at high rate such as latches and spinlocks and highly contended resources such as the lock manager, the tail of the transaction log, or the last page of a B-tree index.

Hekaton’s answer to that is to remove locks and latches entirely from its hot paths:

All Hekaton’s internal data structures… are entirely latch-free (lock-free). There are no latches or spinlocks on any performance-critical paths in the system.

The common thread in both papers is that the difference does not come from “RAM is faster than disk”. It comes from an engine that can assume data is always resident being able to delete an entire layer of machinery that only exists to cope with data that might not be.

With pg_memtable the intention is to see how far it can get in creating a fast in-memory table access method inspired by the papers above. Following this idea, this series is going to first follow a standard approach to implementing an in-memory table access method, then consider what can be done to eliminate precisely the items that were found to cause a slowdown. The result are compared with an unlogged heap table to see if it can go faster (spoiler: it can).

Extension configuration

Table access methods are easiest to deploy as an extension to Postgres, so the first step is to set up all the configuration necessary for an extension.

The details for setting up an extension were covered in The Nobel Art of Writing PostgreSQL Extensions, so it is sufficient to quickly go over the boilerplate needed before getting to the table access method itself.

The extension control file memtable.control contains information about the extension, and the full file is:

comment = 'MemTable - in-memory table using B+-tree for primary index'
default_version = '0.1'
module_pathname = '$libdir/memtable'
relocatable = false
schema = 'memtable'

The fields have different meanings and usage:

Field Description
comment A short description of the extension, shown by tools like \dx
default_version The version installed unless you require a specific version
module_pathname Substituted for every MODULE_PATHNAME placeholder in the extension script.
relocatable Whether the extension’s objects can be moved to a different schema after creation.
schema Forces the extension into exactly this schema. Only meaningful when relocatable is false

As discussed in The Nobel Art of Writing PostgreSQL Extensions, the extension install script memtable--0.1.sql contains the code that is executed when installing (version 0.1 of) the extension. This is the file where you create the access method object using the CREATE ACCESS METHOD statement:

CREATE FUNCTION memtable_handler(internal) RETURNS table_am_handler
    AS 'MODULE_PATHNAME' LANGUAGE C STRICT;

CREATE ACCESS METHOD memtable TYPE TABLE HANDLER memtable.memtable_handler;

To create an access method, you need a handler function, which is what the first line creates. The handler function returns a table access method handler, which contains all the callbacks that are going to be used for the implementation. This is what the pseudo-type table_am_handler represents. It exists only so the function signature can be checked at CREATE ACCESS METHOD time (if you try to convert anything to a table_am_handler it will throw an error).

Following the definition of the handler function, the CREATE ACCESS METHOD ... TYPE TABLE is what adds an access method to Postgres.

If you install the extension and run CREATE EXTENSION, you will see that the extension is properly installed and the table access method is defined. If you list the access methods, you will see the standard ones installed, but also your new access method.

mats=# CREATE EXTENSION memtable;
CREATE EXTENSION
mats=# \dA
List of access methods
   Name   | Type  
----------+-------
 brin     | Index
 btree    | Index
 gin      | Index
 gist     | Index
 hash     | Index
 heap     | Table
 memtable | Table
 spgist   | Index
(8 rows)

Understanding how relations are stored in the catalog

Before implementing the handler function, some basic understanding of how relations are represented in the catalog is needed.

All relations (including indexes) are stored in the pg_class catalog table. There are a lot of fields in the table, but there are a few fields that are relevant to the implementation of a table access method:

Field Description
oid The object identity (OID) of the relation.
relname The name of the table.
relnamespace The OID of the schema the table lives in.
relfilenode A separate number for the physical storage of the relation.
relam The OID of the access method.
reltablespace The tablespace.

Postgres tables are stored in a pg_class as a two-level structure: there is an OID for the relation and never changes for the lifetime of the relation, and there is a file node identifier that represents the actual storage, separate from the name used to refer to it. The first field is the oid and is generated once when the relation is created, the second field is the relfilenode. Typical examples are TRUNCATE, CLUSTER, and VACUUM FULL. Each of these commands builds new physical storage and swaps it in under the same OID.

Implementing the handler function

The sole purpose of a handler function is to hand back a pointer to a static struct containing all the callbacks. It needs to be implemented in C since there is no way to construct a table_am_handler object in PL/pgSQL. Fortunately, it is very simple to implement: you create your callback structure, set the type of it (because it is formally a node in Postgres parlance), add pointers to all the callbacks, and return a pointer to the structure:

static const TableAmRoutine memtable_methods = {
    .type = T_TableAmRoutine,
    /* ... every callback goes here ... */
};

PG_FUNCTION_INFO_V1(memtable_handler);

Datum
memtable_handler(PG_FUNCTION_ARGS)
{
    PG_RETURN_POINTER(&memtable_methods);
}

Now, what are the callbacks that you put into it?

Implementing CREATE TABLE

Since the goal of this post is to implement support for CREATE TABLE, the first question is how a table is created in Postgres. In particular, you need to know when and how the different callbacks are called because while creating a table, there are particular pieces of information that are not available, depending on where in the sequence you are. This affects when and how you can fetch and override information.

As an example, assume that you are executing the statement:

CREATE TABLE events (
    id serial,
    val text
) USING memtable;

After parsing, CREATE TABLE will be processed inside standard_ProcessUtility, which in turn calls ProcessUtilitySlow.

Note: The reason for this split is that standard_ProcessUtility itself deals only with utility commands which do not provide event trigger support. Commands that have event trigger support are passed down to ProcessUtilitySlow, which contains the necessary infrastructure for dealing with event triggers.

The full sequence diagram for the calls is:

sequenceDiagram
    actor caller
    participant tcop
    participant commands
    participant catalog
    participant TAM

    caller->>tcop: ProcessUtility
    tcop->>commands: DefineRelation
    commands->>catalog: heap_create_with_catalog
    catalog->>TAM: relation_set_new_filelocator
    catalog->>catalog: AddNewRelationTuple
    catalog-->>commands: return
    commands-->>tcop: return
    tcop->>catalog: NewRelationCreateToastTable
    catalog->>TAM: relation_needs_toast_table

From the diagram above, there are a few things worth noting:

  • ProcessUtilitySlow expands the statement into a list of sub-statements, which are executed in sequence. In this case, it will:
    1. Define the relation by calling DefineRelation.
    2. Create the toast table, if necessary, by calling NewRelationCreateToastTable, which calls relation_needs_toast_table on the table access method to decide whether one is needed.
  • DefineRelation will define the new relation by:
    1. Assigning a new OID to the relation that was not previously used.
    2. Creating a relation file locator for the relation. More about that soon.
    3. Identifying what table access method should be used.
    4. Calling relation_set_new_filelocator of the table access method to set the file locator for the relation.
    5. Registering the relation in pg_class by calling AddNewRelationTuple.

Note: Since the catalog tuple is only registered in step 5 above, after relation_set_new_filelocator was already called in step 4, there is no way to look up the relation in the catalog inside relation_set_new_filelocator even if you wanted to. This is something that is relevant in later posts.

This means that the table access method needs to define two callbacks for this to work: relation_set_new_filelocator and relation_needs_toast_table. A primary key implies an index, and building an index reaches a few more callbacks (relation_estimate_size, index_build_range_scan). That path is covered later in the series, once indexes for pg_memtable are introduced, so this example table has no primary key for now.

Implementing relation_set_new_filelocator

The relation_set_new_filelocator just sets a new file locator (that is, a new relation storage) for the relation. Right now, there is not much to do, so the function just defines the bare minimum. As actual storage is added, you will see this function contain a lot more.

static void
memtable_relation_set_new_filelocator(Relation rel,
                                       const RelFileLocator *newrlocator,
                                       char persistence,
                                       TransactionId *freezeXid,
                                       MultiXactId *minmulti)
{
    *freezeXid = InvalidTransactionId;
    *minmulti = InvalidMultiXactId;
}

Parameter freezeXid and minmulti are not used by pg_memtable, so they are just set to an invalid value.

Parameter rel is the relation being created and persistence tells you whether this is a regular table, an unlogged table, or a temporary table. As a storage implementor, you might want to treat these differently.

Parameter newrlocator is a file locator for the relation. It is a small struct with everything needed to physically locate the files of a relation:

typedef struct RelFileLocator
{
    Oid            spcOid;       /* tablespace OID */
    Oid            dbOid;        /* database OID */
    RelFileNumber  relNumber;    /* relation file node number */
} RelFileLocator;

Note that the relNumber is the relfilenode, not the OID of the relation.

Implementing relation_needs_toast_table

The callback relation_needs_toast_table only needs to answer the question: “does this table need a TOAST table?” Since this first version of MemTable does not use TOAST tables, it just returns false.

static bool
memtable_relation_needs_toast_table(Relation rel)
{
    return false;
}

Where things stand currently

You now have the basics of a table access method that can create a table, but not do much else. It does not have storage, it does not support inserts or scans, and there is no way to define indexes for it. All of these will error right now, but you can still define a new table using it.

mats=# CREATE EXTENSION memtable;
CREATE EXTENSION
mats=# CREATE TABLE events (id serial, val text) USING memtable;
CREATE TABLE
mats=# \d+ events
                                                       Table "public.events"
 Column |  Type   | Collation | Nullable |              Default               | Storage  | Compression | Stats target | Description 
--------+---------+-----------+----------+------------------------------------+----------+-------------+--------------+-------------
 id     | integer |           | not null | nextval('events_id_seq'::regclass) | plain    |             |              | 
 val    | text    |           |          |                                    | extended |             |              | 
Not-null constraints:
    "events_id_not_null" NOT NULL "id"
Access method: memtable

As noted, trying to call INSERT or SELECT would generate an error (try it out!). That’s fine, the point of this post was CREATE TABLE itself, and the executor is now genuinely calling into your code to make it happen, which is the foundation everything else in this series builds on.

Before implementing INSERT and SELECT it is necessary to cover how shared memory works in Postgres, which the next post will focus on. The goal is to understand how shared memory is allocated and used by Postgres and how it can be used to implement the actual storage for the table access method.

Appendix: the full skeleton

The full code of the skeleton is given as an appendix. There is not yet a repository, and it is not going to be very interesting until at least the insert is created, so just be patient. You can test this code if you want for now, or you can wait until there is a repository after the post implementing INSERT and SELECT.

File memtable.control:

comment = 'MemTable - in-memory table using B+-tree for primary index'
default_version = '0.1'
module_pathname = '$libdir/memtable'
relocatable = false
schema = 'memtable'

File memtable--0.1.sql:

\echo Use "CREATE EXTENSION memtable" to load this file. \quit

CREATE FUNCTION memtable_handler(internal) RETURNS table_am_handler
    AS 'MODULE_PATHNAME' LANGUAGE C STRICT;

CREATE ACCESS METHOD memtable TYPE TABLE HANDLER memtable.memtable_handler;

File Makefile:

MODULES = memtable
EXTENSION = memtable
DATA = memtable--0.1.sql
PGFILEDESC = "memtable - in-memory table access method skeleton"

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

File memtable.c:

#include <postgres.h>
#include <fmgr.h>

#include <access/tableam.h>
#include <access/heapam.h>
#include <access/multixact.h>
#include <utils/rel.h>

PG_MODULE_MAGIC;

#define NOT_IMPLEMENTED \
    ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
                    errmsg("memtable callback %s not yet implemented", __func__)))

/* ------------------------------------------------------------------------
 * Slot related callbacks
 * ------------------------------------------------------------------------ */

static const TupleTableSlotOps *
memtable_slot_callbacks(Relation rel)
{
    NOT_IMPLEMENTED;
    return NULL;
}

/* ------------------------------------------------------------------------
 * Table scan callbacks
 * ------------------------------------------------------------------------ */

static TableScanDesc
memtable_scan_begin(Relation rel, Snapshot snapshot,
                     int nkeys, struct ScanKeyData *key,
                     ParallelTableScanDesc pscan, uint32 flags)
{
    NOT_IMPLEMENTED;
    return NULL;
}

static void
memtable_scan_end(TableScanDesc scan)
{
    NOT_IMPLEMENTED;
}

static void
memtable_scan_rescan(TableScanDesc scan, struct ScanKeyData *key,
                      bool set_params, bool allow_strat,
                      bool allow_sync, bool allow_pagemode)
{
    NOT_IMPLEMENTED;
}

static bool
memtable_scan_getnextslot(TableScanDesc scan, ScanDirection direction,
                           TupleTableSlot *slot)
{
    NOT_IMPLEMENTED;
    return false;
}

static void
memtable_scan_set_tidrange(TableScanDesc scan, ItemPointer mintid,
                            ItemPointer maxtid)
{
    NOT_IMPLEMENTED;
}

static bool
memtable_scan_getnextslot_tidrange(TableScanDesc scan, ScanDirection direction,
                                    TupleTableSlot *slot)
{
    NOT_IMPLEMENTED;
    return false;
}

/* ------------------------------------------------------------------------
 * Parallel table scan related functions
 * ------------------------------------------------------------------------ */

static Size
memtable_parallelscan_estimate(Relation rel)
{
    NOT_IMPLEMENTED;
    return 0;
}

static Size
memtable_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan)
{
    NOT_IMPLEMENTED;
    return 0;
}

static void
memtable_parallelscan_reinitialize(Relation rel, ParallelTableScanDesc pscan)
{
    NOT_IMPLEMENTED;
}

/* ------------------------------------------------------------------------
 * Index scan callbacks
 * ------------------------------------------------------------------------ */

static struct IndexFetchTableData *
memtable_index_fetch_begin(Relation rel)
{
    NOT_IMPLEMENTED;
    return NULL;
}

static void
memtable_index_fetch_reset(struct IndexFetchTableData *data)
{
    NOT_IMPLEMENTED;
}

static void
memtable_index_fetch_end(struct IndexFetchTableData *data)
{
    NOT_IMPLEMENTED;
}

static bool
memtable_index_fetch_tuple(struct IndexFetchTableData *scan, ItemPointer tid,
                            Snapshot snapshot, TupleTableSlot *slot,
                            bool *call_again, bool *all_dead)
{
    NOT_IMPLEMENTED;
    return false;
}

/* ------------------------------------------------------------------------
 * Callbacks for non-modifying operations on individual tuples
 * ------------------------------------------------------------------------ */

static bool
memtable_tuple_fetch_row_version(Relation rel, ItemPointer tid,
                                  Snapshot snapshot, TupleTableSlot *slot)
{
    NOT_IMPLEMENTED;
    return false;
}

static bool
memtable_tuple_tid_valid(TableScanDesc scan, ItemPointer tid)
{
    NOT_IMPLEMENTED;
    return false;
}

static void
memtable_tuple_get_latest_tid(TableScanDesc scan, ItemPointer tid)
{
    NOT_IMPLEMENTED;
}

static bool
memtable_tuple_satisfies_snapshot(Relation rel, TupleTableSlot *slot,
                                   Snapshot snapshot)
{
    NOT_IMPLEMENTED;
    return false;
}

static TransactionId
memtable_index_delete_tuples(Relation rel, TM_IndexDeleteOp *delstate)
{
    NOT_IMPLEMENTED;
    return InvalidTransactionId;
}

/* ------------------------------------------------------------------------
 * Manipulations of physical tuples
 * ------------------------------------------------------------------------ */

static void
memtable_tuple_insert(Relation rel, TupleTableSlot *slot, CommandId cid,
                       int options, struct BulkInsertStateData *bistate)
{
    NOT_IMPLEMENTED;
}

static void
memtable_tuple_insert_speculative(Relation rel, TupleTableSlot *slot,
                                   CommandId cid, int options,
                                   struct BulkInsertStateData *bistate,
                                   uint32 specToken)
{
    NOT_IMPLEMENTED;
}

static void
memtable_tuple_complete_speculative(Relation rel, TupleTableSlot *slot,
                                     uint32 specToken, bool succeeded)
{
    NOT_IMPLEMENTED;
}

static void
memtable_multi_insert(Relation rel, TupleTableSlot **slots, int nslots,
                       CommandId cid, int options,
                       struct BulkInsertStateData *bistate)
{
    NOT_IMPLEMENTED;
}

static TM_Result
memtable_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
                       Snapshot snapshot, Snapshot crosscheck, bool wait,
                       TM_FailureData *tmfd, bool changingPart)
{
    NOT_IMPLEMENTED;
    return TM_Ok;
}

static TM_Result
memtable_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
                       CommandId cid, Snapshot snapshot, Snapshot crosscheck,
                       bool wait, TM_FailureData *tmfd,
                       LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes)
{
    NOT_IMPLEMENTED;
    return TM_Ok;
}

static TM_Result
memtable_tuple_lock(Relation rel, ItemPointer tid, Snapshot snapshot,
                     TupleTableSlot *slot, CommandId cid, LockTupleMode mode,
                     LockWaitPolicy wait_policy, uint8 flags,
                     TM_FailureData *tmfd)
{
    NOT_IMPLEMENTED;
    return TM_Ok;
}

/* finish_bulk_insert is the one optional callback -- left NULL below. */

/* ------------------------------------------------------------------------
 * DDL related functionality
 * ------------------------------------------------------------------------ */

static void
memtable_relation_set_new_filelocator(Relation rel,
                                       const RelFileLocator *newrlocator,
                                       char persistence,
                                       TransactionId *freezeXid,
                                       MultiXactId *minmulti)
{
    *freezeXid = InvalidTransactionId;
    *minmulti = InvalidMultiXactId;
}

static void
memtable_relation_nontransactional_truncate(Relation rel)
{
    NOT_IMPLEMENTED;
}

static void
memtable_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
{
    NOT_IMPLEMENTED;
}

static void
memtable_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
                                    Relation OldIndex, bool use_sort,
                                    TransactionId OldestXmin,
                                    TransactionId *xid_cutoff,
                                    MultiXactId *multi_cutoff,
                                    double *num_tuples, double *tups_vacuumed,
                                    double *tups_recently_dead)
{
    NOT_IMPLEMENTED;
}

static void
memtable_relation_vacuum(Relation rel, struct VacuumParams *params,
                          BufferAccessStrategy bstrategy)
{
    NOT_IMPLEMENTED;
}

static bool
memtable_scan_analyze_next_block(TableScanDesc scan, ReadStream *stream)
{
    NOT_IMPLEMENTED;
    return false;
}

static bool
memtable_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin,
                                  double *liverows, double *deadrows,
                                  TupleTableSlot *slot)
{
    NOT_IMPLEMENTED;
    return false;
}

static double
memtable_index_build_range_scan(Relation table_rel, Relation index_rel,
                                 struct IndexInfo *index_info, bool allow_sync,
                                 bool anyvisible, bool progress,
                                 BlockNumber start_blockno, BlockNumber numblocks,
                                 IndexBuildCallback callback, void *callback_state,
                                 TableScanDesc scan)
{
    NOT_IMPLEMENTED;
    return 0;
}

static void
memtable_index_validate_scan(Relation table_rel, Relation index_rel,
                              struct IndexInfo *index_info, Snapshot snapshot,
                              struct ValidateIndexState *state)
{
    NOT_IMPLEMENTED;
}

/* ------------------------------------------------------------------------
 * Miscellaneous functions
 * ------------------------------------------------------------------------ */

static uint64
memtable_relation_size(Relation rel, ForkNumber forkNumber)
{
    NOT_IMPLEMENTED;
    return 0;
}

static bool
memtable_relation_needs_toast_table(Relation rel)
{
    /* Queried during CREATE TABLE itself, so it can't just error out. */
    return false;
}

static Oid
memtable_relation_toast_am(Relation rel)
{
    NOT_IMPLEMENTED;
    return InvalidOid;
}

static void
memtable_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
                                     int32 attrsize, int32 sliceoffset,
                                     int32 slicelength, struct varlena *result)
{
    NOT_IMPLEMENTED;
}

/* ------------------------------------------------------------------------
 * Planner related functions
 * ------------------------------------------------------------------------ */

static void
memtable_relation_estimate_size(Relation rel, int32 *attr_widths,
                                 BlockNumber *pages, double *tuples,
                                 double *allvisfrac)
{
    NOT_IMPLEMENTED;
}

/* ------------------------------------------------------------------------
 * Executor related functions
 * ------------------------------------------------------------------------ */

static bool
memtable_scan_bitmap_next_tuple(TableScanDesc scan, TupleTableSlot *slot,
                                 bool *recheck, uint64 *lossy_pages,
                                 uint64 *exact_pages)
{
    NOT_IMPLEMENTED;
    return false;
}

static bool
memtable_scan_sample_next_block(TableScanDesc scan,
                                 struct SampleScanState *scanstate)
{
    NOT_IMPLEMENTED;
    return false;
}

static bool
memtable_scan_sample_next_tuple(TableScanDesc scan,
                                 struct SampleScanState *scanstate,
                                 TupleTableSlot *slot)
{
    NOT_IMPLEMENTED;
    return false;
}

/* ------------------------------------------------------------------------
 * The routine itself
 * ------------------------------------------------------------------------ */

static const TableAmRoutine memtable_methods = {
    .type = T_TableAmRoutine,

    .slot_callbacks = memtable_slot_callbacks,

    .scan_begin = memtable_scan_begin,
    .scan_end = memtable_scan_end,
    .scan_rescan = memtable_scan_rescan,
    .scan_getnextslot = memtable_scan_getnextslot,
    .scan_set_tidrange = memtable_scan_set_tidrange,
    .scan_getnextslot_tidrange = memtable_scan_getnextslot_tidrange,

    .parallelscan_estimate = memtable_parallelscan_estimate,
    .parallelscan_initialize = memtable_parallelscan_initialize,
    .parallelscan_reinitialize = memtable_parallelscan_reinitialize,

    .index_fetch_begin = memtable_index_fetch_begin,
    .index_fetch_reset = memtable_index_fetch_reset,
    .index_fetch_end = memtable_index_fetch_end,
    .index_fetch_tuple = memtable_index_fetch_tuple,

    .tuple_fetch_row_version = memtable_tuple_fetch_row_version,
    .tuple_tid_valid = memtable_tuple_tid_valid,
    .tuple_get_latest_tid = memtable_tuple_get_latest_tid,
    .tuple_satisfies_snapshot = memtable_tuple_satisfies_snapshot,
    .index_delete_tuples = memtable_index_delete_tuples,

    .tuple_insert = memtable_tuple_insert,
    .tuple_insert_speculative = memtable_tuple_insert_speculative,
    .tuple_complete_speculative = memtable_tuple_complete_speculative,
    .multi_insert = memtable_multi_insert,
    .tuple_delete = memtable_tuple_delete,
    .tuple_update = memtable_tuple_update,
    .tuple_lock = memtable_tuple_lock,
    .finish_bulk_insert = NULL,     /* the one optional callback */

    .relation_set_new_filelocator = memtable_relation_set_new_filelocator,
    .relation_nontransactional_truncate = memtable_relation_nontransactional_truncate,
    .relation_copy_data = memtable_relation_copy_data,
    .relation_copy_for_cluster = memtable_relation_copy_for_cluster,
    .relation_vacuum = memtable_relation_vacuum,
    .scan_analyze_next_block = memtable_scan_analyze_next_block,
    .scan_analyze_next_tuple = memtable_scan_analyze_next_tuple,
    .index_build_range_scan = memtable_index_build_range_scan,
    .index_validate_scan = memtable_index_validate_scan,

    .relation_size = memtable_relation_size,
    .relation_needs_toast_table = memtable_relation_needs_toast_table,
    .relation_toast_am = memtable_relation_toast_am,
    .relation_fetch_toast_slice = memtable_relation_fetch_toast_slice,

    .relation_estimate_size = memtable_relation_estimate_size,

    .scan_bitmap_next_tuple = memtable_scan_bitmap_next_tuple,
    .scan_sample_next_block = memtable_scan_sample_next_block,
    .scan_sample_next_tuple = memtable_scan_sample_next_tuple,
};

PG_FUNCTION_INFO_V1(memtable_handler);

Datum
memtable_handler(PG_FUNCTION_ARGS)
{
    PG_RETURN_POINTER(&memtable_methods);
}

Mats

dbmsdrops.kindahl.net

Long time developer with a keen interest in databases, distributed systems, and programming languages. Currently working as a Multigres engineer at Supabase.

Comments

Leave a Reply