Subject: Re: Add test module for Table Access Method

From: Fabrízio de Royes Mello <[email-masked]>
Date: Tue, 26 Sep 2023 10:00:08 -0300

On Mon, Jun 5, 2023 at 1:24 PM Fabrízio de Royes Mello <
[email-masked]> wrote:
>
> On Sat, Jun 3, 2023 at 7:42 PM Fabrízio de Royes Mello <
[email-masked]> wrote:
> >
> >
> > Hi all,
> >
> > During the PGCon Unconference session about Table Access Method one
missing item pointed out is that currently we lack documentation and
examples of TAM.
> >
> > So in order to improve things a bit in this area I'm proposing to add a
test module for Table Access Method similar what we already have for Index
Access Method.
> >
> > This code is based on the "blackhole_am" implemented by Michael
Paquier: https://github.com/michaelpq/pg_plugins/tree/main/blackhole_am
> >
>
> Just added some more tests, ran pgindent and also organized a bit some
comments and README.txt.

Rebased version.

-- 
Fabrízio de Royes Mello
From 5b6642b520874f4ca7023fc33d2e8e875fb64693 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabr=C3=ADzio=20de=20Royes=20Mello?=
 <[email-masked]>
Date: Sat, 3 Jun 2023 17:23:05 -0400
Subject: [PATCH v3] Add test module for Table Access Method

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/dummy_table_am/.gitignore    |   3 +
 src/test/modules/dummy_table_am/Makefile      |  20 +
 src/test/modules/dummy_table_am/README        |   5 +
 .../dummy_table_am/dummy_table_am--1.0.sql    |  14 +
 .../modules/dummy_table_am/dummy_table_am.c   | 500 ++++++++++++++++++
 .../dummy_table_am/dummy_table_am.control     |   5 +
 .../expected/dummy_table_am.out               | 207 ++++++++
 src/test/modules/dummy_table_am/meson.build   |  33 ++
 .../dummy_table_am/sql/dummy_table_am.sql     |  55 ++
 src/test/modules/meson.build                  |   1 +
 11 files changed, 844 insertions(+)
 create mode 100644 src/test/modules/dummy_table_am/.gitignore
 create mode 100644 src/test/modules/dummy_table_am/Makefile
 create mode 100644 src/test/modules/dummy_table_am/README
 create mode 100644 src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
 create mode 100644 src/test/modules/dummy_table_am/dummy_table_am.c
 create mode 100644 src/test/modules/dummy_table_am/dummy_table_am.control
 create mode 100644 src/test/modules/dummy_table_am/expected/dummy_table_am.out
 create mode 100644 src/test/modules/dummy_table_am/meson.build
 create mode 100644 src/test/modules/dummy_table_am/sql/dummy_table_am.sql

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index e81873cb5a..cb7a5b970a 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -10,6 +10,7 @@ SUBDIRS = \
 		  delay_execution \
 		  dummy_index_am \
 		  dummy_seclabel \
+		  dummy_table_am \
 		  libpq_pipeline \
 		  plsample \
 		  spgist_name_ops \
