Skip to content

Commit

Permalink
Support +/- infinity in the interval data type.
Browse files Browse the repository at this point in the history
This adds support for infinity to the interval data type, using the
same input/output representation as the other date/time data types
that support infinity. This allows various arithmetic operations on
infinite dates, timestamps and intervals.

The new values are represented by setting all fields of the interval
to INT32/64_MIN for -infinity, and INT32/64_MAX for +infinity. This
ensures that they compare as less/greater than all other interval
values, without the need for any special-case comparison code.

Note that, since those 2 values were formerly accepted as legal finite
intervals, pg_upgrade and dump/restore from an old database will turn
them from finite to infinite intervals. That seems OK, since those
exact values should be extremely rare in practice, and they are
outside the documented range supported by the interval type, which
gives us a certain amount of leeway.

Bump catalog version.

Joseph Koshakow, Jian He, and Ashutosh Bapat, reviewed by me.

Discussion: https://postgr.es/m/CAAvxfHea4%2BsPybKK7agDYOMo9N-Z3J6ZXf3BOM79pFsFNcRjwA%40mail.gmail.com
  • Loading branch information
deanrasheed committed Nov 14, 2023
1 parent b41b1a7 commit 519fc1b
Show file tree
Hide file tree
Showing 25 changed files with 2,541 additions and 297 deletions.
5 changes: 2 additions & 3 deletions contrib/btree_gin/btree_gin.c
Expand Up @@ -306,9 +306,8 @@ leftmostvalue_interval(void)
{
Interval *v = palloc(sizeof(Interval));

v->time = PG_INT64_MIN;
v->day = PG_INT32_MIN;
v->month = PG_INT32_MIN;
INTERVAL_NOBEGIN(v);

return IntervalPGetDatum(v);
}

Expand Down
4 changes: 2 additions & 2 deletions doc/src/sgml/datatype.sgml
Expand Up @@ -2328,12 +2328,12 @@ TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54+02'
</row>
<row>
<entry><literal>infinity</literal></entry>
<entry><type>date</type>, <type>timestamp</type></entry>
<entry><type>date</type>, <type>timestamp</type>, <type>interval</type></entry>
<entry>later than all other time stamps</entry>
</row>
<row>
<entry><literal>-infinity</literal></entry>
<entry><type>date</type>, <type>timestamp</type></entry>
<entry><type>date</type>, <type>timestamp</type>, <type>interval</type></entry>
<entry>earlier than all other time stamps</entry>
</row>
<row>
Expand Down
8 changes: 6 additions & 2 deletions doc/src/sgml/func.sgml
Expand Up @@ -9565,7 +9565,7 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
<returnvalue>boolean</returnvalue>
</para>
<para>
Test for finite interval (currently always true)
Test for finite interval (not +/-infinity)
</para>
<para>
<literal>isfinite(interval '4 hours')</literal>
Expand Down Expand Up @@ -10462,7 +10462,11 @@ SELECT EXTRACT(YEAR FROM TIMESTAMP '2001-02-16 20:38:40');
When the input value is +/-Infinity, <function>extract</function> returns
+/-Infinity for monotonically-increasing fields (<literal>epoch</literal>,
<literal>julian</literal>, <literal>year</literal>, <literal>isoyear</literal>,
<literal>decade</literal>, <literal>century</literal>, and <literal>millennium</literal>).
<literal>decade</literal>, <literal>century</literal>, and <literal>millennium</literal>
for <type>timestamp</type> inputs; <literal>epoch</literal>, <literal>hour</literal>,
<literal>day</literal>, <literal>year</literal>, <literal>decade</literal>,
<literal>century</literal>, and <literal>millennium</literal> for
<type>interval</type> inputs).
For other fields, NULL is returned. <productname>PostgreSQL</productname>
versions before 9.6 returned zero for all cases of infinite input.
</para>
Expand Down
57 changes: 47 additions & 10 deletions src/backend/utils/adt/date.c
Expand Up @@ -24,6 +24,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "common/hashfn.h"
#include "common/int.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
#include "nodes/supportnodes.h"
Expand Down Expand Up @@ -2013,6 +2014,11 @@ interval_time(PG_FUNCTION_ARGS)
Interval *span = PG_GETARG_INTERVAL_P(0);
TimeADT result;

