From 023ba457fb18ef6c1fba658578e5c0ce04ac854b Mon Sep 17 00:00:00 2001
From: damien <damien.clochard@dalibo.com>
Date: Thu, 3 Jul 2025 13:13:30 +0000
Subject: [PATCH v1] Generate random dates/times in a specified range

This adds 5 new variants of the random() function:

    random(min date, max date) returns date
    random(min time, max time) returns time
    random(min time, max time, zone text) returns timetz
    random(min timestamp, max timestamp) returns timestamp
    random(min timestamptz, max timestamptz) returns timestamptz

Each one returns a random date/time value t in the range min <= t <= max.

For the timetz function, a third parameter is required to define the timezone.
However if the value is an empty string, the session timezone is used.

These functions all rely on the pg_prng_int64_range function developped in
PG 17 for the random(bigint,bigint) function.
---
 doc/src/sgml/func.sgml                    |  23 +++
 src/backend/utils/adt/pseudorandomfuncs.c | 169 +++++++++++++++++++++-
 src/include/catalog/pg_proc.dat           |  20 +++
 src/test/regress/expected/random.out      | 163 +++++++++++++++++++++
 src/test/regress/sql/random.sql           |  45 ++++++
 5 files changed, 412 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index c28aa71f570..a98bb501e9b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -1926,12 +1926,35 @@ SELECT NOT(ROW(table.*) IS NOT NULL) FROM TABLE; -- detect at least one null in
         <function>random</function> ( <parameter>min</parameter> <type>numeric</type>, <parameter>max</parameter> <type>numeric</type> )
         <returnvalue>numeric</returnvalue>
        </para>
+       <para role="func_signature">
+        <function>random</function> ( <parameter>min</parameter> <type>date</type>, <parameter>max</parameter> <type>date</type> )
+        <returnvalue>date</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>random</function> ( <parameter>min</parameter> <type>time</type>, <parameter>max</parameter> <type>time</type> )
+        <returnvalue>time</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>random</function> ( <parameter>min</parameter> <type>time</type>, <parameter>max</parameter> <type>time</type> <parameter>zone</parameter> <type>text</type>)
+        <returnvalue>timetz</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>random</function> ( <parameter>min</parameter> <type>timestamp</type>, <parameter>max</parameter> <type>timestamp</type> )
+        <returnvalue>timestamp</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>random</function> ( <parameter>min</parameter> <type>timestamptz</type>, <parameter>max</parameter> <type>timestamptz</type> )
+        <returnvalue>timestamptz</returnvalue>
+       </para>
        <para>
         Returns a random value in the range
         <parameter>min</parameter> &lt;= x &lt;= <parameter>max</parameter>.
         For type <type>numeric</type>, the result will have the same number of
         fractional decimal digits as <parameter>min</parameter> or
         <parameter>max</parameter>, whichever has more.
+        For type <type>timetz</type>, a third parameter is necessary to speficy 
+        in which timezone the random time will be generated. If the zone parameter
+        is an empty string, then the session timezone is used instead.
        </para>
        <para>
         <literal>random(1, 10)</literal>
diff --git a/src/backend/utils/adt/pseudorandomfuncs.c b/src/backend/utils/adt/pseudorandomfuncs.c
index e7b8045f925..5b23e2e2b32 100644
--- a/src/backend/utils/adt/pseudorandomfuncs.c
+++ b/src/backend/utils/adt/pseudorandomfuncs.c
@@ -17,6 +17,9 @@
 
 #include "common/pg_prng.h"
 #include "miscadmin.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datetime.h"
 #include "utils/fmgrprotos.h"
 #include "utils/numeric.h"
 #include "utils/timestamp.h"
@@ -25,6 +28,20 @@
 static pg_prng_state prng_state;
 static bool prng_seed_set = false;
 
