From 5ba2b6745e95bb68674444ceba4fcb07102249a7 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Fri, 21 Aug 2026 07:14:01 +0100
Subject: [PATCH-v15.3 6/8] SLOPE Timezone

Add slope support to functions that use a `timestamp` computed from
a `timestamp with timezone`. Time in certain time zones might have
discontinuities, mostly due to dailight saving time (DST), but maybe
others. The `timestamp with timezone` is represented with resolution
of 1 microsecond at UTC, when converted to a `timestamp` the session
timezone offset of the timezone at the given time is computed and
applied, the offset varies with time, and the two common discontinuities
are a spring-forward when the clock advances creating effectively
skipping a range of timestamps, or a fall-back when the clock moves
backward, creating a range of ambiguous timestamps. The fall-back
discontinuities cause the timestamp to be discontinuous, but certain
parts of the timestamp might remain monotonic. The most relevant
example is the date of a timestamp, in most of the timezones, a
fall back stays within the same day.

Follows a query to the discontinuities discussed above.

    WITH RECURSIVE
    bin AS (
        -- search a intervals of duration t_step
    SELECT n.name AS tz,
            t AS lo,
            t + t_step AS hi,
            t_step
    FROM pg_timezone_names n,
        (VALUES (timestamptz '1900-01-01',
                    timestamptz '2026-01-01',
                    interval '7 days'))
            AS range(t_from, t_to, t_step),
        generate_series(t_from, t_to, t_step) AS t
    WHERE
        -- keep only those having a discontinuity
        timezone(name, t + t_step) - timezone(name, t) <> t_step

    UNION ALL
    SELECT tz,
            CASE WHEN pick_hi
                THEN mid ELSE lo END,
            CASE WHEN pick_hi
                THEN hi ELSE mid END,
            t_step / 2
    FROM bin,
            LATERAL (SELECT (lo + t_step / 2) AS mid,
                    timezone(tz, lo + t_step / 2) - timezone(tz, lo) =
                                    t_step / 2 as pick_hi
                )
    WHERE t_step >= interval '2 microseconds'
    ),
    discontinuities AS (
    SELECT tz,
            hi               AS utc,
            timezone(tz, lo) AS before,
            timezone(tz, hi) AS after
    FROM bin
    WHERE hi - lo < interval '2 microseconds'
    )
    SELECT *
    FROM discontinuities
    -- Having a list of discontinuities, it is easier to respond
    -- questions such as which discontinuities caused the date to
    -- move back
    WHERE after::date < before::date

The functions supported now are
 - date_trunc(unit text, timestamptz)
 - date_trunc(unit text, timestamptz, timezone text)
 - timezone(timestamptz)
 - timezone(timezone text, timestamptz)
 - date(timestamptz)

The timezone monotonicity is performed once when the timezone
is loaded and checked when the monotonicity of one of those
functions is requested.
---
 src/backend/utils/adt/timestamp.c           |   8 +-
 src/backend/utils/fmgr/slopesupport.c       | 197 +++++++++++++++++
 src/include/c.h                             |   5 +
 src/include/catalog/pg_proc.dat             |  22 +-
 src/include/pgtime.h                        |  19 ++
 src/test/regress/expected/slope.out         | 222 +++++++++++++++++---
 src/test/regress/expected/slope_catalog.out | 156 +++++++-------
 src/test/regress/sql/slope.sql              | 123 +++++++++--
 src/test/regress/sql/slope_catalog.sql      |  13 +-
 src/timezone/localtime.c                    |  15 ++
 src/timezone/pgtz.c                         | 161 +++++++++++++-
 src/timezone/pgtz.h                         |   7 +
 12 files changed, 817 insertions(+), 131 deletions(-)

diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 9c17ba2f905..18e94d7bb03 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -1921,10 +1921,6 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, const char
 	Timestamp	time;
 	pg_time_t	utime;
 
-	/* Use session timezone if caller asks for default */
-	if (attimezone == NULL)
-		attimezone = session_timezone;
-
 	time = dt;
 	TMODULO(time, date, USECS_PER_DAY);
 
