From af2ba7350867661754774502f6b4b7b8738213fd Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Sun, 30 Aug 2026 08:20:51 +0100
Subject: [PATCH 7/8] SLOPE: timezone errsave

Update DecodeTimezoneName to accept an ErrorSaveConext
The previous code was already calling DateTimeParseError
passing a NULL escontext in one case, but throwing an
for the missing timezone file. This commit adds an escontex
parameter to DecodeTimezoneName, and use DateTimeParseError
for the two error conditions.

The original call sites pass NULL, to throw as before, slopesupport
that calls it at planning time passes an error context node, that
captures the error, and simply declares it non-monotonic if it throws.

The logic in DecodeTimezoneNameToTz was moved into
DecodeTimezoneName, passing a NULL offset will save
the result to tz, as required by DecodeTimezoneNameToTz.
---
 src/backend/utils/adt/date.c          |  2 +-
 src/backend/utils/adt/datetime.c      | 61 +++++++++++++++++----------
 src/backend/utils/adt/timestamp.c     | 13 +++---
 src/backend/utils/fmgr/slopesupport.c | 23 ++++++----
 src/include/utils/datetime.h          |  3 +-
 src/test/regress/expected/slope.out   | 23 ++++++++--
 src/test/regress/sql/slope.sql        |  9 ++++
 7 files changed, 92 insertions(+), 42 deletions(-)

diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 7f746dd84c9..70ee3ba838b 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -3201,7 +3201,7 @@ timetz_zone(PG_FUNCTION_ARGS)
 	 */
 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-	type = DecodeTimezoneName(tzname, &val, &tzp);
+	type = DecodeTimezoneName(tzname, &val, &tzp, NULL);
 
 	if (type == TZNAME_FIXED_OFFSET)
 	{
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 04ebc632178..fb627193655 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -3293,24 +3293,35 @@ DecodeSpecial(int field, const char *lowtoken, int *val)
 /*
  * DecodeTimezoneName()
  * Interpret string as a timezone abbreviation or name.
- * Throw error if the name is not recognized.
  *
- * The return value indicates what kind of zone identifier it is:
+ * Returns a TZNAME_* code on success.  On failure, reports via
+ * DateTimeParseError.  If escontext is an ErrorSaveContext the error is
+ * saved and a DTERR_BAD_TIMEZONE code is returned; otherwise it throws
+ * an error.
+ *
+ * Fixed offset timezones will be returned as ISO offset (east+) in
+ * offset if not NULL, otherwise a constant-offset POSIX zone will
+ * be constructed cached and returned in tz.
+ *
+ * The success codes indicate what kind of zone identifier it is:
  *	TZNAME_FIXED_OFFSET: fixed offset from UTC
  *	TZNAME_DYNTZ: dynamic timezone abbreviation
  *	TZNAME_ZONE: full tzdb zone name
  *
  * For TZNAME_FIXED_OFFSET, *offset receives the UTC offset (in seconds,
- * with ISO sign convention: positive is east of Greenwich).
- * For the other two cases, *tz receives the timezone struct representing
- * the zone name or the abbreviation's underlying zone.
+ * with ISO sign convention: positive is east of Greenwich) and *tz is a
+ * constant-offset POSIX zone (via pg_tzset_offset).  For the other two
+ * success cases, *tz receives the timezone struct representing the zone
+ * name or the abbreviation's underlying zone.
  */
 int
-DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz)
+DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz,
+				   Node *escontext)
 {
 	char	   *lowzone;
 	int			dterr,
-				type;
+				type,
+				abbr_offset;
 	DateTimeErrorExtra extra;
 
 	/*
@@ -3327,14 +3338,22 @@ DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz)
 										   strlen(tzname),
 										   false);
 
-	dterr = DecodeTimezoneAbbrev(0, lowzone, &type, offset, tz, &extra);
+	dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &abbr_offset, tz, &extra);
 	if (dterr)
-		DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
+	{
+		DateTimeParseError(dterr, &extra, NULL, NULL, escontext);
+		return DTERR_BAD_TIMEZONE;
+	}
 
 	if (type == TZ || type == DTZ)
 	{
-		/* fixed-offset abbreviation, return the offset */
+		/* ISO offset (east+) -> POSIX (west+) */
+		if (offset != NULL)
+			*offset = abbr_offset;
+		else
+			*tz = pg_tzset_offset(-abbr_offset);
 		return TZNAME_FIXED_OFFSET;
+
 	}
 	else if (type == DYNTZ)
 	{
@@ -3346,9 +3365,11 @@ DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz)
 		/* try it as a full zone name */
 		*tz = pg_tzset(tzname);
 		if (*tz == NULL)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("time zone \"%s\" not recognized", tzname)));
