]> granicus.if.org Git - postgresql/commitdiff
Be more careful about out-of-range dates and timestamps.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 16 Mar 2016 23:09:04 +0000 (19:09 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 16 Mar 2016 23:09:28 +0000 (19:09 -0400)
Tighten the semantics of boundary-case timestamptz so that we allow
timestamps >= '4714-11-24 00:00+00 BC' and < 'ENDYEAR-01-01 00:00+00 AD'
exactly, no more and no less, but it is allowed to enter timestamps
within that range using non-GMT timezone offsets (which could make the
nominal date 4714-11-23 BC or ENDYEAR-01-01 AD).  This eliminates
dump/reload failure conditions for timestamps near the endpoints.
To do this, separate checking of the inputs for date2j() from the
final range check, and allow the Julian date code to handle a range
slightly wider than the nominal range of the datatypes.

Also add a bunch of checks to detect out-of-range dates and timestamps
that formerly could be returned by operations such as date-plus-integer.
All C-level functions that return date, timestamp, or timestamptz should
now be proof against returning a value that doesn't pass IS_VALID_DATE()
or IS_VALID_TIMESTAMP().

Vitaly Burovoy, reviewed by Anastasia Lubennikova, and substantially
whacked around by me

13 files changed:
src/backend/utils/adt/date.c
src/backend/utils/adt/datetime.c
src/backend/utils/adt/formatting.c
src/backend/utils/adt/timestamp.c
src/include/datatype/timestamp.h
src/interfaces/ecpg/pgtypeslib/dt.h
src/interfaces/ecpg/pgtypeslib/timestamp.c
src/test/regress/expected/date.out
src/test/regress/expected/timestamp.out
src/test/regress/expected/timestamptz.out
src/test/regress/sql/date.sql
src/test/regress/sql/timestamp.sql
src/test/regress/sql/timestamptz.sql

index 332db7e9c006e3ebc298ad22cb0ec0f3051f6e37..420f383a804cf136d61aa512074b1b8e12900508 100644 (file)
@@ -160,6 +160,7 @@ date_in(PG_FUNCTION_ARGS)
                        break;
        }
 
+       /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm->tm_year, tm->tm_mon, tm->tm_mday))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
@@ -167,6 +168,12 @@ date_in(PG_FUNCTION_ARGS)
 
        date = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;
 
+       /* Now check for just-out-of-range dates */
+       if (!IS_VALID_DATE(date))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("date out of range: \"%s\"", str)));
+
        PG_RETURN_DATEADT(date);
 }
 
@@ -209,8 +216,7 @@ date_recv(PG_FUNCTION_ARGS)
        /* Limit to the same range that date_in() accepts. */
        if (DATE_NOT_FINITE(result))
                 /* ok */ ;
-       else if (result < -POSTGRES_EPOCH_JDATE ||
-                        result >= JULIAN_MAX - POSTGRES_EPOCH_JDATE)
+       else if (!IS_VALID_DATE(result))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                                 errmsg("date out of range")));
@@ -258,6 +264,7 @@ make_date(PG_FUNCTION_ARGS)
                                 errmsg("date field value out of range: %d-%02d-%02d",
                                                tm.tm_year, tm.tm_mon, tm.tm_mday)));
 
+       /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm.tm_year, tm.tm_mon, tm.tm_mday))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
@@ -266,6 +273,13 @@ make_date(PG_FUNCTION_ARGS)
 
        date = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE;
 
+       /* Now check for just-out-of-range dates */
+       if (!IS_VALID_DATE(date))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("date out of range: %d-%02d-%02d",
+                                               tm.tm_year, tm.tm_mon, tm.tm_mday)));
+
        PG_RETURN_DATEADT(date);
 }
 
