Refactor datetime functions' timezone lookup code to reduce duplication.
authorTom Lane <[email protected]>
Fri, 17 Mar 2023 21:47:15 +0000 (17:47 -0400)
committerTom Lane <[email protected]>
Fri, 17 Mar 2023 21:47:19 +0000 (17:47 -0400)
We already had five copies of essentially the same logic, and an
upcoming patch introduces yet another use-case.  That's past my
threshold of pain, so introduce a common subroutine.  There's not
that much net code savings, but the chance of typos should go down.

Inspired by a patch from Przemysław Sztoch, but different in detail.

Discussion: https://postgr.es/m/01a84551-48dd-1359-bf7e-f6b0203a6bd0@sztoch.pl

src/backend/utils/adt/date.c
src/backend/utils/adt/datetime.c
src/backend/utils/adt/timestamp.c
src/include/utils/datetime.h

index 99171d9c928c43a2593295c6432a945d0fb40abd..a163fbb4ab5deb8b5cae62853a94f83fb674826a 100644 (file)
@@ -3052,38 +3052,23 @@ timetz_zone(PG_FUNCTION_ARGS)
        TimeTzADT  *result;
        int                     tz;
        char            tzname[TZ_STRLEN_MAX + 1];
-       char       *lowzone;
-       int                     dterr,
-                               type,
+       int                     type,
                                val;
        pg_tz      *tzp;
-       DateTimeErrorExtra extra;
 
        /*
-        * Look up the requested timezone.  First we look in the timezone
-        * abbreviation table (to handle cases like "EST"), and if that fails, we
-        * look in the timezone database (to handle cases like
-        * "America/New_York").  (This matches the order in which timestamp input
-        * checks the cases; it's important because the timezone database unwisely
-        * uses a few zone names that are identical to offset abbreviations.)
+        * Look up the requested timezone.
         */
        text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-       /* DecodeTimezoneAbbrev requires lowercase input */
-       lowzone = downcase_truncate_identifier(tzname,
-                                                                                  strlen(tzname),
-                                                                                  false);
+       type = DecodeTimezoneName(tzname, &val, &tzp);
 
-       dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
-       if (dterr)
-               DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
-
-       if (type == TZ || type == DTZ)
+       if (type == TZNAME_FIXED_OFFSET)
        {
                /* fixed-offset abbreviation */
                tz = -val;
        }
-       else if (type == DYNTZ)
+       else if (type == TZNAME_DYNTZ)
        {
                /* dynamic-offset abbreviation, resolve using transaction start time */
                TimestampTz now = GetCurrentTransactionStartTimestamp();
@@ -3093,27 +3078,15 @@ timetz_zone(PG_FUNCTION_ARGS)
        }
        else
        {
-               /* try it as a full zone name */
-               tzp = pg_tzset(tzname);
-               if (tzp)
-               {
-                       /* Get the offset-from-GMT that is valid now for the zone */
-                       TimestampTz now = GetCurrentTransactionStartTimestamp();
-                       struct pg_tm tm;
-                       fsec_t          fsec;
+               /* Get the offset-from-GMT that is valid now for the zone name */
+               TimestampTz now = GetCurrentTransactionStartTimestamp();
+               struct pg_tm tm;
+               fsec_t          fsec;
 
-                       if (timestamp2tm(now, &tz, &tm, &fsec, NULL, tzp) != 0)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                                errmsg("timestamp out of range")));
-               }
-               else
-               {
+               if (timestamp2tm(now, &tz, &tm, &fsec, NULL, tzp) != 0)
                        ereport(ERROR,
-                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                        errmsg("time zone \"%s\" not recognized", tzname)));
-                       tz = 0;                         /* keep compiler quiet */
-               }
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
        }
 
        result = (TimeTzADT *) palloc(sizeof(TimeTzADT));
index 516ee9c154b25de74125800df7f935eab638b8dc..be2e55bb29fa21e8081d022300487564f8b5124a 100644 (file)
@@ -26,6 +26,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "parser/scansup.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/datetime.h"
@@ -3162,6 +3163,90 @@ DecodeSpecial(int field, const char *lowtoken, int *val)
 }
 
 