@@ -1970,7 +1966,9 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, const char
 	utime = (pg_time_t) dt;
 	if ((Timestamp) utime == dt)
 	{
-		struct pg_tm *tx = pg_localtime(&utime, attimezone);
+		/* Use session timezone doesn't pass one */
+		struct pg_tm *tx = pg_localtime(&utime,
+			attimezone == NULL ? session_timezone : attimezone);
 
 		tm->tm_year = tx->tm_year + 1900;
 		tm->tm_mon = tx->tm_mon + 1;
diff --git a/src/backend/utils/fmgr/slopesupport.c b/src/backend/utils/fmgr/slopesupport.c
index 69ae8d39bf5..7c02de01e7d 100644
--- a/src/backend/utils/fmgr/slopesupport.c
+++ b/src/backend/utils/fmgr/slopesupport.c
@@ -3,9 +3,17 @@
 #include <math.h>
 
 #include "c.h"
+#include "catalog/pg_type.h"
+#include "nodes/primnodes.h"
 #include "nodes/supportnodes.h"
+#include "parser/scansup.h"
+#include "pgtime.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/datetime.h"
 #include "utils/fmgrprotos.h"
 #include "utils/numeric.h"
+#include "varatt.h"
 
 /**
  * Extended numeric sign, the usual -1, 0, 1,
@@ -342,3 +350,192 @@ divide_slope_support(PG_FUNCTION_ARGS)
 	}
 	PG_RETURN_POINTER(NULL);
 }
+
+/*
+ * Look up a timezone from a constant text argument.
+ */
+static pg_tz *
+get_const_timezone_arg(List *args, int argno)
+{
+	Node	   *tz_arg_node;
+	Const	   *tz_const;
+	text	   *zone;
+	char		tzname[TZ_STRLEN_MAX + 1];
+
+	if (args == NULL || list_length(args) <= argno)
+		return NULL;
+
+	tz_arg_node = list_nth(args, argno);
+	if (!IsA(tz_arg_node, Const))
+		return NULL;
+
+	tz_const = (Const *) tz_arg_node;
+
+	if (tz_const->constisnull || tz_const->consttype != TEXTOID)
+		return NULL;
+
+	zone = DatumGetTextPP(tz_const->constvalue);
+	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+	return DecodeTimezoneNameToTz(tzname);
+}
+
+/*
+ * Slope support for date(timestamptz)
+ */
+Datum
+timestamptz_date_slope_support(PG_FUNCTION_ARGS)
+{
+	SLOPE_REQUEST(req);
+
+	if (pg_timezone_is_monotonic(session_timezone, TZ_GAP_DAY, false))
+		return monotonic_slope_support(req, 1, asc0_slope);
+	else
+		PG_RETURN_POINTER(NULL);
+}
+
+static Oid
+get_monotonic_expr_funcid(SupportRequestMonotonic *req)
+{
+	Node	   *expr = req->expr;
+	if (IsA(expr, FuncExpr))
+		return ((FuncExpr *) expr)->funcid;
+	return InvalidOid;
+}
+
+static Datum
+timestamptz_support_lookup(SupportRequestMonotonic *req, Datum fixed_unit, pg_tz *tzp)
+{
+	text	   *units;
+	int			val;
+	char	   *lowunits;
+	int			type;
+	TZMonotonicityBits tz_unit;
+
+	units = DatumGetTextPP(fixed_unit);
+
+	lowunits = downcase_truncate_identifier(VARDATA_ANY(units),
+											VARSIZE_ANY_EXHDR(units),
+											false);
+
+	type = DecodeUnits(0, lowunits, &val);
+
+	if (type != UNITS)
+		PG_RETURN_POINTER(NULL);
+
+	switch (val)
+	{
+		case DTK_WEEK:
+			PG_RETURN_POINTER(NULL);
+		case DTK_MILLENNIUM:
+		case DTK_CENTURY:
+		case DTK_DECADE:
+		case DTK_YEAR:
+			tz_unit = TZ_GAP_YEAR;
+			break;
+		case DTK_QUARTER:
+		case DTK_MONTH:
+			tz_unit = TZ_GAP_MONTH;
+			break;
+		case DTK_DAY:
+			tz_unit = TZ_GAP_DAY;
+			break;
+		case DTK_HOUR:
+			tz_unit = TZ_GAP_HOUR;
+			break;
+		case DTK_MINUTE:
+			tz_unit = TZ_GAP_MINUTE;
+			break;
+		case DTK_SECOND:
+		case DTK_MILLISEC:
+		case DTK_MICROSEC:
+			tz_unit = TZ_GAP_SECOND;
+			break;
+		default:
+			PG_RETURN_POINTER(NULL);
+	}
+	if (pg_timezone_is_monotonic(tzp, tz_unit, false))
+		return monotonic_slope_support(req, 2, asc1_slope);
+	else
+		PG_RETURN_POINTER(NULL);
+}
+
+/*
+ * Prosupport for timezone(...) overloads with timestamp types.
+ */
+Datum
+timezone_prosupport(PG_FUNCTION_ARGS)
+{
+	pg_tz	   *tzp;
+	bool		to_utc = false;
+	SLOPE_REQUEST_ARGS(req, args, 1);
+
+
+	switch (get_monotonic_expr_funcid(req))
+	{
+
+		case F_TIMEZONE_TEXT_TIMESTAMP:
+			to_utc = true;
+			pg_fallthrough;
+		case F_TIMEZONE_TEXT_TIMESTAMPTZ:
+			tzp = get_const_timezone_arg(args, 0);
+			break;
+		case F_TIMEZONE_TIMESTAMP:
+			to_utc = true;
+			pg_fallthrough;
+		case F_TIMEZONE_TIMESTAMPTZ:
+			tzp = session_timezone;
+			break;
+
+		default:
+			PG_RETURN_POINTER(NULL);
+	}
+
+	if (tzp == NULL || !pg_timezone_is_monotonic(tzp, TZ_GAP_SECOND, to_utc))
+		PG_RETURN_POINTER(NULL);
+
+	/*
+	 * We need MONOTONICFUNC_INCREASING for either the first or
+	 * second argument, but the other argument is either a constant
+	 * or missing, so we can simply return MONOTONICFUNC_INCREASING
+	 * for both.
+	 */
+	return monotonic_slope_support(req, 2, asc_slope);
+}
+
+/*
+ * Prosupport for date_trunc(...) overloads with timestamp types.
+ */
+Datum
+date_trunc_slope_support(PG_FUNCTION_ARGS)
+{
+	Const	   *unit_arg;
+	pg_tz	   *tzp = NULL;
+	SLOPE_REQUEST_ARGS(req, args, 2);
+
+	unit_arg = (Const *) linitial(args);
+	if (!IsA(unit_arg, Const) || unit_arg->constisnull)
+		PG_RETURN_POINTER(NULL);
+
+	switch (get_monotonic_expr_funcid(req))
+	{
+		case F_DATE_TRUNC_TEXT_TIMESTAMP:
+			tzp = DecodeTimezoneNameToTz("UTC");
+			break;
+
+		case F_DATE_TRUNC_TEXT_TIMESTAMPTZ_TEXT:
+			tzp = get_const_timezone_arg(args, 2);
+			break;
+
+		case F_DATE_TRUNC_TEXT_TIMESTAMPTZ:
+			tzp = session_timezone;
+			break;
+
+		default:
+			PG_RETURN_POINTER(NULL);
+	}
+
+	if (tzp == NULL)
+		PG_RETURN_POINTER(NULL);
+
+	return timestamptz_support_lookup(req, unit_arg->constvalue, tzp);
+}
diff --git a/src/include/c.h b/src/include/c.h
index 20cfbac54e7..6328dd1a39a 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -1285,6 +1285,9 @@ typedef struct PGAlignedXLogBlock PGAlignedXLogBlock;
 /* msb for char */
 #define HIGHBIT					(0x80)
 #define IS_HIGHBIT_SET(ch)		((unsigned char)(ch) & HIGHBIT)
+/* ____-___ */ #define BITWISE_HI_AT(n)		(1llu << n)
+/* _____--- */ #define BITWISE_HI_RIGHT(n)		((1llu << (n + 1)) - 1)
+/* -----___ */ #define BITWISE_LO_RIGHT(n)		~BIT_MASK_HI_RIGHT(n)
 
 /*
  * Support macros for escaping strings.  escape_backslash should be true
@@ -1565,6 +1568,8 @@ typedef uint32_t char32_t;
 #endif
 #endif
 
+
+
 /* IWYU pragma: end_exports */
 
 #endif							/* C_H */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6adf4cc9c19..9e7d8f2e6bd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2471,10 +2471,12 @@
   prorettype => 'timestamptz',
   proargtypes => 'float8', prosrc => 'float8_timestamptz' },
 { oid => '1159', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', prorettype => 'timestamp',
+  proname => 'timezone', prosupport => 'timezone_prosupport',
+  prorettype => 'timestamp',
   proargtypes => 'text timestamptz', prosrc => 'timestamptz_zone' },
 { oid => '6334', descr => 'adjust timestamp to local time zone',
-  proname => 'timezone', provolatile => 's', prorettype => 'timestamp',
+  proname => 'timezone', prosupport => 'timezone_prosupport',
+  provolatile => 's', prorettype => 'timestamp',
   proargtypes => 'timestamptz', prosrc => 'timestamptz_at_local' },
 
 { oid => '1160', descr => 'I/O',
@@ -2552,6 +2554,7 @@
   prosrc => 'see system_functions.sql' },
 { oid => '1178', descr => 'convert timestamp with time zone to date',
   proname => 'date', provolatile => 's', prorettype => 'date',
+  prosupport => 'timestamptz_date_slope_support',
   proargtypes => 'timestamptz', prosrc => 'timestamptz_date' },
 { oid => '1181',
   descr => 'age of a transaction ID, in transactions before current transaction',
@@ -2635,10 +2638,12 @@
 { oid => '1217',
   descr => 'truncate timestamp with time zone to specified units',
   proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
+  prosupport => 'date_trunc_slope_support',
   proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' },
 { oid => '1284',
   descr => 'truncate timestamp with time zone to specified units in specified time zone',
   proname => 'date_trunc', prorettype => 'timestamptz',
+  prosupport => 'date_trunc_slope_support',
   proargtypes => 'text timestamptz text', prosrc => 'timestamptz_trunc_zone' },
 { oid => '1218', descr => 'truncate interval to specified units',
   proname => 'date_trunc', prorettype => 'interval',
@@ -6573,7 +6578,7 @@
   proname => 'time', provolatile => 's', prorettype => 'time',
   proargtypes => 'timestamptz', prosrc => 'timestamptz_time' },
 { oid => '2020', descr => 'truncate timestamp to specified units',
-  proname => 'date_trunc', prosupport => 'arg1_asc_slope_support',
+  proname => 'date_trunc', prosupport => 'date_trunc_slope_support',
   prorettype => 'timestamp', proargtypes => 'text timestamp', prosrc => 'timestamp_trunc' },
 
 { oid => '6177', descr => 'bin timestamp into specified interval',
@@ -6736,12 +6741,14 @@
 
 { oid => '2069', descr => 'adjust timestamp to new time zone',
   proname => 'timezone', prorettype => 'timestamptz',
+  prosupport => 'timezone_prosupport',
   proargtypes => 'text timestamp', prosrc => 'timestamp_zone' },
 { oid => '2070', descr => 'adjust timestamp to new time zone',
   proname => 'timezone', prorettype => 'timestamptz',
   proargtypes => 'interval timestamp', prosrc => 'timestamp_izone' },
 { oid => '6335', descr => 'adjust timestamp to local time zone',
   proname => 'timezone', provolatile => 's', prorettype => 'timestamptz',
+  prosupport => 'timezone_prosupport',
   proargtypes => 'timestamp', prosrc => 'timestamp_at_local' },
 { oid => '2071',
   proname => 'date_pl_interval',
@@ -12865,6 +12872,15 @@
 { oid => '9960', descr => 'planner support for divide slope (sign-dependent)',
   proname => 'divide_slope_support', prorettype => 'internal',
   proargtypes => 'internal', prosrc => 'divide_slope_support' },
+{ oid => '9961', descr => 'planner support date(timestamptz)',
+  proname => 'timestamptz_date_slope_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'timestamptz_date_slope_support' },
+{ oid => '9962', descr => 'planner support date_trunc(...)',
+  proname => 'date_trunc_slope_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'date_trunc_slope_support' },
+{ oid => '9965', descr => 'planner support timezone(...)',
+  proname => 'timezone_prosupport', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'timezone_prosupport' }
 
 # AIO related functions
 { oid => '6399', descr => 'information about in-progress asynchronous IOs',
diff --git a/src/include/pgtime.h b/src/include/pgtime.h
index ba6705f71bd..14d077aa467 100644
--- a/src/include/pgtime.h
+++ b/src/include/pgtime.h
@@ -46,6 +46,23 @@ struct pg_tm
 	const char *tm_zone;
 };
 
+/*
+ * These values are used to encode the gaps when in when converting
+ * timestamp representations between timezones across a time change,
+ * the most common case being DST. If a time change affects
+ * only minutes and seconds, would be represented using bits
+ * TZ_GAP_MINUTE and TZ_GAP_SECOND.
+ */
+typedef enum TZGapEffectBits {
+	TZ_GAP_YEAR  ,
+	TZ_GAP_MONTH ,
+	TZ_GAP_DAY   ,
+	TZ_GAP_HOUR  ,
+	TZ_GAP_MINUTE,
+	TZ_GAP_SECOND,
+	TZ_GAP_NUM_BITS
+} TZMonotonicityBits;
+
 /* These structs are opaque outside the timezone library */
 typedef struct pg_tz pg_tz;
 typedef struct pg_tzenum pg_tzenum;
@@ -56,6 +73,8 @@ typedef struct pg_tzenum pg_tzenum;
 /* these functions are in localtime.c */
 
 extern struct pg_tm *pg_localtime(const pg_time_t *timep, const pg_tz *tz);
+extern bool pg_timezone_is_monotonic(const pg_tz *tz, TZMonotonicityBits unit, bool to_utc);
+
 extern struct pg_tm *pg_gmtime(const pg_time_t *timep);
 extern int	pg_next_dst_boundary(const pg_time_t *timep,
 								 long int *before_gmtoff,
diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out
index ebc4bd9b9d2..21a13857d05 100644
--- a/src/test/regress/expected/slope.out
+++ b/src/test/regress/expected/slope.out
@@ -252,21 +252,6 @@ select date_trunc('day', ts), count(*) from src group by 1;
          Output: date_trunc('day'::text, ts)
 (5 rows)
 
--- date_trunc on timestamptz should not use index
-explain (costs off, verbose)
-select date_trunc('day', tstz), count(*) from src group by 1;
-                         QUERY PLAN                          
--------------------------------------------------------------
- GroupAggregate
-   Output: (date_trunc('day'::text, tstz)), count(*)
-   Group Key: (date_trunc('day'::text, src.tstz))
-   ->  Sort
-         Output: (date_trunc('day'::text, tstz))
-         Sort Key: (date_trunc('day'::text, src.tstz))
-         ->  Index Only Scan using src_tstz_idx on slope.src
-               Output: date_trunc('day'::text, tstz)
-(8 rows)
-
 --
 -- Test arithmetic operations
 --
@@ -425,23 +410,6 @@ select -v_int4 from src order by 1;
          Output: (- v_int4)
 (5 rows)
 
---
--- Group and order
---
-explain (costs off, verbose)
-select tstz::date, count(*) from src group by 1 order by 1;
-                         QUERY PLAN                          
--------------------------------------------------------------
- GroupAggregate
-   Output: ((tstz)::date), count(*)
-   Group Key: ((src.tstz)::date)
-   ->  Sort
-         Output: ((tstz)::date)
-         Sort Key: ((src.tstz)::date)
-         ->  Index Only Scan using src_tstz_idx on slope.src
-               Output: (tstz)::date
-(8 rows)
-
 --
 -- Test nested monotonic function
 --
@@ -844,8 +812,195 @@ SELECT v_int4::oid FROM src ORDER BY 1;
    ->  Index Only Scan using src_v_int4_idx on src
 (3 rows)
 
+--
+-- Now that some plans were shown, and we see that in many cases what
+-- we care is whether a plan has a index scan or not. This function will
+-- check that for us and just return
+--
+CREATE OR REPLACE FUNCTION index_plan (query text) RETURNS BOOLEAN LANGUAGE plpgsql AS $$
+DECLARE
+    plan json;
+BEGIN
+   EXECUTE 'EXPLAIN (COSTS OFF, FORMAT JSON) SELECT ' || query || ' ORDER BY 1' into plan;
+   RETURN  plan->0->'Plan'->>'Node Type' LIKE 'Index%Scan';
+END
+$$;
+--
+-- Functions that take a timezone argument
+--
+PREPARE query(text) AS
+WITH
+cols AS (
+    SELECT * FROM unnest(ARRAY['ts', 'tstz'])
+        WITH ORDINALITY AS c(col, ord)
+),
+units AS (
+    SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second'])
+        WITH ORDINALITY AS s(unit, ord)
+)
+SELECT
+    f AS expression,
+    coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER (
+        WHERE index_plan(format('timezone(%L, %I) FROM src', $1, col))
+    ), 'none') AS monotonic
+FROM cols,
+     unnest(ARRAY['timezone(TZ, <>)']) AS f(f)
+GROUP BY f
+UNION ALL
+SELECT
+    'date_trunc(<>, tstz, TZ)' AS expression,
+    coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER (
+        WHERE index_plan(format('date_trunc(%L, tstz, %L) FROM src', unit, $1))
+    ), 'none') AS monotonic
+FROM units
+ORDER BY 1;
+EXECUTE query('UTC');
+        expression        |               monotonic                
+--------------------------+----------------------------------------
+ date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second
+ timezone(TZ, <>)         | ts, tstz
+(2 rows)
+
+EXECUTE query('Africa/Ouagadougou');
+        expression        |               monotonic                
+--------------------------+----------------------------------------
+ date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second
+ timezone(TZ, <>)         | tstz
+(2 rows)
+
+EXECUTE query('Europe/London');
+        expression        |       monotonic        
+--------------------------+------------------------
+ date_trunc(<>, tstz, TZ) | year, month, day, hour
+ timezone(TZ, <>)         | none
+(2 rows)
+
+EXECUTE query('Antarctica/Troll');
+        expression        |    monotonic     
+--------------------------+------------------
+ date_trunc(<>, tstz, TZ) | year, month, day
+ timezone(TZ, <>)         | none
+(2 rows)
+
+EXECUTE query('Pacific/Guam');
+        expression        |  monotonic  
+--------------------------+-------------
+ date_trunc(<>, tstz, TZ) | year, month
+ timezone(TZ, <>)         | none
+(2 rows)
+
+EXECUTE query('America/Goose_Bay');
+        expression        | monotonic 
+--------------------------+-----------
+ date_trunc(<>, tstz, TZ) | year
+ timezone(TZ, <>)         | none
+(2 rows)
+
+--
+-- Functions that depend on session timezone
+--
+PREPARE query_local AS
+WITH
+cols AS (
+    SELECT * FROM unnest(ARRAY['ts', 'tstz'])
+        WITH ORDINALITY AS c(col, ord)
+),
+units AS (
+    SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second'])
+        WITH ORDINALITY AS s(unit, ord)
+)
+SELECT
+    format(f, '<>') AS expression,
+    coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER (
+        WHERE index_plan(format(f, col) || ' FROM src')
+    ), 'none') AS monotonic
+FROM cols,
+     unnest(ARRAY['%s AT LOCAL', 'timezone(%s)', '%s::date']) AS f(f)
+GROUP BY f
+UNION ALL
+SELECT
+    format('date_trunc(<>, %s)', col) AS expression,
+    coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER (
+        WHERE index_plan(format('date_trunc(%L, %I) FROM src', unit, col))
+    ), 'none') AS monotonic
+FROM cols, units
+GROUP BY col
+ORDER BY 1;
+-- UTC
+SET timezone = 'UTC';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | ts, tstz
+ <>::date             | ts, tstz
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year, month, day, hour, minute, second
+ timezone(<>)         | ts, tstz
+(5 rows)
+
+-- No DST at all
+SET timezone = 'Africa/Ouagadougou';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | tstz
+ <>::date             | ts, tstz
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year, month, day, hour, minute, second
+ timezone(<>)         | tstz
+(5 rows)
+
+-- The common case: one hour DST
+SET timezone = 'Europe/London';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | none
+ <>::date             | ts, tstz
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year, month, day, hour
+ timezone(<>)         | none
+(5 rows)
+
+-- DST might affect the hour
+SET timezone = 'Antarctica/Troll';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | none
+ <>::date             | ts, tstz
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year, month, day
+ timezone(<>)         | none
+(5 rows)
+
+-- A weird case: used to go back a day
+SET timezone = 'Pacific/Guam';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | none
+ <>::date             | ts
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year, month
+ timezone(<>)         | none
+(5 rows)
+
+-- Even weirder, went back a month
+SET timezone = 'America/Goose_Bay';
+EXECUTE query_local;
+      expression      |               monotonic                
+----------------------+----------------------------------------
+ <> AT LOCAL          | none
+ <>::date             | ts
+ date_trunc(<>, ts)   | year, month, day, hour, minute, second
+ date_trunc(<>, tstz) | year
+ timezone(<>)         | none
+(5 rows)
+
+DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
-NOTICE:  drop cascades to 14 other objects
+NOTICE:  drop cascades to 15 other objects
 DETAIL:  drop cascades to table t
 drop cascades to table u
 drop cascades to operator family test_int4_ops for access method btree
@@ -860,3 +1015,4 @@ drop cascades to type non42
 drop cascades to type fp_real
 drop cascades to table numeric_corners
 drop cascades to table inc
+drop cascades to function index_plan(text)
diff --git a/src/test/regress/expected/slope_catalog.out b/src/test/regress/expected/slope_catalog.out
index 670f711466e..40357be8d1f 100644
--- a/src/test/regress/expected/slope_catalog.out
+++ b/src/test/regress/expected/slope_catalog.out
@@ -12,6 +12,7 @@ FROM pg_operator o
 JOIN pg_proc p ON p.oid = o.oprcode
 JOIN pg_proc sp ON sp.oid = p.prosupport
 WHERE sp.proname LIKE '%slope%'
+   OR sp.proname LIKE '%prosupport'
 ORDER BY sp.proname, o.oprname, left_type, right_type;
  oid  | operator |          left_type          |         right_type          |       prosupport        
 ------+----------+-----------------------------+-----------------------------+-------------------------
@@ -130,61 +131,68 @@ SELECT
     sp.proname AS prosupport
 FROM pg_proc p
 JOIN pg_proc sp ON sp.oid = p.prosupport
-WHERE sp.proname LIKE '%slope%'
+WHERE (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')
   AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = p.oid)
 ORDER BY sp.proname, p.proname, arguments;
- oid  |   function   |             arguments             |           returns           |       prosupport        
-------+--------------+-----------------------------------+-----------------------------+-------------------------
- 2025 | timestamp    | date, time without time zone      | timestamp without time zone | addition_slope_support
- 2466 | acosh        | double precision                  | double precision            | arg0_asc_slope_support
- 1600 | asin         | double precision                  | double precision            | arg0_asc_slope_support
- 2731 | asind        | double precision                  | double precision            | arg0_asc_slope_support
- 2465 | asinh        | double precision                  | double precision            | arg0_asc_slope_support
- 1602 | atan         | double precision                  | double precision            | arg0_asc_slope_support
- 2733 | atand        | double precision                  | double precision            | arg0_asc_slope_support
- 2467 | atanh        | double precision                  | double precision            | arg0_asc_slope_support
- 1345 | cbrt         | double precision                  | double precision            | arg0_asc_slope_support
- 2308 | ceil         | double precision                  | double precision            | arg0_asc_slope_support
- 1711 | ceil         | numeric                           | numeric                     | arg0_asc_slope_support
- 2320 | ceiling      | double precision                  | double precision            | arg0_asc_slope_support
- 2167 | ceiling      | numeric                           | numeric                     | arg0_asc_slope_support
- 2029 | date         | timestamp without time zone       | date                        | arg0_asc_slope_support
- 1608 | degrees      | double precision                  | double precision            | arg0_asc_slope_support
- 6219 | erf          | double precision                  | double precision            | arg0_asc_slope_support
- 1347 | exp          | double precision                  | double precision            | arg0_asc_slope_support
- 1732 | exp          | numeric                           | numeric                     | arg0_asc_slope_support
- 2309 | floor        | double precision                  | double precision            | arg0_asc_slope_support
- 1712 | floor        | numeric                           | numeric                     | arg0_asc_slope_support
- 1341 | ln           | double precision                  | double precision            | arg0_asc_slope_support
- 1734 | ln           | numeric                           | numeric                     | arg0_asc_slope_support
- 1340 | log          | double precision                  | double precision            | arg0_asc_slope_support
- 1741 | log          | numeric                           | numeric                     | arg0_asc_slope_support
- 1194 | log10        | double precision                  | double precision            | arg0_asc_slope_support
- 1481 | log10        | numeric                           | numeric                     | arg0_asc_slope_support
- 1733 | numeric_exp  | numeric                           | numeric                     | arg0_asc_slope_support
- 1735 | numeric_ln   | numeric                           | numeric                     | arg0_asc_slope_support
- 1731 | numeric_sqrt | numeric                           | numeric                     | arg0_asc_slope_support
- 1609 | radians      | double precision                  | double precision            | arg0_asc_slope_support
- 1342 | round        | double precision                  | double precision            | arg0_asc_slope_support
- 1708 | round        | numeric                           | numeric                     | arg0_asc_slope_support
- 1707 | round        | numeric, integer                  | numeric                     | arg0_asc_slope_support
- 2462 | sinh         | double precision                  | double precision            | arg0_asc_slope_support
- 1344 | sqrt         | double precision                  | double precision            | arg0_asc_slope_support
- 1730 | sqrt         | numeric                           | numeric                     | arg0_asc_slope_support
- 2464 | tanh         | double precision                  | double precision            | arg0_asc_slope_support
- 2024 | timestamp    | date                              | timestamp without time zone | arg0_asc_slope_support
- 1174 | timestamptz  | date                              | timestamp with time zone    | arg0_asc_slope_support
- 1359 | timestamptz  | date, time with time zone         | timestamp with time zone    | arg0_asc_slope_support
- 1176 | timestamptz  | date, time without time zone      | timestamp with time zone    | arg0_asc_slope_support
- 1158 | to_timestamp | double precision                  | timestamp with time zone    | arg0_asc_slope_support
- 1343 | trunc        | double precision                  | double precision            | arg0_asc_slope_support
- 1710 | trunc        | numeric                           | numeric                     | arg0_asc_slope_support
- 1709 | trunc        | numeric, integer                  | numeric                     | arg0_asc_slope_support
- 1601 | acos         | double precision                  | double precision            | arg0_desc_slope_support
- 2732 | acosd        | double precision                  | double precision            | arg0_desc_slope_support
- 6220 | erfc         | double precision                  | double precision            | arg0_desc_slope_support
- 2020 | date_trunc   | text, timestamp without time zone | timestamp without time zone | arg1_asc_slope_support
-(49 rows)
+ oid  |   function   |              arguments               |           returns           |           prosupport           
+------+--------------+--------------------------------------+-----------------------------+--------------------------------
+ 2025 | timestamp    | date, time without time zone         | timestamp without time zone | addition_slope_support
+ 2466 | acosh        | double precision                     | double precision            | arg0_asc_slope_support
+ 1600 | asin         | double precision                     | double precision            | arg0_asc_slope_support
+ 2731 | asind        | double precision                     | double precision            | arg0_asc_slope_support
+ 2465 | asinh        | double precision                     | double precision            | arg0_asc_slope_support
+ 1602 | atan         | double precision                     | double precision            | arg0_asc_slope_support
+ 2733 | atand        | double precision                     | double precision            | arg0_asc_slope_support
+ 2467 | atanh        | double precision                     | double precision            | arg0_asc_slope_support
+ 1345 | cbrt         | double precision                     | double precision            | arg0_asc_slope_support
+ 2308 | ceil         | double precision                     | double precision            | arg0_asc_slope_support
+ 1711 | ceil         | numeric                              | numeric                     | arg0_asc_slope_support
+ 2320 | ceiling      | double precision                     | double precision            | arg0_asc_slope_support
+ 2167 | ceiling      | numeric                              | numeric                     | arg0_asc_slope_support
+ 2029 | date         | timestamp without time zone          | date                        | arg0_asc_slope_support
+ 1608 | degrees      | double precision                     | double precision            | arg0_asc_slope_support
+ 6219 | erf          | double precision                     | double precision            | arg0_asc_slope_support
+ 1347 | exp          | double precision                     | double precision            | arg0_asc_slope_support
+ 1732 | exp          | numeric                              | numeric                     | arg0_asc_slope_support
+ 2309 | floor        | double precision                     | double precision            | arg0_asc_slope_support
+ 1712 | floor        | numeric                              | numeric                     | arg0_asc_slope_support
+ 1341 | ln           | double precision                     | double precision            | arg0_asc_slope_support
+ 1734 | ln           | numeric                              | numeric                     | arg0_asc_slope_support
+ 1340 | log          | double precision                     | double precision            | arg0_asc_slope_support
+ 1741 | log          | numeric                              | numeric                     | arg0_asc_slope_support
+ 1194 | log10        | double precision                     | double precision            | arg0_asc_slope_support
+ 1481 | log10        | numeric                              | numeric                     | arg0_asc_slope_support
+ 1733 | numeric_exp  | numeric                              | numeric                     | arg0_asc_slope_support
+ 1735 | numeric_ln   | numeric                              | numeric                     | arg0_asc_slope_support
+ 1731 | numeric_sqrt | numeric                              | numeric                     | arg0_asc_slope_support
+ 1609 | radians      | double precision                     | double precision            | arg0_asc_slope_support
+ 1342 | round        | double precision                     | double precision            | arg0_asc_slope_support
+ 1708 | round        | numeric                              | numeric                     | arg0_asc_slope_support
+ 1707 | round        | numeric, integer                     | numeric                     | arg0_asc_slope_support
+ 2462 | sinh         | double precision                     | double precision            | arg0_asc_slope_support
+ 1344 | sqrt         | double precision                     | double precision            | arg0_asc_slope_support
+ 1730 | sqrt         | numeric                              | numeric                     | arg0_asc_slope_support
+ 2464 | tanh         | double precision                     | double precision            | arg0_asc_slope_support
+ 2024 | timestamp    | date                                 | timestamp without time zone | arg0_asc_slope_support
+ 1174 | timestamptz  | date                                 | timestamp with time zone    | arg0_asc_slope_support
+ 1359 | timestamptz  | date, time with time zone            | timestamp with time zone    | arg0_asc_slope_support
+ 1176 | timestamptz  | date, time without time zone         | timestamp with time zone    | arg0_asc_slope_support
+ 1158 | to_timestamp | double precision                     | timestamp with time zone    | arg0_asc_slope_support
+ 1343 | trunc        | double precision                     | double precision            | arg0_asc_slope_support
+ 1710 | trunc        | numeric                              | numeric                     | arg0_asc_slope_support
+ 1709 | trunc        | numeric, integer                     | numeric                     | arg0_asc_slope_support
+ 1601 | acos         | double precision                     | double precision            | arg0_desc_slope_support
+ 2732 | acosd        | double precision                     | double precision            | arg0_desc_slope_support
+ 6220 | erfc         | double precision                     | double precision            | arg0_desc_slope_support
+ 1217 | date_trunc   | text, timestamp with time zone       | timestamp with time zone    | date_trunc_slope_support
+ 1284 | date_trunc   | text, timestamp with time zone, text | timestamp with time zone    | date_trunc_slope_support
+ 2020 | date_trunc   | text, timestamp without time zone    | timestamp without time zone | date_trunc_slope_support
+ 1178 | date         | timestamp with time zone             | date                        | timestamptz_date_slope_support
+ 1159 | timezone     | text, timestamp with time zone       | timestamp without time zone | timezone_prosupport
+ 2069 | timezone     | text, timestamp without time zone    | timestamp with time zone    | timezone_prosupport
+ 6334 | timezone     | timestamp with time zone             | timestamp without time zone | timezone_prosupport
+ 6335 | timezone     | timestamp without time zone          | timestamp with time zone    | timezone_prosupport
+(56 rows)
 
 -- Operators whose name has slope support for some types but not others
 SELECT
@@ -196,14 +204,15 @@ FROM pg_operator u
 JOIN pg_proc up ON up.oid = u.oprcode
 WHERE (up.prosupport = 0 OR NOT EXISTS (
         SELECT 1 FROM pg_proc sp
-        WHERE sp.oid = up.prosupport AND sp.proname LIKE '%slope%'))
+        WHERE sp.oid = up.prosupport
+          AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')))
   AND EXISTS (
       SELECT 1
       FROM pg_operator s
       JOIN pg_proc sp_impl ON sp_impl.oid = s.oprcode
       JOIN pg_proc sp_sup ON sp_sup.oid = sp_impl.prosupport
       WHERE s.oprname = u.oprname
-        AND sp_sup.proname LIKE '%slope%')
+        AND (sp_sup.proname LIKE '%slope%' OR sp_sup.proname LIKE '%prosupport'))
 ORDER BY u.oprname, left_type, right_type;
  oid  | operator |          left_type          |         right_type          
 ------+----------+-----------------------------+-----------------------------
@@ -268,29 +277,32 @@ SELECT
 FROM pg_proc u
 WHERE (u.prosupport = 0 OR NOT EXISTS (
         SELECT 1 FROM pg_proc sp
-        WHERE sp.oid = u.prosupport AND sp.proname LIKE '%slope%'))
+        WHERE sp.oid = u.prosupport
+          AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')))
   AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = u.oid)
   AND EXISTS (
       SELECT 1
       FROM pg_proc s
       JOIN pg_proc sp ON sp.oid = s.prosupport
       WHERE s.proname = u.proname
-        AND sp.proname LIKE '%slope%'
+        AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')
         AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = s.oid))
 ORDER BY u.proname, arguments;