@@ -427,11 +441,21 @@ date_pli(PG_FUNCTION_ARGS)
 {
        DateADT         dateVal = PG_GETARG_DATEADT(0);
        int32           days = PG_GETARG_INT32(1);
+       DateADT         result;
 
        if (DATE_NOT_FINITE(dateVal))
-               days = 0;                               /* can't change infinity */
+               PG_RETURN_DATEADT(dateVal);             /* can't change infinity */
+
+       result = dateVal + days;
+
+       /* Check for integer overflow and out-of-allowed-range */
+       if ((days >= 0 ? (result < dateVal) : (result > dateVal)) ||
+               !IS_VALID_DATE(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("date out of range")));
 
-       PG_RETURN_DATEADT(dateVal + days);
+       PG_RETURN_DATEADT(result);
 }
 
 /* Subtract a number of days from a date, giving a new date.
@@ -441,11 +465,21 @@ date_mii(PG_FUNCTION_ARGS)
 {
        DateADT         dateVal = PG_GETARG_DATEADT(0);
        int32           days = PG_GETARG_INT32(1);
+       DateADT         result;
 
        if (DATE_NOT_FINITE(dateVal))
-               days = 0;                               /* can't change infinity */
+               PG_RETURN_DATEADT(dateVal);             /* can't change infinity */
+
+       result = dateVal - days;
+
+       /* Check for integer overflow and out-of-allowed-range */
+       if ((days >= 0 ? (result > dateVal) : (result < dateVal)) ||
+               !IS_VALID_DATE(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("date out of range")));
 
-       PG_RETURN_DATEADT(dateVal - days);
+       PG_RETURN_DATEADT(result);
 }
 
 /*
@@ -464,14 +498,18 @@ date2timestamp(DateADT dateVal)
                TIMESTAMP_NOEND(result);
        else
        {
-#ifdef HAVE_INT64_TIMESTAMP
-               /* date is days since 2000, timestamp is microseconds since same... */
-               result = dateVal * USECS_PER_DAY;
-               /* Date's range is wider than timestamp's, so check for overflow */
-               if (result / USECS_PER_DAY != dateVal)
+               /*
+                * Date's range is wider than timestamp's, so check for boundaries.
+                * Since dates have the same minimum values as timestamps, only upper
+                * boundary need be checked for overflow.
+                */
+               if (dateVal >= (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE))
                        ereport(ERROR,
                                        (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                                         errmsg("date out of range for timestamp")));
+#ifdef HAVE_INT64_TIMESTAMP
+               /* date is days since 2000, timestamp is microseconds since same... */
+               result = dateVal * USECS_PER_DAY;
 #else
                /* date is days since 2000, timestamp is seconds since same... */
                result = dateVal * (double) SECS_PER_DAY;
@@ -495,6 +533,16 @@ date2timestamptz(DateADT dateVal)
                TIMESTAMP_NOEND(result);
        else
        {
+               /*
+                * Date's range is wider than timestamp's, so check for boundaries.
+                * Since dates have the same minimum values as timestamps, only upper
+                * boundary need be checked for overflow.
+                */
+               if (dateVal >= (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("date out of range for timestamp")));
+
                j2date(dateVal + POSTGRES_EPOCH_JDATE,
                           &(tm->tm_year), &(tm->tm_mon), &(tm->tm_mday));
                tm->tm_hour = 0;
@@ -504,14 +552,18 @@ date2timestamptz(DateADT dateVal)
 
 #ifdef HAVE_INT64_TIMESTAMP
                result = dateVal * USECS_PER_DAY + tz * USECS_PER_SEC;
-               /* Date's range is wider than timestamp's, so check for overflow */
-               if ((result - tz * USECS_PER_SEC) / USECS_PER_DAY != dateVal)
-                       ereport(ERROR,
-                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                        errmsg("date out of range for timestamp")));
 #else
                result = dateVal * (double) SECS_PER_DAY + tz;
 #endif
+
+               /*
+                * Since it is possible to go beyond allowed timestamptz range because
+                * of time zone, check for allowed timestamp range after adding tz.
+                */
+               if (!IS_VALID_TIMESTAMP(result))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("date out of range for timestamp")));
        }
 
        return result;
@@ -1053,7 +1105,17 @@ abstime_date(PG_FUNCTION_ARGS)
 
                default:
                        abstime2tm(abstime, &tz, tm, NULL);
+                       /* Prevent overflow in Julian-day routines */
+                       if (!IS_VALID_JULIAN(tm->tm_year, tm->tm_mon, tm->tm_mday))
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                                errmsg("abstime out of range for date")));
                        result = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;
+                       /* Now check for just-out-of-range dates */
+                       if (!IS_VALID_DATE(result))
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                                errmsg("abstime out of range for date")));
                        break;
        }
 
@@ -1678,7 +1740,13 @@ datetime_timestamp(PG_FUNCTION_ARGS)
 
        result = date2timestamp(date);
        if (!TIMESTAMP_NOT_FINITE(result))
