From 53c905816e20947d8ff0cd0c51e8b5690834255b Mon Sep 17 00:00:00 2001
From: Hannu Krosing <hannuk@google.com>
Date: Tue, 25 Aug 2026 22:13:19 +0000
Subject: [PATCH v2 2/2] Add contrib/pgbench extension exposing benchmark
 functions in SCHEMA pgbench

Add a new contrib extension `pgbench` that exposes the extracted
distribution, permutation, and hashing functions as SQL-callable
routines in schema `pgbench`:
- pgbench.random(min, max)
- pgbench.random_gaussian(min, max, parameter)
- pgbench.random_exponential(min, max, parameter)
- pgbench.random_zipfian(min, max, parameter)
- pgbench.random_poisson(center)
- pgbench.hash_murmur2(val, seed)
- pgbench.hash_fnv1a(val, seed)
- pgbench.hash(val, seed)
- pgbench.permute(val, size, seed)
- pgbench.setseed(seed)

Includes regression test suite and build definitions for both Make and Meson.
---
 contrib/Makefile                     |   1 +
 contrib/meson.build                  |   1 +
 contrib/pgbench/Makefile             |  30 ++++
 contrib/pgbench/expected/pgbench.out | 153 +++++++++++++++++
 contrib/pgbench/meson.build          |  36 ++++
 contrib/pgbench/pgbench--1.0.sql     |  69 ++++++++
 contrib/pgbench/pgbench.c            | 228 +++++++++++++++++++++++++
 contrib/pgbench/pgbench.control      |   6 +
 contrib/pgbench/sql/pgbench.sql      |  52 ++++++
 doc/src/sgml/contrib.sgml            |   2 +
 doc/src/sgml/filelist.sgml           |   1 +
 doc/src/sgml/pgbench-ext.sgml        | 240 +++++++++++++++++++++++++++
 12 files changed, 819 insertions(+)
 create mode 100644 contrib/pgbench/Makefile
 create mode 100644 contrib/pgbench/expected/pgbench.out
 create mode 100644 contrib/pgbench/meson.build
 create mode 100644 contrib/pgbench/pgbench--1.0.sql
 create mode 100644 contrib/pgbench/pgbench.c
 create mode 100644 contrib/pgbench/pgbench.control
 create mode 100644 contrib/pgbench/sql/pgbench.sql
 create mode 100644 doc/src/sgml/pgbench-ext.sgml

diff --git a/contrib/Makefile b/contrib/Makefile
index 7d91fe77db3..dd74e9ce996 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -44,6 +44,7 @@ SUBDIRS = \
 		pgstattuple	\
 		pg_visibility	\
 		pg_walinspect	\
+		pgbench		\
 		postgres_fdw	\
 		seg		\
 		spi		\
diff --git a/contrib/meson.build b/contrib/meson.build
index ebb7f83d8c5..8fbaa2f12f7 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -58,6 +58,7 @@ subdir('pg_surgery')
 subdir('pg_trgm')
 subdir('pg_visibility')
 subdir('pg_walinspect')
+subdir('pgbench')
 subdir('postgres_fdw')
 subdir('seg')
 subdir('sepgsql')