+		{
+			extra.dtee_timezone = tzname;
+			DateTimeParseError(DTERR_BAD_TIMEZONE, &extra, NULL, NULL, escontext);
+			return DTERR_BAD_TIMEZONE;
+		}
 		return TZNAME_ZONE;
 	}
 }
@@ -3358,21 +3379,15 @@ DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz)
  * Interpret string as a timezone abbreviation or name.
  * Throw error if the name is not recognized.
  *
- * This is a simple wrapper for DecodeTimezoneName that produces a pg_tz *
- * result in all cases.
+ * Wrapper for DecodeTimezoneName that produces a pg_tz * in all cases.
  */
 pg_tz *
 DecodeTimezoneNameToTz(const char *tzname)
 {
-	pg_tz	   *result;
-	int			offset;
+	pg_tz	   *tz;
 
-	if (DecodeTimezoneName(tzname, &offset, &result) == TZNAME_FIXED_OFFSET)
-	{
-		/* fixed-offset abbreviation, get a pg_tz descriptor for that */
-		result = pg_tzset_offset(-offset);	/* flip to POSIX sign convention */
-	}
-	return result;
+	DecodeTimezoneName(tzname, NULL, &tz, NULL);
+	return tz;
 }
 
 /*
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 18e94d7bb03..6c226d1340d 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -525,7 +525,7 @@ parse_sane_timezone(struct pg_tm *tm, text *zone)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("time zone \"%s\" not recognized", tzname)));
 
-		type = DecodeTimezoneName(tzname, &val, &tzp);
+		type = DecodeTimezoneName(tzname, &val, &tzp, NULL);
 
 		if (type == TZNAME_FIXED_OFFSET)
 		{
@@ -1967,8 +1967,11 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, const char
 	if ((Timestamp) utime == dt)
 	{
 		/* Use session timezone doesn't pass one */
-		struct pg_tm *tx = pg_localtime(&utime,
-			attimezone == NULL ? session_timezone : attimezone);
+		pg_tz		 *tz;
+		struct pg_tm *tx;
+
+		tz = attimezone == NULL ? session_timezone : attimezone;
+		tx = pg_localtime(&utime, tz);
 
 		tm->tm_year = tx->tm_year + 1900;
 		tm->tm_mon = tx->tm_mon + 1;
@@ -6369,7 +6372,7 @@ timestamp_zone(PG_FUNCTION_ARGS)
 	 */
 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-	type = DecodeTimezoneName(tzname, &val, &tzp);
+	type = DecodeTimezoneName(tzname, &val, &tzp, NULL);
 
 	if (type == TZNAME_FIXED_OFFSET)
 	{
@@ -6637,7 +6640,7 @@ timestamptz_zone(PG_FUNCTION_ARGS)
 	 */
 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-	type = DecodeTimezoneName(tzname, &val, &tzp);
+	type = DecodeTimezoneName(tzname, &val, &tzp, NULL);
 
 	if (type == TZNAME_FIXED_OFFSET)
 	{
diff --git a/src/backend/utils/fmgr/slopesupport.c b/src/backend/utils/fmgr/slopesupport.c
index 7c02de01e7d..e6ca370f8b8 100644
--- a/src/backend/utils/fmgr/slopesupport.c
+++ b/src/backend/utils/fmgr/slopesupport.c
@@ -4,6 +4,7 @@
 
 #include "c.h"
 #include "catalog/pg_type.h"
+#include "nodes/miscnodes.h"
 #include "nodes/primnodes.h"
 #include "nodes/supportnodes.h"
 #include "parser/scansup.h"
@@ -30,7 +31,7 @@ typedef enum SLOPE_SIGN
 	SLOPE_SIGN_PINF = 2,
 	SLOPE_SIGN_NAN = 3,
 	SLOPE_SIGN_NULL = 4,
-} SLOPE_SIGN;
+}			SLOPE_SIGN;
 
 #define SLOPE_REQUEST(req) \
 	SupportRequestMonotonic *req; \