+       {
                result += time;
+               if (!IS_VALID_TIMESTAMP(result))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+       }
 
        PG_RETURN_TIMESTAMP(result);
 }
@@ -2550,11 +2618,29 @@ datetimetz_timestamptz(PG_FUNCTION_ARGS)
                TIMESTAMP_NOEND(result);
        else
        {
+               /*
+                * Date's range is wider than timestamp's, so check for boundaries.
+                * Since dates have the same minimum values as timestamps, only upper
+                * boundary need be checked for overflow.
+                */
+               if (date >= (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("date out of range for timestamp")));
 #ifdef HAVE_INT64_TIMESTAMP
                result = date * USECS_PER_DAY + time->time + time->zone * USECS_PER_SEC;
 #else
                result = date * (double) SECS_PER_DAY + time->time + time->zone;
 #endif
+
+               /*
+                * Since it is possible to go beyond allowed timestamptz range because
+                * of time zone, check for allowed timestamp range after adding tz.
+                */
+               if (!IS_VALID_TIMESTAMP(result))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("date out of range for timestamp")));
        }
 
        PG_RETURN_TIMESTAMP(result);
index cdbf72cf699de80412949e39ffe6d9509ddf3f93..2ea21b7028ab2b48544d4e41908d26b1b7f82ffc 100644 (file)
@@ -280,14 +280,16 @@ strtoi(const char *nptr, char **endptr, int base)
  *     and calendar date for all non-negative Julian days
  *     (i.e. from Nov 24, -4713 on).
  *
- * These routines will be used by other date/time packages
- * - thomas 97/02/25
- *
  * Rewritten to eliminate overflow problems. This now allows the
  * routines to work correctly for all Julian day counts from
  * 0 to 2147483647     (Nov 24, -4713 to Jun 3, 5874898) assuming
  * a 32-bit integer. Longer types should also work to the limits
  * of their precision.
+ *
+ * Actually, date2j() will work sanely, in the sense of producing
+ * valid negative Julian dates, significantly before Nov 24, -4713.
+ * We rely on it to do so back to Nov 1, -4713; see IS_VALID_JULIAN()
+ * and associated commentary in timestamp.h.
  */
 
 int
index 495676431991c43ea0e85cd8029cc735eca15e9d..2b5622a9ee0d0078b0cbf32f39fefb12ee731bf9 100644 (file)
@@ -3525,6 +3525,7 @@ to_date(PG_FUNCTION_ARGS)
 
        do_to_timestamp(date_txt, fmt, &tm, &fsec);
 
+       /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm.tm_year, tm.tm_mon, tm.tm_mday))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
@@ -3533,6 +3534,13 @@ to_date(PG_FUNCTION_ARGS)
 
        result = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE;
 
+       /* Now check for just-out-of-range dates */
+       if (!IS_VALID_DATE(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("date out of range: \"%s\"",
+                                               text_to_cstring(date_txt))));
+
        PG_RETURN_DATEADT(result);
 }
 
index c4f556a3ffa9a2caf122db9e565216a089815f7a..3f013e3f5bb290f2665056e0b0748ac14517521d 100644 (file)
@@ -261,7 +261,8 @@ timestamp_recv(PG_FUNCTION_ARGS)
        /* rangecheck: see if timestamp_out would like it */
        if (TIMESTAMP_NOT_FINITE(timestamp))
                 /* ok */ ;
-       else if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) != 0)
+       else if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) != 0 ||
+                        !IS_VALID_TIMESTAMP(timestamp))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                                 errmsg("timestamp out of range")));
@@ -645,6 +646,14 @@ make_timestamp_internal(int year, int month, int day,
        result = date * SECS_PER_DAY + time;
 #endif
 
+       /* final range check catches just-out-of-range timestamps */
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range: %d-%02d-%02d %d:%02d:%02g",
+                                               year, month, day,
+                                               hour, min, sec)));
+
        return result;
 }
 
@@ -702,6 +711,7 @@ make_timestamptz_at_timezone(PG_FUNCTION_ARGS)
        int32           min = PG_GETARG_INT32(4);
        float8          sec = PG_GETARG_FLOAT8(5);
        text       *zone = PG_GETARG_TEXT_PP(6);
+       TimestampTz result;
        Timestamp       timestamp;
        struct pg_tm tt;
        int                     tz;