diff --git a/contrib/pgbench/Makefile b/contrib/pgbench/Makefile
new file mode 100644
index 00000000000..8209d596613
--- /dev/null
+++ b/contrib/pgbench/Makefile
@@ -0,0 +1,30 @@
+# contrib/pgbench/Makefile
+
+MODULE_big = pgbench
+OBJS = \
+	$(WIN32RES) \
+	pgbench.o \
+	pgbench_funcs.o
+
+EXTENSION = pgbench
+DATA = pgbench--1.0.sql
+PGFILEDESC = "pgbench - random distributions and benchmarking utilities"
+
+REGRESS = pgbench
+
+EXTRA_CLEAN = pgbench_funcs.c
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pgbench
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+# pgbench_funcs.c is shared from src/common
+pgbench_funcs.c: % : $(top_srcdir)/src/common/%
+	rm -f $@ && $(LN_S) $< .
diff --git a/contrib/pgbench/expected/pgbench.out b/contrib/pgbench/expected/pgbench.out
new file mode 100644
index 00000000000..ba6be8a79c2
--- /dev/null
+++ b/contrib/pgbench/expected/pgbench.out
@@ -0,0 +1,153 @@
+CREATE EXTENSION pgbench;
+-- Check schema and functions
+\dx+ pgbench
+                   Objects in extension "pgbench"
+                         Object description                          
+---------------------------------------------------------------------
+ function pgbench.hash(bigint,bigint)
+ function pgbench.hash_fnv1a(bigint,bigint)
+ function pgbench.hash_murmur2(bigint,bigint)
+ function pgbench.permute(bigint,bigint,bigint)
+ function pgbench.random(bigint,bigint)
+ function pgbench.random_exponential(bigint,bigint,double precision)
+ function pgbench.random_gaussian(bigint,bigint,double precision)
+ function pgbench.random_poisson(double precision)
+ function pgbench.random_zipfian(bigint,bigint,double precision)
+ function pgbench.setseed(bigint)
+ function pgbench.setseed(double precision)
+(11 rows)
+
+-- Test Hashing functions
+SELECT pgbench.hash_murmur2(12345, 0);
+     hash_murmur2     
+----------------------
+ -8599128181304237022
+(1 row)
+
+SELECT pgbench.hash_murmur2(12345, 42);
+    hash_murmur2     
+---------------------
+ -591810079667318857
+(1 row)
+
+SELECT pgbench.hash_fnv1a(12345, 0);
+      hash_fnv1a      
+----------------------
+ -1792800413050876852
+(1 row)
+
+SELECT pgbench.hash_fnv1a(12345, 42);
+      hash_fnv1a      
+----------------------
+ -5267424053501251834
+(1 row)
+
+-- pgbench.hash is alias for hash_murmur2
+SELECT pgbench.hash(12345, 0) = pgbench.hash_murmur2(12345, 0);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT pgbench.hash(12345, 42) = pgbench.hash_murmur2(12345, 42);
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Test Permutation function (bijective mapping over 0..9)
+SELECT array_agg(pgbench.permute(i, 10, 42) ORDER BY i) FROM generate_series(0, 9) i;
+       array_agg       
+-----------------------
+ {1,7,0,4,3,8,6,5,9,2}
+(1 row)
+
+SELECT count(DISTINCT pgbench.permute(i, 10, 42)) = 10 FROM generate_series(0, 9) i;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT count(DISTINCT pgbench.permute(i, 100, 12345)) = 100 FROM generate_series(0, 99) i;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Test setseed and reproducibility
+SELECT pgbench.setseed(0.5);
+ setseed 
+---------
+ 
+(1 row)
+
+SELECT pgbench.random(1, 100) AS r_unif,
+       pgbench.random_gaussian(1, 100, 2.5) AS r_gauss,
+       pgbench.random_exponential(1, 100, 3.0) AS r_exp,
+       pgbench.random_zipfian(1, 100, 1.5) AS r_zipf,
+       pgbench.random_poisson(50.0) AS r_poiss;
+ r_unif | r_gauss | r_exp | r_zipf | r_poiss 
+--------+---------+-------+--------+---------
+     17 |      60 |    61 |     50 |      36
+(1 row)
+
+-- Reset seed and verify exact same values
+SELECT pgbench.setseed(0.5);
+ setseed 
+---------
+ 
+(1 row)
+
+SELECT pgbench.random(1, 100) AS r_unif,
+       pgbench.random_gaussian(1, 100, 2.5) AS r_gauss,
+       pgbench.random_exponential(1, 100, 3.0) AS r_exp,
+       pgbench.random_zipfian(1, 100, 1.5) AS r_zipf,
+       pgbench.random_poisson(50.0) AS r_poiss;
+ r_unif | r_gauss | r_exp | r_zipf | r_poiss 
+--------+---------+-------+--------+---------
+     17 |      60 |    61 |     50 |      36
+(1 row)
+
+-- Test bigint setseed
+SELECT pgbench.setseed(123456789::bigint);
+ setseed 
+---------
+ 
+(1 row)
+
+SELECT pgbench.random(1, 100);
+ random 
+--------
+     41
+(1 row)
+
+SELECT pgbench.setseed(123456789::bigint);
+ setseed 
+---------
+ 
+(1 row)
+
+SELECT pgbench.random(1, 100);
+ random 
+--------
+     41
+(1 row)
+
+-- Error cases: parameter boundaries
+SELECT pgbench.random(10, 5);
+ERROR:  empty range given to random: lower bound 10 is greater than upper bound 5
+SELECT pgbench.random_gaussian(1, 10, 1.5);
+ERROR:  gaussian parameter must be at least 2.000000 (not 1.500000)
+SELECT pgbench.random_exponential(1, 10, 0.0);
+ERROR:  exponential parameter must be greater than zero (not 0.000000)
+SELECT pgbench.random_zipfian(1, 10, 1.0);
+ERROR:  zipfian parameter must be in range [1.001, 1000] (not 1.000000)
+SELECT pgbench.random_zipfian(1, 10, 1001.0);
+ERROR:  zipfian parameter must be in range [1.001, 1000] (not 1001.000000)
+SELECT pgbench.random_poisson(0.0);
+ERROR:  poisson center parameter must be greater than zero (not 0.000000)
+SELECT pgbench.permute(5, 0, 42);
+ERROR:  permute size parameter must be greater than zero
+SELECT pgbench.setseed(1.5);
+ERROR:  setseed parameter 1.5 is out of allowed range [-1,1]
+DROP EXTENSION pgbench;
diff --git a/contrib/pgbench/meson.build b/contrib/pgbench/meson.build
new file mode 100644
index 00000000000..256970c9daf
--- /dev/null
+++ b/contrib/pgbench/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+pgbench_ext_sources = files(
+  'pgbench.c',
+  '../../src/common/pgbench_funcs.c',
+)
+
+if host_system == 'windows'
+  pgbench_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'pgbench',
+    '--FILEDESC', 'pgbench - random distributions and benchmarking utilities',])
+endif
+
+pgbench_ext = shared_module('pgbench',
+  pgbench_ext_sources,
+  c_pch: pch_postgres_h,
+  kwargs: contrib_mod_args,
+)
+contrib_targets += pgbench_ext
+
+install_data(
+  'pgbench.control',
+  'pgbench--1.0.sql',
+  kwargs: contrib_data_args,
+)
+
+tests += {
+  'name': 'pgbench_ext',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'pgbench',
+    ],
+  },
+}
diff --git a/contrib/pgbench/pgbench--1.0.sql b/contrib/pgbench/pgbench--1.0.sql
new file mode 100644
index 00000000000..28bdb7cc546
--- /dev/null
+++ b/contrib/pgbench/pgbench--1.0.sql
@@ -0,0 +1,69 @@
+/* contrib/pgbench/pgbench--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pgbench" to load this file. \quit
+
+-- PRNG Seeding
+CREATE FUNCTION setseed(seed double precision)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgbench_setseed_double'
+LANGUAGE C STRICT VOLATILE PARALLEL UNSAFE;
+
+CREATE FUNCTION setseed(seed bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgbench_setseed_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL UNSAFE;
+
+-- Uniform Random
+CREATE FUNCTION random(min bigint, max bigint)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_random_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+-- Gaussian (Normal) Random
+CREATE FUNCTION random_gaussian(min bigint, max bigint, parameter double precision)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_random_gaussian_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+-- Exponential Random
+CREATE FUNCTION random_exponential(min bigint, max bigint, parameter double precision)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_random_exponential_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+-- Zipfian Random
+CREATE FUNCTION random_zipfian(min bigint, max bigint, parameter double precision)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_random_zipfian_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+-- Poisson Random
+CREATE FUNCTION random_poisson(center double precision)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_random_poisson_int64'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+-- MurmurHash2 (64-bit)
+CREATE FUNCTION hash_murmur2(val bigint, seed bigint DEFAULT 0)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_hash_murmur2_int64'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+-- FNV-1a Hash (64-bit)
+CREATE FUNCTION hash_fnv1a(val bigint, seed bigint DEFAULT 0)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_hash_fnv1a_int64'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+-- Alias for hash_murmur2
+CREATE FUNCTION hash(val bigint, seed bigint DEFAULT 0)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_hash_murmur2_int64'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+-- Pseudorandom Permutation
+CREATE FUNCTION permute(val bigint, size bigint, seed bigint DEFAULT 0)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'pgbench_permute_int64'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
diff --git a/contrib/pgbench/pgbench.c b/contrib/pgbench/pgbench.c
new file mode 100644
index 00000000000..f0cb3cb5679
--- /dev/null
+++ b/contrib/pgbench/pgbench.c
@@ -0,0 +1,228 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgbench.c
+ *	  Server-side extension functions exposing pgbench random distributions,
+ *	  permutation, and hashing functions.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  contrib/pgbench/pgbench.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <math.h>
+
+#include "common/int.h"
+#include "common/pgbench_funcs.h"
+#include "common/pg_prng.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+/* Shared PRNG state used by pgbench extension random functions */
+static pg_prng_state pgbench_prng_state;
+static bool pgbench_prng_seed_set = false;
+
+/*
+ * Initialize (seed) the PRNG, if not done yet in this backend process.
+ */
+static void
+initialize_prng(void)
+{
+	if (unlikely(!pgbench_prng_seed_set))
+	{
+		if (unlikely(!pg_prng_strong_seed(&pgbench_prng_state)))
+		{
+			TimestampTz now = GetCurrentTimestamp();
+			uint64		iseed;
+
+			/* Mix the PID with the most predictable bits of the timestamp */
+			iseed = (uint64) now ^ ((uint64) MyProcPid << 32);
+			pg_prng_seed(&pgbench_prng_state, iseed);
+		}
+		pgbench_prng_seed_set = true;
+	}
+}
+
+static inline void
+check_random_range(int64 min, int64 max)
+{
+	int64		delta;
+
+	if (unlikely(min > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty range given to random: lower bound " INT64_FORMAT " is greater than upper bound " INT64_FORMAT,
+						min, max)));
+
+	if (unlikely(pg_sub_s64_overflow(max, min, &delta) ||
+				 pg_add_s64_overflow(delta, 1, &delta)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("random range is too large")));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_setseed_double);
+Datum
+pgbench_setseed_double(PG_FUNCTION_ARGS)
+{
+	float8		seed = PG_GETARG_FLOAT8(0);
+
+	if (seed < -1.0 || seed > 1.0 || isnan(seed))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("setseed parameter %g is out of allowed range [-1,1]",
+						seed)));
+
+	pg_prng_fseed(&pgbench_prng_state, seed);
+	pgbench_prng_seed_set = true;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(pgbench_setseed_int64);
+Datum
+pgbench_setseed_int64(PG_FUNCTION_ARGS)
+{
+	int64		seed = PG_GETARG_INT64(0);
+
+	pg_prng_seed(&pgbench_prng_state, (uint64) seed);
+	pgbench_prng_seed_set = true;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(pgbench_random_int64);
+Datum
+pgbench_random_int64(PG_FUNCTION_ARGS)
+{
+	int64		min = PG_GETARG_INT64(0);
+	int64		max = PG_GETARG_INT64(1);
+
+	check_random_range(min, max);
+	initialize_prng();
+
+	PG_RETURN_INT64(pgbench_random(&pgbench_prng_state, min, max));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_random_gaussian_int64);
+Datum
+pgbench_random_gaussian_int64(PG_FUNCTION_ARGS)
+{
+	int64		min = PG_GETARG_INT64(0);
+	int64		max = PG_GETARG_INT64(1);
+	float8		param = PG_GETARG_FLOAT8(2);
+
+	check_random_range(min, max);
+
+	if (isnan(param) || param < PGBENCH_MIN_GAUSSIAN_PARAM)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("gaussian parameter must be at least %f (not %f)",
+						PGBENCH_MIN_GAUSSIAN_PARAM, param)));
+
+	initialize_prng();
+
+	PG_RETURN_INT64(pgbench_random_gaussian(&pgbench_prng_state, min, max, param));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_random_exponential_int64);
+Datum
+pgbench_random_exponential_int64(PG_FUNCTION_ARGS)
+{
+	int64		min = PG_GETARG_INT64(0);
+	int64		max = PG_GETARG_INT64(1);
+	float8		param = PG_GETARG_FLOAT8(2);
+
+	check_random_range(min, max);
+
+	if (isnan(param) || param <= 0.0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("exponential parameter must be greater than zero (not %f)",
+						param)));
+
+	initialize_prng();
+
+	PG_RETURN_INT64(pgbench_random_exponential(&pgbench_prng_state, min, max, param));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_random_zipfian_int64);
+Datum
+pgbench_random_zipfian_int64(PG_FUNCTION_ARGS)
+{
+	int64		min = PG_GETARG_INT64(0);
+	int64		max = PG_GETARG_INT64(1);
+	float8		param = PG_GETARG_FLOAT8(2);
+
+	check_random_range(min, max);
+
+	if (isnan(param) || param < PGBENCH_MIN_ZIPFIAN_PARAM || param > PGBENCH_MAX_ZIPFIAN_PARAM)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("zipfian parameter must be in range [%.3f, %.0f] (not %f)",
+						PGBENCH_MIN_ZIPFIAN_PARAM, PGBENCH_MAX_ZIPFIAN_PARAM, param)));
+
+	initialize_prng();
+
+	PG_RETURN_INT64(pgbench_random_zipfian(&pgbench_prng_state, min, max, param));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_random_poisson_int64);
+Datum
+pgbench_random_poisson_int64(PG_FUNCTION_ARGS)
+{
+	float8		center = PG_GETARG_FLOAT8(0);
+
+	if (isnan(center) || center <= 0.0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("poisson center parameter must be greater than zero (not %f)",
+						center)));
+
+	initialize_prng();
+
+	PG_RETURN_INT64(pgbench_random_poisson(&pgbench_prng_state, center));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_hash_murmur2_int64);
+Datum
+pgbench_hash_murmur2_int64(PG_FUNCTION_ARGS)
+{
+	int64		val = PG_GETARG_INT64(0);
+	int64		seed = PG_GETARG_INT64(1);
+
+	PG_RETURN_INT64(pgbench_hash_murmur2(val, (uint64) seed));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_hash_fnv1a_int64);
+Datum
+pgbench_hash_fnv1a_int64(PG_FUNCTION_ARGS)
+{
+	int64		val = PG_GETARG_INT64(0);
+	int64		seed = PG_GETARG_INT64(1);
+
+	PG_RETURN_INT64(pgbench_hash_fnv1a(val, (uint64) seed));
+}
+
+PG_FUNCTION_INFO_V1(pgbench_permute_int64);
+Datum
+pgbench_permute_int64(PG_FUNCTION_ARGS)
+{
+	int64		val = PG_GETARG_INT64(0);
+	int64		size = PG_GETARG_INT64(1);
+	int64		seed = PG_GETARG_INT64(2);
+
+	if (size <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("permute size parameter must be greater than zero")));
+
+	PG_RETURN_INT64(pgbench_permute(val, size, seed));
+}
diff --git a/contrib/pgbench/pgbench.control b/contrib/pgbench/pgbench.control
new file mode 100644
index 00000000000..0c38189cb99
--- /dev/null
+++ b/contrib/pgbench/pgbench.control
@@ -0,0 +1,6 @@
+# pgbench extension
+comment = 'pgbench distribution, permutation, and hashing functions'
+default_version = '1.0'
+module_pathname = '$libdir/pgbench'
+relocatable = false
+schema = 'pgbench'
diff --git a/contrib/pgbench/sql/pgbench.sql b/contrib/pgbench/sql/pgbench.sql
new file mode 100644
index 00000000000..a966f56f433
--- /dev/null
+++ b/contrib/pgbench/sql/pgbench.sql
@@ -0,0 +1,52 @@
+CREATE EXTENSION pgbench;
+
+-- Check schema and functions
+\dx+ pgbench
+
+-- Test Hashing functions
+SELECT pgbench.hash_murmur2(12345, 0);
+SELECT pgbench.hash_murmur2(12345, 42);
+SELECT pgbench.hash_fnv1a(12345, 0);
+SELECT pgbench.hash_fnv1a(12345, 42);
+-- pgbench.hash is alias for hash_murmur2
+SELECT pgbench.hash(12345, 0) = pgbench.hash_murmur2(12345, 0);
+SELECT pgbench.hash(12345, 42) = pgbench.hash_murmur2(12345, 42);
+
+-- Test Permutation function (bijective mapping over 0..9)
+SELECT array_agg(pgbench.permute(i, 10, 42) ORDER BY i) FROM generate_series(0, 9) i;
+SELECT count(DISTINCT pgbench.permute(i, 10, 42)) = 10 FROM generate_series(0, 9) i;
+SELECT count(DISTINCT pgbench.permute(i, 100, 12345)) = 100 FROM generate_series(0, 99) i;
+
+-- Test setseed and reproducibility
+SELECT pgbench.setseed(0.5);
+SELECT pgbench.random(1, 100) AS r_unif,
+       pgbench.random_gaussian(1, 100, 2.5) AS r_gauss,
+       pgbench.random_exponential(1, 100, 3.0) AS r_exp,
+       pgbench.random_zipfian(1, 100, 1.5) AS r_zipf,
+       pgbench.random_poisson(50.0) AS r_poiss;
+
+-- Reset seed and verify exact same values
+SELECT pgbench.setseed(0.5);
+SELECT pgbench.random(1, 100) AS r_unif,
+       pgbench.random_gaussian(1, 100, 2.5) AS r_gauss,
+       pgbench.random_exponential(1, 100, 3.0) AS r_exp,
+       pgbench.random_zipfian(1, 100, 1.5) AS r_zipf,
+       pgbench.random_poisson(50.0) AS r_poiss;
+
+-- Test bigint setseed
+SELECT pgbench.setseed(123456789::bigint);
+SELECT pgbench.random(1, 100);
+SELECT pgbench.setseed(123456789::bigint);
+SELECT pgbench.random(1, 100);
+
+-- Error cases: parameter boundaries
+SELECT pgbench.random(10, 5);
+SELECT pgbench.random_gaussian(1, 10, 1.5);
+SELECT pgbench.random_exponential(1, 10, 0.0);
+SELECT pgbench.random_zipfian(1, 10, 1.0);
+SELECT pgbench.random_zipfian(1, 10, 1001.0);
+SELECT pgbench.random_poisson(0.0);
+SELECT pgbench.permute(5, 0, 42);
+SELECT pgbench.setseed(1.5);
+
+DROP EXTENSION pgbench;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index b9b03654aad..0edd60b4a66 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -100,6 +100,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
    <member><xref linkend="lo"/></member>
    <member><xref linkend="ltree"/></member>
    <member><xref linkend="pgcrypto"/></member>