@@ -361,6 +362,9 @@ get_const_timezone_arg(List *args, int argno)
 	Const	   *tz_const;
 	text	   *zone;
 	char		tzname[TZ_STRLEN_MAX + 1];
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+	int			offset;
+	pg_tz	   *tz;
 
 	if (args == NULL || list_length(args) <= argno)
 		return NULL;
@@ -376,7 +380,11 @@ get_const_timezone_arg(List *args, int argno)
 
 	zone = DatumGetTextPP(tz_const->constvalue);
 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
-	return DecodeTimezoneNameToTz(tzname);
+
+	DecodeTimezoneName(tzname, &offset, &tz, (Node *) &escontext);
+	if (escontext.error_occurred)
+		return NULL;
+	return tz;
 }
 
 /*
@@ -394,9 +402,10 @@ timestamptz_date_slope_support(PG_FUNCTION_ARGS)
 }
 
 static Oid
-get_monotonic_expr_funcid(SupportRequestMonotonic *req)
+get_monotonic_expr_funcid(SupportRequestMonotonic * req)
 {
 	Node	   *expr = req->expr;
+
 	if (IsA(expr, FuncExpr))
 		return ((FuncExpr *) expr)->funcid;
 	return InvalidOid;
@@ -469,7 +478,6 @@ timezone_prosupport(PG_FUNCTION_ARGS)
 	bool		to_utc = false;
 	SLOPE_REQUEST_ARGS(req, args, 1);
 
-
 	switch (get_monotonic_expr_funcid(req))
 	{
 
@@ -494,10 +502,9 @@ timezone_prosupport(PG_FUNCTION_ARGS)
 		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.
+	 * 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);
 }
diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h
index 87c50eebf12..51604cc5b25 100644
--- a/src/include/utils/datetime.h
+++ b/src/include/utils/datetime.h
@@ -347,7 +347,8 @@ extern int	DecodeTimezoneAbbrev(int field, const char *lowtoken,
 extern int	DecodeSpecial(int field, const char *lowtoken, int *val);
 extern int	DecodeUnits(int field, const char *lowtoken, int *val);
 
-extern int	DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz);
+extern int	DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz, 
+							   Node *errorcontext);
 extern pg_tz *DecodeTimezoneNameToTz(const char *tzname);
 
 extern int	DecodeTimezoneAbbrevPrefix(const char *str,
diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out
index 21a13857d05..9f652839c94 100644
--- a/src/test/regress/expected/slope.out
+++ b/src/test/regress/expected/slope.out
@@ -855,10 +855,10 @@ SELECT
 FROM units
 ORDER BY 1;
 EXECUTE query('UTC');
-        expression        |               monotonic                
---------------------------+----------------------------------------
- date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second
- timezone(TZ, <>)         | ts, tstz
+        expression        | monotonic 
+--------------------------+-----------
+ date_trunc(<>, tstz, TZ) | none
+ timezone(TZ, <>)         | none
 (2 rows)
 
 EXECUTE query('Africa/Ouagadougou');
@@ -998,6 +998,21 @@ EXECUTE query_local;
  timezone(<>)         | none
 (5 rows)
 
+--
+-- TimeZone lookup error handling
+--
+-- Errors should not be thrown at planning time
+-- if the timezone lookup fails.
+EXPLAIN (COSTS OFF)
+SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Aggregate
+   ->  Index Only Scan using src_ts_idx on src
+(2 rows)
+
+SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
+ERROR:  time zone "Bogus/Zone" not recognized
 DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
 NOTICE:  drop cascades to 15 other objects
diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql
index 7e8b85b9ec3..2894f26ef47 100644
--- a/src/test/regress/sql/slope.sql
+++ b/src/test/regress/sql/slope.sql
@@ -633,6 +633,15 @@ EXECUTE query_local;
 SET timezone = 'America/Goose_Bay';
 EXECUTE query_local;
 
+--
+-- TimeZone lookup error handling
+--
+-- Errors should not be thrown at planning time
+-- if the timezone lookup fails.
+
+EXPLAIN (COSTS OFF)
+SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
+SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
 
 DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
\ No newline at end of file
-- 
2.53.0