@@ -717,7 +727,14 @@ make_timestamptz_at_timezone(PG_FUNCTION_ARGS)
 
        tz = parse_sane_timezone(&tt, zone);
 
-       PG_RETURN_TIMESTAMPTZ((TimestampTz) dt2local(timestamp, -tz));
+       result = dt2local(timestamp, -tz);
+
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range")));
+
+       PG_RETURN_TIMESTAMPTZ(result);
 }
 
 /* timestamptz_out()
@@ -778,7 +795,8 @@ timestamptz_recv(PG_FUNCTION_ARGS)
        /* rangecheck: see if timestamptz_out would like it */
        if (TIMESTAMP_NOT_FINITE(timestamp))
                 /* ok */ ;
-       else if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
+       else if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0 ||
+                        !IS_VALID_TIMESTAMP(timestamp))
                ereport(ERROR,
                                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                                 errmsg("timestamp out of range")));
@@ -1925,7 +1943,7 @@ tm2timestamp(struct pg_tm * tm, fsec_t fsec, int *tzp, Timestamp *result)
        TimeOffset      date;
        TimeOffset      time;
 
-       /* Julian day routines are not correct for negative Julian days */
+       /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm->tm_year, tm->tm_mon, tm->tm_mday))
        {
                *result = 0;                    /* keep compiler quiet */
@@ -1957,6 +1975,13 @@ tm2timestamp(struct pg_tm * tm, fsec_t fsec, int *tzp, Timestamp *result)
        if (tzp != NULL)
                *result = dt2local(*result, -(*tzp));
 
+       /* final range check catches just-out-of-range timestamps */
+       if (!IS_VALID_TIMESTAMP(*result))
+       {
+               *result = 0;                    /* keep compiler quiet */
+               return -1;
+       }
+
        return 0;
 }
 
@@ -2982,6 +3007,12 @@ timestamp_pl_interval(PG_FUNCTION_ARGS)
                }
 
                timestamp += span->time;
+
+               if (!IS_VALID_TIMESTAMP(timestamp))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+
                result = timestamp;
        }
 
@@ -3086,6 +3117,12 @@ timestamptz_pl_interval(PG_FUNCTION_ARGS)
                }
 
                timestamp += span->time;
+
+               if (!IS_VALID_TIMESTAMP(timestamp))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+
                result = timestamp;
        }
 
@@ -4397,6 +4434,7 @@ timestamp_part(PG_FUNCTION_ARGS)
        text       *units = PG_GETARG_TEXT_PP(0);
        Timestamp       timestamp = PG_GETARG_TIMESTAMP(1);
        float8          result;
+       Timestamp       epoch;
        int                     type,
                                val;
        char       *lowunits;
@@ -4575,10 +4613,15 @@ timestamp_part(PG_FUNCTION_ARGS)
                switch (val)
                {
                        case DTK_EPOCH:
+                               epoch = SetEpochTimestamp();
 #ifdef HAVE_INT64_TIMESTAMP
-                               result = (timestamp - SetEpochTimestamp()) / 1000000.0;
+                               /* try to avoid precision loss in subtraction */
+                               if (timestamp < (PG_INT64_MAX + epoch))
+                                       result = (timestamp - epoch) / 1000000.0;
+                               else
+                                       result = ((float8) timestamp - epoch) / 1000000.0;
 #else
-                               result = timestamp - SetEpochTimestamp();
+                               result = timestamp - epoch;
 #endif
                                break;
 
@@ -4611,6 +4654,7 @@ timestamptz_part(PG_FUNCTION_ARGS)
        text       *units = PG_GETARG_TEXT_PP(0);
        TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
        float8          result;
+       Timestamp       epoch;
        int                     tz;
        int                     type,
                                val;
@@ -4792,10 +4836,15 @@ timestamptz_part(PG_FUNCTION_ARGS)
                switch (val)
                {
                        case DTK_EPOCH:
+                               epoch = SetEpochTimestamp();
 #ifdef HAVE_INT64_TIMESTAMP
-                               result = (timestamp - SetEpochTimestamp()) / 1000000.0;
+                               /* try to avoid precision loss in subtraction */
+                               if (timestamp < (PG_INT64_MAX + epoch))
+                                       result = (timestamp - epoch) / 1000000.0;
+                               else
+                                       result = ((float8) timestamp - epoch) / 1000000.0;
 #else
-                               result = timestamp - SetEpochTimestamp();
+                               result = timestamp - epoch;
 #endif
                                break;
 
@@ -5107,9 +5156,8 @@ timestamp_zone(PG_FUNCTION_ARGS)
                        tz = DetermineTimeZoneOffset(&tm, tzp);
                        if (tm2timestamp(&tm, fsec, &tz, &result) != 0)
                                ereport(ERROR,
-                                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                                errmsg("could not convert to time zone \"%s\"",
-                                                               tzname)));
+                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                                errmsg("timestamp out of range")));
                }
                else
                {
@@ -5120,6 +5168,11 @@ timestamp_zone(PG_FUNCTION_ARGS)
                }
        }
 
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range")));
+
        PG_RETURN_TIMESTAMPTZ(result);
 }
 