if (INTERVAL_NOT_FINITE(span))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("cannot convert infinite interval to time")));

result = span->time % USECS_PER_DAY;
if (result < 0)
result += USECS_PER_DAY;
Expand Down Expand Up @@ -2049,6 +2055,11 @@ time_pl_interval(PG_FUNCTION_ARGS)
Interval *span = PG_GETARG_INTERVAL_P(1);
TimeADT result;

if (INTERVAL_NOT_FINITE(span))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("cannot add infinite interval to time")));

result = time + span->time;
result -= result / USECS_PER_DAY * USECS_PER_DAY;
if (result < INT64CONST(0))
Expand All @@ -2067,6 +2078,11 @@ time_mi_interval(PG_FUNCTION_ARGS)
Interval *span = PG_GETARG_INTERVAL_P(1);
TimeADT result;

if (INTERVAL_NOT_FINITE(span))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("cannot subtract infinite interval from time")));

result = time - span->time;
result -= result / USECS_PER_DAY * USECS_PER_DAY;
if (result < INT64CONST(0))
Expand All @@ -2090,7 +2106,8 @@ in_range_time_interval(PG_FUNCTION_ARGS)

/*
* Like time_pl_interval/time_mi_interval, we disregard the month and day
* fields of the offset. So our test for negative should too.
* fields of the offset. So our test for negative should too. This also
* catches -infinity, so we only need worry about +infinity below.
*/
if (offset->time < 0)
ereport(ERROR,
Expand All @@ -2100,13 +2117,14 @@ in_range_time_interval(PG_FUNCTION_ARGS)
/*
* We can't use time_pl_interval/time_mi_interval here, because their
* wraparound behavior would give wrong (or at least undesirable) answers.
* Fortunately the equivalent non-wrapping behavior is trivial, especially
* since we don't worry about integer overflow.
* Fortunately the equivalent non-wrapping behavior is trivial, except
* that adding an infinite (or very large) interval might cause integer
* overflow. Subtraction cannot overflow here.
*/
if (sub)
sum = base - offset->time;
else
sum = base + offset->time;
else if (pg_add_s64_overflow(base, offset->time, &sum))
PG_RETURN_BOOL(less);

if (less)
PG_RETURN_BOOL(val <= sum);
Expand Down Expand Up @@ -2581,6 +2599,11 @@ timetz_pl_interval(PG_FUNCTION_ARGS)
Interval *span = PG_GETARG_INTERVAL_P(1);
TimeTzADT *result;

if (INTERVAL_NOT_FINITE(span))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("cannot add infinite interval to time")));

result = (TimeTzADT *) palloc(sizeof(TimeTzADT));

result->time = time->time + span->time;
Expand All @@ -2603,6 +2626,11 @@ timetz_mi_interval(PG_FUNCTION_ARGS)
Interval *span = PG_GETARG_INTERVAL_P(1);
TimeTzADT *result;

if (INTERVAL_NOT_FINITE(span))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("cannot subtract infinite interval from time")));

result = (TimeTzADT *) palloc(sizeof(TimeTzADT));

result->time = time->time - span->time;
Expand Down Expand Up @@ -2630,7 +2658,8 @@ in_range_timetz_interval(PG_FUNCTION_ARGS)

