summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTom Lane2025-01-16 19:11:19 +0000
committerTom Lane2025-01-16 19:11:19 +0000
commitd7674c9fab09d5bab427ba3b9b7a20b169aba715 (patch)
tree081f69dbaf1d06de875389d6b898fcb279c70982 /src
parentbc10219b9c8931ff4f872b3e799da2208101c574 (diff)
Seek zone abbreviations in the IANA data before timezone_abbreviations.
If a time zone abbreviation used in datetime input is defined in the currently active timezone, use that definition in preference to looking in the timezone_abbreviations list. That allows us to correctly handle abbreviations that have different meanings in different timezones. Also, it eliminates an inconsistency between datetime input and datetime output: the non-ISO datestyles for timestamptz have always printed abbreviations taken from the IANA data, not from timezone_abbreviations. Before this fix, it was possible to demonstrate cases where casting a timestamp to text and back fails or changes the value significantly because of that inconsistency. While this change removes the ability to override the IANA data about an abbreviation known in the current zone, it's not clear that there's any real use-case for doing so. But it is clear that this makes life a lot easier for dealing with abbreviations that have conflicts across different time zones. Also update the pg_timezone_abbrevs view to 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 intend to document the functions explicitly, they might be useful to call directly. Also improve DecodeTimezoneAbbrev's caching logic so that it can cache zone abbreviations found in the IANA data. Without that, this patch would have caused a noticeable degradation of the runtime of timestamptz_in. Per report from Aleksander Alekseev and additional investigation. Discussion: https://postgr.es/m/CAJ7c6TOATjJqvhnYsui0=CO5XFMF4dvTGH+skzB--jNhqSQu5g@mail.gmail.com
Diffstat (limited to 'src')
-rw-r--r--src/backend/catalog/system_views.sql7
-rw-r--r--src/backend/commands/variable.c2
-rw-r--r--src/backend/utils/adt/datetime.c242
-rw-r--r--src/include/catalog/catversion.h2
-rw-r--r--src/include/catalog/pg_proc.dat12
-rw-r--r--src/include/pgtime.h7
-rw-r--r--src/include/utils/datetime.h2
-rw-r--r--src/test/regress/expected/horology.out6
-rw-r--r--src/test/regress/expected/rules.out17
-rw-r--r--src/test/regress/expected/sysviews.out8
-rw-r--r--src/test/regress/expected/timestamptz.out59
-rw-r--r--src/test/regress/sql/horology.sql1
-rw-r--r--src/test/regress/sql/sysviews.sql3
-rw-r--r--src/test/regress/sql/timestamptz.sql17
-rw-r--r--src/timezone/localtime.c114
-rw-r--r--src/tools/pgindent/typedefs.list1
16 files changed, 472 insertions, 28 deletions
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 64a873a16e3..46868bf7e89 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/commands/variable.c b/src/backend/commands/variable.c
index 44796bf15ad..4ad6e236d69 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -381,6 +381,8 @@ void
assign_timezone(const char *newval, void *extra)
{
session_timezone = *((pg_tz **) extra);
+ /* datetime.c's cache of timezone abbrevs may now be obsolete */
+ ClearTimeZoneAbbrevCache();
}
/*
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index d8af3591d17..5d893cff50c 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -259,7 +259,17 @@ static const datetkn *datecache[MAXDATEFIELDS] = {NULL};
static const datetkn *deltacache[MAXDATEFIELDS] = {NULL};
-static const datetkn *abbrevcache[MAXDATEFIELDS] = {NULL};
+/* Cache for results of timezone abbreviation lookups */
+
+typedef struct TzAbbrevCache
+{
+ char abbrev[TOKMAXLEN + 1]; /* always NUL-terminated */
+ char ftype; /* TZ, DTZ, or DYNTZ */
+ int offset; /* GMT offset, if fixed-offset */
+ pg_tz *tz; /* relevant zone, if variable-offset */
+} TzAbbrevCache;
+
+static TzAbbrevCache tzabbrevcache[MAXDATEFIELDS];
/*
@@ -1845,6 +1855,40 @@ DetermineTimeZoneAbbrevOffsetInternal(pg_time_t t, const char *abbr, pg_tz *tzp,
}
+/* TimeZoneAbbrevIsKnown()
+ *
+ * Detect whether the given string is a time zone abbreviation that's known
+ * in the specified TZDB timezone, and if so whether it's fixed or varying
+ * meaning. The match is not case-sensitive.
+ */
+static bool
+TimeZoneAbbrevIsKnown(const char *abbr, pg_tz *tzp,
+ bool *isfixed, int *offset, int *isdst)
+{
+ char upabbr[TZ_STRLEN_MAX + 1];
+ unsigned char *p;
+ long int gmtoff;
+
+ /* We need to force the abbrev to upper case */
+ strlcpy(upabbr, abbr, sizeof(upabbr));
+ for (p = (unsigned char *) upabbr; *p; p++)
+ *p = pg_toupper(*p);
+
+ /* Look up the abbrev's meaning in this zone */
+ if (pg_timezone_abbrev_is_known(upabbr,
+ isfixed,
+ &gmtoff,
+ isdst,
+ tzp))
+ {
+ /* Change sign to agree with DetermineTimeZoneOffset() */
+ *offset = (int) -gmtoff;
+ return true;
+ }
+ return false;
+}
+
+
/* DecodeTimeOnly()
* Interpret parsed string as time fields only.
* Returns 0 if successful, DTERR code if bogus input detected.
@@ -3092,27 +3136,60 @@ DecodeTimezoneAbbrev(int field, const char *lowtoken,
int *ftype, int *offset, pg_tz **tz,
DateTimeErrorExtra *extra)
{
+ TzAbbrevCache *tzc = &tzabbrevcache[field];
+ bool isfixed;
+ int isdst;
const datetkn *tp;
- tp = abbrevcache[field];
- /* use strncmp so that we match truncated tokens */
- if (tp == NULL || strncmp(lowtoken, tp->token, TOKMAXLEN) != 0)
+ /*
+ * Do we have a cached result? Use strncmp so that we match truncated
+ * names, although we shouldn't really see that happen with normal
+ * abbreviations.
+ */
+ if (strncmp(lowtoken, tzc->abbrev, TOKMAXLEN) == 0)
{
- if (zoneabbrevtbl)
- tp = datebsearch(lowtoken, zoneabbrevtbl->abbrevs,
- zoneabbrevtbl->numabbrevs);
- else
- tp = NULL;
+ *ftype = tzc->ftype;
+ *offset = tzc->offset;
+ *tz = tzc->tz;
+ return 0;
+ }
+
+ /*
+ * See if the current session_timezone recognizes it. Checking this
+ * before zoneabbrevtbl allows us to correctly handle abbreviations whose
+ * meaning varies across zones, such as "LMT".
+ */
+ if (session_timezone &&
+ TimeZoneAbbrevIsKnown(lowtoken, session_timezone,
+ &isfixed, offset, &isdst))
+ {
+ *ftype = (isfixed ? (isdst ? DTZ : TZ) : DYNTZ);
+ *tz = (isfixed ? NULL : session_timezone);
+ /* flip sign to agree with the convention used in zoneabbrevtbl */
+ *offset = -(*offset);
+ /* cache result; use strlcpy to truncate name if necessary */
+ strlcpy(tzc->abbrev, lowtoken, TOKMAXLEN + 1);
+ tzc->ftype = *ftype;
+ tzc->offset = *offset;
+ tzc->tz = *tz;
+ return 0;
}
+
+ /* Nope, so look in zoneabbrevtbl */
+ if (zoneabbrevtbl)
+ tp = datebsearch(lowtoken, zoneabbrevtbl->abbrevs,
+ zoneabbrevtbl->numabbrevs);
+ else
+ tp = NULL;
if (tp == NULL)
{
*ftype = UNKNOWN_FIELD;
*offset = 0;
*tz = NULL;
+ /* failure results are not cached */
}
else
{
- abbrevcache[field] = tp;
*ftype = tp->type;
if (tp->type == DYNTZ)
{
@@ -3126,11 +3203,26 @@ DecodeTimezoneAbbrev(int field, const char *lowtoken,
*offset = tp->value;
*tz = NULL;
}
+
+ /* cache result; use strlcpy to truncate name if necessary */
+ strlcpy(tzc->abbrev, lowtoken, TOKMAXLEN + 1);
+ tzc->ftype = *ftype;
+ tzc->offset = *offset;
+ tzc->tz = *tz;
}
return 0;
}
+/*
+ * Reset tzabbrevcache after a change in session_timezone.
+ */
+void
+ClearTimeZoneAbbrevCache(void)
+{
+ memset(tzabbrevcache, 0, sizeof(tzabbrevcache));
+}
+
/* DecodeSpecial()
* Decode text string using lookup table.
@@ -3278,9 +3370,6 @@ DecodeTimezoneAbbrevPrefix(const char *str, int *offset, pg_tz **tz)
*offset = 0; /* avoid uninitialized vars on failure */
*tz = NULL;
- if (!zoneabbrevtbl)
- return -1; /* no abbrevs known, so fail immediately */
-
/* Downcase as much of the string as we could need */
for (len = 0; len < TOKMAXLEN; len++)
{
@@ -3299,9 +3388,34 @@ DecodeTimezoneAbbrevPrefix(const char *str, int *offset, pg_tz **tz)
*/
while (len > 0)
{
- const datetkn *tp = datebsearch(lowtoken, zoneabbrevtbl->abbrevs,
- zoneabbrevtbl->numabbrevs);
+ bool isfixed;
+ int isdst;
+ const datetkn *tp;
+
+ /* See if the current session_timezone recognizes it. */
+ if (session_timezone &&
+ TimeZoneAbbrevIsKnown(lowtoken, session_timezone,
+ &isfixed, offset, &isdst))
+ {
+ if (isfixed)
+ {
+ /* flip sign to agree with the convention in zoneabbrevtbl */
+ *offset = -(*offset);
+ }
+ else
+ {
+ /* Caller must resolve the abbrev's current meaning */
+ *tz = session_timezone;
+ }
+ return len;
+ }
+ /* Known in zoneabbrevtbl? */
+ if (zoneabbrevtbl)
+ tp = datebsearch(lowtoken, zoneabbrevtbl->abbrevs,
+ zoneabbrevtbl->numabbrevs);
+ else
+ tp = NULL;
if (tp != NULL)
{
if (tp->type == DYNTZ)
@@ -3324,6 +3438,8 @@ DecodeTimezoneAbbrevPrefix(const char *str, int *offset, pg_tz **tz)
return len;
}
}
+
+ /* Nope, try the next shorter string. */
lowtoken[--len] = '\0';
}
@@ -4957,8 +5073,8 @@ void
InstallTimeZoneAbbrevs(TimeZoneAbbrevTable *tbl)
{
zoneabbrevtbl = tbl;
- /* reset abbrevcache, which may contain pointers into old table */
- memset(abbrevcache, 0, sizeof(abbrevcache));
+ /* reset tzabbrevcache, which may contain results from old table */
+ memset(tzabbrevcache, 0, sizeof(tzabbrevcache));
}
/*
@@ -4994,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_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(PG_FUNCTION_ARGS)
+pg_timezone_abbrevs_abbrevs(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
int *pindex;
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index e5446845614..54856ab214d 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202501161
+#define CATALOG_VERSION_NO 202501162
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ba02ba53b29..18560755d26 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 37171f17374..5fc9f223de3 100644
--- a/src/include/pgtime.h
+++ b/src/include/pgtime.h
@@ -69,6 +69,13 @@ extern bool pg_interpret_timezone_abbrev(const char *abbrev,
long int *gmtoff,
int *isdst,
const pg_tz *tz);
+extern bool pg_timezone_abbrev_is_known(const char *abbrev,
+ bool *isfixed,
+ 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/include/utils/datetime.h b/src/include/utils/datetime.h
index 7fe12a4ea70..53a1c69eda5 100644
--- a/src/include/utils/datetime.h
+++ b/src/include/utils/datetime.h
@@ -351,6 +351,8 @@ extern pg_tz *DecodeTimezoneNameToTz(const char *tzname);
extern int DecodeTimezoneAbbrevPrefix(const char *str,
int *offset, pg_tz **tz);
+extern void ClearTimeZoneAbbrevCache(void);
+
extern int j2day(int date);
extern struct Node *TemporalSimplify(int32 max_precis, struct Node *node);
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index cb28dfbaee7..b90bfcd794f 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3332,6 +3332,12 @@ SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ'); -- dyntz
Sat Dec 17 23:38:00 2011 PST
(1 row)
+SELECT to_timestamp('2011-12-18 00:00 LMT', 'YYYY-MM-DD HH24:MI TZ'); -- dyntz
+ to_timestamp
+------------------------------
+ Sat Dec 17 23:52:58 2011 PST
+(1 row)
+
SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
to_timestamp
------------------------------
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index ff921bbda00..856a8349c50 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2629,10 +2629,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 91089ac215f..352abc0bd42 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/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out
index a6dd45626ce..36349e363f2 100644
--- a/src/test/regress/expected/timestamptz.out
+++ b/src/test/regress/expected/timestamptz.out
@@ -176,6 +176,65 @@ SELECT '205000-01-10 17:32:01 Europe/Helsinki'::timestamptz; -- non-DST
Fri Jan 10 07:32:01 205000 PST
(1 row)
+-- Recognize "LMT" as whatever it means in the current zone
+SELECT 'Jan 01 00:00:00 1000 LMT'::timestamptz;
+ timestamptz
+------------------------------
+ Wed Jan 01 00:00:00 1000 LMT
+(1 row)
+
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz;
+ timestamptz
+------------------------------
+ Sun Dec 31 23:52:58 2023 PST
+(1 row)
+
+SET timezone = 'Europe/London';
+SELECT 'Jan 01 00:00:00 1000 LMT'::timestamptz;
+ timestamptz
+------------------------------
+ Wed Jan 01 00:00:00 1000 LMT
+(1 row)
+
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz;
+ timestamptz
+------------------------------
+ Mon Jan 01 00:01:15 2024 GMT
+(1 row)
+
+-- which might be nothing
+SET timezone = 'UTC';
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz; -- fail
+ERROR: invalid input syntax for type timestamp with time zone: "Jan 01 00:00:00 2024 LMT"
+LINE 1: SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz;
+ ^
+-- Another example of an abbrev that varies across zones
+SELECT '1912-01-01 00:00 MMT'::timestamptz; -- from timezone_abbreviations
+ timestamptz
+------------------------------
+ Sun Dec 31 17:30:00 1911 UTC
+(1 row)
+
+SET timezone = 'America/Montevideo';
+SELECT '1912-01-01 00:00'::timestamptz;
+ timestamptz
+------------------------------
+ Mon Jan 01 00:00:00 1912 MMT
+(1 row)
+
+SELECT '1912-01-01 00:00 MMT'::timestamptz;
+ timestamptz
+------------------------------
+ Mon Jan 01 00:00:00 1912 MMT
+(1 row)
+
+SELECT '1912-01-01 00:00 MMT'::timestamptz AT TIME ZONE 'UTC';
+ timezone
+--------------------------
+ Mon Jan 01 03:44:51 1912
+(1 row)
+
+RESET timezone;
-- Test non-error-throwing API
SELECT pg_input_is_valid('now', 'timestamptz');
pg_input_is_valid
diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql
index 4aa88b4ba9a..1310b432773 100644
--- a/src/test/regress/sql/horology.sql
+++ b/src/test/regress/sql/horology.sql
@@ -538,6 +538,7 @@ SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ'); -- dyntz
+SELECT to_timestamp('2011-12-18 00:00 LMT', 'YYYY-MM-DD HH24:MI TZ'); -- dyntz
SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ'); -- error
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index b2a79237543..66179f026b3 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/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql
index a92586c363e..2fa5378a572 100644
--- a/src/test/regress/sql/timestamptz.sql
+++ b/src/test/regress/sql/timestamptz.sql
@@ -109,6 +109,23 @@ SELECT '20500110 173201 Europe/Helsinki'::timestamptz; -- non-DST
SELECT '205000-07-10 17:32:01 Europe/Helsinki'::timestamptz; -- DST
SELECT '205000-01-10 17:32:01 Europe/Helsinki'::timestamptz; -- non-DST
+-- Recognize "LMT" as whatever it means in the current zone
+SELECT 'Jan 01 00:00:00 1000 LMT'::timestamptz;
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz;
+SET timezone = 'Europe/London';
+SELECT 'Jan 01 00:00:00 1000 LMT'::timestamptz;
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz;
+-- which might be nothing
+SET timezone = 'UTC';
+SELECT 'Jan 01 00:00:00 2024 LMT'::timestamptz; -- fail
+-- Another example of an abbrev that varies across zones
+SELECT '1912-01-01 00:00 MMT'::timestamptz; -- from timezone_abbreviations
+SET timezone = 'America/Montevideo';
+SELECT '1912-01-01 00:00'::timestamptz;
+SELECT '1912-01-01 00:00 MMT'::timestamptz;
+SELECT '1912-01-01 00:00 MMT'::timestamptz AT TIME ZONE 'UTC';
+RESET timezone;
+
-- Test non-error-throwing API
SELECT pg_input_is_valid('now', 'timestamptz');
SELECT pg_input_is_valid('garbage', 'timestamptz');
diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c
index 21516c65082..8eb02ef1469 100644
--- a/src/timezone/localtime.c
+++ b/src/timezone/localtime.c
@@ -1844,6 +1844,120 @@ pg_interpret_timezone_abbrev(const char *abbrev,
}
/*
+ * Detect whether a timezone abbreviation is defined within the given zone.
+ *
+ * This is similar to pg_interpret_timezone_abbrev() but is not concerned
+ * with a specific point in time. We want to know if the abbreviation is
+ * known at all, and if so whether it has one meaning or several.
+ *
+ * Returns true if the abbreviation is known, false if not.
+ * If the abbreviation is known and has a single meaning (only one value
+ * of gmtoff/isdst), sets *isfixed = true and sets *gmtoff and *isdst.
+ * If there are multiple meanings, sets *isfixed = false.
+ *
+ * Note: abbrev is matched case-sensitively; it should be all-upper-case.
+ */
+bool
+pg_timezone_abbrev_is_known(const char *abbrev,
+ bool *isfixed,
+ long int *gmtoff,
+ int *isdst,
+ const pg_tz *tz)
+{
+ bool result = false;
+ const struct state *sp = &tz->state;
+ const char *abbrs;
+ int abbrind;
+
+ /*
+ * Locate the abbreviation in the zone's abbreviation list. We assume
+ * there are not duplicates in the list.
+ */
+ abbrs = sp->chars;
+ abbrind = 0;
+ while (abbrind < sp->charcnt)
+ {
+ if (strcmp(abbrev, abbrs + abbrind) == 0)
+ break;
+ while (abbrs[abbrind] != '\0')
+ abbrind++;
+ abbrind++;
+ }
+ if (abbrind >= sp->charcnt)
+ return false; /* definitely not there */
+
+ /*
+ * Scan the ttinfo array to find uses of the abbreviation.
+ */
+ for (int i = 0; i < sp->typecnt; i++)
+ {
+ const struct ttinfo *ttisp = &sp->ttis[i];
+
+ if (ttisp->tt_desigidx == abbrind)
+ {
+ if (!result)
+ {
+ /* First usage */
+ *isfixed = true; /* for the moment */
+ *gmtoff = ttisp->tt_utoff;
+ *isdst = ttisp->tt_isdst;
+ result = true;
+ }
+ else
+ {
+ /* Second or later usage, does it match? */
+ if (*gmtoff != ttisp->tt_utoff ||
+ *isdst != ttisp->tt_isdst)
+ {
+ *isfixed = false;
+ break; /* no point in looking further */
+ }
+ }
+ }
+ }
+
+ 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.
*/
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56ba63f3d92..ebba5b7c953 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3036,6 +3036,7 @@ TypeCat
TypeFuncClass
TypeInfo
TypeName
+TzAbbrevCache
U32
U8
UChar