From 965039281f377ee2bbdf4daf8541279f458abde1 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Sun, 30 Aug 2026 22:29:40 +0100
Subject: [PATCH-v15.3 8/8] SLOPE: plan-cache guards

This patch makes sure that plans being cached don't depend
on session time zone, preventing plans from becoming stale
when prepared in one timezone and later used with a different
timezone.

This was achieved by plumbing the is_oneshot flag all the
way from the plan source to the SupportRequestMonotonic.
---
 src/backend/commands/explain.c        |  5 +++
 src/backend/optimizer/path/pathkeys.c |  8 +++--
 src/backend/optimizer/plan/planner.c  |  1 +
 src/backend/utils/cache/plancache.c   |  8 +++++
 src/backend/utils/fmgr/slopesupport.c | 42 ++++++++++++++++------
 src/include/nodes/parsenodes.h        |  1 +
 src/include/nodes/pathnodes.h         |  3 ++
 src/include/nodes/supportnodes.h      |  3 +-
 src/test/regress/expected/slope.out   | 52 ++++++++++++++++++++++++---
 src/test/regress/sql/slope.sql        | 21 +++++++++++
 10 files changed, 124 insertions(+), 20 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e35e0a649b3..473b51836d2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -355,6 +355,11 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 		bufusage_start = pgBufferUsage;
 	INSTR_TIME_SET_CURRENT(planstart);
 
+	/*
+	 * Plans produced by EXPLAIN are never cached.
+	 */
+	cursorOptions |= CURSOR_OPT_ONESHOT;
+
 	/* plan the query */
 	plan = pg_plan_query(query, queryString, cursorOptions, params, es);
 
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index b7629bc9f92..93936d82b24 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -48,7 +48,7 @@ static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
 											 int partkeycol);
 static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