/*
* Like timetz_pl_interval/timetz_mi_interval, we disregard the month and
* day fields of the offset. So our test for negative should too.
* day fields of the offset. So our test for negative should too. This
* also catches -infinity, so we only need worry about +infinity below.
*/
if (offset->time < 0)
ereport(ERROR,
Expand All @@ -2640,13 +2669,14 @@ in_range_timetz_interval(PG_FUNCTION_ARGS)
/*
* We can't use timetz_pl_interval/timetz_mi_interval here, because their
* wraparound behavior would give wrong (or at least undesirable) answers.
* Fortunately the equivalent non-wrapping behavior is trivial, especially
* since we don't worry about integer overflow.
* Fortunately the equivalent non-wrapping behavior is trivial, except
* that adding an infinite (or very large) interval might cause integer
* overflow. Subtraction cannot overflow here.
*/
if (sub)
sum.time = base->time - offset->time;
else
sum.time = base->time + offset->time;
else if (pg_add_s64_overflow(base->time, offset->time, &sum.time))
PG_RETURN_BOOL(less);
sum.zone = base->zone;

if (less)
Expand Down Expand Up @@ -3096,6 +3126,13 @@ timetz_izone(PG_FUNCTION_ARGS)
TimeTzADT *result;
int tz;

if (INTERVAL_NOT_FINITE(zone))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("interval time zone \"%s\" must be finite",
DatumGetCString(DirectFunctionCall1(interval_out,
PointerGetDatum(zone))))));

if (zone->month != 0 || zone->day != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
Expand Down
26 changes: 26 additions & 0 deletions src/backend/utils/adt/datetime.c
Expand Up @@ -3271,6 +3271,9 @@ ClearPgItmIn(struct pg_itm_in *itm_in)
*
* Allow ISO-style time span, with implicit units on number of days
* preceding an hh:mm:ss field. - thomas 1998-04-30
*
* itm_in remains undefined for infinite interval values for which dtype alone
* suffices.
*/
int
DecodeInterval(char **field, int *ftype, int nf, int range,
Expand Down Expand Up @@ -3574,6 +3577,8 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
if (parsing_unit_val)
return DTERR_BAD_FORMAT;
type = DecodeUnits(i, field[i], &uval);
if (type == UNKNOWN_FIELD)
type = DecodeSpecial(i, field[i], &uval);
if (type == IGNORE_DTF)
continue;

Expand All @@ -3597,6 +3602,27 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
type = uval;
break;

case RESERV:
tmask = (DTK_DATE_M | DTK_TIME_M);

/*
* Only reserved words corresponding to infinite
* intervals are accepted.
*/
if (uval != DTK_LATE && uval != DTK_EARLY)
return DTERR_BAD_FORMAT;

/*
* Infinity cannot be followed by anything else. We
* could allow "ago" to reverse the sign of infinity
* but using signed infinity is more intuitive.
*/
if (i != nf - 1)
return DTERR_BAD_FORMAT;

*dtype = uval;
break;

default:
return DTERR_BAD_FORMAT;
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/utils/adt/formatting.c
Expand Up @@ -4127,7 +4127,7 @@ interval_to_char(PG_FUNCTION_ARGS)
struct pg_itm tt,
*itm = &tt;

if (VARSIZE_ANY_EXHDR(fmt) <= 0)
if (VARSIZE_ANY_EXHDR(fmt) <= 0 || INTERVAL_NOT_FINITE(it))
PG_RETURN_NULL();

ZERO_tmtc(&tmtc);
Expand Down
4 changes: 4 additions & 0 deletions src/backend/utils/adt/selfuncs.c
Expand Up @@ -4802,6 +4802,10 @@ convert_timevalue_to_scalar(Datum value, Oid typid, bool *failure)
* Convert the month part of Interval to days using assumed
* average month length of 365.25/12.0 days. Not too
* accurate, but plenty good enough for our purposes.
*
* This also works for infinite intervals, which just have all
* fields set to INT_MIN/INT_MAX, and so will produce a result
* smaller/larger than any finite interval.
*/
return interval->time + interval->day * (double) USECS_PER_DAY +
interval->month * ((DAYS_PER_YEAR / (double) MONTHS_PER_YEAR) * USECS_PER_DAY);
Expand Down

0 comments on commit 519fc1b

Please sign in to comment.