@@ -5198,6 +5251,11 @@ timestamp_izone(PG_FUNCTION_ARGS)
 
        result = dt2local(timestamp, tz);
 
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range")));
+
        PG_RETURN_TIMESTAMPTZ(result);
 }      /* timestamp_izone() */
 
@@ -5337,9 +5395,8 @@ timestamptz_zone(PG_FUNCTION_ARGS)
                                                 errmsg("timestamp out of range")));
                        if (tm2timestamp(&tm, fsec, NULL, &result) != 0)
                                ereport(ERROR,
-                                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                                errmsg("could not convert to time zone \"%s\"",
-                                                               tzname)));
+                                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                                errmsg("timestamp out of range")));
                }
                else
                {
@@ -5350,6 +5407,11 @@ timestamptz_zone(PG_FUNCTION_ARGS)
                }
        }
 
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range")));
+
        PG_RETURN_TIMESTAMP(result);
 }
 
@@ -5383,6 +5445,11 @@ timestamptz_izone(PG_FUNCTION_ARGS)
 
        result = dt2local(timestamp, tz);
 
+       if (!IS_VALID_TIMESTAMP(result))
+               ereport(ERROR,
+                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                errmsg("timestamp out of range")));
+
        PG_RETURN_TIMESTAMP(result);
 }
 
index 12a2a8afadbf06b534d2eeedb73d13da25b96034..68a41ebdc51d82c6b3d93c83173d6333da0f9267 100644 (file)
@@ -147,27 +147,86 @@ typedef struct
 /*
  * Julian date support.
  *
- * IS_VALID_JULIAN checks the minimum date exactly, but is a bit sloppy
- * about the maximum, since it's far enough out to not be especially
- * interesting.
+ * date2j() and j2date() nominally handle the Julian date range 0..INT_MAX,
+ * or 4714-11-24 BC to 5874898-06-03 AD.  In practice, date2j() will work and
+ * give correct negative Julian dates for dates before 4714-11-24 BC as well.
+ * We rely on it to do so back to 4714-11-01 BC.  Allowing at least one day's
+ * slop is necessary so that timestamp rotation doesn't produce dates that
+ * would be rejected on input.  For example, '4714-11-24 00:00 GMT BC' is a
+ * legal timestamptz value, but in zones east of Greenwich it would print as
+ * sometime in the afternoon of 4714-11-23 BC; if we couldn't process such a
+ * date we'd have a dump/reload failure.  So the idea is for IS_VALID_JULIAN
+ * to accept a slightly wider range of dates than we really support, and
+ * then we apply the exact checks in IS_VALID_DATE or IS_VALID_TIMESTAMP,
+ * after timezone rotation if any.  To save a few cycles, we can make
+ * IS_VALID_JULIAN check only to the month boundary, since its exact cutoffs
+ * are not very critical in this scheme.
+ *
+ * It is correct that JULIAN_MINYEAR is -4713, not -4714; it is defined to
+ * allow easy comparison to tm_year values, in which we follow the convention
+ * that tm_year <= 0 represents abs(tm_year)+1 BC.
  */
 
 #define JULIAN_MINYEAR (-4713)
 #define JULIAN_MINMONTH (11)
 #define JULIAN_MINDAY (24)
 #define JULIAN_MAXYEAR (5874898)
+#define JULIAN_MAXMONTH (6)
+#define JULIAN_MAXDAY (3)
 
 #define IS_VALID_JULIAN(y,m,d) \
