From 932935ecdbcffda7824d1decb8d8ff8a2ad8a28e Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Thu, 9 Jan 2025 18:51:45 -0500
Subject: [PATCH v2 3/4] Add zone-derived abbreviations to the
 pg_timezone_abbrevs view.

This ensures that pg_timezone_abbrevs will report abbreviations
that are recognized via the IANA data, and *not* report any
timezone_abbreviations entries that are thereby overridden.

Under the hood, there are now two SRFs, one that pulls the IANA
data and one that pulls timezone_abbreviations entries.  They're
combined by logic in the view.  This approach was useful for
debugging (since the functions can be called on their own).
While I don't propose to document the functions explicitly,
they might be useful to call directly.

XXX: don't forget catversion bump.

Per report from Aleksander Alekseev and additional investigation.

Discussion: https://postgr.es/m/CAJ7c6TOATjJqvhnYsui0=CO5XFMF4dvTGH+skzB--jNhqSQu5g@mail.gmail.com
---
 doc/src/sgml/system-views.sgml         |  4 +-
 src/backend/catalog/system_views.sql   |  7 +-
 src/backend/utils/adt/datetime.c       | 92 +++++++++++++++++++++++++-
 src/include/catalog/pg_proc.dat        | 12 +++-
 src/include/pgtime.h                   |  2 +
 src/test/regress/expected/rules.out    | 17 +++--
 src/test/regress/expected/sysviews.out |  8 +++
 src/test/regress/sql/sysviews.sql      |  3 +
 src/timezone/localtime.c               | 38 +++++++++++
 9 files changed, 172 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index a586156614..8e2b0a7927 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -4566,7 +4566,9 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
    The view <structname>pg_timezone_abbrevs</structname> provides a list
    of time zone abbreviations that are currently recognized by the datetime
    input routines.  The contents of this view change when the
-   <xref linkend="guc-timezone-abbreviations"/> run-time parameter is modified.
+   <xref linkend="guc-timezone"/> or
+   <xref linkend="guc-timezone-abbreviations"/> run-time parameters are
+   modified.
   </para>
 
   <table>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7a595c84db..8e98bf847d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -634,7 +634,12 @@ REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
 
 CREATE VIEW pg_timezone_abbrevs AS