-static MonotonicFunction get_expr_slope_wrt(Expr *expr, Expr *target);
+static MonotonicFunction get_expr_slope_wrt(Expr *expr, Expr *target, PlannerInfo *root);
 static bool indexcol_is_equalimage(IndexOptInfo *index, int colno);
 static PathKey *slope_emit_pathkey(PlannerInfo *root, PathKey *pk,
 								   Expr *indexkey, bool reverse_sort,
@@ -913,7 +913,7 @@ expr_can_nan(Expr *target)
  *   MONOTONICFUNC_NONE:       cannot determine monotonicity
  */
 static MonotonicFunction
-get_expr_slope_wrt(Expr *expr, Expr *target)
+get_expr_slope_wrt(Expr *expr, Expr *target, PlannerInfo *root)
 {
 	MonotonicFunction slope = MONOTONICFUNC_INCREASING;
 
@@ -990,6 +990,7 @@ get_expr_slope_wrt(Expr *expr, Expr *target)
 		/* Call prosupport to get slope pattern */
 		req.type = T_SupportRequestMonotonic;
 		req.expr = (Node *) expr;
+		req.root = root;
 		req.slopes = NULL;
 		req.nslopes = 0;
 
@@ -1296,7 +1297,8 @@ build_index_pathkeys(PlannerInfo *root,
 
 					em = linitial(qpk->pk_eclass->ec_members);
 					qpk->pk_slope = get_expr_slope_wrt(em->em_expr,
-													   qpk->pk_var);
+													   qpk->pk_var,
+													   root);
 				}
 
 				/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 482dc209e22..6c495f4d6db 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -366,6 +366,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob = makeNode(PlannerGlobal);
 
 	glob->boundParams = boundParams;
+	glob->is_oneshot = (cursorOptions & CURSOR_OPT_ONESHOT) != 0;
 	glob->subplans = NIL;
 	glob->subpaths = NIL;
 	glob->subroots = NIL;
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index fb3b38ffbbf..1fe0a8c6548 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -489,6 +489,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	plansource->parserSetup = parserSetup;
 	plansource->parserSetupArg = parserSetupArg;
 	plansource->cursor_options = cursor_options;
+	/*
+	 * Mirror CachedPlanSource.is_oneshot into cursor_options.
+	 * Oneshot plans may use statement-local optimizations;
+	 * if the bit is unset the plan may be reused and must
+	 * not go stale.
+	 */
+	if (plansource->is_oneshot)
+		plansource->cursor_options |= CURSOR_OPT_ONESHOT;
 	plansource->fixed_result = fixed_result;
 
 	/*
diff --git a/src/backend/utils/fmgr/slopesupport.c b/src/backend/utils/fmgr/slopesupport.c
index e6ca370f8b8..d889ba8e07e 100644
--- a/src/backend/utils/fmgr/slopesupport.c
+++ b/src/backend/utils/fmgr/slopesupport.c
@@ -5,6 +5,7 @@
 #include "c.h"
 #include "catalog/pg_type.h"
 #include "nodes/miscnodes.h"
+#include "nodes/pathnodes.h"
 #include "nodes/primnodes.h"
 #include "nodes/supportnodes.h"
 #include "parser/scansup.h"
@@ -31,7 +32,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; \
@@ -363,7 +364,6 @@ get_const_timezone_arg(List *args, int argno)
 	text	   *zone;
 	char		tzname[TZ_STRLEN_MAX + 1];
 	ErrorSaveContext escontext = {T_ErrorSaveContext};
-	int			offset;
 	pg_tz	   *tz;
 
 	if (args == NULL || list_length(args) <= argno)
@@ -381,31 +381,50 @@ get_const_timezone_arg(List *args, int argno)
 	zone = DatumGetTextPP(tz_const->constvalue);
 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-	DecodeTimezoneName(tzname, &offset, &tz, (Node *) &escontext);
+	DecodeTimezoneName(tzname, NULL, &tz, (Node *) &escontext);
 	if (escontext.error_occurred)
 		return NULL;
 	return tz;
 }
 
+
+/*
+ * Session TimeZone is only safe to consult for oneshot plans.  A cached
+ * plan that baked in the current zone would go stale after SET TimeZone.
+ * Non-monotonic prosupport requests (and a NULL root) must not be
+ * treated as SupportRequestMonotonic: the struct layouts differ.
+ */
+static pg_tz *
+get_plan_timezone(SupportRequestMonotonic *req)
+{
+	if (req->root != NULL &&
+		req->root->glob != NULL &&
+		req->root->glob->is_oneshot)
+		return session_timezone;
+	return NULL;
+}
+
 /*
  * Slope support for date(timestamptz)
  */
 Datum
 timestamptz_date_slope_support(PG_FUNCTION_ARGS)
 {
+	pg_tz		 *tzp;
 	SLOPE_REQUEST(req);
 
-	if (pg_timezone_is_monotonic(session_timezone, TZ_GAP_DAY, false))
+	tzp = get_plan_timezone(req);
+
+	if (tzp != NULL && pg_timezone_is_monotonic(tzp, 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)
+get_monotonic_expr_funcid(SupportRequestMonotonic *req)
 {
 	Node	   *expr = req->expr;
-
 	if (IsA(expr, FuncExpr))
 		return ((FuncExpr *) expr)->funcid;
 	return InvalidOid;
@@ -491,7 +510,7 @@ timezone_prosupport(PG_FUNCTION_ARGS)
 			to_utc = true;
 			pg_fallthrough;
 		case F_TIMEZONE_TIMESTAMPTZ:
-			tzp = session_timezone;
+			tzp = get_plan_timezone(req);
 			break;
 
 		default:
@@ -502,9 +521,10 @@ 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);
 }
@@ -534,7 +554,7 @@ date_trunc_slope_support(PG_FUNCTION_ARGS)
 			break;
 
 		case F_DATE_TRUNC_TEXT_TIMESTAMPTZ:
-			tzp = session_timezone;
+			tzp = get_plan_timezone(req);
 			break;
 
 		default:
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d3835af8f9e..5b2a15b10a4 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3514,6 +3514,7 @@ typedef struct SecLabelStmt
 #define CURSOR_OPT_GENERIC_PLAN 0x0200	/* force use of generic plan */
 #define CURSOR_OPT_CUSTOM_PLAN	0x0400	/* force use of custom plan */
 #define CURSOR_OPT_PARALLEL_OK	0x0800	/* parallel mode OK */
+#define CURSOR_OPT_ONESHOT		0x1000	/* oneshot plan; may use statement-local opts */
 
 typedef struct DeclareCursorStmt
 {
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index ea7cd78b797..ba2036d04ac 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -250,6 +250,9 @@ typedef struct PlannerGlobal
 	/* is plan specific to current role? */
 	bool		dependsOnRole;
 
+	/* oneshot plan? statement-local optimizations are allowed */
+	bool		is_oneshot;
+
 	/* parallel mode potentially OK? */
 	bool		parallelModeOK;
 
diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h
index c576533c321..63af811c8e9 100644
--- a/src/include/nodes/supportnodes.h
+++ b/src/include/nodes/supportnodes.h
@@ -465,7 +465,8 @@ typedef struct SupportRequestMonotonic
 	NodeTag		type;
 
 	/* Input fields: */
-	Node	   *expr;			/* FuncExpr or OpExpr */
+	Node		*expr;			/* FuncExpr or OpExpr */
+	PlannerInfo *root;			/* Additional context */
 
 	/* Output fields (set by prosupport function): */
 	int			nslopes;		/* number of slopes in array */
diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out
index 9f652839c94..3a2ca6a8728 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) | none
- timezone(TZ, <>)         | none
+        expression        |               monotonic                
+--------------------------+----------------------------------------
+ date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second
+ timezone(TZ, <>)         | ts, tstz
 (2 rows)
 
 EXECUTE query('Africa/Ouagadougou');
@@ -1013,9 +1013,50 @@ SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
 
 SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
 ERROR:  time zone "Bogus/Zone" not recognized
+--
+-- Plan cache
+--
+-- if a plan is cached ignore monotonicity that depends on session timezone.
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+CREATE TABLE pc(t timestamptz);
+INSERT INTO pc VALUES ('2025-11-02 05:59:59+00'),
+                      ('2025-11-02 06:00:00+00');
+CREATE INDEX ON pc(t);
+SET TimeZone = 'UTC';
+PREPARE q AS SELECT t AT LOCAL AS l FROM pc ORDER BY 1;
+EXECUTE q;                             -- generic plan, built under Etc/UTC
+            l             
+--------------------------
+ Sun Nov 02 05:59:59 2025
+ Sun Nov 02 06:00:00 2025
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT t AT LOCAL as l FROM pc ORDER BY 1;
+              QUERY PLAN              
+--------------------------------------
+ Index Only Scan using pc_t_idx on pc
+(1 row)
+
+SET TimeZone = 'America/New_York';
+EXECUTE q;
+            l             
+--------------------------
+ Sun Nov 02 01:00:00 2025
+ Sun Nov 02 01:59:59 2025
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT t AT LOCAL as l FROM pc ORDER BY 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Sort
+   Sort Key: ((t AT LOCAL))
+   ->  Index Only Scan using pc_t_idx on pc
+(3 rows)
+
 DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
-NOTICE:  drop cascades to 15 other objects
+NOTICE:  drop cascades to 16 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
@@ -1031,3 +1072,4 @@ drop cascades to type fp_real
 drop cascades to table numeric_corners
 drop cascades to table inc
 drop cascades to function index_plan(text)
+drop cascades to table pc
diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql
index 2894f26ef47..85f033cd17f 100644
--- a/src/test/regress/sql/slope.sql
+++ b/src/test/regress/sql/slope.sql
@@ -643,5 +643,26 @@ EXPLAIN (COSTS OFF)
 SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
 SELECT min(date_trunc('day', ts, 'Bogus/Zone')) FROM src;
 
+--
+-- Plan cache
+--
+-- if a plan is cached ignore monotonicity that depends on session timezone.
+
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+CREATE TABLE pc(t timestamptz);
+INSERT INTO pc VALUES ('2025-11-02 05:59:59+00'),
+                      ('2025-11-02 06:00:00+00');
+CREATE INDEX ON pc(t);
+
+SET TimeZone = 'UTC';
+PREPARE q AS SELECT t AT LOCAL AS l FROM pc ORDER BY 1;
+EXECUTE q;                             -- generic plan, built under Etc/UTC
+EXPLAIN (COSTS OFF) SELECT t AT LOCAL as l FROM pc ORDER BY 1;
+SET TimeZone = 'America/New_York';
+EXECUTE q;
+EXPLAIN (COSTS OFF) SELECT t AT LOCAL as l FROM pc ORDER BY 1;
+
+
 DEALLOCATE ALL;
 DROP SCHEMA slope CASCADE;
\ No newline at end of file
-- 
2.53.0