-       (((y) > JULIAN_MINYEAR \
-         || ((y) == JULIAN_MINYEAR && \
-                 ((m) > JULIAN_MINMONTH \
-                  || ((m) == JULIAN_MINMONTH && (d) >= JULIAN_MINDAY)))) \
-        && (y) < JULIAN_MAXYEAR)
-
-#define JULIAN_MAX (2147483494) /* == date2j(JULIAN_MAXYEAR, 1, 1) */
+       (((y) > JULIAN_MINYEAR || \
+         ((y) == JULIAN_MINYEAR && ((m) >= JULIAN_MINMONTH))) && \
+        ((y) < JULIAN_MAXYEAR || \
+         ((y) == JULIAN_MAXYEAR && ((m) < JULIAN_MAXMONTH))))
 
 /* Julian-date equivalents of Day 0 in Unix and Postgres reckoning */
 #define UNIX_EPOCH_JDATE               2440588 /* == date2j(1970, 1, 1) */
 #define POSTGRES_EPOCH_JDATE   2451545 /* == date2j(2000, 1, 1) */
 
+/*
+ * Range limits for dates and timestamps.
+ *
+ * We have traditionally allowed Julian day zero as a valid datetime value,
+ * so that is the lower bound for both dates and timestamps.
+ *
+ * The upper limit for dates is 5874897-12-31, which is a bit less than what
+ * the Julian-date code can allow.  We use that same limit for timestamps when
+ * using floating-point timestamps (so that the timezone offset problem would
+ * exist here too if there were no slop).  For integer timestamps, the upper
+ * limit is 294276-12-31.  The int64 overflow limit would be a few days later;
+ * again, leaving some slop avoids worries about corner-case overflow, and
+ * provides a simpler user-visible definition.
+ */
+
+/* First allowed date, and first disallowed date, in Julian-date form */
+#define DATETIME_MIN_JULIAN (0)
+#define DATE_END_JULIAN (2147483494)   /* == date2j(JULIAN_MAXYEAR, 1, 1) */
+#ifdef HAVE_INT64_TIMESTAMP
+#define TIMESTAMP_END_JULIAN (109203528)               /* == date2j(294277, 1, 1) */
+#else
+#define TIMESTAMP_END_JULIAN DATE_END_JULIAN
+#endif
+
+/* Timestamp limits */
+#ifdef HAVE_INT64_TIMESTAMP
+#define MIN_TIMESTAMP  INT64CONST(-211813488000000000)
+/* == (DATETIME_MIN_JULIAN - POSTGRES_EPOCH_JDATE) * USECS_PER_DAY */
+#define END_TIMESTAMP  INT64CONST(9223371331200000000)
+/* == (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE) * USECS_PER_DAY */
+#else
+#define MIN_TIMESTAMP  (-211813488000.0)
+/* == (DATETIME_MIN_JULIAN - POSTGRES_EPOCH_JDATE) * SECS_PER_DAY */
+#define END_TIMESTAMP  185330760393600.0
+/* == (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE) * SECS_PER_DAY */
+#endif
+
+/* Range-check a date (given in Postgres, not Julian, numbering) */
+#define IS_VALID_DATE(d) \
+       ((DATETIME_MIN_JULIAN - POSTGRES_EPOCH_JDATE) <= (d) && \
+        (d) < (DATE_END_JULIAN - POSTGRES_EPOCH_JDATE))
+
+/* Range-check a timestamp */
+#define IS_VALID_TIMESTAMP(t)  (MIN_TIMESTAMP <= (t) && (t) < END_TIMESTAMP)
+
 #endif   /* DATATYPE_TIMESTAMP_H */
