From 3e59e5048d0f20debe4ad79b2e02ca2a76c5daed Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 17 Mar 2023 17:47:15 -0400 Subject: [PATCH] Refactor datetime functions' timezone lookup code to reduce duplication. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 | 51 ++------- src/backend/utils/adt/datetime.c | 85 ++++++++++++++ src/backend/utils/adt/timestamp.c | 178 +++++++----------------------- src/include/utils/datetime.h | 8 ++ 4 files changed, 142 insertions(+), 180 deletions(-) diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c index 99171d9c92..a163fbb4ab 100644 --- a/src/backend/utils/adt/date.c +++ b/src/backend/utils/adt/date.c @@ -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)); diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 516ee9c154..be2e55bb29 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -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 diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index de93db89d4..c266d0d02e 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -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)) diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h index c8f78a9f1e..a871e3223d 100644 --- a/src/include/utils/datetime.h +++ b/src/include/utils/datetime.h @@ -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); -- 2.30.2