From 965039281f377ee2bbdf4daf8541279f458abde1 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe 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