From 4993045532760593bce948577209b9285e0ca01c Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Thu, 19 Mar 2026 15:26:46 +0000
Subject: [PATCH v2 1/1] Add test module for dshash

This introduces comprehensive tests to the dshash APIs: insert, find,
sequential scan, delete_entry, delete_current, delete_key,
and dshash_dump, payload verification, resize and OOM handling.
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_dshash/.gitignore       |   4 +
 src/test/modules/test_dshash/Makefile         |  23 ++
 .../test_dshash/expected/test_dshash.out      |  14 ++
 src/test/modules/test_dshash/meson.build      |  33 +++
 .../modules/test_dshash/sql/test_dshash.sql   |  10 +
 .../modules/test_dshash/test_dshash--1.0.sql  |  10 +
 src/test/modules/test_dshash/test_dshash.c    | 218 ++++++++++++++++++
 .../modules/test_dshash/test_dshash.control   |   4 +
 10 files changed, 318 insertions(+)
 create mode 100644 src/test/modules/test_dshash/.gitignore
 create mode 100644 src/test/modules/test_dshash/Makefile
 create mode 100644 src/test/modules/test_dshash/expected/test_dshash.out
 create mode 100644 src/test/modules/test_dshash/meson.build
 create mode 100644 src/test/modules/test_dshash/sql/test_dshash.sql
 create mode 100644 src/test/modules/test_dshash/test_dshash--1.0.sql
 create mode 100644 src/test/modules/test_dshash/test_dshash.c
 create mode 100644 src/test/modules/test_dshash/test_dshash.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..51f2b1b48c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -26,6 +26,7 @@ SUBDIRS = \
 		  test_custom_types \
 		  test_ddl_deparse \
 		  test_dsa \
+		  test_dshash \
 		  test_dsm_registry \
 		  test_escape \
 		  test_extensions \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..dcf813b0823 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -27,6 +27,7 @@ subdir('test_custom_stats')
 subdir('test_custom_types')
 subdir('test_ddl_deparse')
 subdir('test_dsa')
+subdir('test_dshash')
 subdir('test_dsm_registry')
 subdir('test_escape')
 subdir('test_extensions')
