On Sun, Apr 3, 2022 at 12:44 PM Joseph Koshakow <kosh...@gmail.com> wrote:
>
> On Sun, Apr 3, 2022 at 12:30 PM Tom Lane <t...@sss.pgh.pa.us> wrote:
> >
> > Joseph Koshakow <kosh...@gmail.com> writes:
> > > So I think we need to check that endptr has moved both after
> > > the call to strtoi64() and strtod().
> >
> > I'm not sure we need to do that explicitly, given that there's
> > a check later as to whether endptr is pointing at \0; that will
> > fail if endptr wasn't advanced.
> >
> > The fix I was loosely envisioning was to check for cp[1] == '\0'
> > and not bother calling strtod() in that case.
>
> Ah, ok I see what you mean. I agree an approach like that should
> work, but I don't actually think cp is null terminated in this case. The
> entire Interval is passed to DecodeISO8601Interval() as one big
> string, so the specific number we're parsing may be somewhere
> in the middle.
>
> If we just do the opposite and check isdigit(cp[1]) and only call
> strtod() in that case I think it should work.
>
> - Joe Koshakow

How does this patch look? I don't really have any way to test it on
AIX.

- Joe Koshakow
From 46b1ce5a78e21b65536c62ca6270c26c992a1ef7 Mon Sep 17 00:00:00 2001
From: Joseph Koshakow <kosh...@gmail.com>
Date: Sun, 3 Apr 2022 12:58:36 -0400
Subject: [PATCH] Fix parsing trailing decimal point in ISO8601

---
 src/backend/utils/adt/datetime.c       |  9 +++--
 src/test/regress/expected/interval.out | 49 ++++++++++++++++++++++++--
 src/test/regress/sql/interval.sql      | 11 +++++-
 3 files changed, 63 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 462f2ed7a8..178313e0d1 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -3676,8 +3676,13 @@ ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart)
 
 	/* Parse fractional part if there is any */
 	if (**endptr == '.')
-		*fpart = strtod(*endptr, endptr) * sign;
-
+	{
+		/* A decimal point with no trailing numbers should be parsed as 0 */
+		if (isdigit((unsigned char) *(*endptr + 1)))
+			*fpart = strtod(*endptr, endptr) * sign;
+		else
+			(*endptr)++;
+	}
 	/* did we not see anything that looks like a number? */
 	if (*endptr == str || errno != 0)
 		return DTERR_BAD_FORMAT;
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index 86c8d4bc99..ed051a55c4 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -1464,9 +1464,9 @@ select interval 'PT2562047788.1:00:54.775807';
 ERROR:  interval field value out of range: "PT2562047788.1:00:54.775807"
 LINE 1: select interval 'PT2562047788.1:00:54.775807';
                         ^
-select interval 'PT2562047788:01.:54.775807';
-ERROR:  interval field value out of range: "PT2562047788:01.:54.775807"
-LINE 1: select interval 'PT2562047788:01.:54.775807';
+select interval 'PT2562047788:01:54.775807';
+ERROR:  interval field value out of range: "PT2562047788:01:54.775807"
+LINE 1: select interval 'PT2562047788:01:54.775807';
                         ^
 -- overflowing with fractional fields - SQL standard format
 select interval '0.1 2562047788:0:54.775807';
@@ -1539,6 +1539,49 @@ select interval '-2147483648 months -2147483648 days -9223372036854775808 us';
  @ 178956970 years 8 mons 2147483648 days 2562047788 hours 54.775808 secs ago
 (1 row)
 
+-- check that ISO8601 format accepts trailing '.'
+select interval 'P1.Y2M3DT4H5M6S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1Y2.M3DT4H5M6S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1Y2M3.DT4H5M6S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1Y2M3DT4.H5M6S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1Y2M3DT4H5.M6S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1Y2M3DT4H5M6.S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
+select interval 'P1.Y2.M3.DT4.H5.M6.S';
+                   interval                   
+----------------------------------------------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs
+(1 row)
+
 -- check that '30 days' equals '1 month' according to the hash function
 select '30 days'::interval = '1 month'::interval as t;
  t 
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index f05055e03a..62d97f232c 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -474,7 +474,7 @@ select interval 'P00-0.1-2147483647';
 select interval 'P00-0.01-00T2562047788:00:54.775807';
 select interval 'P00-00-0.1T2562047788:00:54.775807';
 select interval 'PT2562047788.1:00:54.775807';
-select interval 'PT2562047788:01.:54.775807';
+select interval 'PT2562047788:01:54.775807';
 
 -- overflowing with fractional fields - SQL standard format
 select interval '0.1 2562047788:0:54.775807';
@@ -502,6 +502,15 @@ select interval '-2147483648 months -2147483648 days -9223372036854775808 us';
 SET IntervalStyle to postgres_verbose;
 select interval '-2147483648 months -2147483648 days -9223372036854775808 us';
 
+-- check that ISO8601 format accepts trailing '.'
+select interval 'P1.Y2M3DT4H5M6S';
+select interval 'P1Y2.M3DT4H5M6S';
+select interval 'P1Y2M3.DT4H5M6S';
+select interval 'P1Y2M3DT4.H5M6S';
+select interval 'P1Y2M3DT4H5.M6S';
+select interval 'P1Y2M3DT4H5M6.S';
+select interval 'P1.Y2.M3.DT4.H5.M6.S';
+
 -- check that '30 days' equals '1 month' according to the hash function
 select '30 days'::interval = '1 month'::interval as t;
 select interval_hash('30 days'::interval) = interval_hash('1 month'::interval) as t;
-- 
2.25.1

Reply via email to