From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sat, 1 Aug 2026 21:23:00 +0500
Subject: [PATCH] Fix DCH format-cache recycle to update std flag

DCH_cache_getnew() set ent->std only when allocating a new entry, not
when recycling an old one.  DCH_cache_search() matches on (str, std), so
a format tree parsed in one mode could be reused under the other.

That let jsonpath .datetime() (SQL/JSON standard mode) reuse a format
tree built by lenient to_char(), once enough distinct format strings
had filled the cache and forced an eviction.

Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Michael Malis <malis@pgrust.com>
Discussion: https://www.postgresql.org/message-id/19594-5d9bdc019e3f7f6e%40postgresql.org
---
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index d52d71b0a8c..93a2336d951 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -3814,6 +3814,7 @@ DCH_cache_getnew(const char *str, bool std)
 #endif
 		old->valid = false;
 		strlcpy(old->str, str, DCH_CACHE_SIZE + 1);
+		old->std = std;
 		old->age = (++DCHCounter);
 		/* caller is expected to fill format, then set valid */
 		return old;
diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out
index c7b8c36c842..d512328e836 100644
--- a/src/test/regress/expected/jsonb_jsonpath.out
+++ b/src/test/regress/expected/jsonb_jsonpath.out
@@ -1782,6 +1782,25 @@ select jsonb_path_query('"10-03-2017t12:34:56"', '$.datetime("dd-mm-yyyy\"T\"HH2
 ERROR:  unmatched format character "T"
 select jsonb_path_query('"10-03-2017 12:34:56"', '$.datetime("dd-mm-yyyy\"T\"HH24:MI:SS")');
 ERROR:  unmatched format character "T"
+-- DCH cache recycle must preserve std vs lenient mode
+SELECT count(*) FROM (
+  SELECT jsonb_path_query(to_jsonb('x' || g::text || '1234'),
+						  format('$.datetime("\"x%s\"HH24MI")', g)::jsonpath)
+  FROM generate_series(1, 20) g
+) s;
+ count 
+-------
+    20
+(1 row)
+
+SELECT to_char(timestamp '2000-01-01 12:34:00', 'HH24zMI');
+ to_char 
+---------
+ 12z34
+(1 row)
+
+SELECT jsonb_path_query('"12z34"'::jsonb, '$.datetime("HH24zMI")');
+ERROR:  invalid datetime format separator: "z"
 -- Test .bigint()
 select jsonb_path_query('null', '$.bigint()');
 ERROR:  jsonpath item method .bigint() can only be applied to a string or numeric value
diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql
index c37dc3817ff..209ca470a9f 100644
--- a/src/test/regress/sql/jsonb_jsonpath.sql
+++ b/src/test/regress/sql/jsonb_jsonpath.sql
@@ -383,6 +383,15 @@ select jsonb_path_query('"10-03-2017T12:34:56"', '$.datetime("dd-mm-yyyy\"T\"HH2
 select jsonb_path_query('"10-03-2017t12:34:56"', '$.datetime("dd-mm-yyyy\"T\"HH24:MI:SS")');
 select jsonb_path_query('"10-03-2017 12:34:56"', '$.datetime("dd-mm-yyyy\"T\"HH24:MI:SS")');
 
+-- DCH cache recycle must preserve std vs lenient mode
+SELECT count(*) FROM (
+  SELECT jsonb_path_query(to_jsonb('x' || g::text || '1234'),
+						  format('$.datetime("\"x%s\"HH24MI")', g)::jsonpath)
+  FROM generate_series(1, 20) g
+) s;
+SELECT to_char(timestamp '2000-01-01 12:34:00', 'HH24zMI');
+SELECT jsonb_path_query('"12z34"'::jsonb, '$.datetime("HH24zMI")');
+
 -- Test .bigint()
 select jsonb_path_query('null', '$.bigint()');
 select jsonb_path_query('true', '$.bigint()');
