Skip to content

Commit f0d0394

Browse files
committed
Fix parsing of ISO-8601 interval fields with exponential notation.
Historically we've accepted interval input like 'P.1e10D'. This is probably an accident of having used strtod() to do the parsing, rather than something anyone intended, but it's been that way for a long time. Commit e39f990 broke this by trying to parse the integer and fractional parts separately, without accounting for the possibility of an exponent. In principle that coding allowed for precise conversions of field values wider than 15 decimal digits, but that does not seem like a goal worth sweating bullets for. So, rather than trying to manage an exponent on top of the existing complexity, let's just revert to the previous coding that used strtod() by itself. We can still improve on the old code to the extent of allowing the value to range up to 1.0e15 rather than only INT_MAX. (Allowing more than that risks creating problems due to precision loss: the converted fractional part might have absolute value more than 1. Perhaps that could be dealt with in some way, but it really does not seem worth additional effort.) Per bug #17795 from Alexander Lakhin. Back-patch to v15 where the faulty code came in. Discussion: https://postgr.es/m/17795-748d6db3ed95d313@postgresql.org
1 parent f6db76c commit f0d0394

File tree

3 files changed

+37
-38
lines changed

3 files changed

+37
-38
lines changed

src/backend/utils/adt/datetime.c

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3702,46 +3702,38 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
37023702
static int
37033703
ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart)
37043704
{
3705-
int sign = 1;
3705+
double val;
37063706

3707-
*ipart = 0;
3708-
*fpart = 0.0;
3709-
3710-
/* Parse sign if there is any */
3711-
if (*str == '-')
3712-
{
3713-
sign = -1;
3714-
str++;
3715-
}
3716-
3717-
*endptr = str;
3707+
/*
3708+
* Historically this has accepted anything that strtod() would take,
3709+
* notably including "e" notation, so continue doing that. This is
3710+
* slightly annoying because the precision of double is less than that of
3711+
* int64, so we would lose accuracy for inputs larger than 2^53 or so.
3712+
* However, historically we rejected inputs outside the int32 range,
3713+
* making that concern moot. What we do now is reject abs(val) above
3714+
* 1.0e15 (a round number a bit less than 2^50), so that any accepted
3715+
* value will have an exact integer part, and thereby a fraction part with
3716+
* abs(*fpart) less than 1. In the absence of field complaints it doesn't
3717+
* seem worth working harder.
3718+
*/
3719+
if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.'))
3720+
return DTERR_BAD_FORMAT;
37183721
errno = 0;
3719-
3720-
/* Parse int64 part if there is any */
3721-
if (isdigit((unsigned char) **endptr))
3722-
*ipart = strtoi64(*endptr, endptr, 10) * sign;
3723-
3724-
/* Parse fractional part if there is any */
3725-
if (**endptr == '.')
3726-
{
3727-
/*
3728-
* As in ParseFraction, some versions of strtod insist on seeing some
3729-
* digits after '.', but some don't. We want to allow zero digits
3730-
* after '.' as long as there were some before it.
3731-
*/
3732-
if (isdigit((unsigned char) *(*endptr + 1)))
3733-
*fpart = strtod(*endptr, endptr) * sign;
3734-
else
3735-
{
3736-
(*endptr)++; /* advance over '.' */
3737-
str++; /* so next test will fail if no digits */
3738-
}
3739-
}
3740-
3741-
/* did we not see anything that looks like a number? */
3722+
val = strtod(str, endptr);
3723+
/* did we not see anything that looks like a double? */
37423724
if (*endptr == str || errno != 0)
37433725
return DTERR_BAD_FORMAT;
3744-
3726+
/* watch out for overflow, including infinities; reject NaN too */
3727+
if (isnan(val) || val < -1.0e15 || val > 1.0e15)
3728+
return DTERR_FIELD_OVERFLOW;
3729+
/* be very sure we truncate towards zero (cf dtrunc()) */
3730+
if (val >= 0)
3731+
*ipart = (int64) floor(val);
3732+
else
3733+
*ipart = (int64) -floor(-val);
3734+
*fpart = val - *ipart;
3735+
/* Callers expect this to hold */
3736+
Assert(*fpart > -1.0 && *fpart < 1.0);
37453737
return 0;
37463738
}
37473739

src/test/regress/expected/interval.out

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,12 @@ select interval 'P.1Y0M3DT4H5M6S';
975975
1 mon 3 days 04:05:06
976976
(1 row)
977977

978+
select interval 'P10.5e4Y'; -- not per spec, but we've historically taken it
979+
interval
980+
--------------
981+
105000 years
982+
(1 row)
983+
978984
select interval 'P.Y0M3DT4H5M6S'; -- error
979985
ERROR: invalid input syntax for type interval: "P.Y0M3DT4H5M6S"
980986
LINE 1: select interval 'P.Y0M3DT4H5M6S';
@@ -1081,13 +1087,13 @@ select interval 'PT2562047788:00:54.775807';
10811087
select interval 'PT2562047788.0152155019444';
10821088
interval
10831089
-----------------------------------
1084-
@ 2562047788 hours 54.775807 secs
1090+
@ 2562047788 hours 54.775429 secs
10851091
(1 row)
10861092

10871093
select interval 'PT-2562047788.0152155022222';
10881094
interval
10891095
---------------------------------------
1090-
@ 2562047788 hours 54.775808 secs ago
1096+
@ 2562047788 hours 54.775429 secs ago
10911097
(1 row)
10921098

10931099
-- overflow each date/time field

src/test/regress/sql/interval.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ select interval 'P1.0Y0M3DT4H5M6S';
328328
select interval 'P1.1Y0M3DT4H5M6S';
329329
select interval 'P1.Y0M3DT4H5M6S';
330330
select interval 'P.1Y0M3DT4H5M6S';
331+
select interval 'P10.5e4Y'; -- not per spec, but we've historically taken it
331332
select interval 'P.Y0M3DT4H5M6S'; -- error
332333

333334
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.

0 commit comments

Comments
 (0)