From ac0859238dbb85ed830f77695d4fc0bb9ffc7c25 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 4 Dec 2025 15:35:08 +0100
Subject: [PATCH v13 1/5] Add hash_make macros

This adds a bunch of hash_make* and shmem_hash_make* macros to make it
easier and less error prone to create HTABs. These macros are
implemented as wrappers around the already existing hash_create
function. Using the new macros is preferred, due to the additional
compile time checks that they bring.

Co-Authored-By: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
---
 doc/src/sgml/system-views.sgml                |   5 +-
 src/backend/utils/hash/dynahash.c             | 115 ++++++++++++
 src/include/c.h                               |  18 ++
 src/include/storage/shmem.h                   |  32 ++++
 src/include/utils/hsearch.h                   | 166 +++++++++++++++++-
 .../test_cplusplusext/test_cplusplusext.cpp   |  46 +++++
 src/tools/pgindent/typedefs.list              |   1 +
 7 files changed, 377 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 9ee1a2bfc6a..c7c05956820 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -4254,7 +4254,10 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   <para>
    Anonymous allocations are allocations that have been made
    with <literal>ShmemAlloc()</literal> directly, rather than via
-   <literal>ShmemInitStruct()</literal> or
+   <literal>ShmemInitStruct()</literal>,
+   <literal>shmem_hash_make()</literal>,
+   <literal>shmem_hash_make_ext()</literal>,
+   or
    <literal>ShmemInitHash()</literal>.
   </para>
 
diff --git a/src/backend/utils/hash/dynahash.c b/src/backend/utils/hash/dynahash.c
index f8317add68f..4071bfe404f 100644
--- a/src/backend/utils/hash/dynahash.c
+++ b/src/backend/utils/hash/dynahash.c
@@ -621,6 +621,121 @@ hash_create(const char *tabname, int64 nelem, const HASHCTL *info, int flags)
 	return hashp;
 }
 
+/*
+ * hash_opts_init -- initialize HASHCTL and flags from HASHOPTS
+ *
+ * This processes HASHOPTS fields into HASHCTL and flags. It's used by code
+ * that needs to call the low-level hash_create function.
+ */
+void
+hash_opts_init(HASHCTL *ctl, int *flags,
+			   Size keysize, Size entrysize, bool string_key,
+			   const HASHOPTS *opts)
+{
+	MemSet(ctl, 0, sizeof(*ctl));
+	ctl->keysize = keysize;
+	ctl->entrysize = entrysize;
+
+	*flags = HASH_ELEM;
+
+	if (opts == NULL)
+	{
+		*flags |= string_key ? HASH_STRINGS : HASH_BLOBS;
+		return;
+	}
+
+	if (opts->hash != NULL)
+	{
+		/* force_blobs only affects default hash selection, not custom hash */
+		Assert(!opts->force_blobs);
+		ctl->hash = opts->hash;
+		*flags |= HASH_FUNCTION;
+	}
+	else if (opts->force_blobs)
+	{
+		*flags |= HASH_BLOBS;
+	}
+	else
+	{
+		*flags |= string_key ? HASH_STRINGS : HASH_BLOBS;
+	}
+
+	if (opts->match != NULL)
+	{
+		ctl->match = opts->match;
+		*flags |= HASH_COMPARE;
+	}
+
+	if (opts->keycopy != NULL)
+	{
+		ctl->keycopy = opts->keycopy;
+		*flags |= HASH_KEYCOPY;
+	}
+
+	if (opts->alloc != NULL)
+	{
+		ctl->alloc = opts->alloc;
+		*flags |= HASH_ALLOC;
+	}
+
+	if (opts->num_partitions > 0)
+	{
+		ctl->num_partitions = opts->num_partitions;
+		*flags |= HASH_PARTITION;
+	}
+
+	if (opts->fixed_size)
+		*flags |= HASH_FIXED_SIZE;
+}
+
+/*
+ * hash_make_impl -- simplified hash table creation
+ *
+ * This is the implementation function for the hash_make() and hash_make_ext()
+ * macros. It creates a hash table with sensible defaults.
+ *
+ * If string_key is true, the key is treated as a null-terminated string
+ * (uses HASH_STRINGS). Otherwise, the key is treated as a binary blob
+ * (uses HASH_BLOBS).
+ *
+ * Pass NULL for opts to use all defaults.
+ */
+HTAB *
+hash_make_impl(const char *tabname, int64 nelem,
+			   Size keysize, Size entrysize,
+			   bool string_key,
+			   const HASHOPTS *opts,
+			   MemoryContext mcxt)
+{
+	HASHCTL		ctl;
+	int			flags;
+
+	hash_opts_init(&ctl, &flags, keysize, entrysize, string_key, opts);
+
+	ctl.hcxt = mcxt;
+	flags |= HASH_CONTEXT;
+
+	return hash_create(tabname, nelem, &ctl, flags);
+}
+
+/*
+ * hash_make_fn_impl -- create a hash table with custom functions
+ */
+HTAB *
+hash_make_fn_impl(const char *tabname, int64 nelem,
+				  Size keysize, Size entrysize, bool string_key,
+				  HashValueFunc hashfn, HashCompareFunc matchfn,
+				  MemoryContext mcxt)
+{
+	HASHOPTS	opts = {
+		.hash = hashfn,
+		.match = matchfn
+	};
+
+	return hash_make_impl(tabname, nelem, keysize, entrysize,
+						  string_key, &opts, mcxt);
+}
+
 /*
  * Set default HASHHDR parameters.
  */