+/* DecodeTimezoneName()
+ * Interpret string as a timezone abbreviation or name.
+ * Throw error if the name is not recognized.
+ *
+ * The return value indicates what kind of zone identifier it is:
+ *     TZNAME_FIXED_OFFSET: fixed offset from UTC
+ *     TZNAME_DYNTZ: dynamic timezone abbreviation
+ *     TZNAME_ZONE: full tzdb zone name
+ *
+ * For TZNAME_FIXED_OFFSET, *offset receives the UTC offset (in seconds,
+ * with ISO sign convention: positive is east of Greenwich).
+ * For the other two cases, *tz receives the timezone struct representing
+ * the zone name or the abbreviation's underlying zone.
+ */
+int
+DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz)
+{
+       char       *lowzone;
+       int                     dterr,
+                               type;
+       DateTimeErrorExtra extra;
+
+       /*
+        * First we look in the timezone abbreviation table (to handle cases like
+        * "EST"), and if that fails, we look in the timezone database (to handle
+        * cases like "America/New_York").  This matches the order in which
+        * timestamp input checks the cases; it's important because the timezone
+        * database unwisely uses a few zone names that are identical to offset
+        * abbreviations.
+        */
+
+       /* DecodeTimezoneAbbrev requires lowercase input */
+       lowzone = downcase_truncate_identifier(tzname,
+                                                                                  strlen(tzname),
+                                                                                  false);
+
+       dterr = DecodeTimezoneAbbrev(0, lowzone, &type, offset, tz, &extra);
+       if (dterr)
+               DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
+
+       if (type == TZ || type == DTZ)
+       {
+               /* fixed-offset abbreviation, return the offset */
+               return TZNAME_FIXED_OFFSET;
+       }
+       else if (type == DYNTZ)
+       {
+               /* dynamic-offset abbreviation, return its referenced timezone */
+               return TZNAME_DYNTZ;
+       }
+       else
+       {
+               /* try it as a full zone name */
+               *tz = pg_tzset(tzname);
+               if (*tz == NULL)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                        errmsg("time zone \"%s\" not recognized", tzname)));
+               return TZNAME_ZONE;
+       }
+}
+
+/* DecodeTimezoneNameToTz()
+ * Interpret string as a timezone abbreviation or name.
+ * Throw error if the name is not recognized.
+ *
+ * This is a simple wrapper for DecodeTimezoneName that produces a pg_tz *
+ * result in all cases.
+ */
+pg_tz *
+DecodeTimezoneNameToTz(const char *tzname)
+{
+       pg_tz      *result;
+       int                     offset;
+
+       if (DecodeTimezoneName(tzname, &offset, &result) == TZNAME_FIXED_OFFSET)
+       {
+               /* fixed-offset abbreviation, get a pg_tz descriptor for that */
+               result = pg_tzset_offset(-offset);      /* flip to POSIX sign convention */
+       }
+       return result;
+}
+
+
 /* ClearPgItmIn
  *
  * Zero out a pg_itm_in
index de93db89d4838986a9748ee381f16a72d8acf121..c266d0d02edb031c68aa05a7f36b653c1d5da2be 100644 (file)
@@ -479,12 +479,7 @@ parse_sane_timezone(struct pg_tm *tm, text *zone)
        /*
         * Look up the requested timezone.  First we try to interpret it as a
         * numeric timezone specification; if DecodeTimezone decides it doesn't
-        * like the format, we look in the timezone abbreviation table (to handle
-        * cases like "EST"), and if that also fails, we look in the timezone
-        * database (to handle cases like "America/New_York").  (This matches the
-        * order in which timestamp input checks the cases; it's important because
-        * the timezone database unwisely uses a few zone names that are identical
-        * to offset abbreviations.)
+        * like the format, we try timezone abbreviations and names.
         *
         * Note pg_tzset happily parses numeric input that DecodeTimezone would
         * reject.  To avoid having it accept input that would otherwise be seen
@@ -501,11 +496,9 @@ parse_sane_timezone(struct pg_tm *tm, text *zone)
        dterr = DecodeTimezone(tzname, &tz);
        if (dterr != 0)
        {
-               char       *lowzone;
                int                     type,
                                        val;
                pg_tz      *tzp;
-               DateTimeErrorExtra extra;
 
                if (dterr == DTERR_TZDISP_OVERFLOW)
                        ereport(ERROR,
@@ -516,34 +509,22 @@ parse_sane_timezone(struct pg_tm *tm, text *zone)
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("time zone \"%s\" not recognized", tzname)));
 
-               /* DecodeTimezoneAbbrev requires lowercase input */
-               lowzone = downcase_truncate_identifier(tzname,
-                                                                                          strlen(tzname),
-                                                                                          false);
-               dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
-               if (dterr)
-                       DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
+               type = DecodeTimezoneName(tzname, &val, &tzp);
 
-               if (type == TZ || type == DTZ)
+               if (type == TZNAME_FIXED_OFFSET)
                {
                        /* fixed-offset abbreviation */
                        tz = -val;
                }
