diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/catalog/system_views.sql | 7 | ||||
-rw-r--r-- | src/backend/commands/variable.c | 2 | ||||
-rw-r--r-- | src/backend/utils/adt/datetime.c | 242 | ||||
-rw-r--r-- | src/include/catalog/catversion.h | 2 | ||||
-rw-r--r-- | src/include/catalog/pg_proc.dat | 12 | ||||
-rw-r--r-- | src/include/pgtime.h | 7 | ||||
-rw-r--r-- | src/include/utils/datetime.h | 2 | ||||
-rw-r--r-- | src/test/regress/expected/horology.out | 6 | ||||
-rw-r--r-- | src/test/regress/expected/rules.out | 17 | ||||
-rw-r--r-- | src/test/regress/expected/sysviews.out | 8 | ||||
-rw-r--r-- | src/test/regress/expected/timestamptz.out | 59 | ||||
-rw-r--r-- | src/test/regress/sql/horology.sql | 1 | ||||
-rw-r--r-- | src/test/regress/sql/sysviews.sql | 3 | ||||
-rw-r--r-- | src/test/regress/sql/timestamptz.sql | 17 | ||||
-rw-r--r-- | src/timezone/localtime.c | 114 | ||||
-rw-r--r-- | src/tools/pgindent/typedefs.list | 1 |
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 |