- oid  |   function   |              arguments               |           returns           
-------+--------------+--------------------------------------+-----------------------------
- 1178 | date         | timestamp with time zone             | date
- 1218 | date_trunc   | text, interval                       | interval
- 1217 | date_trunc   | text, timestamp with time zone       | timestamp with time zone
- 1284 | date_trunc   | text, timestamp with time zone, text | timestamp with time zone
- 1736 | log          | numeric, numeric                     | numeric
- 2027 | timestamp    | timestamp with time zone             | timestamp without time zone
- 1961 | timestamp    | timestamp without time zone, integer | timestamp without time zone
- 1967 | timestamptz  | timestamp with time zone, integer    | timestamp with time zone
- 2028 | timestamptz  | timestamp without time zone          | timestamp with time zone
- 1778 | to_timestamp | text, text                           | timestamp with time zone
-  753 | trunc        | macaddr                              | macaddr
- 4112 | trunc        | macaddr8                             | macaddr8
-(12 rows)
+ oid  |   function   |               arguments               |           returns           
+------+--------------+---------------------------------------+-----------------------------
+ 1218 | date_trunc   | text, interval                        | interval
+ 1736 | log          | numeric, numeric                      | numeric
+ 2027 | timestamp    | timestamp with time zone              | timestamp without time zone
+ 1961 | timestamp    | timestamp without time zone, integer  | timestamp without time zone
+ 1967 | timestamptz  | timestamp with time zone, integer     | timestamp with time zone
+ 2028 | timestamptz  | timestamp without time zone           | timestamp with time zone
+ 2038 | timezone     | interval, time with time zone         | time with time zone
+ 1026 | timezone     | interval, timestamp with time zone    | timestamp without time zone
+ 2070 | timezone     | interval, timestamp without time zone | timestamp with time zone
+ 2037 | timezone     | text, time with time zone             | time with time zone
+ 6336 | timezone     | time with time zone                   | time with time zone
+ 1778 | to_timestamp | text, text                            | timestamp with time zone
+  753 | trunc        | macaddr                               | macaddr
+ 4112 | trunc        | macaddr8                              | macaddr8
+(14 rows)
 
diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql
index 1defff4d955..7e8b85b9ec3 100644
--- a/src/test/regress/sql/slope.sql
+++ b/src/test/regress/sql/slope.sql
@@ -178,10 +178,6 @@ select ts::date, count(*) from src group by 1;
 explain (costs off, verbose)
 select date_trunc('day', ts), count(*) from src group by 1;
 
--- date_trunc on timestamptz should not use index
-explain (costs off, verbose)
-select date_trunc('day', tstz), count(*) from src group by 1;
-
 
 --
 -- Test arithmetic operations
@@ -203,7 +199,6 @@ select v_int4 * 2, count(*) from src group by 1;
 explain (costs off, verbose)
 select v_int4 / 2, count(*) from src group by 1;
 
-
 --
 -- Test decreasing functions
 -- These queries can't use the index order because group pathkeys
@@ -248,13 +243,6 @@ select -v_int4 from src order by 1 desc;
 explain (costs off, verbose)
 select -v_int4 from src order by 1;
 
---
--- Group and order
---
-
-explain (costs off, verbose)
-select tstz::date, count(*) from src group by 1 order by 1;
-
 --
 -- Test nested monotonic function
 --
@@ -536,4 +524,115 @@ from src;
 EXPLAIN (COSTS OFF)
 SELECT v_int4::oid FROM src ORDER BY 1;
 