+   <member><xref linkend="pgbench-ext"/></member>
    <member><xref linkend="pgtrgm"/></member>
    <member><xref linkend="seg"/></member>
    <member><xref linkend="tablefunc"/></member>
@@ -166,6 +167,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
  &pgtrgm;
  &pgvisibility;
  &pgwalinspect;
+ &pgbench-ext;
  &postgres-fdw;
  &seg;
  &sepgsql;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 66ea8b988a1..0ae47865d12 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -159,6 +159,7 @@
 <!ENTITY pgtrgm          SYSTEM "pgtrgm.sgml">
 <!ENTITY pgvisibility    SYSTEM "pgvisibility.sgml">
 <!ENTITY pgwalinspect    SYSTEM "pgwalinspect.sgml">
+<!ENTITY pgbench-ext     SYSTEM "pgbench-ext.sgml">
 <!ENTITY postgres-fdw    SYSTEM "postgres-fdw.sgml">
 <!ENTITY seg             SYSTEM "seg.sgml">
 <!ENTITY contrib-spi     SYSTEM "contrib-spi.sgml">
diff --git a/doc/src/sgml/pgbench-ext.sgml b/doc/src/sgml/pgbench-ext.sgml
new file mode 100644
index 00000000000..bbd94388457
--- /dev/null
+++ b/doc/src/sgml/pgbench-ext.sgml
@@ -0,0 +1,240 @@
+<!-- doc/src/sgml/pgbench-ext.sgml -->
+
+<sect1 id="pgbench-ext" xreflabel="pgbench (extension)">
+ <title>pgbench &mdash; benchmark random distributions, hashing, and permutation functions</title>
+
+ <indexterm zone="pgbench-ext">
+  <primary>pgbench</primary>
+  <secondary>extension</secondary>
+ </indexterm>
+
+ <para>
+  The <filename>pgbench</filename> module provides SQL-callable functions
+  implementing the statistical random distribution generators, domain
+  permutations, and 64-bit hashing routines from the
+  <xref linkend="pgbench"/> benchmarking tool.
+ </para>
+
+ <para>
+  These functions are installed into the <literal>pgbench</literal> schema and
+  enable reproducible server-side synthetic data generation, procedural
+  workload simulation, and client/server algorithmic parity in custom test
+  harnesses.
+ </para>
+
+ <para>
+  This module is considered <quote>trusted</quote>, that is, it can be
+  installed by non-superusers who have <literal>CREATE</literal> privilege
+  on the current database.
+ </para>
+
+ <sect2 id="pgbench-ext-funcs">
+  <title>Functions</title>
+
+  <para>
+   All functions provided by this extension are installed in the
+   <literal>pgbench</literal> schema.
+  </para>
+
+  <variablelist>
+   <varlistentry id="pgbench-ext-funcs-setseed">
+    <term>
+     <function>pgbench.setseed(seed double precision) returns void</function>
+     <indexterm>
+      <primary>setseed</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <term>
+     <function>pgbench.setseed(seed bigint) returns void</function>
+    </term>
+    <listitem>
+     <para>
+      Sets the seed for the backend's internal pseudorandom number generator used
+      by the <literal>pgbench</literal> random functions.  When passed a
+      <type>double precision</type> value, it must be in the range
+      <literal>[-1.0, 1.0]</literal>.  When passed a <type>bigint</type> value,
+      it initializes the full 64-bit state directly.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-random">
+    <term>
+     <function>pgbench.random(min bigint, max bigint) returns bigint</function>
+     <indexterm>
+      <primary>random</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Generates a uniformly-distributed random integer between
+      <parameter>min</parameter> and <parameter>max</parameter>, inclusive.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-random-gaussian">
+    <term>
+     <function>pgbench.random_gaussian(min bigint, max bigint, parameter double precision) returns bigint</function>
+     <indexterm>
+      <primary>random_gaussian</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Generates a Gaussian-distributed random integer between
+      <parameter>min</parameter> and <parameter>max</parameter>, inclusive.
+      The <parameter>parameter</parameter> defines how concentrated the values
+      are toward the center of the range and must be at least <literal>2.0</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-random-exponential">
+    <term>
+     <function>pgbench.random_exponential(min bigint, max bigint, parameter double precision) returns bigint</function>
+     <indexterm>
+      <primary>random_exponential</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Generates an exponentially-distributed random integer between
+      <parameter>min</parameter> and <parameter>max</parameter>, inclusive.
+      The <parameter>parameter</parameter> controls the distribution density and
+      must be greater than <literal>0.0</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-random-zipfian">
+    <term>
+     <function>pgbench.random_zipfian(min bigint, max bigint, parameter double precision) returns bigint</function>
+     <indexterm>
+      <primary>random_zipfian</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Generates a Zipfian-distributed random integer between
+      <parameter>min</parameter> and <parameter>max</parameter>, inclusive.
+      The <parameter>parameter</parameter> (Zipfian skew factor <literal>s</literal>)
+      must be in the range <literal>[1.001, 1000.0]</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-random-poisson">
+    <term>
+     <function>pgbench.random_poisson(center double precision) returns bigint</function>
+     <indexterm>
+      <primary>random_poisson</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Generates a Poisson-distributed random integer centered on
+      <parameter>center</parameter>, which must be greater than <literal>0.0</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-hash-murmur2">
+    <term>
+     <function>pgbench.hash_murmur2(val bigint [, seed bigint ]) returns bigint</function>
+     <indexterm>
+      <primary>hash_murmur2</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Computes the 64-bit Austin Appleby MurmurHash2 of <parameter>val</parameter>
+      using optional <parameter>seed</parameter> (default is <literal>0</literal>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-hash-fnv1a">
+    <term>
+     <function>pgbench.hash_fnv1a(val bigint [, seed bigint ]) returns bigint</function>
+     <indexterm>
+      <primary>hash_fnv1a</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Computes the 64-bit Fowler&ndash;Noll&ndash;Vo 1a hash of
+      <parameter>val</parameter> using optional <parameter>seed</parameter>
+      (default is <literal>0</literal>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-hash">
+    <term>
+     <function>pgbench.hash(val bigint [, seed bigint ]) returns bigint</function>
+     <indexterm>
+      <primary>hash</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      An alias for <function>pgbench.hash_murmur2</function>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="pgbench-ext-funcs-permute">
+    <term>
+     <function>pgbench.permute(val bigint, size bigint [, seed bigint ]) returns bigint</function>
+     <indexterm>
+      <primary>permute</primary>
+      <secondary>in pgbench</secondary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Permutes <parameter>val</parameter> into the range
+      <literal>[0, size)</literal> using a multi-round Feistel-style bijective
+      permutation seeded with <parameter>seed</parameter> (default is
+      <literal>0</literal>).  <parameter>size</parameter> must be greater than
+      <literal>0</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </sect2>
+
+ <sect2 id="pgbench-ext-examples">
+  <title>Examples</title>
+
+  <para>
+   Generate a reproducible synthetic customer workload directly within SQL:
+  </para>
+
+<programlisting>
+CREATE EXTENSION pgbench;
+
+-- Seed PRNG for reproducible test run
+SELECT pgbench.setseed(42::bigint);
+
+-- Generate non-uniform account IDs following a Zipfian distribution
+SELECT pgbench.random_zipfian(1, 100000, 1.2) AS aid
+FROM generate_series(1, 10);
+
+-- Generate a unique permutation of customer IDs from 0 to 999
+SELECT pgbench.permute(i, 1000, 12345) AS permuted_id
+FROM generate_series(0, 999) i;
+</programlisting>
+ </sect2>
+
+</sect1>
-- 
2.55.0.897.gb25b4bd76c-goog