+/*
+ * check_range_boundaries() -
+ *
+ *	throw an error if the range boundaries are inverted
+ */
+static void
+check_range_boundaries(int64 rmin, int64 rmax)
+{
+	if (rmin > rmax)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("lower bound must be less than or equal to upper bound"));
+}
+
 /*
  * initialize_prng() -
  *
@@ -129,10 +146,7 @@ int4random(PG_FUNCTION_ARGS)
 	int32		rmax = PG_GETARG_INT32(1);
 	int32		result;
 
-	if (rmin > rmax)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("lower bound must be less than or equal to upper bound"));
+	check_range_boundaries(rmin, rmax);
 
 	initialize_prng();
 
@@ -153,10 +167,7 @@ int8random(PG_FUNCTION_ARGS)
 	int64		rmax = PG_GETARG_INT64(1);
 	int64		result;
 
-	if (rmin > rmax)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("lower bound must be less than or equal to upper bound"));
+	check_range_boundaries(rmin, rmax);
 
 	initialize_prng();
 
@@ -183,3 +194,145 @@ numeric_random(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NUMERIC(result);
 }
+
+
+/*
+ * daterandom() -
+ *
+ *	Returns a random date chosen uniformly in the specified range.
+ */
+Datum
+daterandom(PG_FUNCTION_ARGS)
+{
+	int32		rmin = (int32) PG_GETARG_DATEADT(0);
+	int32		rmax = (int32) PG_GETARG_DATEADT(1);
+	DateADT		result;
+
+	check_range_boundaries(rmin, rmax);
+
+	initialize_prng();
+
+	result = (DateADT) (int32) pg_prng_int64_range(&prng_state, rmin, rmax);
+
+	PG_RETURN_DATEADT(result);
+}
+
+
+/*
+ * timerandom() -
+ *
+ *	Generate random time chosen uniformly in the specified range.
+ */
+Datum
+timerandom(PG_FUNCTION_ARGS)
+{
+	int64		rmin = (int64) PG_GETARG_TIMEADT(0);
+	int64		rmax = (int64) PG_GETARG_TIMEADT(1);
+	TimeADT		result;
+
+	check_range_boundaries(rmin, rmax);
+
+	initialize_prng();
+
+	result = (TimeADT) pg_prng_int64_range(&prng_state, rmin, rmax);
+
+	PG_RETURN_TIMEADT(result);
+}
+
+/*
+ * timetzrandom() -
+ *
+ *	Generate random timetz chosen uniformly in the specified range.
+ *
+ * This function is a bit more complex that the other temporal random generators
+ * because the TIMETZ type stores the timezone.
+ * So we allow users to provide a third parameter in order to define in which
+ * zone the random value will be generated.
+ * If the zone is not specified (i.e. an empty string), then the session timezone
+ * is used instead.
+ *
+ * This behaviour is based on generate_series_timestamptz_internal
+ *
+ */
+Datum
+timetzrandom(PG_FUNCTION_ARGS)
+{
+	int64		rmin = (int64) PG_GETARG_TIMEADT(0);
+	int64		rmax = (int64) PG_GETARG_TIMEADT(1);
+	text		*zone_ptr = PG_GETARG_TEXT_PP(2);
+	char		zone_str[TZ_STRLEN_MAX + 1];
+	TimeADT		time;
+	TimeTzADT	*result;
+	struct pg_tm	tt, *tm = &tt;
+	fsec_t		fsec;
+	int			zone;
+	pg_tz		*tzp;
+
+	check_range_boundaries(rmin, rmax);
+
+	initialize_prng();
+
+	time = (TimeADT) pg_prng_int64_range(&prng_state, rmin, rmax);
+
+	text_to_cstring_buffer(zone_ptr, zone_str, sizeof(zone_str));
+
+	GetCurrentDateTime(tm);
+	time2tm(time, tm, &fsec);
+
+	if ( strlen(zone_str) > 0 ) {
+		DecodeTimezoneName(zone_str, &zone, &tzp);
+		zone = DetermineTimeZoneOffset(tm, tzp);
+	} else {
+		zone = DetermineTimeZoneOffset(tm,session_timezone);
+	}
+
+	result = (TimeTzADT *) palloc(sizeof(TimeTzADT));
+
+	result->time = time;
+	result->zone = zone;
+
+	PG_RETURN_TIMETZADT_P(result);
+}
+
+/*
+ * timestamprandom() -
+ *
+ *	Generate random timestamp chosen uniformly in the specified range.
+ */
+Datum
+timestamprandom(PG_FUNCTION_ARGS)
+{
+	int64		rmin = (int64) PG_GETARG_TIMESTAMP(0);
+	int64		rmax = (int64) PG_GETARG_TIMESTAMP(1);
+	Timestamp	result;
+
+	check_range_boundaries(rmin, rmax);
+
+	initialize_prng();
+
+	result = (Timestamp) pg_prng_int64_range(&prng_state, rmin, rmax);
+
+	PG_RETURN_TIMESTAMP(result);
+}
+
+/*
+ * timestamptzrandom() -
+ *
+ *	Generate random timestamptz chosen uniformly in the specified range.
+ */
+Datum
+timestamptzrandom(PG_FUNCTION_ARGS)
+{
+	int64		rmin = (int64) PG_GETARG_TIMESTAMPTZ(0);
+	int64		rmax = (int64) PG_GETARG_TIMESTAMPTZ(1);
+	TimestampTz	result;
+
+	check_range_boundaries(rmin, rmax);
+
+	initialize_prng();
+
+	result = (TimestampTz) pg_prng_int64_range(&prng_state, rmin, rmax);
+
+	PG_RETURN_TIMESTAMPTZ(result);
+}
+
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1fc19146f46..d7c6e77f693 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3503,6 +3503,26 @@
   proname => 'random', provolatile => 'v', proparallel => 'r',
   prorettype => 'numeric', proargtypes => 'numeric numeric',
   proargnames => '{min,max}', prosrc => 'numeric_random' },