+
+
+--
+-- Now that some plans were shown, and we see that in many cases what
+-- we care is whether a plan has a index scan or not. This function will
+-- check that for us and just return
+--
+CREATE OR REPLACE FUNCTION index_plan (query text) RETURNS BOOLEAN LANGUAGE plpgsql AS $$
+DECLARE
+    plan json;
+BEGIN
+   EXECUTE 'EXPLAIN (COSTS OFF, FORMAT JSON) SELECT ' || query || ' ORDER BY 1' into plan;
+   RETURN  plan->0->'Plan'->>'Node Type' LIKE 'Index%Scan';
+END
+$$;
+
+
+--
+-- Functions that take a timezone argument
+--
+PREPARE query(text) AS
+WITH
+cols AS (
+    SELECT * FROM unnest(ARRAY['ts', 'tstz'])
+        WITH ORDINALITY AS c(col, ord)
+),
+units AS (
+    SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second'])
+        WITH ORDINALITY AS s(unit, ord)
+)
+SELECT
+    f AS expression,
+    coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER (
+        WHERE index_plan(format('timezone(%L, %I) FROM src', $1, col))
+    ), 'none') AS monotonic
+FROM cols,
+     unnest(ARRAY['timezone(TZ, <>)']) AS f(f)
+GROUP BY f
+UNION ALL
+SELECT
+    'date_trunc(<>, tstz, TZ)' AS expression,
+    coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER (
+        WHERE index_plan(format('date_trunc(%L, tstz, %L) FROM src', unit, $1))
+    ), 'none') AS monotonic
+FROM units
+ORDER BY 1;
+
+EXECUTE query('UTC');
+EXECUTE query('Africa/Ouagadougou');
+EXECUTE query('Europe/London');
+EXECUTE query('Antarctica/Troll');
+EXECUTE query('Pacific/Guam');
+EXECUTE query('America/Goose_Bay');
+
+--
+-- Functions that depend on session timezone
+--
+PREPARE query_local AS
+WITH
+cols AS (
+    SELECT * FROM unnest(ARRAY['ts', 'tstz'])
+        WITH ORDINALITY AS c(col, ord)
+),
+units AS (
+    SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second'])
+        WITH ORDINALITY AS s(unit, ord)
+)
+SELECT
+    format(f, '<>') AS expression,
+    coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER (
+        WHERE index_plan(format(f, col) || ' FROM src')
+    ), 'none') AS monotonic
+FROM cols,
+     unnest(ARRAY['%s AT LOCAL', 'timezone(%s)', '%s::date']) AS f(f)
+GROUP BY f
+UNION ALL
+SELECT
+    format('date_trunc(<>, %s)', col) AS expression,
+    coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER (
+        WHERE index_plan(format('date_trunc(%L, %I) FROM src', unit, col))
+    ), 'none') AS monotonic
+FROM cols, units
+GROUP BY col
+ORDER BY 1;
+
+-- UTC
+SET timezone = 'UTC';
+EXECUTE query_local;
+
+-- No DST at all
+SET timezone = 'Africa/Ouagadougou';
+EXECUTE query_local;
+
+-- The common case: one hour DST
+SET timezone = 'Europe/London';
+EXECUTE query_local;
+
+-- DST might affect the hour
+SET timezone = 'Antarctica/Troll';
+EXECUTE query_local;
+
+-- A weird case: used to go back a day
+SET timezone = 'Pacific/Guam';
+EXECUTE query_local;
+
+-- Even weirder, went back a month
+SET timezone = 'America/Goose_Bay';
+EXECUTE query_local;
+
+
+DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
\ No newline at end of file
diff --git a/src/test/regress/sql/slope_catalog.sql b/src/test/regress/sql/slope_catalog.sql
index 8256cd232ab..79cb3666f84 100644
--- a/src/test/regress/sql/slope_catalog.sql
+++ b/src/test/regress/sql/slope_catalog.sql
@@ -13,6 +13,7 @@ FROM pg_operator o
 JOIN pg_proc p ON p.oid = o.oprcode
 JOIN pg_proc sp ON sp.oid = p.prosupport
 WHERE sp.proname LIKE '%slope%'