index 145e2b7c4fbd11f47c761c11b541c63e5bf8e6ee..c0c3ac1901612874535956aeb85d5eed0a240aba 100644 (file)
@@ -287,22 +287,32 @@ do { \
  */
 #define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
 
-/* Julian date support for date2j() and j2date()
- *
- * IS_VALID_JULIAN checks the minimum date exactly, but is a bit sloppy
- * about the maximum, since it's far enough out to not be especially
- * interesting.
+/*
+ * Julian date support --- see comments in backend's timestamp.h.
  */
 
 #define JULIAN_MINYEAR (-4713)
 #define JULIAN_MINMONTH (11)
 #define JULIAN_MINDAY (24)
 #define JULIAN_MAXYEAR (5874898)
+#define JULIAN_MAXMONTH (6)
+#define JULIAN_MAXDAY (3)
+
+#define IS_VALID_JULIAN(y,m,d) \
+       (((y) > JULIAN_MINYEAR || \
+         ((y) == JULIAN_MINYEAR && ((m) >= JULIAN_MINMONTH))) && \
+        ((y) < JULIAN_MAXYEAR || \
+         ((y) == JULIAN_MAXYEAR && ((m) < JULIAN_MAXMONTH))))
+
+#ifdef HAVE_INT64_TIMESTAMP
+#define MIN_TIMESTAMP  INT64CONST(-211813488000000000)
+#define END_TIMESTAMP  INT64CONST(9223371331200000000)
+#else
+#define MIN_TIMESTAMP  (-211813488000.0)
+#define END_TIMESTAMP  185330760393600.0
+#endif
 
-#define IS_VALID_JULIAN(y,m,d) ((((y) > JULIAN_MINYEAR) \
-  || (((y) == JULIAN_MINYEAR) && (((m) > JULIAN_MINMONTH) \
-  || (((m) == JULIAN_MINMONTH) && ((d) >= JULIAN_MINDAY))))) \
- && ((y) < JULIAN_MAXYEAR))
+#define IS_VALID_TIMESTAMP(t)  (MIN_TIMESTAMP <= (t) && (t) < END_TIMESTAMP)
 
 #define UTIME_MINYEAR (1901)
 #define UTIME_MINMONTH (12)
index cf1fed2c00bf345880d7be3008930cc1894500dd..f746a90f8b1d841d4387d2ba9ca26f4536f718e1 100644 (file)
@@ -61,7 +61,7 @@ tm2timestamp(struct tm * tm, fsec_t fsec, int *tzp, timestamp * result)
                                time;
 #endif
 
-       /* Julian day routines are not correct for negative Julian days */
+       /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm->tm_year, tm->tm_mon, tm->tm_mday))
                return -1;
 
@@ -83,6 +83,10 @@ tm2timestamp(struct tm * tm, fsec_t fsec, int *tzp, timestamp * result)
        if (tzp != NULL)
                *result = dt2local(*result, -(*tzp));
 
+       /* final range check catches just-out-of-range timestamps */
+       if (!IS_VALID_TIMESTAMP(*result))
+               return -1;
+
        return 0;
 }      /* tm2timestamp() */
 
index 56c55201f59defe4e1996bc3b617472b0213cdc6..418b1464250d4a018476cfa03ce7b99899d0f114 100644 (file)
@@ -816,6 +816,27 @@ SELECT date '1999 08 01';
  1999-08-01
 (1 row)
 
+-- Check upper and lower limits of date range
+SELECT date '4714-11-24 BC';
+     date      
+---------------
+ 4714-11-24 BC
+(1 row)
+
+SELECT date '4714-11-23 BC';  -- out of range
+ERROR:  date out of range: "4714-11-23 BC"
+LINE 1: SELECT date '4714-11-23 BC';
+                    ^
+SELECT date '5874897-12-31';
+     date      
+---------------
+ 5874897-12-31
+(1 row)
+
+SELECT date '5874898-01-01';  -- out of range
+ERROR:  date out of range: "5874898-01-01"
+LINE 1: SELECT date '5874898-01-01';
+                    ^
 RESET datestyle;
 --
 -- Simple math
index e9f5e7790f5b798618aba63bd0c47d99712a3dc6..4a2fabddd9f68473c90b3e8263069a0375dcffb6 100644 (file)
@@ -250,6 +250,18 @@ SELECT '' AS "64", d1 FROM TIMESTAMP_TBL;
     | Mon Jan 01 17:32:01 2001
 (65 rows)
 
+-- Check behavior at the lower boundary of the timestamp range
+SELECT '4714-11-24 00:00:00 BC'::timestamp;
+          timestamp          
+-----------------------------
+ Mon Nov 24 00:00:00 4714 BC
+(1 row)
+
+SELECT '4714-11-23 23:59:59 BC'::timestamp;  -- out of range
+ERROR:  timestamp out of range: "4714-11-23 23:59:59 BC"
+LINE 1: SELECT '4714-11-23 23:59:59 BC'::timestamp;
+               ^
+-- The upper boundary differs between integer and float timestamps, so no check
 -- Demonstrate functions and operators
 SELECT '' AS "48", d1 FROM TIMESTAMP_TBL
    WHERE d1 > timestamp without time zone '1997-01-02';