+{ oid => '6431', descr => 'random date in range',
+  proname => 'random', provolatile => 'v', proparallel => 'r',
+  prorettype => 'date', proargtypes => 'date date',
+  proargnames => '{min,max}', prosrc => 'daterandom' },
+{ oid => '6432', descr => 'random time in range',
+  proname => 'random', provolatile => 'v', proparallel => 'r',
+  prorettype => 'time', proargtypes => 'time time',
+  proargnames => '{min,max}', prosrc => 'timerandom' },
+{ oid => '6433', descr => 'random time in range',
+  proname => 'random', provolatile => 'v', proparallel => 'r',
+  prorettype => 'timetz', proargtypes => 'time time text',
+  proargnames => '{min,max,timezone}', prosrc => 'timetzrandom' },
+{ oid => '6434', descr => 'random timestamp in range',
+  proname => 'random', provolatile => 'v', proparallel => 'r',
+  prorettype => 'timestamp', proargtypes => 'timestamp timestamp',
+  proargnames => '{min,max}', prosrc => 'timestamprandom' },
+{ oid => '6435', descr => 'random timestamptz in range',
+  proname => 'random', provolatile => 'v', proparallel => 'r',
+  prorettype => 'timestamptz', proargtypes => 'timestamptz timestamptz',
+  proargnames => '{min,max}', prosrc => 'timestamptzrandom' },
 { oid => '1599', descr => 'set random seed',
   proname => 'setseed', provolatile => 'v', proparallel => 'r',
   prorettype => 'void', proargtypes => 'float8', prosrc => 'setseed' },