-               else if (type == DYNTZ)
+               else if (type == TZNAME_DYNTZ)
                {
                        /* dynamic-offset abbreviation, resolve using specified time */
                        tz = DetermineTimeZoneAbbrevOffset(tm, tzname, tzp);
                }
                else
                {
-                       /* try it as a full zone name */
-                       tzp = pg_tzset(tzname);
-                       if (tzp)
-                               tz = DetermineTimeZoneOffset(tm, tzp);
-                       else
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                                errmsg("time zone \"%s\" not recognized", tzname)));
+                       /* full zone name */
+                       tz = DetermineTimeZoneOffset(tm, tzp);
                }
        }
 
@@ -4304,12 +4285,7 @@ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
        text       *zone = PG_GETARG_TEXT_PP(2);
        TimestampTz result;
        char            tzname[TZ_STRLEN_MAX + 1];
-       char       *lowzone;
-       int                     dterr,
-                               type,
-                               val;
        pg_tz      *tzp;
-       DateTimeErrorExtra extra;
 
        /*
         * timestamptz_zone() doesn't look up the zone for infinite inputs, so we
@@ -4319,37 +4295,11 @@ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
                PG_RETURN_TIMESTAMP(timestamp);
 
        /*
-        * Look up the requested timezone (see notes in timestamptz_zone()).
+        * Look up the requested timezone.
         */
        text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-       /* DecodeTimezoneAbbrev requires lowercase input */
-       lowzone = downcase_truncate_identifier(tzname,
-                                                                                  strlen(tzname),
-                                                                                  false);
-
-       dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
-       if (dterr)
-               DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
-
-       if (type == TZ || type == DTZ)
-       {
-               /* fixed-offset abbreviation, get a pg_tz descriptor for that */
-               tzp = pg_tzset_offset(-val);
-       }
-       else if (type == DYNTZ)
-       {
-               /* dynamic-offset abbreviation, use its referenced timezone */
-       }
-       else
-       {
-               /* try it as a full zone name */
-               tzp = pg_tzset(tzname);
-               if (!tzp)
-                       ereport(ERROR,
-                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                        errmsg("time zone \"%s\" not recognized", tzname)));
-       }
+       tzp = DecodeTimezoneNameToTz(tzname);
 
        result = timestamptz_trunc_internal(units, timestamp, tzp);
 
@@ -5429,12 +5379,9 @@ timestamp_zone(PG_FUNCTION_ARGS)
        TimestampTz result;
        int                     tz;
        char            tzname[TZ_STRLEN_MAX + 1];
-       char       *lowzone;
-       int                     dterr,
-                               type,
+       int                     type,
                                val;
        pg_tz      *tzp;
-       DateTimeErrorExtra extra;
        struct pg_tm tm;
        fsec_t          fsec;
 
@@ -5442,31 +5389,19 @@ timestamp_zone(PG_FUNCTION_ARGS)
                PG_RETURN_TIMESTAMPTZ(timestamp);
 
        /*
-        * Look up the requested timezone.  First we look in the timezone
-        * abbreviation table (to handle cases like "EST"), and if that fails, we
-        * look in the timezone database (to handle cases like
-        * "America/New_York").  (This matches the order in which timestamp input
-        * checks the cases; it's important because the timezone database unwisely
-        * uses a few zone names that are identical to offset abbreviations.)
+        * Look up the requested timezone.
         */
        text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-       /* DecodeTimezoneAbbrev requires lowercase input */
-       lowzone = downcase_truncate_identifier(tzname,
-                                                                                  strlen(tzname),
-                                                                                  false);
+       type = DecodeTimezoneName(tzname, &val, &tzp);
 
-       dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
-       if (dterr)
-               DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
-
-       if (type == TZ || type == DTZ)
+       if (type == TZNAME_FIXED_OFFSET)
        {
                /* fixed-offset abbreviation */
                tz = val;
                result = dt2local(timestamp, tz);
        }
-       else if (type == DYNTZ)
+       else if (type == TZNAME_DYNTZ)
        {
                /* dynamic-offset abbreviation, resolve using specified time */
                if (timestamp2tm(timestamp, NULL, &tm, &fsec, NULL, tzp) != 0)
@@ -5478,28 +5413,16 @@ timestamp_zone(PG_FUNCTION_ARGS)
        }
        else
        {
-               /* try it as a full zone name */
-               tzp = pg_tzset(tzname);
-               if (tzp)
-               {
-                       /* Apply the timezone change */
-                       if (timestamp2tm(timestamp, NULL, &tm, &fsec, NULL, tzp) != 0)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                                errmsg("timestamp out of range")));
-                       tz = DetermineTimeZoneOffset(&tm, tzp);
-                       if (tm2timestamp(&tm, fsec, &tz, &result) != 0)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                                errmsg("timestamp out of range")));
-               }
-               else
-               {
+               /* full zone name, rotate to that zone */
+               if (timestamp2tm(timestamp, NULL, &tm, &fsec, NULL, tzp) != 0)
                        ereport(ERROR,
-                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                        errmsg("time zone \"%s\" not recognized", tzname)));
-                       result = 0;                     /* keep compiler quiet */
-               }
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+               tz = DetermineTimeZoneOffset(&tm, tzp);
+               if (tm2timestamp(&tm, fsec, &tz, &result) != 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
        }
 
        if (!IS_VALID_TIMESTAMP(result))
