diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 1525d2a..46140ef 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -475,6 +475,79 @@ timestamptz_in(PG_FUNCTION_ARGS) PG_RETURN_TIMESTAMPTZ(result); } +/* to_timestamp(double precision) + * Convert UNIX epoch to timestamptz. + */ +Datum +unixtime_timestamptz(PG_FUNCTION_ARGS) +{ + float8 seconds = PG_GETARG_FLOAT8(0); + TimestampTz result; + + int is_inf = is_infinite(seconds); + + static const float8 epoch_lbound = (float8)SECS_PER_DAY * -UNIX_EPOCH_JDATE; + static const float8 epoch_ubound = (float8)SECS_PER_DAY * + (JULIAN_MAX4STAMPS - UNIX_EPOCH_JDATE); + + if(isnan(seconds)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for UNIX epoch \"%f\"", seconds), + errhint("Valid units for this parameter are finite floats, \"Infinity\" and \"-Infinity\""))); + + if (is_inf < 0) + TIMESTAMP_NOBEGIN(result); + + else if (is_inf > 0) + TIMESTAMP_NOEND(result); + + else + { + if (seconds < epoch_lbound) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("UNIX epoch out of range: \"%lf\"", seconds), + errhint("Minimal UNIX epoch value is \"%lf\"", epoch_lbound))); + + if (seconds >= epoch_ubound) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("UNIX epoch out of range: \"%lf\"", seconds), + errhint("Maximal UNIX epoch value is \"%lf\"", epoch_ubound))); + + /* Convert seconds to Postgres' epoch */ + seconds -= ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY); + +#ifdef HAVE_INT64_TIMESTAMP + /* + * Here should be just "result = seconds * USECS_PER_SEC;", + * but for big values it leads loosing precision. + * Do as many operations as possible as integers. + */ + { + int64 seconds_int = (int64)seconds; + fsec_t msec = (seconds - seconds_int) * USECS_PER_SEC; + + result = (seconds_int * USECS_PER_SEC) + msec; + } +#else + result = seconds; +#endif + /* + * There can't be overflow. Just check timestamp for correctness again + * because of possible loosing precision on float8-int86 conversion. + */ + if (!IS_VALID_TIMESTAMP(result)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("UNIX epoch out of range: \"%lf\"", + PG_GETARG_FLOAT8(0)))); + } + + PG_RETURN_TIMESTAMP(result); +} + /* * Try to parse a timezone specification, and return its timezone offset value * if it's acceptable. Otherwise, an error is thrown. diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 62b9125..7f3433a 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -1202,7 +1202,7 @@ DATA(insert OID = 1154 ( timestamptz_lt PGNSP PGUID 12 1 0 0 0 f f f t t f i DATA(insert OID = 1155 ( timestamptz_le PGNSP PGUID 12 1 0 0 0 f f f t t f i s 2 0 16 "1184 1184" _null_ _null_ _null_ _null_ _null_ timestamp_le _null_ _null_ _null_ )); DATA(insert OID = 1156 ( timestamptz_ge PGNSP PGUID 12 1 0 0 0 f f f t t f i s 2 0 16 "1184 1184" _null_ _null_ _null_ _null_ _null_ timestamp_ge _null_ _null_ _null_ )); DATA(insert OID = 1157 ( timestamptz_gt PGNSP PGUID 12 1 0 0 0 f f f t t f i s 2 0 16 "1184 1184" _null_ _null_ _null_ _null_ _null_ timestamp_gt _null_ _null_ _null_ )); -DATA(insert OID = 1158 ( to_timestamp PGNSP PGUID 14 1 0 0 0 f f f f t f i s 1 0 1184 "701" _null_ _null_ _null_ _null_ _null_ "select (''epoch''::pg_catalog.timestamptz + $1 * ''1 second''::pg_catalog.interval)" _null_ _null_ _null_ )); +DATA(insert OID = 1158 ( to_timestamp PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 1184 "701" _null_ _null_ _null_ _null_ _null_ unixtime_timestamptz _null_ _null_ _null_ )); DESCR("convert UNIX epoch to timestamptz"); DATA(insert OID = 3995 ( timestamp_zone_transform PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ timestamp_zone_transform _null_ _null_ _null_ )); DESCR("transform a time zone adjustment"); diff --git a/src/include/datatype/timestamp.h b/src/include/datatype/timestamp.h index 12a2a8a..9b21ba8 100644 --- a/src/include/datatype/timestamp.h +++ b/src/include/datatype/timestamp.h @@ -105,6 +105,20 @@ typedef struct #define USECS_PER_MINUTE INT64CONST(60000000) #define USECS_PER_SEC INT64CONST(1000000) +#ifdef HAVE_INT64_TIMESTAMP +#define MIN_TIMESTAMP INT64CONST(-211813488000000000) +/* == (0 - POSTGRES_EPOCH_JDATE) * 86400 * USECS_PER_SEC */ +#define MAX_TIMESTAMP INT64CONST(9223371331200000000) +/* == (JULIAN_MAX4STAMPS - POSTGRES_EPOCH_JDATE) * 86400 * USECS_PER_SEC */ +#else +#define MIN_TIMESTAMP -211813488000.0 +/* == (0 - POSTGRES_EPOCH_JDATE) * 86400 */ +#define MAX_TIMESTAMP 9223371331200.0 +/* == (JULIAN_MAX4STAMPS - POSTGRES_EPOCH_JDATE) * 86400 */ +#endif + +#define IS_VALID_TIMESTAMP(j) ((MIN_TIMESTAMP <= (j)) && ((j) < MAX_TIMESTAMP)) + /* * We allow numeric timezone offsets up to 15:59:59 either way from Greenwich. * Currently, the record holders for wackiest offsets in actual use are zones @@ -156,6 +170,7 @@ typedef struct #define JULIAN_MINMONTH (11) #define JULIAN_MINDAY (24) #define JULIAN_MAXYEAR (5874898) +#define JULIAN_MAXYEAR4STAMPS (294277) #define IS_VALID_JULIAN(y,m,d) \ (((y) > JULIAN_MINYEAR \ @@ -164,10 +179,22 @@ typedef struct || ((m) == JULIAN_MINMONTH && (d) >= JULIAN_MINDAY)))) \ && (y) < JULIAN_MAXYEAR) +#define IS_VALID_JULIAN4STAMPS(y,m,d) \ + (((y) > JULIAN_MINYEAR \ + || ((y) == JULIAN_MINYEAR && \ + ((m) > JULIAN_MINMONTH \ + || ((m) == JULIAN_MINMONTH && (d) >= JULIAN_MINDAY)))) \ + && (y) < JULIAN_MAXYEAR4STAMPS) + #define JULIAN_MAX (2147483494) /* == date2j(JULIAN_MAXYEAR, 1, 1) */ +#define JULIAN_MAX4STAMPS (109203528) /* == date2j(JULIAN_MAXYEAR4STAMPS, 1, 1) */ /* 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) */ +#define IS_VALID_DATE(j) \ + ((-POSTGRES_EPOCH_JDATE <= (j)) && \ + ((j) < (JULIAN_MAX - POSTGRES_EPOCH_JDATE))) + #endif /* DATATYPE_TIMESTAMP_H */ diff --git a/src/include/utils/timestamp.h b/src/include/utils/timestamp.h index fbead3a..e18211d 100644 --- a/src/include/utils/timestamp.h +++ b/src/include/utils/timestamp.h @@ -124,6 +124,7 @@ extern Datum timestamp_cmp_timestamptz(PG_FUNCTION_ARGS); extern Datum make_timestamp(PG_FUNCTION_ARGS); extern Datum make_timestamptz(PG_FUNCTION_ARGS); extern Datum make_timestamptz_at_timezone(PG_FUNCTION_ARGS); +extern Datum unixtime_timestamptz(PG_FUNCTION_ARGS); extern Datum timestamptz_eq_timestamp(PG_FUNCTION_ARGS); extern Datum timestamptz_ne_timestamp(PG_FUNCTION_ARGS); diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out index 40a2254..8d7f981 100644 --- a/src/test/regress/expected/timestamptz.out +++ b/src/test/regress/expected/timestamptz.out @@ -2232,6 +2232,66 @@ SELECT make_timestamptz(2007, 12, 9, 3, 0, 0, 'VET'); Sun Dec 09 07:30:00 2007 UTC (1 row) +SELECT to_timestamp( 0); -- 1970-01-01 00:00:00+00 + to_timestamp +------------------------------ + Thu Jan 01 00:00:00 1970 UTC +(1 row) + +SELECT to_timestamp( 946684800); -- 2000-01-01 00:00:00+00 + to_timestamp +------------------------------ + Sat Jan 01 00:00:00 2000 UTC +(1 row) + +SELECT to_timestamp(1262349296.7890123); -- 2010-01-01 12:34:56.789012+00 + to_timestamp +------------------------------------- + Fri Jan 01 12:34:56.789012 2010 UTC +(1 row) + +-- corner cases +SELECT to_timestamp(-1e20::float8); -- Error: UNIX epoch out of range +ERROR: UNIX epoch out of range: "-100000000000000000000.000000" +HINT: Minimal UNIX epoch value is "-210866803200.000000" +SELECT to_timestamp(-210866803200.0625); -- Error: UNIX epoch out of range +ERROR: UNIX epoch out of range: "-210866803200.062500" +HINT: Minimal UNIX epoch value is "-210866803200.000000" +SELECT to_timestamp(-210866803200); -- 4714-11-24 00:00:00+00 BC + to_timestamp +--------------------------------- + Mon Nov 24 00:00:00 4714 UTC BC +(1 row) + +-- Maximal allowed input value +SELECT to_timestamp(9224318015999.875); -- 294276-12-31 23:59:59.875+00 + to_timestamp +------------------------------------ + Sun Dec 31 23:59:59.875 294276 UTC +(1 row) + +SELECT to_timestamp(9224318016000); -- Error: UNIX epoch out of range +ERROR: UNIX epoch out of range: "9224318016000.000000" +HINT: Maximal UNIX epoch value is "9224318016000.000000" +SELECT to_timestamp(1e20::float8); -- Error: UNIX epoch out of range +ERROR: UNIX epoch out of range: "100000000000000000000.000000" +HINT: Maximal UNIX epoch value is "9224318016000.000000" +-- infinity values +SELECT to_timestamp(' Infinity'::float); + to_timestamp +-------------- + infinity +(1 row) + +SELECT to_timestamp('-Infinity'::float); + to_timestamp +-------------- + -infinity +(1 row) + +SELECT to_timestamp('NaN'::float); +ERROR: invalid value for UNIX epoch "nan" +HINT: Valid units for this parameter are finite floats, "Infinity" and "-Infinity" SET TimeZone to 'Europe/Moscow'; SELECT '2011-03-26 21:00:00 UTC'::timestamptz; timestamptz diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql index f4b455e..fdad178 100644 --- a/src/test/regress/sql/timestamptz.sql +++ b/src/test/regress/sql/timestamptz.sql @@ -379,6 +379,23 @@ SELECT '2007-12-09 04:00:00'::timestamp AT TIME ZONE 'VET'; SELECT make_timestamptz(2007, 12, 9, 2, 0, 0, 'VET'); SELECT make_timestamptz(2007, 12, 9, 3, 0, 0, 'VET'); +SELECT to_timestamp( 0); -- 1970-01-01 00:00:00+00 +SELECT to_timestamp( 946684800); -- 2000-01-01 00:00:00+00 +SELECT to_timestamp(1262349296.7890123); -- 2010-01-01 12:34:56.789012+00 +-- corner cases +SELECT to_timestamp(-1e20::float8); -- Error: UNIX epoch out of range +SELECT to_timestamp(-210866803200.0625); -- Error: UNIX epoch out of range +SELECT to_timestamp(-210866803200); -- 4714-11-24 00:00:00+00 BC +-- Maximal allowed input value +SELECT to_timestamp(9224318015999.875); -- 294276-12-31 23:59:59.875+00 +SELECT to_timestamp(9224318016000); -- Error: UNIX epoch out of range +SELECT to_timestamp(1e20::float8); -- Error: UNIX epoch out of range +-- infinity values +SELECT to_timestamp(' Infinity'::float); +SELECT to_timestamp('-Infinity'::float); +SELECT to_timestamp('NaN'::float); + + SET TimeZone to 'Europe/Moscow'; SELECT '2011-03-26 21:00:00 UTC'::timestamptz;