-    SELECT * FROM pg_timezone_abbrevs();
+    SELECT * FROM pg_timezone_abbrevs_zone() z
+    UNION ALL
+    (SELECT * FROM pg_timezone_abbrevs_abbrevs() a
+     WHERE NOT EXISTS (SELECT 1 FROM pg_timezone_abbrevs_zone() z2
+                       WHERE z2.abbrev = a.abbrev))
+    ORDER BY abbrev;
 
 CREATE VIEW pg_timezone_names AS
     SELECT * FROM pg_timezone_names();
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index ef04112602..5d893cff50 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -5110,11 +5110,99 @@ FetchDynamicTimeZone(TimeZoneAbbrevTable *tbl, const datetkn *tp,
 
 
 /*
- * This set-returning function reads all the available time zone abbreviations
+ * This set-returning function reads all the time zone abbreviations
+ * defined by the IANA data for the current timezone setting,
  * and returns a set of (abbrev, utc_offset, is_dst).
  */
 Datum
-pg_timezone_abbrevs(PG_FUNCTION_ARGS)
+pg_timezone_abbrevs_zone(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	int		   *pindex;
+	Datum		result;
+	HeapTuple	tuple;
+	Datum		values[3];
+	bool		nulls[3] = {0};
+	TimestampTz now = GetCurrentTransactionStartTimestamp();
+	pg_time_t	t = timestamptz_to_time_t(now);
+	const char *abbrev;
+	long int	gmtoff;
+	int			isdst;
+	struct pg_itm_in itm_in;
+	Interval   *resInterval;
+
+	/* stuff done only on the first call of the function */
+	if (SRF_IS_FIRSTCALL())
+	{
+		TupleDesc	tupdesc;
+		MemoryContext oldcontext;
+
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		/* allocate memory for user context */
+		pindex = (int *) palloc(sizeof(int));
+		*pindex = 0;
+		funcctx->user_fctx = pindex;
+
+		if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+			elog(ERROR, "return type must be a row type");
+		funcctx->tuple_desc = tupdesc;
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	pindex = (int *) funcctx->user_fctx;
+
+	while ((abbrev = pg_get_next_timezone_abbrev(pindex,
+												 session_timezone)) != NULL)
+	{
+		/* Ignore abbreviations that aren't all-alphabetic */
+		if (strspn(abbrev, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") != strlen(abbrev))
+			continue;
+
+		/* Determine the current meaning of the abbrev */
+		if (!pg_interpret_timezone_abbrev(abbrev,
+										  &t,
+										  &gmtoff,
+										  &isdst,
+										  session_timezone))
+			continue;			/* hm, not actually used in this zone? */
+
+		values[0] = CStringGetTextDatum(abbrev);
+
+		/* Convert offset (in seconds) to an interval; can't overflow */
+		MemSet(&itm_in, 0, sizeof(struct pg_itm_in));
+		itm_in.tm_usec = (int64) gmtoff * USECS_PER_SEC;
+		resInterval = (Interval *) palloc(sizeof(Interval));
+		(void) itmin2interval(&itm_in, resInterval);
+		values[1] = IntervalPGetDatum(resInterval);
+
+		values[2] = BoolGetDatum(isdst);
+
+		tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+		result = HeapTupleGetDatum(tuple);
+
+		SRF_RETURN_NEXT(funcctx, result);
+	}
+
+	SRF_RETURN_DONE(funcctx);
+}
+
+/*
+ * This set-returning function reads all the time zone abbreviations
+ * defined by the timezone_abbreviations setting,
+ * and returns a set of (abbrev, utc_offset, is_dst).
+ */
+Datum
+pg_timezone_abbrevs_abbrevs(PG_FUNCTION_ARGS)
 {
 	FuncCallContext *funcctx;
 	int		   *pindex;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b37e8a6f88..eed78a8a1e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8392,12 +8392,18 @@
   proargmodes => '{o,o,o,o,o,o}',
   proargnames => '{name,statement,is_holdable,is_binary,is_scrollable,creation_time}',
   prosrc => 'pg_cursor' },
-{ oid => '2599', descr => 'get the available time zone abbreviations',
-  proname => 'pg_timezone_abbrevs', prorows => '1000', proretset => 't',
+{ oid => '9221', descr => 'get abbreviations from current timezone',
+  proname => 'pg_timezone_abbrevs_zone', prorows => '10', proretset => 't',
   provolatile => 's', prorettype => 'record', proargtypes => '',
   proallargtypes => '{text,interval,bool}', proargmodes => '{o,o,o}',
   proargnames => '{abbrev,utc_offset,is_dst}',
-  prosrc => 'pg_timezone_abbrevs' },
+  prosrc => 'pg_timezone_abbrevs_zone' },
+{ oid => '2599', descr => 'get abbreviations from timezone_abbreviations',
+  proname => 'pg_timezone_abbrevs_abbrevs', prorows => '1000', proretset => 't',
+  provolatile => 's', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,interval,bool}', proargmodes => '{o,o,o}',
+  proargnames => '{abbrev,utc_offset,is_dst}',
+  prosrc => 'pg_timezone_abbrevs_abbrevs' },
 { oid => '2856', descr => 'get the available time zone names',
   proname => 'pg_timezone_names', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgtime.h b/src/include/pgtime.h
index b8b898a69c..5fc9f223de 100644
--- a/src/include/pgtime.h
+++ b/src/include/pgtime.h
@@ -74,6 +74,8 @@ extern bool pg_timezone_abbrev_is_known(const char *abbrev,
 										long int *gmtoff,
 										int *isdst,
 										const pg_tz *tz);
+extern const char *pg_get_next_timezone_abbrev(int *indx,
+											   const pg_tz *tz);
 extern bool pg_get_timezone_offset(const pg_tz *tz, long int *gmtoff);
 extern const char *pg_get_timezone_name(pg_tz *tz);
 extern bool pg_tz_acceptable(pg_tz *tz);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 3014d047fe..91d316e3c4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2627,10 +2627,19 @@ pg_tables| SELECT n.nspname AS schemaname,
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
      LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace)))
   WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"]));
-pg_timezone_abbrevs| SELECT abbrev,
-    utc_offset,
-    is_dst
-   FROM pg_timezone_abbrevs() pg_timezone_abbrevs(abbrev, utc_offset, is_dst);
+pg_timezone_abbrevs| SELECT z.abbrev,
+    z.utc_offset,
+    z.is_dst
+   FROM pg_timezone_abbrevs_zone() z(abbrev, utc_offset, is_dst)
+UNION ALL
+ SELECT a.abbrev,
+    a.utc_offset,
+    a.is_dst
+   FROM pg_timezone_abbrevs_abbrevs() a(abbrev, utc_offset, is_dst)
+  WHERE (NOT (EXISTS ( SELECT 1
+           FROM pg_timezone_abbrevs_zone() z2(abbrev, utc_offset, is_dst)
+          WHERE (z2.abbrev = a.abbrev))))
+  ORDER BY 1;
 pg_timezone_names| SELECT name,
     abbrev,
     utc_offset,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 91089ac215..352abc0bd4 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -223,3 +223,11 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs;
  t
 (1 row)
 
+-- One specific case we can check without much fear of breakage
+-- is the historical local-mean-time value used for America/Los_Angeles.
+select * from pg_timezone_abbrevs where abbrev = 'LMT';
+ abbrev |          utc_offset           | is_dst 
+--------+-------------------------------+--------
+ LMT    | @ 7 hours 52 mins 58 secs ago | f
+(1 row)
+
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index b2a7923754..66179f026b 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -98,3 +98,6 @@ set timezone_abbreviations = 'Australia';
 select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs;
 set timezone_abbreviations = 'India';
 select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs;
+-- One specific case we can check without much fear of breakage
+-- is the historical local-mean-time value used for America/Los_Angeles.
+select * from pg_timezone_abbrevs where abbrev = 'LMT';
diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c
index 65511ae8be..9f76212b7b 100644
--- a/src/timezone/localtime.c
+++ b/src/timezone/localtime.c
@@ -1919,6 +1919,44 @@ pg_timezone_abbrev_is_known(const char *abbrev,
 	return result;
 }
 
+/*
+ * Iteratively fetch all the abbreviations used in the given time zone.
+ *
+ * *indx is a state counter that the caller must initialize to zero
+ * before the first call, and not touch between calls.
+ *
+ * Returns the next known abbreviation, or NULL if there are no more.
+ *
+ * Note: the caller typically applies pg_interpret_timezone_abbrev()
+ * to each result.  While that nominally results in O(N^2) time spent
+ * searching the sp->chars[] array, we don't expect any zone to have
+ * enough abbreviations to make that meaningful.
+ */
+const char *
+pg_get_next_timezone_abbrev(int *indx,
+							const pg_tz *tz)
+{
+	const char *result;
+	const struct state *sp = &tz->state;
+	const char *abbrs;
+	int			abbrind;
+
+	/* If we're still in range, the result is the current abbrev. */
+	abbrs = sp->chars;
+	abbrind = *indx;
+	if (abbrind < 0 || abbrind >= sp->charcnt)
+		return NULL;
+	result = abbrs + abbrind;
+
+	/* Advance *indx past this abbrev and its trailing null. */
+	while (abbrs[abbrind] != '\0')
+		abbrind++;
+	abbrind++;
+	*indx = abbrind;
+
+	return result;
+}
+
 /*
  * If the given timezone uses only one GMT offset, store that offset
  * into *gmtoff and return true, else return false.
-- 
2.43.5