index 40a22540e559265b75bcb931e966e91bb1ecd467..fffcaf4bf8d7b7380bddd61b36a643b91d5aa2fb 100644 (file)
@@ -322,6 +322,30 @@ SELECT '' AS "64", d1 FROM TIMESTAMPTZ_TBL;
     | Mon Jan 01 17:32:01 2001 PST
 (66 rows)
 
+-- Check behavior at the lower boundary of the timestamp range
+SELECT '4714-11-24 00:00:00+00 BC'::timestamptz;
+           timestamptz           
+---------------------------------
+ Sun Nov 23 16:00:00 4714 PST BC
+(1 row)
+
+SELECT '4714-11-23 16:00:00-08 BC'::timestamptz;
+           timestamptz           
+---------------------------------
+ Sun Nov 23 16:00:00 4714 PST BC
+(1 row)
+
+SELECT 'Sun Nov 23 16:00:00 4714 PST BC'::timestamptz;
+           timestamptz           
+---------------------------------
+ Sun Nov 23 16:00:00 4714 PST BC
+(1 row)
+
+SELECT '4714-11-23 23:59:59+00 BC'::timestamptz;  -- out of range
+ERROR:  timestamp out of range: "4714-11-23 23:59:59+00 BC"
+LINE 1: SELECT '4714-11-23 23:59:59+00 BC'::timestamptz;
+               ^
+-- The upper boundary differs between integer and float timestamps, so no check
 -- Demonstrate functions and operators
 SELECT '' AS "48", d1 FROM TIMESTAMPTZ_TBL
    WHERE d1 > timestamp with time zone '1997-01-02';
index e40b4c4856dc7205e57fbcfea48d7f5c116fb3e2..4553fd1c975b0657cb8a297b189f8030f9a1bede 100644 (file)
@@ -186,6 +186,12 @@ SELECT date '01 08 1999';
 SELECT date '99 08 01';
 SELECT date '1999 08 01';
 
+-- Check upper and lower limits of date range
+SELECT date '4714-11-24 BC';
+SELECT date '4714-11-23 BC';  -- out of range
+SELECT date '5874897-12-31';
+SELECT date '5874898-01-01';  -- out of range
+
 RESET datestyle;
 
 --
index b22cd4871e34f559e063261977e6c3c2aeca2cab..b7957cbb9aa85927972410bc003e9d173d1a34a6 100644 (file)
@@ -143,6 +143,11 @@ INSERT INTO TIMESTAMP_TBL VALUES ('Feb 16 17:32:01 5097 BC');
 
 SELECT '' AS "64", d1 FROM TIMESTAMP_TBL;
 
+-- Check behavior at the lower boundary of the timestamp range
+SELECT '4714-11-24 00:00:00 BC'::timestamp;
+SELECT '4714-11-23 23:59:59 BC'::timestamp;  -- out of range
+-- The upper boundary differs between integer and float timestamps, so no check
+
 -- Demonstrate functions and operators
 SELECT '' AS "48", d1 FROM TIMESTAMP_TBL
    WHERE d1 > timestamp without time zone '1997-01-02';
index f4b455e7032e8682e5905fa1e8976d6cbfcf5a95..03dbc05aab7bfca7d25f6f18bdee8a7debe64e69 100644 (file)
@@ -162,6 +162,13 @@ SELECT 'Wed Jul 11 10:51:14 PST+03:00 2001'::timestamptz;
 
 SELECT '' AS "64", d1 FROM TIMESTAMPTZ_TBL;
 
+-- Check behavior at the lower boundary of the timestamp range
+SELECT '4714-11-24 00:00:00+00 BC'::timestamptz;
+SELECT '4714-11-23 16:00:00-08 BC'::timestamptz;
+SELECT 'Sun Nov 23 16:00:00 4714 PST BC'::timestamptz;
+SELECT '4714-11-23 23:59:59+00 BC'::timestamptz;  -- out of range
+-- The upper boundary differs between integer and float timestamps, so no check
+
 -- Demonstrate functions and operators
 SELECT '' AS "48", d1 FROM TIMESTAMPTZ_TBL
    WHERE d1 > timestamp with time zone '1997-01-02';