diff --git a/src/test/regress/expected/random.out b/src/test/regress/expected/random.out
index 43cf88a3634..e6942b5bf58 100644
--- a/src/test/regress/expected/random.out
+++ b/src/test/regress/expected/random.out
@@ -536,3 +536,166 @@ SELECT n, random(0, trim_scale(abs(1 - 10.0^(-n)))) FROM generate_series(-20, 20
   20 | 0.60795101234744211935
 (41 rows)
 
+-- random dates
+SELECT random('1979-02-08'::date,'2025-07-03'::date) AS random_date_multiple_years;
+ random_date_multiple_years 
+----------------------------
+ 04-09-1986
+(1 row)
+
+SELECT random('4714-11-24 BC'::date,'5874897-12-31 AD'::date) AS random_date_maximum_range;
+ random_date_maximum_range 
+---------------------------
+ 10-02-2898131
+(1 row)
+
+SELECT random('1979-02-08'::date,'1979-02-08'::date) AS random_date_empty_range;
+ random_date_empty_range 
+-------------------------
+ 02-08-1979
+(1 row)
+
+SELECT random('2024-12-31'::date, '2024-01-01'::date); -- Should error
+ERROR:  lower bound must be less than or equal to upper bound
+-- random times
+SELECT random('08:00:00'::time, '17:00:00'::time) AS random_time_business_hours;
+ random_time_business_hours 
+----------------------------
+ 13:04:44.044271
+(1 row)
+
+SELECT random('00:00:00'::time, '23:59:59'::time) AS random_time_full_day;
+ random_time_full_day 
+----------------------
+ 15:06:47.706519
+(1 row)
+
+SELECT random('12:00:00'::time, '12:00:01'::time) AS random_time_narrow;
+ random_time_narrow 
+--------------------
+ 12:00:00.999285
+(1 row)
+
+SELECT random('12:00:00'::time, '12:00:00'::time) AS random_time_empty_range;
+ random_time_empty_range 
+-------------------------
+ 12:00:00
+(1 row)
+
+SELECT random('17:00:00'::time, '08:00:00'::time); -- Should error
+ERROR:  lower bound must be less than or equal to upper bound
+-- random times with timezone
+SELECT random('08:00:00'::time, '17:00:00'::time,'') AS random_timetz_business_hours;
+ random_timetz_business_hours 
+------------------------------
+ 12:59:44.437204-07
+(1 row)
+
+SELECT random('08:00:00'::time, '17:00:00'::time,'America/Mexico_City') AS random_timetz_mexico;
+ random_timetz_mexico 
+----------------------
+ 15:34:53.273047-06
+(1 row)
+
+SELECT random('08:00:00'::time, '17:00:00'::time,'Asia/Tokyo') AS random_timetz_tokyo;
+ random_timetz_tokyo 
+---------------------
+ 08:35:34.593753+09
+(1 row)
+
+SELECT random('08:00:00'::time, '17:00:00'::time,'JST-9') AS random_timetz_abbrev;
+ random_timetz_abbrev 
+----------------------
+ 15:36:27.750013+09
+(1 row)
+
+SELECT random('08:00:00'::time, '17:00:00'::time,'+06:00') AS random_timetz_offset;
+ random_timetz_offset 
+----------------------
+ 12:29:07.530111-06
+(1 row)
+
+SET TIMEZONE TO 'Asia/Tokyo';
+SELECT random('08:00:00'::time, '17:00:00'::time,'') AS random_timetz_session_timezone;
+ random_timetz_session_timezone 
+--------------------------------
+ 08:12:39.946095+09
+(1 row)
+
+RESET TIMEZONE;
+SELECT random('00:00:00'::time, '23:59:59'::time,'') AS random_timetz_full_day;
+ random_timetz_full_day 
+------------------------
+ 15:58:30.589641-07
+(1 row)
+
+SELECT random('12:00:00'::time, '12:00:01'::time,'') AS random_timetz_narrow;
+ random_timetz_narrow 
+----------------------
+ 12:00:00.980783-07
+(1 row)
+
+SELECT random('12:00:00'::time, '12:00:00'::time,'') AS random_timetz_empty_range;
+ random_timetz_empty_range 
+---------------------------
+ 12:00:00-07
+(1 row)
+
+SELECT random('17:00:00'::time, '08:00:00'::time,''); -- Should error
+ERROR:  lower bound must be less than or equal to upper bound
+SELECT random('08:00:00'::time, '17:00:00'::time,'NOT A TIMEZONE'); -- Should error
+ERROR:  time zone "NOT A TIMEZONE" not recognized
+-- random timestamps
+SELECT random('1979-02-08'::timestamp,'2025-07-03'::timestamp) AS random_timestamp_multiple_years;
+ random_timestamp_multiple_years 
+---------------------------------
+ Fri Feb 28 23:29:49.737033 1986
+(1 row)
+
+SELECT random('4714-11-24 BC'::timestamp,'294276-12-31 23:59:59.999999'::timestamp) AS random_timestamp_maximum_range;
+  random_timestamp_maximum_range  
+----------------------------------
+ Fri Feb 05 07:37:30.307404 12613
+(1 row)
+
+SELECT random('2024-07-01 12:00:00.000001'::timestamp, '2024-07-01 12:00:00.999999'::timestamp) AS random_narrow_range;
+       random_narrow_range       
+---------------------------------
+ Mon Jul 01 12:00:00.969524 2024
+(1 row)
+
+SELECT random('1979-02-08'::timestamp,'1979-02-08'::timestamp) AS random_timestamp_empty_range;
+ random_timestamp_empty_range 
+------------------------------
+ Thu Feb 08 00:00:00 1979
+(1 row)
+
+SELECT random('2024-12-31'::timestamp, '2024-01-01'::timestamp); -- Should error
+ERROR:  lower bound must be less than or equal to upper bound
+-- random timestamps with timezone
+SELECT random('1979-02-08 +01'::timestamptz,'2025-07-03 +02'::timestamptz) AS random_timestamptz_multiple_years;
+ random_timestamptz_multiple_years  
+------------------------------------
+ Sat Apr 16 05:46:15.53804 2016 PDT
+(1 row)
+
+SELECT random('4714-11-24 BC +00'::timestamptz,'294276-12-31 23:59:59.999999 +00'::timestamptz) AS random_timestamptz_maximum_range;
+  random_timestamptz_maximum_range   
+-------------------------------------
+ Mon Oct 16 01:43:21.780585 8344 PDT
+(1 row)
+
+SELECT random('2024-07-01 12:00:00.000001 +04'::timestamptz, '2024-07-01 12:00:00.999999 +04'::timestamptz) AS random_timestamptz_narrow_range;
+   random_timestamptz_narrow_range   
+-------------------------------------
+ Mon Jul 01 01:00:00.863068 2024 PDT
+(1 row)
+
+SELECT random('1979-02-08 +05'::timestamptz,'1979-02-08 +05'::timestamptz) AS random_timestamptz_empty_range;
+ random_timestamptz_empty_range 
+--------------------------------
+ Wed Feb 07 11:00:00 1979 PST
+(1 row)
+
+SELECT random('2024-01-01 +06'::timestamptz, '2024-01-01 +07'::timestamptz); -- Should error
+ERROR:  lower bound must be less than or equal to upper bound
diff --git a/src/test/regress/sql/random.sql b/src/test/regress/sql/random.sql
index ebfa7539ede..00e03cb4bcb 100644
--- a/src/test/regress/sql/random.sql
+++ b/src/test/regress/sql/random.sql
@@ -277,3 +277,48 @@ SELECT random(-1e30, 1e30) FROM generate_series(1, 10);
 SELECT random(-0.4, 0.4) FROM generate_series(1, 10);
 SELECT random(0, 1 - 1e-30) FROM generate_series(1, 10);
 SELECT n, random(0, trim_scale(abs(1 - 10.0^(-n)))) FROM generate_series(-20, 20) n;
+
+-- random dates
+SELECT random('1979-02-08'::date,'2025-07-03'::date) AS random_date_multiple_years;
+SELECT random('4714-11-24 BC'::date,'5874897-12-31 AD'::date) AS random_date_maximum_range;
+SELECT random('1979-02-08'::date,'1979-02-08'::date) AS random_date_empty_range;
+SELECT random('2024-12-31'::date, '2024-01-01'::date); -- Should error
+
+-- random times
+SELECT random('08:00:00'::time, '17:00:00'::time) AS random_time_business_hours;
+SELECT random('00:00:00'::time, '23:59:59'::time) AS random_time_full_day;
+SELECT random('12:00:00'::time, '12:00:01'::time) AS random_time_narrow;
+SELECT random('12:00:00'::time, '12:00:00'::time) AS random_time_empty_range;
+SELECT random('17:00:00'::time, '08:00:00'::time); -- Should error
+
+-- random times with timezone
+SELECT random('08:00:00'::time, '17:00:00'::time,'') AS random_timetz_business_hours;
+SELECT random('08:00:00'::time, '17:00:00'::time,'America/Mexico_City') AS random_timetz_mexico;
+SELECT random('08:00:00'::time, '17:00:00'::time,'Asia/Tokyo') AS random_timetz_tokyo;
+SELECT random('08:00:00'::time, '17:00:00'::time,'JST-9') AS random_timetz_abbrev;
+SELECT random('08:00:00'::time, '17:00:00'::time,'+06:00') AS random_timetz_offset;
+
+SET TIMEZONE TO 'Asia/Tokyo';
+SELECT random('08:00:00'::time, '17:00:00'::time,'') AS random_timetz_session_timezone;
+RESET TIMEZONE;
+
+SELECT random('00:00:00'::time, '23:59:59'::time,'') AS random_timetz_full_day;
+SELECT random('12:00:00'::time, '12:00:01'::time,'') AS random_timetz_narrow;
+SELECT random('12:00:00'::time, '12:00:00'::time,'') AS random_timetz_empty_range;
+SELECT random('17:00:00'::time, '08:00:00'::time,''); -- Should error
+SELECT random('08:00:00'::time, '17:00:00'::time,'NOT A TIMEZONE'); -- Should error
+
+-- random timestamps
+SELECT random('1979-02-08'::timestamp,'2025-07-03'::timestamp) AS random_timestamp_multiple_years;
+SELECT random('4714-11-24 BC'::timestamp,'294276-12-31 23:59:59.999999'::timestamp) AS random_timestamp_maximum_range;
+SELECT random('2024-07-01 12:00:00.000001'::timestamp, '2024-07-01 12:00:00.999999'::timestamp) AS random_narrow_range;
+SELECT random('1979-02-08'::timestamp,'1979-02-08'::timestamp) AS random_timestamp_empty_range;
+SELECT random('2024-12-31'::timestamp, '2024-01-01'::timestamp); -- Should error
+
+-- random timestamps with timezone
+SELECT random('1979-02-08 +01'::timestamptz,'2025-07-03 +02'::timestamptz) AS random_timestamptz_multiple_years;
+SELECT random('4714-11-24 BC +00'::timestamptz,'294276-12-31 23:59:59.999999 +00'::timestamptz) AS random_timestamptz_maximum_range;
+SELECT random('2024-07-01 12:00:00.000001 +04'::timestamptz, '2024-07-01 12:00:00.999999 +04'::timestamptz) AS random_timestamptz_narrow_range;
+SELECT random('1979-02-08 +05'::timestamptz,'1979-02-08 +05'::timestamptz) AS random_timestamptz_empty_range;
+SELECT random('2024-01-01 +06'::timestamptz, '2024-01-01 +07'::timestamptz); -- Should error
+
-- 
2.39.5