diff --git a/src/test/modules/dummy_table_am/.gitignore b/src/test/modules/dummy_table_am/.gitignore
new file mode 100644
index 0000000000..44d119cfcc
--- /dev/null
+++ b/src/test/modules/dummy_table_am/.gitignore
@@ -0,0 +1,3 @@
+# Generated subdirectories
+/log/
+/results/
diff --git a/src/test/modules/dummy_table_am/Makefile b/src/test/modules/dummy_table_am/Makefile
new file mode 100644
index 0000000000..9ea4a590c6
--- /dev/null
+++ b/src/test/modules/dummy_table_am/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/dummy_table_am/Makefile
+
+MODULES = dummy_table_am
+
+EXTENSION = dummy_table_am
+DATA = dummy_table_am--1.0.sql
+PGFILEDESC = "dummy_table_am - table access method template"
+
+REGRESS = dummy_table_am
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/dummy_table_am
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/dummy_table_am/README b/src/test/modules/dummy_table_am/README
new file mode 100644
index 0000000000..35211554b0
--- /dev/null
+++ b/src/test/modules/dummy_table_am/README
@@ -0,0 +1,5 @@
+Dummy Table AM
+==============
+
+Dummy table AM is a module for testing any facility usable by an table
+access method, whose code is kept a maximum simple.
diff --git a/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql b/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
new file mode 100644
index 0000000000..aa0fd82e61
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
@@ -0,0 +1,14 @@
+/* src/test/modules/dummy_table_am/dummy_table_am--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION dummy_table_am" to load this file. \quit
+
+CREATE FUNCTION dummy_table_am_handler(internal)
+RETURNS table_am_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+-- Access method
+CREATE ACCESS METHOD dummy_table_am TYPE TABLE HANDLER dummy_table_am_handler;
+COMMENT ON ACCESS METHOD dummy_table_am IS 'dummy table access method';
+
diff --git a/src/test/modules/dummy_table_am/dummy_table_am.c b/src/test/modules/dummy_table_am/dummy_table_am.c
new file mode 100644
index 0000000000..132f7b18cd
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am.c
@@ -0,0 +1,500 @@
+/*-------------------------------------------------------------------------
+ *
+ * dummy_table_am.c
+ *		Index AM template main file.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/dummy_table_am/dummy_table_am.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/tableam.h"
+#include "catalog/index.h"
+#include "commands/vacuum.h"
+
+PG_MODULE_MAGIC;
+
+/* Handler for table AM */
+PG_FUNCTION_INFO_V1(dummy_table_am_handler);
+
+/* Base structures for scans */
+typedef struct DummyScanDescData
+{
+	TableScanDescData rs_base;
+}			DummyScanDescData;
+
+typedef struct DummyScanDescData *DummyScanDesc;
+
+/*
+ * Slot related callbacks for Dummy Table Access Method
+ */
+static const TupleTableSlotOps *
+dummy_table_am_slot_callbacks(Relation relation)
+{
+	elog(INFO, "%s", __func__);
+	return &TTSOpsMinimalTuple;
+}
+
+/*
+ * Table Scan Callbacks for dummy_table_am AM
+ */
+static TableScanDesc
+dummy_table_am_scan_begin(Relation relation, Snapshot snapshot,
+						  int nkeys, ScanKey key,
+						  ParallelTableScanDesc parallel_scan,
+						  uint32 flags)
+{
+	DummyScanDesc scan;
+
+	scan = (DummyScanDesc) palloc(sizeof(DummyScanDescData));
+
+	scan->rs_base.rs_rd = relation;
+	scan->rs_base.rs_snapshot = snapshot;
+	scan->rs_base.rs_nkeys = nkeys;
+	scan->rs_base.rs_flags = flags;
+	scan->rs_base.rs_parallel = parallel_scan;
+
+	elog(INFO, "%s", __func__);
+
+	return (TableScanDesc) scan;
+}
+
+static void
+dummy_table_am_scan_end(TableScanDesc sscan)
+{
+	DummyScanDesc scan = (DummyScanDesc) sscan;
+
+	elog(INFO, "%s", __func__);
+
+	pfree(scan);
+}
+
+static void
+dummy_table_am_scan_rescan(TableScanDesc sscan, ScanKey key, bool set_params,
+						   bool allow_strat, bool allow_sync, bool allow_pagemode)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static bool
+dummy_table_am_scan_getnextslot(TableScanDesc sscan, ScanDirection direction,
+								TupleTableSlot *slot)
+{
+	elog(INFO, "%s", __func__);
+	return false;
+}
+
+/*
+ * Index Scan Callbacks for dummy_table_am AM
+ */
+static IndexFetchTableData *
+dummy_table_am_index_fetch_begin(Relation rel)
+{
+	elog(INFO, "%s", __func__);
+	return NULL;
+}
+
+static void
+dummy_table_am_index_fetch_reset(IndexFetchTableData *scan)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_index_fetch_end(IndexFetchTableData *scan)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static bool
+dummy_table_am_index_fetch_tuple(struct IndexFetchTableData *scan,
+								 ItemPointer tid,
+								 Snapshot snapshot,
+								 TupleTableSlot *slot,
+								 bool *call_again, bool *all_dead)
+{
+	elog(INFO, "%s", __func__);
+	return 0;
+}
+
+/*
+ * Callbacks for non-modifying operations on individual tuples for
+ * dummy_table_am AM.
+ */
+static bool
+dummy_table_am_fetch_row_version(Relation relation,
+								 ItemPointer tid,
+								 Snapshot snapshot,
+								 TupleTableSlot *slot)
+{
+	elog(INFO, "%s", __func__);
+	return false;
+}
+
+static void
+dummy_table_am_get_latest_tid(TableScanDesc sscan,
+							  ItemPointer tid)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static bool
+dummy_table_am_tuple_tid_valid(TableScanDesc scan, ItemPointer tid)
+{
+	elog(INFO, "%s", __func__);
+	return false;
+}
+
+static bool
+dummy_table_am_tuple_satisfies_snapshot(Relation rel, TupleTableSlot *slot,
+										Snapshot snapshot)
+{
+	elog(INFO, "%s", __func__);
+	return false;
+}
+
+static TransactionId
+dummy_table_am_index_delete_tuples(Relation rel,
+								   TM_IndexDeleteOp *delstate)
+{
+	elog(INFO, "%s", __func__);
+	return InvalidTransactionId;
+}
+
+/*
+ * Functions for manipulations of physical tuples for dummy_table_am AM.
+ */
+static void
+dummy_table_am_tuple_insert(Relation relation, TupleTableSlot *slot,
+							CommandId cid, int options, BulkInsertState bistate)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_tuple_insert_speculative(Relation relation, TupleTableSlot *slot,
+										CommandId cid, int options,
+										BulkInsertState bistate,
+										uint32 specToken)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_tuple_complete_speculative(Relation relation, TupleTableSlot *slot,
+										  uint32 spekToken, bool succeeded)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_multi_insert(Relation relation, TupleTableSlot **slots,
+							int ntuples, CommandId cid, int options,
+							BulkInsertState bistate)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static TM_Result
+dummy_table_am_tuple_delete(Relation relation, ItemPointer tid, CommandId cid,
+							Snapshot snapshot, Snapshot crosscheck, bool wait,
+							TM_FailureData *tmfd, bool changingPart)
+{
+	elog(INFO, "%s", __func__);
+
+	/* nothing to do, so it is always OK */
+	return TM_Ok;
+}
+
+static TM_Result
+dummy_table_am_tuple_update(Relation relation, ItemPointer otid,
+							TupleTableSlot *slot, CommandId cid,
+							Snapshot snapshot, Snapshot crosscheck,
+							bool wait, TM_FailureData *tmfd,
+							LockTupleMode *lockmode,
+							TU_UpdateIndexes *update_indexes)
+{
+	elog(INFO, "%s", __func__);
+
+	/* nothing to do, so it is always OK */
+	return TM_Ok;
+}
+
+static TM_Result
+dummy_table_am_tuple_lock(Relation relation, ItemPointer tid, Snapshot snapshot,
+						  TupleTableSlot *slot, CommandId cid, LockTupleMode mode,
+						  LockWaitPolicy wait_policy, uint8 flags,
+						  TM_FailureData *tmfd)
+{
+	elog(INFO, "%s", __func__);
+
+	/* nothing to do, so it is always OK */
+	return TM_Ok;
+}
+
+static void
+dummy_table_am_finish_bulk_insert(Relation relation, int options)
+{
+	elog(INFO, "%s", __func__);
+}
+
+/*
+ * DDL related callbacks for dummy_table_am AM.
+ */
+static void
+dummy_table_am_relation_set_new_filelocator(Relation rel,
+											const RelFileLocator *newrnode,
+											char persistence,
+											TransactionId *freezeXid,
+											MultiXactId *minmulti)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_relation_nontransactional_truncate(Relation rel)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_copy_data(Relation rel, const RelFileLocator *newrnode)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_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)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static void
+dummy_table_am_vacuum(Relation onerel, VacuumParams *params,
+					  BufferAccessStrategy bstrategy)
+{
+	elog(INFO, "%s", __func__);
+}
+
+static bool
+dummy_table_am_scan_analyze_next_block(TableScanDesc scan, BlockNumber blockno,
+									   BufferAccessStrategy bstrategy)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to analyze next block */
+	return false;
+}
+
+static bool
+dummy_table_am_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin,
+									   double *liverows, double *deadrows,
+									   TupleTableSlot *slot)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to analyze next tuple */
+	return false;
+}
+
+static double
+dummy_table_am_index_build_range_scan(Relation tableRelation,
+									  Relation indexRelation,
+									  IndexInfo *indexInfo,
+									  bool allow_sync,
+									  bool anyvisible,
+									  bool progress,
+									  BlockNumber start_blockno,
+									  BlockNumber numblocks,
+									  IndexBuildCallback callback,
+									  void *callback_state,
+									  TableScanDesc scan)
+{
+	elog(ERROR, "%s", __func__);
+
+	/* no data, so no tuples */
+	return 0;
+}
+
+static void
+dummy_table_am_index_validate_scan(Relation tableRelation,
+								   Relation indexRelation,
+								   IndexInfo *indexInfo,
+								   Snapshot snapshot,
+								   ValidateIndexState *state)
+{
+	elog(INFO, "%s", __func__);
+}
+
+/*
+ * Miscellaneous callbacks for the dummy_table_am AM
+ */
+static uint64
+dummy_table_am_relation_size(Relation rel, ForkNumber forkNumber)
+{
+	elog(INFO, "%s", __func__);
+
+	/* there is nothing */
+	return 0;
+}
+
+/*
+ * Check to see whether the table needs a TOAST table.
+ */
+static bool
+dummy_table_am_relation_needs_toast_table(Relation rel)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no toast table needed */
+	return false;
+}
+
+/*
+ * Planner related callbacks for the dummy_table_am AM
+ */
+static void
+dummy_table_am_estimate_rel_size(Relation rel, int32 *attr_widths,
+								 BlockNumber *pages, double *tuples,
+								 double *allvisfrac)
+{
+	/* no data available */
+	if (attr_widths)
+		*attr_widths = 0;
+	if (pages)
+		*pages = 0;
+	if (tuples)
+		*tuples = 0;
+	if (allvisfrac)
+		*allvisfrac = 0;
+
+	elog(INFO, "%s", __func__);
+}
+
+
+/*
+ * Executor related callbacks for the dummy_table_am AM
+ */
+static bool
+dummy_table_am_scan_bitmap_next_block(TableScanDesc scan,
+									  TBMIterateResult *tbmres)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to scan next block */
+	return false;
+}
+
+static bool
+dummy_table_am_scan_bitmap_next_tuple(TableScanDesc scan,
+									  TBMIterateResult *tbmres,
+									  TupleTableSlot *slot)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to scan next tuple */
+	return false;
+}
+
+static bool
+dummy_table_am_scan_sample_next_block(TableScanDesc scan,
+									  SampleScanState *scanstate)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to scan next block for sampling */
+	return false;
+}
+
+static bool
+dummy_table_am_scan_sample_next_tuple(TableScanDesc scan,
+									  SampleScanState *scanstate,
+									  TupleTableSlot *slot)
+{
+	elog(INFO, "%s", __func__);
+
+	/* no data, so no point to scan next tuple for sampling */
+	return false;
+}
+
+/*
+ * Definition of the dummy_table_am table access method.
+ */
+static const TableAmRoutine dummy_table_am_methods = {
+	.type = T_TableAmRoutine,
+
+	.slot_callbacks = dummy_table_am_slot_callbacks,
+
+	.scan_begin = dummy_table_am_scan_begin,
+	.scan_end = dummy_table_am_scan_end,
+	.scan_rescan = dummy_table_am_scan_rescan,
+	.scan_getnextslot = dummy_table_am_scan_getnextslot,
+
+	/* these are common helper functions */
+	.parallelscan_estimate = table_block_parallelscan_estimate,
+	.parallelscan_initialize = table_block_parallelscan_initialize,
+	.parallelscan_reinitialize = table_block_parallelscan_reinitialize,
+
+	.index_fetch_begin = dummy_table_am_index_fetch_begin,
+	.index_fetch_reset = dummy_table_am_index_fetch_reset,
+	.index_fetch_end = dummy_table_am_index_fetch_end,
+	.index_fetch_tuple = dummy_table_am_index_fetch_tuple,
+
+	.tuple_insert = dummy_table_am_tuple_insert,
+	.tuple_insert_speculative = dummy_table_am_tuple_insert_speculative,
+	.tuple_complete_speculative = dummy_table_am_tuple_complete_speculative,
+	.multi_insert = dummy_table_am_multi_insert,
+	.tuple_delete = dummy_table_am_tuple_delete,
+	.tuple_update = dummy_table_am_tuple_update,
+	.tuple_lock = dummy_table_am_tuple_lock,
+	.finish_bulk_insert = dummy_table_am_finish_bulk_insert,
+
+	.tuple_fetch_row_version = dummy_table_am_fetch_row_version,
+	.tuple_get_latest_tid = dummy_table_am_get_latest_tid,
+	.tuple_tid_valid = dummy_table_am_tuple_tid_valid,
+	.tuple_satisfies_snapshot = dummy_table_am_tuple_satisfies_snapshot,
+	.index_delete_tuples = dummy_table_am_index_delete_tuples,
+
+	.relation_set_new_filelocator = dummy_table_am_relation_set_new_filelocator,
+	.relation_nontransactional_truncate = dummy_table_am_relation_nontransactional_truncate,
+	.relation_copy_data = dummy_table_am_copy_data,
+	.relation_copy_for_cluster = dummy_table_am_copy_for_cluster,
+	.relation_vacuum = dummy_table_am_vacuum,
+	.scan_analyze_next_block = dummy_table_am_scan_analyze_next_block,
+	.scan_analyze_next_tuple = dummy_table_am_scan_analyze_next_tuple,
+	.index_build_range_scan = dummy_table_am_index_build_range_scan,
+	.index_validate_scan = dummy_table_am_index_validate_scan,
+
+	.relation_size = dummy_table_am_relation_size,
+	.relation_needs_toast_table = dummy_table_am_relation_needs_toast_table,
+
+	.relation_estimate_size = dummy_table_am_estimate_rel_size,
+
+	.scan_bitmap_next_block = dummy_table_am_scan_bitmap_next_block,
+	.scan_bitmap_next_tuple = dummy_table_am_scan_bitmap_next_tuple,
+	.scan_sample_next_block = dummy_table_am_scan_sample_next_block,
+	.scan_sample_next_tuple = dummy_table_am_scan_sample_next_tuple
+};
+
+/*
+ * Table AM handler function: returns TableAmRoutine with access method
+ * parameters and callbacks.
+ */
+Datum
+dummy_table_am_handler(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_POINTER(&dummy_table_am_methods);
+}
diff --git a/src/test/modules/dummy_table_am/dummy_table_am.control b/src/test/modules/dummy_table_am/dummy_table_am.control
new file mode 100644
index 0000000000..08f2f868d4
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am.control
@@ -0,0 +1,5 @@
+# dummy_table_am extension
+comment = 'dummy_table_am - table access method template'
+default_version = '1.0'
+module_pathname = '$libdir/dummy_table_am'
+relocatable = true
diff --git a/src/test/modules/dummy_table_am/expected/dummy_table_am.out b/src/test/modules/dummy_table_am/expected/dummy_table_am.out
new file mode 100644
index 0000000000..76029a30ae
--- /dev/null
+++ b/src/test/modules/dummy_table_am/expected/dummy_table_am.out
@@ -0,0 +1,207 @@
+-- Tests for dummy table access method
+CREATE EXTENSION dummy_table_am;
+CREATE TABLE dummy_table (a int, b text) USING dummy_table_am;
+INFO:  dummy_table_am_relation_set_new_filelocator
+INFO:  dummy_table_am_relation_needs_toast_table
+-- Will error out
+CREATE INDEX dummy_table_idx ON dummy_table (a);
+INFO:  dummy_table_am_estimate_rel_size
+ERROR:  dummy_table_am_index_build_range_scan
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+INSERT INTO dummy_table VALUES (1, 'dummy');
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_tuple_insert
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+INSERT INTO dummy_table VALUES (1, 'dummy'), (2, 'dummy');
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_tuple_insert
+INFO:  dummy_table_am_tuple_insert
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+UPDATE dummy_table SET a = 0 WHERE a = 1;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+-- Update without WHERE
+UPDATE dummy_table SET a = 0, b = NULL;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+DELETE FROM dummy_table WHERE a = 1;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+TRUNCATE dummy_table;
+INFO:  dummy_table_am_relation_set_new_filelocator
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+VACUUM dummy_table;
+INFO:  dummy_table_am_vacuum
+ANALYZE dummy_table;
+INFO:  dummy_table_am_relation_size
+INFO:  dummy_table_am_relation_size
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_end
+VACUUM (FULL) dummy_table;
+INFO:  dummy_table_am_relation_set_new_filelocator
+INFO:  dummy_table_am_copy_for_cluster
+INFO:  dummy_table_am_relation_size
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+COPY dummy_table TO STDOUT;
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+COPY dummy_table (a, b) FROM STDIN;
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_multi_insert
+INFO:  dummy_table_am_finish_bulk_insert
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b 
+---+---
+(0 rows)
+
+ALTER TABLE dummy_table ADD COLUMN c BOOLEAN DEFAULT true;
+INFO:  dummy_table_am_relation_needs_toast_table
+INSERT INTO dummy_table VALUES (1, 'dummy');
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_tuple_insert
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b | c 
+---+---+---
+(0 rows)
+
+-- ALTER TABLE SET ACCESS METHOD
+ALTER TABLE dummy_table SET ACCESS METHOD heap;
+INFO:  dummy_table_am_relation_needs_toast_table
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+CREATE INDEX dummy_table_idx ON dummy_table (a);
+INSERT INTO dummy_table VALUES (1, 'heap', false);
+SELECT * FROM dummy_table;
+ a |  b   | c 
+---+------+---
+ 1 | heap | f
+(1 row)
+
+-- Will error out, the index must be dropped
+ALTER TABLE dummy_table SET ACCESS METHOD dummy_table_am;
+INFO:  dummy_table_am_relation_set_new_filelocator
+INFO:  dummy_table_am_relation_needs_toast_table
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_tuple_insert
+INFO:  dummy_table_am_finish_bulk_insert
+INFO:  dummy_table_am_estimate_rel_size
+ERROR:  dummy_table_am_index_build_range_scan
+DROP INDEX dummy_table_idx;
+ALTER TABLE dummy_table SET ACCESS METHOD dummy_table_am;
+INFO:  dummy_table_am_relation_set_new_filelocator
+INFO:  dummy_table_am_relation_needs_toast_table
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_tuple_insert
+INFO:  dummy_table_am_finish_bulk_insert
+SELECT * FROM dummy_table;
+INFO:  dummy_table_am_estimate_rel_size
+INFO:  dummy_table_am_slot_callbacks
+INFO:  dummy_table_am_scan_begin
+INFO:  dummy_table_am_scan_getnextslot
+INFO:  dummy_table_am_scan_end
+ a | b | c 
+---+---+---
+(0 rows)
+
+-- Clean up
+DROP TABLE dummy_table;
diff --git a/src/test/modules/dummy_table_am/meson.build b/src/test/modules/dummy_table_am/meson.build
new file mode 100644
index 0000000000..93f9108f29
--- /dev/null
+++ b/src/test/modules/dummy_table_am/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+dummy_table_am_sources = files(
+  'dummy_table_am.c',
+)
+
+if host_system == 'windows'
+  dummy_table_am_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'dummy_table_am',
+    '--FILEDESC', 'dummy_table_am - table access method template',])
+endif
+
+dummy_table_am = shared_module('dummy_table_am',
+  dummy_table_am_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += dummy_table_am
+
+test_install_data += files(
+  'dummy_table_am.control',
+  'dummy_table_am--1.0.sql',
+)
+
+tests += {
+  'name': 'dummy_table_am',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'dummy_table_am',
+    ],
+  },
+}
diff --git a/src/test/modules/dummy_table_am/sql/dummy_table_am.sql b/src/test/modules/dummy_table_am/sql/dummy_table_am.sql
new file mode 100644
index 0000000000..f17c169255
--- /dev/null
+++ b/src/test/modules/dummy_table_am/sql/dummy_table_am.sql
@@ -0,0 +1,55 @@
+-- Tests for dummy table access method
+CREATE EXTENSION dummy_table_am;
+CREATE TABLE dummy_table (a int, b text) USING dummy_table_am;
+-- Will error out
+CREATE INDEX dummy_table_idx ON dummy_table (a);
+SELECT * FROM dummy_table;
+
+INSERT INTO dummy_table VALUES (1, 'dummy');
+SELECT * FROM dummy_table;
+
+INSERT INTO dummy_table VALUES (1, 'dummy'), (2, 'dummy');
+SELECT * FROM dummy_table;
+
+UPDATE dummy_table SET a = 0 WHERE a = 1;
+SELECT * FROM dummy_table;
+
+-- Update without WHERE
+UPDATE dummy_table SET a = 0, b = NULL;
+SELECT * FROM dummy_table;
+
+DELETE FROM dummy_table WHERE a = 1;
+SELECT * FROM dummy_table;
+
+TRUNCATE dummy_table;
+SELECT * FROM dummy_table;
+
+VACUUM dummy_table;
+ANALYZE dummy_table;
+VACUUM (FULL) dummy_table;
+SELECT * FROM dummy_table;
+
+COPY dummy_table TO STDOUT;
+COPY dummy_table (a, b) FROM STDIN;
+1	dummy
+\.
+SELECT * FROM dummy_table;
+
+ALTER TABLE dummy_table ADD COLUMN c BOOLEAN DEFAULT true;
+INSERT INTO dummy_table VALUES (1, 'dummy');
+SELECT * FROM dummy_table;
+
+-- ALTER TABLE SET ACCESS METHOD
+ALTER TABLE dummy_table SET ACCESS METHOD heap;
+CREATE INDEX dummy_table_idx ON dummy_table (a);
+INSERT INTO dummy_table VALUES (1, 'heap', false);
+SELECT * FROM dummy_table;
+
+-- Will error out, the index must be dropped
+ALTER TABLE dummy_table SET ACCESS METHOD dummy_table_am;
+DROP INDEX dummy_table_idx;
+ALTER TABLE dummy_table SET ACCESS METHOD dummy_table_am;
+SELECT * FROM dummy_table;
+
+-- Clean up
+DROP TABLE dummy_table;
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index fcd643f6f1..bbddef1050 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -5,6 +5,7 @@ subdir('commit_ts')
 subdir('delay_execution')
 subdir('dummy_index_am')
 subdir('dummy_seclabel')
+subdir('dummy_table_am')
 subdir('ldap_password_func')
 subdir('libpq_pipeline')
 subdir('plsample')
-- 
2.34.1