+   OR sp.proname LIKE '%prosupport'
 ORDER BY sp.proname, o.oprname, left_type, right_type;
 
 -- Functions (non-operator) with slope prosupport
@@ -24,7 +25,7 @@ SELECT
     sp.proname AS prosupport
 FROM pg_proc p
 JOIN pg_proc sp ON sp.oid = p.prosupport
-WHERE sp.proname LIKE '%slope%'
+WHERE (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')
   AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = p.oid)
 ORDER BY sp.proname, p.proname, arguments;
 
@@ -38,14 +39,15 @@ FROM pg_operator u
 JOIN pg_proc up ON up.oid = u.oprcode
 WHERE (up.prosupport = 0 OR NOT EXISTS (
         SELECT 1 FROM pg_proc sp
-        WHERE sp.oid = up.prosupport AND sp.proname LIKE '%slope%'))
+        WHERE sp.oid = up.prosupport
+          AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')))
   AND EXISTS (
       SELECT 1
       FROM pg_operator s
       JOIN pg_proc sp_impl ON sp_impl.oid = s.oprcode
       JOIN pg_proc sp_sup ON sp_sup.oid = sp_impl.prosupport
       WHERE s.oprname = u.oprname
-        AND sp_sup.proname LIKE '%slope%')
+        AND (sp_sup.proname LIKE '%slope%' OR sp_sup.proname LIKE '%prosupport'))
 ORDER BY u.oprname, left_type, right_type;
 
 -- Functions whose name has slope support for some signatures but not others