diff --git a/src/include/c.h b/src/include/c.h
index 6db88644491..15451938e1e 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -83,6 +83,12 @@
 #ifdef ENABLE_NLS
 #include <libintl.h>
 #endif
+#ifdef __cplusplus
+extern "C++"
+{
+#include <type_traits>
+}
+#endif
 
 #ifdef __cplusplus
 extern "C++"
@@ -410,6 +416,18 @@ extern "C++"
 #define pg_expr_has_type_p(expr, type) _Generic((expr), type: 1, default: 0)
 #endif
 
+/*
+ * pg_nullptr_of(type) - Create a null pointer of the given type.
+ *
+ * In C, (type *) NULL works for all types. In C++, this syntax fails for types
+ * containing brackets (like char[64]), so we use std::add_pointer_t instead.
+ */
+#if defined(__cplusplus)
+#define pg_nullptr_of(type) (static_cast<std::add_pointer_t<type>>(nullptr))
+#else
+#define pg_nullptr_of(type) ((type *) NULL)
+#endif
+
 /*
  * pg_assume(expr) states that we assume `expr` to evaluate to true. In assert
  * enabled builds pg_assume() is turned into an assertion, in optimized builds
diff --git a/src/include/storage/shmem.h b/src/include/storage/shmem.h
index 2a9e9becd26..1672185cde1 100644
--- a/src/include/storage/shmem.h
+++ b/src/include/storage/shmem.h
@@ -40,6 +40,38 @@ extern Size mul_size(Size s1, Size s2);
 
 extern PGDLLIMPORT Size pg_get_shmem_pagesize(void);
 
+/*
+ * Simplified shared memory hash table creation API
+ *
+ * These macros provide a simpler way to create shared memory hash tables by:
+ * - Automatically determining keysize and entrysize from type information
+ * - Automatically choosing HASH_STRINGS vs HASH_BLOBS based on key type
+ * - Eliminating the need for explicit HASHCTL and flags in common cases
+ *
+ * Usage:
+ *   HTAB *hash = shmem_hash_make(MyEntry, keyfield, "My hash", 64, 128);
+ *
+ * For more options (partitioning, fixed size, custom hash):
+ *   HASHOPTS opts = {.num_partitions = 16, .fixed_size = true};
+ *   HTAB *hash = shmem_hash_make_ext(MyEntry, keyfield, "My hash", 64, 128, &opts);
+ */
+#define shmem_hash_make(entrytype, keymember, tabname, init_size, max_size) \
+	shmem_hash_make_ext(entrytype, keymember, tabname, init_size, max_size, NULL)
+
+#define shmem_hash_make_ext(entrytype, keymember, tabname, init_size, max_size, opts) \
+	(StaticAssertExpr(offsetof(entrytype, keymember) == 0, \
+						 #keymember " must be first member in " #entrytype), \
+	shmem_hash_make_impl( \
+		(tabname), (init_size), (max_size), \
+		sizeof(((entrytype *)0)->keymember), \
+		sizeof(entrytype), \
+		HASH_KEY_AS_STRING(entrytype, keymember), \
+		(opts)))
+
+extern HTAB *shmem_hash_make_impl(const char *name, int64 init_size, int64 max_size,
+								  Size keysize, Size entrysize, bool string_key,
+								  const HASHOPTS *opts);
+
 /* ipci.c */
 extern void RequestAddinShmemSpace(Size size);
 
diff --git a/src/include/utils/hsearch.h b/src/include/utils/hsearch.h
index 337b2e44625..3461cede65d 100644
--- a/src/include/utils/hsearch.h
+++ b/src/include/utils/hsearch.h
@@ -15,6 +15,9 @@
 #define HSEARCH_H
 
 
+/* Hash table control struct is an opaque type known only within dynahash.c */
+typedef struct HTAB HTAB;
+
 /*
  * Hash functions must have this signature.
  */
@@ -42,6 +45,156 @@ typedef void *(*HashCopyFunc) (void *dest, const void *src, Size keysize);
  */
 typedef void *(*HashAllocFunc) (Size request, void *alloc_arg);
 
+/*
+ * Hash options for hash_make_ext and shmem_hash_make_ext macros.
+ * Contains less commonly needed options that aren't covered by the basic macros.
+ * All fields default to NULL/0/false when zero-initialized.
+ */
+typedef struct HASHOPTS
+{
+	HashValueFunc hash;			/* custom hash function (NULL for default) */
+	HashCompareFunc match;		/* custom comparison function (NULL for
+								 * default) */
+	HashCopyFunc keycopy;		/* custom key copy function (NULL for default) */
+	HashAllocFunc alloc;		/* custom allocator (NULL for default) */
+	int64		num_partitions; /* partition count (0 for none) */
+	bool		fixed_size;		/* if true, hash table cannot grow */
+	bool		force_blobs;	/* if true, use HASH_BLOBS even for string
+								 * types */
+} HASHOPTS;
+
+/*
+ * Helpers to detect if a type should be hashed as a string.
+ *
+ * String types include: char arrays and NameData.
+ * Everything else is treated as a binary blob (HASH_BLOBS).
+ */
+#define HASH_PTR_AS_STRING(ptr, size) \
+	(pg_expr_has_type_p((ptr), char(*)[size]) || pg_expr_has_type_p((ptr), NameData *))
+#define HASH_KEY_AS_STRING(entrytype, keymember) \
+	HASH_PTR_AS_STRING(&((entrytype *)0)->keymember, \
+					   sizeof(((entrytype *)0)->keymember))
+#define HASH_TYPE_AS_STRING(type) \
+	HASH_PTR_AS_STRING(pg_nullptr_of(type), sizeof(type))
+
+/*
+ * Create a hash table with minimal boilerplate.
+ *
+ * This is the simplest way to create a hash table. It:
+ * - Derives keysize from the keymember's actual type
+ * - Derives entrysize from the entrytype
+ * - Automatically chooses HASH_STRINGS or HASH_BLOBS based on key type (char arrays and NameData are treated as strings)
+ * - Uses CurrentMemoryContext (not TopMemoryContext)
+ * - Validates that keymember is at offset 0
+ *
+ * NOTE: If you use char[N] to store binary data that might contain null bytes
+ * and/or is not null terminated, the automatic detection will incorrectly
+ * treat it as a string and use string comparison. In such cases, use
+ * hash_make_ext() with .force_blobs = true to override the automatic
+ * detection.
+ *
+ * Usage:
+ *   typedef struct { Oid oid; char *data; } MyEntry;
+ *   HTAB *h = hash_make(MyEntry, oid, "my table", 64);
+ */
+#define hash_make(entrytype, keymember, tabname, nelem) \
+	hash_make_cxt(entrytype, keymember, tabname, nelem, CurrentMemoryContext)
+
+/*
+ * Like hash_make, but allows specifying a memory context.
+ */
+#define hash_make_cxt(entrytype, keymember, tabname, nelem, mcxt) \
+	hash_make_ext_cxt(entrytype, keymember, tabname, nelem, NULL, mcxt)
+
+/*
+ * Hash table with custom hash and/or match functions.
+ *
+ * Like hash_make, but accepts custom hash and match function pointers.
+ * Pass NULL for hashfn to use the default hash (based on key type),
+ * or NULL for matchfn to use the default memcmp-based comparison.
+ */
+#define hash_make_fn(entrytype, keymember, tabname, nelem, hashfn, matchfn) \
+	hash_make_fn_cxt(entrytype, keymember, tabname, nelem, hashfn, matchfn, \
+					 CurrentMemoryContext)
+#define hash_make_fn_cxt(entrytype, keymember, tabname, nelem, hashfn, matchfn, mcxt) \
+	(StaticAssertExpr(offsetof(entrytype, keymember) == 0, \
+					  #keymember " must be first member in " #entrytype), \
+	 hash_make_fn_impl((tabname), (nelem), \
+					   sizeof(((entrytype *)0)->keymember), \
+					   sizeof(entrytype), \
+					   HASH_KEY_AS_STRING(entrytype, keymember), \
+					   (hashfn), (matchfn), (mcxt)))
+
+/*
+ * Hash table with extended options via HASHOPTS struct.
+ *
+ * Like hash_make, but accepts additional options via HASHOPTS struct pointer.
+ * Pass NULL for opts to use all defaults.
+ *
+ * Example usage:
+ *   HASHOPTS opts = {0};
+ *   opts.hash = my_hash_func;
+ *   opts.num_partitions = 16;
+ *   HTAB *h = hash_make_ext(MyEntry, key, "my table", 64, &opts);
+ */
+#define hash_make_ext(entrytype, keymember, tabname, nelem, opts) \
+	hash_make_ext_cxt(entrytype, keymember, tabname, nelem, opts, CurrentMemoryContext)
+
+#define hash_make_ext_cxt(entrytype, keymember, tabname, nelem, opts, mcxt) \
+	(StaticAssertExpr(offsetof(entrytype, keymember) == 0, \
+						 #keymember " must be first member in " #entrytype), \
+	hash_make_impl( \
+		(tabname), (nelem), \
+		sizeof(((entrytype *)0)->keymember), \
+		sizeof(entrytype), \
+		HASH_KEY_AS_STRING(entrytype, keymember), \
+		(opts), \
+		(mcxt)))
+
+
+/*
+ * Create a hash set where the entire entry is the key. This is
+ * like hash_make, but where the key is also the entry.
+ */
+#define hashset_make(entrytype, tabname, nelem) \
+	hashset_make_cxt(entrytype, tabname, nelem, CurrentMemoryContext)
+#define hashset_make_cxt(entrytype, tabname, nelem, mcxt) \
+	hashset_make_ext_cxt(entrytype, tabname, nelem, NULL, mcxt)
+#define hashset_make_fn(entrytype, tabname, nelem, hashfn, matchfn) \
+	hashset_make_fn_cxt(entrytype, tabname, nelem, hashfn, matchfn, \
+							CurrentMemoryContext)
+#define hashset_make_fn_cxt(entrytype, tabname, nelem, hashfn, matchfn, mcxt) \
+	hash_make_fn_impl((tabname), (nelem), sizeof(entrytype), sizeof(entrytype), \
+						 HASH_TYPE_AS_STRING(entrytype), (hashfn), (matchfn), \
+						 mcxt)
+#define hashset_make_ext(entrytype, tabname, nelem, opts) \
+	hashset_make_ext_cxt(entrytype, tabname, nelem, opts, \
+							CurrentMemoryContext)
+#define hashset_make_ext_cxt(entrytype, tabname, nelem, opts, mcxt) \
+	hash_make_impl((tabname), (nelem), sizeof(entrytype), sizeof(entrytype), \
+				   HASH_TYPE_AS_STRING(entrytype), opts, (mcxt))
+
+/*
+ * Implementation function for hash_make macros. Not meant to be called
+ * directly.
+ *
+ * If string_key is true, the key is treated as a null-terminated string.
+ * Pass NULL for opts to use all defaults.
+ */
+extern HTAB *hash_make_impl(const char *tabname, int64 nelem,
+							Size keysize, Size entrysize,
+							bool string_key,
+							const HASHOPTS *opts,
+							MemoryContext mcxt);
+
+/*
+ * Implementation function for hash_make_fn macros.
+ */
+extern HTAB *hash_make_fn_impl(const char *tabname, int64 nelem,
+							   Size keysize, Size entrysize, bool string_key,
+							   HashValueFunc hashfn, HashCompareFunc matchfn,
+							   MemoryContext mcxt);
+
 /*
  * HASHELEMENT is the private part of a hashtable entry.  The caller's data
  * follows the HASHELEMENT structure (on a MAXALIGN'd boundary).  The hash key
@@ -56,11 +209,11 @@ typedef struct HASHELEMENT
 /* Hash table header struct is an opaque type known only within dynahash.c */
 typedef struct HASHHDR HASHHDR;
 
-/* Hash table control struct is an opaque type known only within dynahash.c */
-typedef struct HTAB HTAB;
-
-/* Parameter data structure for hash_create */
-/* Only those fields indicated by hash_flags need be set */
+/*
+ * Parameter data structure for hash_create (which is the low-level method of
+ * initializing hash tables, hash_make macros are preferred)
+ * Only those fields indicated by hash_flags need be set
+ */
 typedef struct HASHCTL
 {
 	/* Used if HASH_PARTITION flag is set: */
@@ -129,6 +282,9 @@ typedef struct
  */
 extern HTAB *hash_create(const char *tabname, int64 nelem,
 						 const HASHCTL *info, int flags);
+extern void hash_opts_init(HASHCTL *ctl, int *flags,
+						   Size keysize, Size entrysize, bool string_key,
+						   const HASHOPTS *opts);
 extern void hash_destroy(HTAB *hashp);
 extern void hash_stats(const char *caller, HTAB *hashp);
 extern void *hash_search(HTAB *hashp, const void *keyPtr, HASHACTION action,
diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext.cpp b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
index df7a592cb70..83bb99c4d2d 100644
--- a/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
+++ b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
@@ -19,6 +19,7 @@ extern "C" {
 #include "fmgr.h"
 #include "nodes/pg_list.h"
 #include "nodes/primnodes.h"
+#include "utils/hsearch.h"
 
 PG_MODULE_MAGIC;
 
@@ -75,6 +76,51 @@ test_cplusplus_add(PG_FUNCTION_ARGS)
 	pfree(node);
 	pfree(copy);
 
+	/* Test hash macros compile under C++ */
+	{
+		typedef struct
+		{
+			Oid			oid;
+			int			data;
+		} OidEntry;
+
+		typedef struct
+		{
+			char		name[NAMEDATALEN];
+			int			value;
+		} NameEntry;
+
+		HTAB	   *htab;
+
+		StaticAssertStmt(!HASH_KEY_AS_STRING(OidEntry, oid),
+						 "Oid key should use HASH_BLOBS");
+		StaticAssertStmt(!HASH_TYPE_AS_STRING(Oid),
+						 "Oid hashset should use HASH_BLOBS");
+		StaticAssertStmt(HASH_KEY_AS_STRING(NameEntry, name),
+						 "char[] key should use HASH_STRINGS");
+		StaticAssertStmt(HASH_TYPE_AS_STRING(NameData),
+						 "NameData should use HASH_STRINGS");
+
+		htab = hash_make(OidEntry, oid, "C++ oid hash", 8);
+		hash_destroy(htab);
+
+		htab = hash_make(NameEntry, name, "C++ name hash", 8);
+		hash_destroy(htab);
+
+		htab = hash_make_cxt(OidEntry, oid, "C++ cxt hash", 8,
+							 CurrentMemoryContext);
+		hash_destroy(htab);
+
+		htab = hash_make_ext(OidEntry, oid, "C++ ext hash", 8, NULL);
+		hash_destroy(htab);
+
+		htab = hash_make_fn(OidEntry, oid, "C++ fn hash", 8, NULL, NULL);
+		hash_destroy(htab);
+
+		htab = hashset_make(Oid, "C++ oid hashset", 8);
+		hash_destroy(htab);
+	}
+
 	switch (a)
 	{
 		case 1:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5bc517602b1..e02318207e5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1187,6 +1187,7 @@ HASHBUCKET
 HASHCTL
 HASHELEMENT
 HASHHDR
+HASHOPTS
 HASHSEGMENT
 HASH_SEQ_STATUS
 HE
-- 
2.53.0