diff --git a/src/test/modules/test_dshash/.gitignore b/src/test/modules/test_dshash/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_dshash/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_dshash/Makefile b/src/test/modules/test_dshash/Makefile
new file mode 100644
index 00000000000..aefba3af5fe
--- /dev/null
+++ b/src/test/modules/test_dshash/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_dshash/Makefile
+
+MODULE_big = test_dshash
+OBJS = \
+	$(WIN32RES) \
+	test_dshash.o
+PGFILEDESC = "test_dshash - test code for dshash"
+
+EXTENSION = test_dshash
+DATA = test_dshash--1.0.sql
+
+REGRESS = test_dshash
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_dshash
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_dshash/expected/test_dshash.out b/src/test/modules/test_dshash/expected/test_dshash.out
new file mode 100644
index 00000000000..69f4628bd5e
--- /dev/null
+++ b/src/test/modules/test_dshash/expected/test_dshash.out
@@ -0,0 +1,14 @@
+CREATE EXTENSION test_dshash;
+-- Exercise core dshash operations.
+SELECT test_dshash_basic();
+ test_dshash_basic 
+-------------------
+ 
+(1 row)
+
+-- Regular dshash_find_or_insert raises ERROR on OOM.
+-- Use terse for verbosity to ignore the DETAIL message.
+\set VERBOSITY terse
+SELECT test_dshash_find_or_insert_oom_error();
+ERROR:  out of memory
+\set VERBOSITY default
diff --git a/src/test/modules/test_dshash/meson.build b/src/test/modules/test_dshash/meson.build
new file mode 100644
index 00000000000..4c0ce938379
--- /dev/null
+++ b/src/test/modules/test_dshash/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+test_dshash_sources = files(
+  'test_dshash.c',
+)
+
+if host_system == 'windows'
+  test_dshash_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_dshash',
+    '--FILEDESC', 'test_dshash - test code for dshash',])
+endif
+
+test_dshash = shared_module('test_dshash',
+  test_dshash_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_dshash
+
+test_install_data += files(
+  'test_dshash.control',
+  'test_dshash--1.0.sql',
+)
+
+tests += {
+  'name': 'test_dshash',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_dshash',
+    ],
+  },
+}
diff --git a/src/test/modules/test_dshash/sql/test_dshash.sql b/src/test/modules/test_dshash/sql/test_dshash.sql
new file mode 100644
index 00000000000..672b87ac035
--- /dev/null
+++ b/src/test/modules/test_dshash/sql/test_dshash.sql
@@ -0,0 +1,10 @@
+CREATE EXTENSION test_dshash;
+
+-- Exercise core dshash operations.
+SELECT test_dshash_basic();
+
+-- Regular dshash_find_or_insert raises ERROR on OOM.
+-- Use terse for verbosity to ignore the DETAIL message.
+\set VERBOSITY terse
+SELECT test_dshash_find_or_insert_oom_error();
+\set VERBOSITY default
diff --git a/src/test/modules/test_dshash/test_dshash--1.0.sql b/src/test/modules/test_dshash/test_dshash--1.0.sql
new file mode 100644
index 00000000000..85a6289850b
--- /dev/null
+++ b/src/test/modules/test_dshash/test_dshash--1.0.sql
@@ -0,0 +1,10 @@
+/* src/test/modules/test_dshash/test_dshash--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_dshash" to load this file. \quit
+
+CREATE FUNCTION test_dshash_basic() RETURNS VOID
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION test_dshash_find_or_insert_oom_error() RETURNS VOID
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_dshash/test_dshash.c b/src/test/modules/test_dshash/test_dshash.c
new file mode 100644
index 00000000000..bfd9abdf164
--- /dev/null
+++ b/src/test/modules/test_dshash/test_dshash.c
@@ -0,0 +1,218 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_dshash.c
+ *		Test dynamic shared hash tables (dshash).
+ *
+ * Copyright (c) 2024-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_dshash/test_dshash.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "lib/dshash.h"
+#include "storage/dsm_registry.h"
+#include "storage/lwlock.h"
+#include "utils/dsa.h"
+
+PG_MODULE_MAGIC;
+
+/* Size limit for OOM tests */
+#define TEST_DSHASH_SIZE_LIMIT	(128 * 1024)
+
+/* More than enough to exhaust TEST_DSHASH_SIZE_LIMIT */
+#define TEST_DSHASH_MAX_OOM_ITERATIONS	10000
+
+typedef struct TestDshashEntry
+{
+	int			key;
+	int			value;
+}			TestDshashEntry;
+
+/* To verify payload integrity */
+#define KEY_TO_VALUE(k)		((k) ^ 0x12345678)
+
+static const dshash_parameters test_params = {
+	sizeof(int),				/* key_size */
+	sizeof(TestDshashEntry),	/* entry_size */
+	dshash_memcmp,
+	dshash_memhash,
+	dshash_memcpy,
+	LWTRANCHE_FIRST_USER_DEFINED	/* tranche_id, overwritten at runtime */
+};
+
+static void
+init_tranche(void *ptr, void *arg)
+{
+	int		   *tranche_id = (int *) ptr;
+
+	*tranche_id = LWLockNewTrancheId("test_dshash");
+}
+
+/*
+ * test_dshash_basic
+ *
+ * Test insert, find, sequential scan, delete_current, delete_key,
+ * delete of a nonexistent key, and dshash_dump, and re-insertions
+ * after deletes.
+ */
+PG_FUNCTION_INFO_V1(test_dshash_basic);
+Datum
+test_dshash_basic(PG_FUNCTION_ARGS)
+{
+	int		   *tranche_id;
+	bool		found;
+	dsa_area   *area;
+	dshash_table *ht;
+	dshash_parameters params = test_params;
+	dshash_seq_status status;
+	TestDshashEntry *entry;
+	int			count = 10;
+	int			scanned;
+	int			nonexistent_key;
+
+	tranche_id = GetNamedDSMSegment("test_dshash", sizeof(int),
+									init_tranche, &found, NULL);
+	params.tranche_id = *tranche_id;
+
+	area = dsa_create(*tranche_id);
+	ht = dshash_create(area, &params, NULL);
+
+	/* Insert entries with a payload. */
+	for (int i = 0; i < count; i++)
+	{
+		entry = dshash_find_or_insert(ht, &i, &found);
+		if (found)
+			elog(ERROR, "unexpected duplicate key %d", i);
+		entry->value = KEY_TO_VALUE(i);
+		dshash_release_lock(ht, entry);
+	}
+
+	/* Verify all entries via find, checking both key and value. */
+	for (int i = 0; i < count; i++)
+	{
+		entry = dshash_find(ht, &i, false);
+		if (entry == NULL || entry->key != i || entry->value != KEY_TO_VALUE(i))
+			elog(ERROR, "key %d not found or corrupted", i);
+		dshash_release_lock(ht, entry);
+	}
+
+	/* Dump the hash table. */
+	dshash_dump(ht);
+
+	/* Try to delete a key that does not exist. */
+	nonexistent_key = count + 1;
+	found = dshash_delete_key(ht, &nonexistent_key);
+	if (found)
+		elog(ERROR, "delete of nonexistent key %d reported found", nonexistent_key);
+
+	/* Verify entry count via sequential scan. */
+	scanned = 0;
+	dshash_seq_init(&status, ht, false);
+	while ((entry = dshash_seq_next(&status)) != NULL)
+		scanned++;
+	dshash_seq_term(&status);
+
+	if (scanned != count)
+		elog(ERROR, "seq scan returned %d entries, expected %d", scanned, count);
+
+	/* Delete one entry via dshash_delete_entry. */
+	{
+		int			delete_key = 0;
+
+		entry = dshash_find(ht, &delete_key, true);
+		if (entry == NULL)
+			elog(ERROR, "key %d not found for delete_entry", delete_key);
+		dshash_delete_entry(ht, entry);
+	}
+
+	/* Verify it's gone. */
+	{
+		int			delete_key = 0;
+
+		entry = dshash_find(ht, &delete_key, false);
+		if (entry != NULL)
+		{
+			dshash_release_lock(ht, entry);
+			elog(ERROR, "key %d still present after delete_entry", delete_key);
+		}
+	}
+
+	/* Delete remaining entries via delete_current. */
+	dshash_seq_init(&status, ht, true);
+	while ((entry = dshash_seq_next(&status)) != NULL)
+		dshash_delete_current(&status);
+	dshash_seq_term(&status);
+
+	/* Verify table is empty. */
+	scanned = 0;
+	dshash_seq_init(&status, ht, false);
+	while ((entry = dshash_seq_next(&status)) != NULL)
+		scanned++;
+	dshash_seq_term(&status);
+
+	if (scanned != 0)
+		elog(ERROR, "expected empty table, got %d entries", scanned);
+
+	/* Re-insert to verify the table is reusable after being emptied. */
+	for (int i = 0; i < count; i++)
+	{
+		entry = dshash_find_or_insert(ht, &i, &found);
+		if (found)
+			elog(ERROR, "unexpected duplicate key %d", i);
+		entry->value = KEY_TO_VALUE(i);
+		dshash_release_lock(ht, entry);
+	}
+
+	dshash_destroy(ht);
+	dsa_detach(area);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * test_dshash_find_or_insert_oom_error
+ *
+ * Test that the regular dshash_find_or_insert (without DSHASH_INSERT_NO_OOM)
+ * raises ERROR when the DSA area is "out of memory". This also exercises
+ * resize() along the way as entries are inserted.
+ */
+PG_FUNCTION_INFO_V1(test_dshash_find_or_insert_oom_error);
+Datum
+test_dshash_find_or_insert_oom_error(PG_FUNCTION_ARGS)
+{
+	int		   *tranche_id;
+	bool		found;
+	dsa_area   *area;
+	dshash_table *ht;
+	dshash_parameters params = test_params;
+	int			key = 0;
+
+	tranche_id = GetNamedDSMSegment("test_dshash", sizeof(int),
+									init_tranche, &found, NULL);
+	params.tranche_id = *tranche_id;
+
+	area = dsa_create(*tranche_id);
+	dsa_set_size_limit(area, TEST_DSHASH_SIZE_LIMIT);
+	ht = dshash_create(area, &params, NULL);
+
+	/* Insert until OOM — without NO_OOM flag, this should raise ERROR. */
+	for (key = 0; key < TEST_DSHASH_MAX_OOM_ITERATIONS; key++)
+	{
+		TestDshashEntry *entry;
+
+		entry = dshash_find_or_insert(ht, &key, &found);
+		if (entry == NULL)
+			elog(ERROR, "dshash_find_or_insert returned NULL unexpectedly");
+		dshash_release_lock(ht, entry);
+	}
+
+	/* Should not reach here — OOM error is expected above. */
+	elog(ERROR, "expected out-of-memory, but completed all %d iterations",
+		 TEST_DSHASH_MAX_OOM_ITERATIONS);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_dshash/test_dshash.control b/src/test/modules/test_dshash/test_dshash.control
new file mode 100644
index 00000000000..7ab0666d227
--- /dev/null
+++ b/src/test/modules/test_dshash/test_dshash.control
@@ -0,0 +1,4 @@
+comment = 'Test code for dshash'
+default_version = '1.0'
+module_pathname = '$libdir/test_dshash'
+relocatable = true
-- 
2.47.3