@@ -57,13 +59,14 @@ SELECT
 FROM pg_proc u
 WHERE (u.prosupport = 0 OR NOT EXISTS (
         SELECT 1 FROM pg_proc sp
-        WHERE sp.oid = u.prosupport AND sp.proname LIKE '%slope%'))
+        WHERE sp.oid = u.prosupport
+          AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')))
   AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = u.oid)
   AND EXISTS (
       SELECT 1
       FROM pg_proc s
       JOIN pg_proc sp ON sp.oid = s.prosupport
       WHERE s.proname = u.proname
-        AND sp.proname LIKE '%slope%'
+        AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport')
         AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = s.oid))
 ORDER BY u.proname, arguments;
diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c
index a3eb3bacb9b..5de2c0cd3e3 100644
--- a/src/timezone/localtime.c
+++ b/src/timezone/localtime.c
@@ -1769,6 +1769,21 @@ pg_next_dst_boundary(const pg_time_t *timep,
 	return 1;
 }
 
+/*
+ * Determine whether timestamps truncated to the specified
+ * unit is monotonic when either converting from UTC to TZ
+ * or from TZ to UTC, the last argument indicates the direction
+ * if the conversion of interest.
+ */
+bool
+pg_timezone_is_monotonic(const pg_tz *tz, TZMonotonicityBits unit, bool to_utc)
+{
+	return ((to_utc ?
+		tz->state.monotonicity.utc :
+		tz->state.monotonicity.local
+	) & BITWISE_HI_AT(unit)) != 0;
+}
+
 /*
  * Identify a timezone abbreviation's meaning in the given zone
  *
diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c
index 9561d94a67d..6725a460459 100644
--- a/src/timezone/pgtz.c
+++ b/src/timezone/pgtz.c
@@ -216,6 +216,165 @@ init_timezone_hashtable(void)
 	return true;
 }
 
+static void
+get_tz_change_gaps(struct pg_tm *tm_before, struct pg_tm *tm_after, short *result)
+{
+	#define mark(field, unit) \
+	do { \
+		if (tm_before->tm_##field > tm_after->tm_##field) \
+		{ \
+			if (TZ_GAP_##unit == 0) \
+				*result = 0; \
+			else \
+				*result &= BITWISE_HI_RIGHT(TZ_GAP_##unit - 1); \
+		} \
+		else if (tm_before->tm_##field < tm_after->tm_##field) \
+			return; \
+	} while (0)
+	mark(year, YEAR);
+	mark(mon, MONTH);
+	mark(mday, DAY);
+	mark(hour, HOUR);
+	mark(min, MINUTE);
+	mark(sec, SECOND);
+	#undef mark
+}
+
+/*
+ * Check every time transition that moves time backwards
+ * up to which part of the timestamp the effects are observable
+ * on a timestamp truncation fashion. This information will be stored
+ * in the timezone state to assist decisions related to monotonicity
+ * analysis.
+ * 2026-07-02 01:59:59.999999 to
+ * 2026-07-02 01:00:00.000000
+ * ==== == == == << xx xxxxxx
+ *
+ * would not violate monotonicity for year, month, day, and hour
+ * but would break for minutes and seconds.
+ */
+static void
+inspect_monotonicity(struct pg_tz *tz)
+{
+	struct state	*sp;
+	struct pg_tm	tm_before;
+	struct pg_tm	tm_after;
+	struct pg_tm	*p;
+
+	int_fast32_t	prev_offset;
+	int_fast32_t	offset;
+	sp = &tz->state;
+
+	if (sp == NULL || sp->typecnt <= 0){
+		/* invalid input, conservative behaviour */
+		tz->state.monotonicity.local = 0;
+		tz->state.monotonicity.utc = 0;
+		return;
+	}
+	/*
+	 *start assuming monotonicity, update as we observe
+	 * counter-examples below
+	 */
+	tz->state.monotonicity.local = BITWISE_HI_RIGHT(TZ_GAP_NUM_BITS);
+	tz->state.monotonicity.utc = BITWISE_HI_RIGHT(TZ_GAP_NUM_BITS);
+	if (sp->timecnt == 0)
+	{
+		return;
+	}
+	offset = sp->ttis[0].tt_utoff;
+
+	for (int i = 0; i < sp->timecnt; i++)
+	{
+		pg_time_t		t_before;
+		pg_time_t		t_after;
+
+		t_after  = sp->ats[i];
+		t_before = t_after - 1;
+
+		prev_offset = offset;
+		offset = sp->ttis[sp->types[i]].tt_utoff;
+		/*
+		 * The variables t_before and t_after are the last
+		 * timestamp before the transition, and the first timestamp
+		 * after the transition at UTC, with 1 microsecond resolution.
+		 *
+		 * The offset indicates the number of seconds the local time
+		 * is ahead of GMT.
+		 *
+		 * Conceptually,
+		 *
+		 * tz_before = (t + prev_offset - 1 micro second)
+		 * tz_after  = (t + offset)
+		 *
+		 */
+		if(offset < prev_offset)
+		{
+			/*
+			 * at this transition we have offset < prev_offset,
+			 * since offset is in seconds,
+			 *      offset <= prev_offset - 1 second
+			 * and offset < prev_offset - 1 microsecond
+			 * and thus
+			 *  t + offset <  t + prev_offset - 1 microsecond
+			 * i.e. tz_after < tz_before
+			 *
+			 * So, local time moves backwards, so fields affected
+			 * by this transition stop being monotonic at the local time.
+			*/
+			p = pg_localtime(&t_before, tz);
+			if (p == NULL)
+				return;
+			tm_before = *p;
+
+			p = pg_localtime(&t_after, tz);
+			if (p == NULL)
+				return;
+			tm_after = *p;
+			get_tz_change_gaps(
+				&tm_before, &tm_after, &tz->state.monotonicity.local
+			);
+		}
+		else
+		{
+			/*
+			 * Similarly, here tz_after jumps forward, while this doesn't
+			 * impact the local time monotonicity, but creates a gap in
+			 * the local time, since
+			 * tz_after = tz_before + (offset - prev_offset) + 1 microsecond
+			 * even if tz_before < t < tz_after never existed in that
+			 * particular timezone, they might exist as postgres timestamp
+			 * value.
+			 *
+			 * The documented behaviour is:
+			 * An invalid timestamp that appears to fall within a jump-forward
+			 * daylight savings transition is assigned the UTC offset that
+			 * prevailed in the time zone just before the transition.
+			 *
+			 * so that means that when converting from local time at the given
+			 * timezone back to UTC, the time will remain continuous tz_before
+			 * to tz_after - 1, the discontinuity will happen at tz_after.
+			 * so in this case we have
+			 *
+			 * utc_before = (utc_after - 1) + (offset - prev_offset)
+			 */
+			t_before += (offset - prev_offset) * USECS_PER_SEC;
+			p = pg_gmtime(&t_before);
+			if(p == NULL)
+				return;
+			tm_before = *p;
+
+			p = pg_gmtime(&t_after);
+			if(p == NULL)
+				return;
+			tm_after = *p;
+
+			get_tz_change_gaps(
+				&tm_before, &tm_after, &tz->state.monotonicity.utc
+			);
+		}
+	}
+}
+
 /*
  * Load a timezone from file or from cache.
  * Does not verify that the timezone is acceptable!
@@ -280,7 +439,7 @@ pg_tzset(const char *tzname)
 	/* hash_search already copied uppername into the hash key */
 	strcpy(tzp->tz.TZname, canonname);
 	memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
-
+	inspect_monotonicity(&tzp->tz);
 	return &tzp->tz;
 }
 
diff --git a/src/timezone/pgtz.h b/src/timezone/pgtz.h
index d4ae94de74a..235178da843 100644
--- a/src/timezone/pgtz.h
+++ b/src/timezone/pgtz.h
@@ -25,6 +25,7 @@
  */
 #define TZ_RUNTIME_LEAPS 1
 
+
 /*
  * Limit to time zone abbreviation length in proleptic TZ strings.
  * This is distinct from TZ_MAX_CHARS, which limits TZif file contents.
@@ -60,6 +61,11 @@ struct lsinfo
 	int_fast32_2s ls_corr;		/* correction to apply */
 };
 
+struct tz_gaps {
+	short local;
+	short utc;
+};
+
 /* This abbreviation means local time is unspecified.  */
 static char const UNSPEC[] = "-00";
 
@@ -90,6 +96,7 @@ struct state
 	int			charcnt;
 	bool		goback;
 	bool		goahead;
+	struct tz_gaps monotonicity;
 	pg_time_t	ats[TZ_MAX_TIMES];
 	unsigned char types[TZ_MAX_TIMES];
 	struct ttinfo ttis[TZ_MAX_TYPES];
-- 
2.53.0

