From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Wed, 10 Sep 2008 18:29:41 +0000 (+0000)
Subject: Make our parsing of INTERVAL literals spec-compliant (or at least a heck of
X-Git-Tag: REL8_4_BETA1~998
X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=f867339c0148381eb1d01f93ab5c79f9d10211de;p=postgresql

Make our parsing of INTERVAL literals spec-compliant (or at least a heck of
a lot closer than it was before).  To do this, tweak coerce_type() to pass
through the typmod information when invoking interval_in() on an UNKNOWN
constant; then fix DecodeInterval to pay attention to the typmod when deciding
how to interpret a units-less integer value.  I changed one or two other
details as well.  I believe the code now reacts as expected by spec for all
the literal syntaxes that are specifically enumerated in the spec.  There
are corner cases involving strings that don't exactly match the set of fields
called out by the typmod, for which we might want to tweak the behavior some
more; but I think this is an area of user friendliness rather than spec
compliance.  There remain some non-compliant details about the SQL syntax
(as opposed to what's inside the literal string); but at least we'll throw
error rather than silently doing the wrong thing in those cases.
---

diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index efe45974c3..86e47661aa 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/parser/parse_coerce.c,v 2.166 2008/09/01 20:42:44 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/parser/parse_coerce.c,v 2.167 2008/09/10 18:29:40 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -179,6 +179,7 @@ coerce_type(ParseState *pstate, Node *node,
 		Const	   *newcon = makeNode(Const);
 		Oid			baseTypeId;
 		int32		baseTypeMod;
+		int32		inputTypeMod;
 		Type		targetType;
 		ParseCallbackState pcbstate;
 
@@ -190,13 +191,27 @@ coerce_type(ParseState *pstate, Node *node,
 		 * what we want here.  The needed check will be applied properly
 		 * inside coerce_to_domain().
 		 */
-		baseTypeMod = -1;
+		baseTypeMod = targetTypeMod;
 		baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
 
+		/*
+		 * For most types we pass typmod -1 to the input routine, because
+		 * existing input routines follow implicit-coercion semantics for
+		 * length checks, which is not always what we want here.  Any length
+		 * constraint will be applied later by our caller.  An exception
+		 * however is the INTERVAL type, for which we *must* pass the typmod
+		 * or it won't be able to obey the bizarre SQL-spec input rules.
+		 * (Ugly as sin, but so is this part of the spec...)
+		 */
+		if (baseTypeId == INTERVALOID)
+			inputTypeMod = baseTypeMod;
+		else
+			inputTypeMod = -1;
+
 		targetType = typeidType(baseTypeId);
 
 		newcon->consttype = baseTypeId;
-		newcon->consttypmod = -1;
+		newcon->consttypmod = inputTypeMod;
 		newcon->constlen = typeLen(targetType);
 		newcon->constbyval = typeByVal(targetType);
 		newcon->constisnull = con->constisnull;
@@ -215,20 +230,17 @@ coerce_type(ParseState *pstate, Node *node,
 		setup_parser_errposition_callback(&pcbstate, pstate, con->location);
 
 		/*
-		 * We pass typmod -1 to the input routine, primarily because existing
-		 * input routines follow implicit-coercion semantics for length
-		 * checks, which is not always what we want here. Any length
-		 * constraint will be applied later by our caller.
-		 *
 		 * We assume here that UNKNOWN's internal representation is the same
 		 * as CSTRING.
 		 */
 		if (!con->constisnull)
 			newcon->constvalue = stringTypeDatum(targetType,
 											DatumGetCString(con->constvalue),
-												 -1);
+												 inputTypeMod);
 		else
-			newcon->constvalue = stringTypeDatum(targetType, NULL, -1);
+			newcon->constvalue = stringTypeDatum(targetType,
+												 NULL,
+												 inputTypeMod);
 
 		cancel_parser_errposition_callback(&pcbstate);
 
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index a92583ca19..6712d6d8d4 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.190 2008/06/09 19:34:02 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.191 2008/09/10 18:29:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -35,8 +35,8 @@ static int DecodeNumber(int flen, char *field, bool haveTextMonth,
 static int DecodeNumberField(int len, char *str,
 				  int fmask, int *tmask,
 				  struct pg_tm * tm, fsec_t *fsec, bool *is2digits);
-static int DecodeTime(char *str, int fmask, int *tmask,
-		   struct pg_tm * tm, fsec_t *fsec);
+static int DecodeTime(char *str, int fmask, int range,
+		   int *tmask, struct pg_tm * tm, fsec_t *fsec);
 static int	DecodeTimezone(char *str, int *tzp);
 static const datetkn *datebsearch(const char *key, const datetkn *base, int nel);
 static int	DecodeDate(char *str, int fmask, int *tmask, bool *is2digits,
@@ -832,7 +832,8 @@ DecodeDateTime(char **field, int *ftype, int nf,
 				break;
 
 			case DTK_TIME:
-				dterr = DecodeTime(field[i], fmask, &tmask, tm, fsec);
+				dterr = DecodeTime(field[i], fmask, INTERVAL_FULL_RANGE,
+								   &tmask, tm, fsec);
 				if (dterr)
 					return dterr;
 
@@ -1563,6 +1564,7 @@ DecodeTimeOnly(char **field, int *ftype, int nf,
 
 			case DTK_TIME:
 				dterr = DecodeTime(field[i], (fmask | DTK_DATE_M),
+								   INTERVAL_FULL_RANGE,
 								   &tmask, tm, fsec);
 				if (dterr)
 					return dterr;
@@ -2224,7 +2226,8 @@ ValidateDate(int fmask, bool is2digits, bool bc, struct pg_tm * tm)
  * used to represent time spans.
  */
 static int
-DecodeTime(char *str, int fmask, int *tmask, struct pg_tm * tm, fsec_t *fsec)
+DecodeTime(char *str, int fmask, int range,
+		   int *tmask, struct pg_tm * tm, fsec_t *fsec)
 {
 	char	   *cp;
 
@@ -2245,6 +2248,13 @@ DecodeTime(char *str, int fmask, int *tmask, struct pg_tm * tm, fsec_t *fsec)
 	{
 		tm->tm_sec = 0;
 		*fsec = 0;
+		/* If it's a MINUTE TO SECOND interval, take 2 fields as being mm:ss */
+		if (range == (INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND)))
+		{
+			tm->tm_sec = tm->tm_min;
+			tm->tm_min = tm->tm_hour;
+			tm->tm_hour = 0;
+		}
 	}
 	else if (*cp != ':')
 		return DTERR_BAD_FORMAT;
@@ -2705,7 +2715,8 @@ DecodeSpecial(int field, char *lowtoken, int *val)
  *	preceding an hh:mm:ss field. - thomas 1998-04-30
  */
 int
-DecodeInterval(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm, fsec_t *fsec)
+DecodeInterval(char **field, int *ftype, int nf, int range,
+			   int *dtype, struct pg_tm * tm, fsec_t *fsec)
 {
 	bool		is_before = FALSE;
 	char	   *cp;
@@ -2734,7 +2745,8 @@ DecodeInterval(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm,
 		switch (ftype[i])
 		{
 			case DTK_TIME:
-				dterr = DecodeTime(field[i], fmask, &tmask, tm, fsec);
+				dterr = DecodeTime(field[i], fmask, range,
+								   &tmask, tm, fsec);
 				if (dterr)
 					return dterr;
 				type = DTK_DAY;
@@ -2757,7 +2769,8 @@ DecodeInterval(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm,
 				while (*cp != '\0' && *cp != ':' && *cp != '.')
 					cp++;
 				if (*cp == ':' &&
-					DecodeTime(field[i] + 1, fmask, &tmask, tm, fsec) == 0)
+					DecodeTime(field[i] + 1, fmask, INTERVAL_FULL_RANGE,
+							   &tmask, tm, fsec) == 0)
 				{
 					if (*field[i] == '-')
 					{
@@ -2796,19 +2809,66 @@ DecodeInterval(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm,
 						type = DTK_HOUR;
 					}
 				}
-				/* DROP THROUGH */
+				/* FALL THROUGH */
 
 			case DTK_DATE:
 			case DTK_NUMBER:
+				if (type == IGNORE_DTF)
+				{
+					/* use typmod to decide what rightmost integer field is */
+					switch (range)
+					{
+						case INTERVAL_MASK(YEAR):
+							type = DTK_YEAR;
+							break;
+						case INTERVAL_MASK(MONTH):
+						case INTERVAL_MASK(YEAR) | INTERVAL_MASK(MONTH):
+							type = DTK_MONTH;
+							break;
+						case INTERVAL_MASK(DAY):
+							type = DTK_DAY;
+							break;
+						case INTERVAL_MASK(HOUR):
+						case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR):
+						case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE):
+						case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
+							type = DTK_HOUR;
+							break;
+						case INTERVAL_MASK(MINUTE):
+						case INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE):
+							type = DTK_MINUTE;
+							break;
+						case INTERVAL_MASK(SECOND):
+						case INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
+						case INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
+							type = DTK_SECOND;
+							break;
+						default:
+							type = DTK_SECOND;
+							break;
+					}
+				}
+
 				errno = 0;
 				val = strtoi(field[i], &cp, 10);
 				if (errno == ERANGE)
 					return DTERR_FIELD_OVERFLOW;
 
-				if (type == IGNORE_DTF)
-					type = DTK_SECOND;
+				if (*cp == '-')
+				{
+					/* SQL "years-months" syntax */
+					int		val2;
 
-				if (*cp == '.')
+					val2 = strtoi(cp + 1, &cp, 10);
+					if (errno == ERANGE || val2 < 0 || val2 >= MONTHS_PER_YEAR)
+						return DTERR_FIELD_OVERFLOW;
+					if (*cp != '\0')
+						return DTERR_BAD_FORMAT;
+					type = DTK_MONTH;
+					val = val * MONTHS_PER_YEAR + val2;
+					fval = 0;
+				}
+				else if (*cp == '.')
 				{
 					fval = strtod(cp, &cp);
 					if (*cp != '\0')
@@ -2896,6 +2956,7 @@ DecodeInterval(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm,
 #endif
 						}
 						tmask = DTK_M(HOUR);
+						type = DTK_DAY;
 						break;
 
 					case DTK_DAY:
diff --git a/src/backend/utils/adt/nabstime.c b/src/backend/utils/adt/nabstime.c
index a40ca5edd0..4a505c341e 100644
--- a/src/backend/utils/adt/nabstime.c
+++ b/src/backend/utils/adt/nabstime.c
@@ -10,7 +10,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/nabstime.c,v 1.155 2008/03/25 22:42:44 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/nabstime.c,v 1.156 2008/09/10 18:29:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -632,7 +632,8 @@ reltimein(PG_FUNCTION_ARGS)
 	dterr = ParseDateTime(str, workbuf, sizeof(workbuf),
 						  field, ftype, MAXDATEFIELDS, &nf);
 	if (dterr == 0)
-		dterr = DecodeInterval(field, ftype, nf, &dtype, tm, &fsec);
+		dterr = DecodeInterval(field, ftype, nf, INTERVAL_FULL_RANGE,
+							   &dtype, tm, &fsec);
 	if (dterr != 0)
 	{
 		if (dterr == DTERR_FIELD_OVERFLOW)
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index d6a5dee084..9060b989f9 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.190 2008/07/07 18:09:46 tgl Exp $
+ *	  $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.191 2008/09/10 18:29:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -604,6 +604,7 @@ interval_in(PG_FUNCTION_ARGS)
 			   *tm = &tt;
 	int			dtype;
 	int			nf;
+	int			range;
 	int			dterr;
 	char	   *field[MAXDATEFIELDS];
 	int			ftype[MAXDATEFIELDS];
@@ -617,10 +618,15 @@ interval_in(PG_FUNCTION_ARGS)
 	tm->tm_sec = 0;
 	fsec = 0;
 
+	if (typmod >= 0)
+		range = INTERVAL_RANGE(typmod);
+	else
+		range = INTERVAL_FULL_RANGE;
+
 	dterr = ParseDateTime(str, workbuf, sizeof(workbuf), field,
 						  ftype, MAXDATEFIELDS, &nf);
 	if (dterr == 0)
-		dterr = DecodeInterval(field, ftype, nf, &dtype, tm, &fsec);
+		dterr = DecodeInterval(field, ftype, nf, range, &dtype, tm, &fsec);
 	if (dterr != 0)
 	{
 		if (dterr == DTERR_FIELD_OVERFLOW)
@@ -945,7 +951,7 @@ AdjustIntervalForTypmod(Interval *interval, int32 typmod)
 	 * Unspecified range and precision? Then not necessary to adjust. Setting
 	 * typmod to -1 is the convention for all types.
 	 */
-	if (typmod != -1)
+	if (typmod >= 0)
 	{
 		int			range = INTERVAL_RANGE(typmod);
 		int			precision = INTERVAL_PRECISION(typmod);
diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h
index ffb4c45240..439e9779d2 100644
--- a/src/include/utils/datetime.h
+++ b/src/include/utils/datetime.h
@@ -9,7 +9,7 @@
  * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/utils/datetime.h,v 1.69 2008/01/01 19:45:59 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/utils/datetime.h,v 1.70 2008/09/10 18:29:41 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -290,7 +290,7 @@ extern int DecodeTimeOnly(char **field, int *ftype,
 			   int nf, int *dtype,
 			   struct pg_tm * tm, fsec_t *fsec, int *tzp);
 extern int DecodeInterval(char **field, int *ftype,
-			   int nf, int *dtype,
+			   int nf, int range, int *dtype,
 			   struct pg_tm * tm, fsec_t *fsec);
 extern void DateTimeParseError(int dterr, const char *str,
 				   const char *datatype);
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index 049850af8e..ede3c58708 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -361,3 +361,160 @@ SELECT '1:20:05 5 microseconds'::interval;          -- error
 ERROR:  invalid input syntax for type interval: "1:20:05 5 microseconds"
 LINE 1: SELECT '1:20:05 5 microseconds'::interval;
                ^
+SELECT interval '1-2';  -- SQL year-month literal
+   interval    
+---------------
+ 1 year 2 mons
+(1 row)
+
+-- test SQL-spec syntaxes for restricted field sets
+SELECT interval '1' year;
+ interval 
+----------
+ 1 year
+(1 row)
+
+SELECT interval '2' month;
+ interval 
+----------
+ 2 mons
+(1 row)
+
+SELECT interval '3' day;
+ interval 
+----------
+ 3 days
+(1 row)
+
+SELECT interval '4' hour;
+ interval 
+----------
+ 04:00:00
+(1 row)
+
+SELECT interval '5' minute;
+ interval 
+----------
+ 00:05:00
+(1 row)
+
+SELECT interval '6' second;
+ interval 
+----------
+ 00:00:06
+(1 row)
+
+SELECT interval '1' year to month;
+ interval 
+----------
+ 1 mon
+(1 row)
+
+SELECT interval '1-2' year to month;
+   interval    
+---------------
+ 1 year 2 mons
+(1 row)
+
+SELECT interval '1 2' day to hour;
+    interval    
+----------------
+ 1 day 02:00:00
+(1 row)
+
+SELECT interval '1 2:03' day to hour;
+    interval    
+----------------
+ 1 day 02:00:00
+(1 row)
+
+SELECT interval '1 2:03:04' day to hour;
+    interval    
+----------------
+ 1 day 02:00:00
+(1 row)
+
+SELECT interval '1 2' day to minute;
+    interval    
+----------------
+ 1 day 02:00:00
+(1 row)
+
+SELECT interval '1 2:03' day to minute;
+    interval    
+----------------
+ 1 day 02:03:00
+(1 row)
+
+SELECT interval '1 2:03:04' day to minute;
+    interval    
+----------------
+ 1 day 02:03:00
+(1 row)
+
+SELECT interval '1 2' day to second;
+    interval    
+----------------
+ 1 day 02:00:00
+(1 row)
+
+SELECT interval '1 2:03' day to second;
+    interval    
+----------------
+ 1 day 02:03:00
+(1 row)
+
+SELECT interval '1 2:03:04' day to second;
+    interval    
+----------------
+ 1 day 02:03:04
+(1 row)
+
+SELECT interval '1 2' hour to minute;
+ERROR:  invalid input syntax for type interval: "1 2"
+LINE 1: SELECT interval '1 2' hour to minute;
+                        ^
+SELECT interval '1 2:03' hour to minute;
+ interval 
+----------
+ 02:03:00
+(1 row)
+
+SELECT interval '1 2:03:04' hour to minute;
+ interval 
+----------
+ 02:03:00
+(1 row)
+
+SELECT interval '1 2' hour to second;
+ERROR:  invalid input syntax for type interval: "1 2"
+LINE 1: SELECT interval '1 2' hour to second;
+                        ^
+SELECT interval '1 2:03' hour to second;
+ interval 
+----------
+ 02:03:00
+(1 row)
+
+SELECT interval '1 2:03:04' hour to second;
+ interval 
+----------
+ 02:03:04
+(1 row)
+
+SELECT interval '1 2' minute to second;
+ERROR:  invalid input syntax for type interval: "1 2"
+LINE 1: SELECT interval '1 2' minute to second;
+                        ^
+SELECT interval '1 2:03' minute to second;
+ interval 
+----------
+ 00:02:03
+(1 row)
+
+SELECT interval '1 2:03:04' minute to second;
+ interval 
+----------
+ 00:03:04
+(1 row)
+
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index d081bf1ffe..2c6aecaa51 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -126,4 +126,33 @@ SELECT '3 days 5 milliseconds'::interval;
 SELECT '1 second 2 seconds'::interval;              -- error
 SELECT '10 milliseconds 20 milliseconds'::interval; -- error
 SELECT '5.5 seconds 3 milliseconds'::interval;      -- error
-SELECT '1:20:05 5 microseconds'::interval;          -- error
\ No newline at end of file
+SELECT '1:20:05 5 microseconds'::interval;          -- error
+SELECT interval '1-2';  -- SQL year-month literal
+
+-- test SQL-spec syntaxes for restricted field sets
+SELECT interval '1' year;
+SELECT interval '2' month;
+SELECT interval '3' day;
+SELECT interval '4' hour;
+SELECT interval '5' minute;
+SELECT interval '6' second;
+SELECT interval '1' year to month;
+SELECT interval '1-2' year to month;
+SELECT interval '1 2' day to hour;
+SELECT interval '1 2:03' day to hour;
+SELECT interval '1 2:03:04' day to hour;
+SELECT interval '1 2' day to minute;
+SELECT interval '1 2:03' day to minute;
+SELECT interval '1 2:03:04' day to minute;
+SELECT interval '1 2' day to second;
+SELECT interval '1 2:03' day to second;
+SELECT interval '1 2:03:04' day to second;
+SELECT interval '1 2' hour to minute;
+SELECT interval '1 2:03' hour to minute;
+SELECT interval '1 2:03:04' hour to minute;
+SELECT interval '1 2' hour to second;
+SELECT interval '1 2:03' hour to second;
+SELECT interval '1 2:03:04' hour to second;
+SELECT interval '1 2' minute to second;
+SELECT interval '1 2:03' minute to second;
+SELECT interval '1 2:03:04' minute to second;