@@ -5687,42 +5610,27 @@ timestamptz_zone(PG_FUNCTION_ARGS)
        Timestamp       result;
        int                     tz;
        char            tzname[TZ_STRLEN_MAX + 1];
-       char       *lowzone;
-       int                     dterr,
-                               type,
+       int                     type,
                                val;
        pg_tz      *tzp;
-       DateTimeErrorExtra extra;
 
        if (TIMESTAMP_NOT_FINITE(timestamp))
                PG_RETURN_TIMESTAMP(timestamp);
 
        /*
-        * Look up the requested timezone.  First we look in the timezone
-        * abbreviation table (to handle cases like "EST"), and if that fails, we
-        * look in the timezone database (to handle cases like
-        * "America/New_York").  (This matches the order in which timestamp input
-        * checks the cases; it's important because the timezone database unwisely
-        * uses a few zone names that are identical to offset abbreviations.)
+        * Look up the requested timezone.
         */
        text_to_cstring_buffer(zone, tzname, sizeof(tzname));
 
-       /* DecodeTimezoneAbbrev requires lowercase input */
-       lowzone = downcase_truncate_identifier(tzname,
-                                                                                  strlen(tzname),
-                                                                                  false);
-
-       dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
-       if (dterr)
-               DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
+       type = DecodeTimezoneName(tzname, &val, &tzp);
 
-       if (type == TZ || type == DTZ)
+       if (type == TZNAME_FIXED_OFFSET)
        {
                /* fixed-offset abbreviation */
                tz = -val;
                result = dt2local(timestamp, tz);
        }
-       else if (type == DYNTZ)
+       else if (type == TZNAME_DYNTZ)
        {
                /* dynamic-offset abbreviation, resolve using specified time */
                int                     isdst;
@@ -5732,30 +5640,18 @@ timestamptz_zone(PG_FUNCTION_ARGS)
        }
        else
        {
-               /* try it as a full zone name */
-               tzp = pg_tzset(tzname);
-               if (tzp)
-               {
-                       /* Apply the timezone change */
-                       struct pg_tm tm;
-                       fsec_t          fsec;
+               /* full zone name, rotate from that zone */
+               struct pg_tm tm;
+               fsec_t          fsec;
 
-                       if (timestamp2tm(timestamp, &tz, &tm, &fsec, NULL, tzp) != 0)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                                errmsg("timestamp out of range")));
-                       if (tm2timestamp(&tm, fsec, NULL, &result) != 0)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                                errmsg("timestamp out of range")));
-               }
-               else
-               {
+               if (timestamp2tm(timestamp, &tz, &tm, &fsec, NULL, tzp) != 0)
                        ereport(ERROR,
-                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                        errmsg("time zone \"%s\" not recognized", tzname)));
-                       result = 0;                     /* keep compiler quiet */
-               }
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+               if (tm2timestamp(&tm, fsec, NULL, &result) != 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
        }
 
        if (!IS_VALID_TIMESTAMP(result))
index c8f78a9f1ec1ff5a01ac90469df643060e69071f..a871e3223de661bab3557ac5725d43c69a6370a9 100644 (file)
@@ -295,6 +295,11 @@ typedef struct DateTimeErrorExtra
        const char *dtee_abbrev;        /* relevant time zone abbreviation */
 } DateTimeErrorExtra;
 
+/* Result codes for DecodeTimezoneName() */
+#define TZNAME_FIXED_OFFSET    0
+#define TZNAME_DYNTZ           1
+#define TZNAME_ZONE                    2
+
 
 extern void GetCurrentDateTime(struct pg_tm *tm);
 extern void GetCurrentTimeUsec(struct pg_tm *tm, fsec_t *fsec, int *tzp);
@@ -340,6 +345,9 @@ extern int  DecodeTimezoneAbbrev(int field, const char *lowtoken,
 extern int     DecodeSpecial(int field, const char *lowtoken, int *val);
 extern int     DecodeUnits(int field, const char *lowtoken, int *val);
 
+extern int     DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz);
+extern pg_tz *DecodeTimezoneNameToTz(const char *tzname);
+
 extern int     j2day(int date);
 
 extern struct Node *TemporalSimplify(int32 max_precis, struct Node *node);