diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 48dfe0a9c4..0b969eaa22 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -1,4 +1,4 @@ - + Data Types @@ -131,7 +131,7 @@ - interval [ (p) ] + interval [ fields ] [ (p) ] time span @@ -1420,7 +1420,7 @@ SELECT b, char_length(b) FROM test2; 1 microsecond / 14 digits - interval [ (p) ] + interval [ fields ] [ (p) ] 12 bytes time intervals -178000000 years @@ -1505,6 +1505,30 @@ SELECT b, char_length(b) FROM test2; storage is used, or from 0 to 10 when floating-point storage is used. + + The interval type has an additional option, which is + to restrict the set of stored fields by writing one of these phrases: + + YEAR + MONTH + DAY + HOUR + MINUTE + SECOND + YEAR TO MONTH + DAY TO HOUR + DAY TO MINUTE + DAY TO SECOND + HOUR TO MINUTE + MINUTE TO SECOND + + Input falling outside the specified set of fields is silently discarded. + Note that if both fields and + precision are specified, the + fields must include SECOND, + since the precision applies only to the seconds. + + The type time with time zone is defined by the SQL standard, but the definition exhibits properties which lead to @@ -1928,18 +1952,26 @@ January 8 04:05:06 1999 PST direction can be ago or empty. The at sign (@) is optional noise. The amounts of different units are implicitly added up with appropriate - sign accounting. + sign accounting. ago negates all the fields. Quantities of days, hours, minutes, and seconds can be specified without explicit unit markings. For example, '1 12:59:10' is read - the same as '1 day 12 hours 59 min 10 sec'. + the same as '1 day 12 hours 59 min 10 sec'. Also, + a combination of years and months can be specified with a dash; + for example '200-10' is read the same as '200 years + 10 months'. (These shorter forms are in fact the only ones allowed + by the SQL standard.) - The optional subsecond precision p should - be between 0 and 6, and defaults to the precision of the input literal. + When writing an interval constant with a fields + specification, or when assigning to an interval column that was defined + with a fields specification, the interpretation of + unmarked quantities depends on the fields. For + example INTERVAL '1' YEAR is read as 1 year, whereas + INTERVAL '1' means 1 second. diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5c716dd8f2..ecbd55d130 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11,7 +11,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.622 2008/09/02 20:37:54 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.623 2008/09/11 15:27:30 tgl Exp $ * * HISTORY * AUTHOR DATE MAJOR EVENT @@ -292,7 +292,7 @@ static TypeName *TableFuncTypeName(List *columns); %type extract_list overlay_list position_list %type substr_list trim_list -%type opt_interval +%type opt_interval interval_second %type overlay_placing substr_from substr_for %type opt_instead opt_analyze @@ -1222,28 +1222,39 @@ zone_value: | ConstInterval Sconst opt_interval { TypeName *t = $1; - if ($3 != INTERVAL_FULL_RANGE) + if ($3 != NIL) { - if (($3 & ~(INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE))) != 0) + A_Const *n = (A_Const *) linitial($3); + if ((n->val.val.ival & ~(INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE))) != 0) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("time zone interval must be HOUR or HOUR TO MINUTE"), scanner_errposition(@3))); - t->typmods = list_make1(makeIntConst($3, @3)); } + t->typmods = $3; $$ = makeStringConstCast($2, @2, t); } | ConstInterval '(' Iconst ')' Sconst opt_interval { TypeName *t = $1; - if (($6 != INTERVAL_FULL_RANGE) - && (($6 & ~(INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE))) != 0)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("time zone interval must be HOUR or HOUR TO MINUTE"), - scanner_errposition(@6))); - t->typmods = list_make2(makeIntConst($6, @6), - makeIntConst($3, @3)); + if ($6 != NIL) + { + A_Const *n = (A_Const *) linitial($6); + if ((n->val.val.ival & ~(INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE))) != 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("time zone interval must be HOUR or HOUR TO MINUTE"), + scanner_errposition(@6))); + if (list_length($6) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("interval precision specified twice"), + scanner_errposition(@1))); + t->typmods = lappend($6, makeIntConst($3, @3)); + } + else + t->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), + makeIntConst($3, @3)); $$ = makeStringConstCast($5, @5, t); } | NumericOnly { $$ = makeAConst($1, @1); } @@ -6983,14 +6994,23 @@ SimpleTypename: | ConstInterval opt_interval { $$ = $1; - if ($2 != INTERVAL_FULL_RANGE) - $$->typmods = list_make1(makeIntConst($2, @2)); + $$->typmods = $2; } | ConstInterval '(' Iconst ')' opt_interval { $$ = $1; - $$->typmods = list_make2(makeIntConst($5, @5), - makeIntConst($3, @3)); + if ($5 != NIL) + { + if (list_length($5) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("interval precision specified twice"), + scanner_errposition(@1))); + $$->typmods = lappend($5, makeIntConst($3, @3)); + } + else + $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), + makeIntConst($3, @3)); } ; @@ -7337,30 +7357,74 @@ opt_timezone: ; opt_interval: - YEAR_P { $$ = INTERVAL_MASK(YEAR); } - | MONTH_P { $$ = INTERVAL_MASK(MONTH); } - | DAY_P { $$ = INTERVAL_MASK(DAY); } - | HOUR_P { $$ = INTERVAL_MASK(HOUR); } - | MINUTE_P { $$ = INTERVAL_MASK(MINUTE); } - | SECOND_P { $$ = INTERVAL_MASK(SECOND); } + YEAR_P + { $$ = list_make1(makeIntConst(INTERVAL_MASK(YEAR), @1)); } + | MONTH_P + { $$ = list_make1(makeIntConst(INTERVAL_MASK(MONTH), @1)); } + | DAY_P + { $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY), @1)); } + | HOUR_P + { $$ = list_make1(makeIntConst(INTERVAL_MASK(HOUR), @1)); } + | MINUTE_P + { $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); } + | interval_second + { $$ = $1; } | YEAR_P TO MONTH_P - { $$ = INTERVAL_MASK(YEAR) | INTERVAL_MASK(MONTH); } + { + $$ = list_make1(makeIntConst(INTERVAL_MASK(YEAR) | + INTERVAL_MASK(MONTH), @1)); + } | DAY_P TO HOUR_P - { $$ = INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR); } + { + $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY) | + INTERVAL_MASK(HOUR), @1)); + } | DAY_P TO MINUTE_P - { $$ = INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) - | INTERVAL_MASK(MINUTE); } - | DAY_P TO SECOND_P - { $$ = INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) - | INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND); } + { + $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY) | + INTERVAL_MASK(HOUR) | + INTERVAL_MASK(MINUTE), @1)); + } + | DAY_P TO interval_second + { + $$ = $3; + linitial($$) = makeIntConst(INTERVAL_MASK(DAY) | + INTERVAL_MASK(HOUR) | + INTERVAL_MASK(MINUTE) | + INTERVAL_MASK(SECOND), @1); + } | HOUR_P TO MINUTE_P - { $$ = INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE); } - | HOUR_P TO SECOND_P - { $$ = INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE) - | INTERVAL_MASK(SECOND); } - | MINUTE_P TO SECOND_P - { $$ = INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND); } - | /*EMPTY*/ { $$ = INTERVAL_FULL_RANGE; } + { + $$ = list_make1(makeIntConst(INTERVAL_MASK(HOUR) | + INTERVAL_MASK(MINUTE), @1)); + } + | HOUR_P TO interval_second + { + $$ = $3; + linitial($$) = makeIntConst(INTERVAL_MASK(HOUR) | + INTERVAL_MASK(MINUTE) | + INTERVAL_MASK(SECOND), @1); + } + | MINUTE_P TO interval_second + { + $$ = $3; + linitial($$) = makeIntConst(INTERVAL_MASK(MINUTE) | + INTERVAL_MASK(SECOND), @1); + } + | /*EMPTY*/ + { $$ = NIL; } + ; + +interval_second: + SECOND_P + { + $$ = list_make1(makeIntConst(INTERVAL_MASK(SECOND), @1)); + } + | SECOND_P '(' Iconst ')' + { + $$ = list_make2(makeIntConst(INTERVAL_MASK(SECOND), @1), + makeIntConst($3, @3)); + } ; @@ -9014,16 +9078,24 @@ AexprConst: Iconst | ConstInterval Sconst opt_interval { TypeName *t = $1; - /* precision is not specified, but fields may be... */ - if ($3 != INTERVAL_FULL_RANGE) - t->typmods = list_make1(makeIntConst($3, @3)); + t->typmods = $3; $$ = makeStringConstCast($2, @2, t); } | ConstInterval '(' Iconst ')' Sconst opt_interval { TypeName *t = $1; - t->typmods = list_make2(makeIntConst($6, @6), - makeIntConst($3, @3)); + if ($6 != NIL) + { + if (list_length($6) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("interval precision specified twice"), + scanner_errposition(@1))); + t->typmods = lappend($6, makeIntConst($3, @3)); + } + else + t->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), + makeIntConst($3, @3)); $$ = makeStringConstCast($5, @5, t); } | TRUE_P diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 6712d6d8d4..a1cf2b4ed5 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.191 2008/09/10 18:29:41 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.192 2008/09/11 15:27:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -2256,9 +2256,25 @@ DecodeTime(char *str, int fmask, int range, tm->tm_hour = 0; } } - else if (*cp != ':') - return DTERR_BAD_FORMAT; - else + else if (*cp == '.') + { + /* always assume mm:ss.sss is MINUTE TO SECOND */ + double frac; + + str = cp; + frac = strtod(str, &cp); + if (*cp != '\0') + return DTERR_BAD_FORMAT; +#ifdef HAVE_INT64_TIMESTAMP + *fsec = rint(frac * 1000000); +#else + *fsec = frac; +#endif + tm->tm_sec = tm->tm_min; + tm->tm_min = tm->tm_hour; + tm->tm_hour = 0; + } + else if (*cp == ':') { str = cp + 1; errno = 0; @@ -2284,6 +2300,8 @@ DecodeTime(char *str, int fmask, int range, else return DTERR_BAD_FORMAT; } + else + return DTERR_BAD_FORMAT; /* do a sanity check */ #ifdef HAVE_INT64_TIMESTAMP diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 9060b989f9..acfc89ba04 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.191 2008/09/10 18:29:41 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.192 2008/09/11 15:27:30 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -744,7 +744,7 @@ intervaltypmodin(PG_FUNCTION_ARGS) tl = ArrayGetIntegerTypmods(ta, &n); /* - * tl[0] - opt_interval tl[1] - Iconst (optional) + * tl[0] - interval range (fields bitmask) tl[1] - precision (optional) * * Note we must validate tl[0] even though it's normally guaranteed * correct by the grammar --- consider SELECT 'foo'::"interval"(1000). @@ -881,7 +881,7 @@ intervaltypmodout(PG_FUNCTION_ARGS) } if (precision != INTERVAL_FULL_PRECISION) - snprintf(res, 64, "(%d)%s", precision, fieldstr); + snprintf(res, 64, "%s(%d)", fieldstr, precision); else snprintf(res, 64, "%s", fieldstr); diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index ede3c58708..a9b5766439 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -518,3 +518,100 @@ SELECT interval '1 2:03:04' minute to second; 00:03:04 (1 row) +-- test syntaxes for restricted precision +SELECT interval(0) '1 day 01:23:45.6789'; + interval +---------------- + 1 day 01:23:46 +(1 row) + +SELECT interval(2) '1 day 01:23:45.6789'; + interval +------------------- + 1 day 01:23:45.68 +(1 row) + +SELECT interval '12:34.5678' minute to second(2); -- per SQL spec + interval +------------- + 00:12:34.57 +(1 row) + +SELECT interval(2) '12:34.5678' minute to second; -- historical PG + interval +------------- + 00:12:34.57 +(1 row) + +SELECT interval(2) '12:34.5678' minute to second(2); -- syntax error +ERROR: interval precision specified twice +LINE 1: SELECT interval(2) '12:34.5678' minute to second(2); + ^ +SELECT interval '1.234' second; + interval +-------------- + 00:00:01.234 +(1 row) + +SELECT interval '1.234' second(2); + interval +------------- + 00:00:01.23 +(1 row) + +SELECT interval '1 2.345' day to second(2); + interval +---------------- + 1 day 02:20:42 +(1 row) + +SELECT interval '1 2:03' day to second(2); + interval +---------------- + 1 day 02:03:00 +(1 row) + +SELECT interval '1 2:03.4567' day to second(2); + interval +------------------- + 1 day 00:02:03.46 +(1 row) + +SELECT interval '1 2:03:04.5678' day to second(2); + interval +------------------- + 1 day 02:03:04.57 +(1 row) + +SELECT interval '1 2.345' hour to second(2); +ERROR: invalid input syntax for type interval: "1 2.345" +LINE 1: SELECT interval '1 2.345' hour to second(2); + ^ +SELECT interval '1 2:03.45678' hour to second(2); + interval +------------- + 00:02:03.46 +(1 row) + +SELECT interval '1 2:03:04.5678' hour to second(2); + interval +------------- + 02:03:04.57 +(1 row) + +SELECT interval '1 2.3456' minute to second(2); +ERROR: invalid input syntax for type interval: "1 2.3456" +LINE 1: SELECT interval '1 2.3456' minute to second(2); + ^ +SELECT interval '1 2:03.5678' minute to second(2); + interval +------------- + 00:02:03.57 +(1 row) + +SELECT interval '1 2:03:04.5678' minute to second(2); + interval +------------- + 00:03:04.57 +(1 row) + diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql index 2c6aecaa51..06e4705859 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -156,3 +156,22 @@ 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; + +-- test syntaxes for restricted precision +SELECT interval(0) '1 day 01:23:45.6789'; +SELECT interval(2) '1 day 01:23:45.6789'; +SELECT interval '12:34.5678' minute to second(2); -- per SQL spec +SELECT interval(2) '12:34.5678' minute to second; -- historical PG +SELECT interval(2) '12:34.5678' minute to second(2); -- syntax error +SELECT interval '1.234' second; +SELECT interval '1.234' second(2); +SELECT interval '1 2.345' day to second(2); +SELECT interval '1 2:03' day to second(2); +SELECT interval '1 2:03.4567' day to second(2); +SELECT interval '1 2:03:04.5678' day to second(2); +SELECT interval '1 2.345' hour to second(2); +SELECT interval '1 2:03.45678' hour to second(2); +SELECT interval '1 2:03:04.5678' hour to second(2); +SELECT interval '1 2.3456' minute to second(2); +SELECT interval '1 2:03.5678' minute to second(2); +SELECT interval '1 2:03:04.5678' minute to second(2);