mirror of https://github.com/postgres/postgres
Fix interval_transform so it doesn't throw away non-no-op casts.
interval_transform() contained two separate bugs that caused it to sometimes mistakenly decide that a cast from interval to restricted interval is a no-op and throw it away. First, it was wrong to rely on dt.h's field type macros to have an ordering consistent with the field's significance; in one case they do not. This led to mistakenly treating YEAR as less significant than MONTH, so that a cast from INTERVAL MONTH to INTERVAL YEAR was incorrectly discarded. Second, fls(1<<k) produces k+1 not k, so comparing its output directly to SECOND was wrong. This led to supposing that a cast to INTERVAL MINUTE was really a cast to INTERVAL SECOND and so could be discarded. To fix, get rid of the use of fls(), and make a function based on intervaltypmodout to produce a field ID code adapted to the need here. Per bug #14479 from Piotr Stefaniak. Back-patch to 9.2 where transform functions were introduced, because this code was born broken. Discussion: https://postgr.es/m/20161227172307.10135.7747@wrigleys.postgresql.org
This commit is contained in:
parent
71f996d221
commit
f0774abde8
|
@ -1250,6 +1250,59 @@ intervaltypmodout(PG_FUNCTION_ARGS)
|
|||
PG_RETURN_CSTRING(res);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given an interval typmod value, return a code for the least-significant
|
||||
* field that the typmod allows to be nonzero, for instance given
|
||||
* INTERVAL DAY TO HOUR we want to identify "hour".
|
||||
*
|
||||
* The results should be ordered by field significance, which means
|
||||
* we can't use the dt.h macros YEAR etc, because for some odd reason
|
||||
* they aren't ordered that way. Instead, arbitrarily represent
|
||||
* SECOND = 0, MINUTE = 1, HOUR = 2, DAY = 3, MONTH = 4, YEAR = 5.
|
||||
*/
|
||||
static int
|
||||
intervaltypmodleastfield(int32 typmod)
|
||||
{
|
||||
if (typmod < 0)
|
||||
return 0; /* SECOND */
|
||||
|
||||
switch (INTERVAL_RANGE(typmod))
|
||||
{
|
||||
case INTERVAL_MASK(YEAR):
|
||||
return 5; /* YEAR */
|
||||
case INTERVAL_MASK(MONTH):
|
||||
return 4; /* MONTH */
|
||||
case INTERVAL_MASK(DAY):
|
||||
return 3; /* DAY */
|
||||
case INTERVAL_MASK(HOUR):
|
||||
return 2; /* HOUR */
|
||||
case INTERVAL_MASK(MINUTE):
|
||||
return 1; /* MINUTE */
|
||||
case INTERVAL_MASK(SECOND):
|
||||
return 0; /* SECOND */
|
||||
case INTERVAL_MASK(YEAR) | INTERVAL_MASK(MONTH):
|
||||
return 4; /* MONTH */
|
||||
case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR):
|
||||
return 2; /* HOUR */
|
||||
case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE):
|
||||
return 1; /* MINUTE */
|
||||
case INTERVAL_MASK(DAY) | INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
|
||||
return 0; /* SECOND */
|
||||
case INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE):
|
||||
return 1; /* MINUTE */
|
||||
case INTERVAL_MASK(HOUR) | INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
|
||||
return 0; /* SECOND */
|
||||
case INTERVAL_MASK(MINUTE) | INTERVAL_MASK(SECOND):
|
||||
return 0; /* SECOND */
|
||||
case INTERVAL_FULL_RANGE:
|
||||
return 0; /* SECOND */
|
||||
default:
|
||||
elog(ERROR, "invalid INTERVAL typmod: 0x%x", typmod);
|
||||
break;
|
||||
}
|
||||
return 0; /* can't get here, but keep compiler quiet */
|
||||
}
|
||||
|
||||
|
||||
/* interval_transform()
|
||||
* Flatten superfluous calls to interval_scale(). The interval typmod is
|
||||
|
@ -1271,39 +1324,39 @@ interval_transform(PG_FUNCTION_ARGS)
|
|||
if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
|
||||
{
|
||||
Node *source = (Node *) linitial(expr->args);
|
||||
int32 old_typmod = exprTypmod(source);
|
||||
int32 new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
|
||||
int old_range;
|
||||
int old_precis;
|
||||
int new_range = INTERVAL_RANGE(new_typmod);
|
||||
int new_precis = INTERVAL_PRECISION(new_typmod);
|
||||
int new_range_fls;
|
||||
int old_range_fls;
|
||||
bool noop;
|
||||
|
||||
if (old_typmod < 0)
|
||||
{
|
||||
old_range = INTERVAL_FULL_RANGE;
|
||||
old_precis = INTERVAL_FULL_PRECISION;
|
||||
}
|
||||
if (new_typmod < 0)
|
||||
noop = true;
|
||||
else
|
||||
{
|
||||
old_range = INTERVAL_RANGE(old_typmod);
|
||||
old_precis = INTERVAL_PRECISION(old_typmod);
|
||||
}
|
||||
int32 old_typmod = exprTypmod(source);
|
||||
int old_least_field;
|
||||
int new_least_field;
|
||||
int old_precis;
|
||||
int new_precis;
|
||||
|
||||
/*
|
||||
* Temporally-smaller fields occupy higher positions in the range
|
||||
* bitmap. Since only the temporally-smallest bit matters for length
|
||||
* coercion purposes, we compare the last-set bits in the ranges.
|
||||
* Precision, which is to say, sub-second precision, only affects
|
||||
* ranges that include SECOND.
|
||||
*/
|
||||
new_range_fls = fls(new_range);
|
||||
old_range_fls = fls(old_range);
|
||||
if (new_typmod < 0 ||
|
||||
((new_range_fls >= SECOND || new_range_fls >= old_range_fls) &&
|
||||
(old_range_fls < SECOND || new_precis >= MAX_INTERVAL_PRECISION ||
|
||||
new_precis >= old_precis)))
|
||||
old_least_field = intervaltypmodleastfield(old_typmod);
|
||||
new_least_field = intervaltypmodleastfield(new_typmod);
|
||||
if (old_typmod < 0)
|
||||
old_precis = INTERVAL_FULL_PRECISION;
|
||||
else
|
||||
old_precis = INTERVAL_PRECISION(old_typmod);
|
||||
new_precis = INTERVAL_PRECISION(new_typmod);
|
||||
|
||||
/*
|
||||
* Cast is a no-op if least field stays the same or decreases
|
||||
* while precision stays the same or increases. But precision,
|
||||
* which is to say, sub-second precision, only affects ranges that
|
||||
* include SECOND.
|
||||
*/
|
||||
noop = (new_least_field <= old_least_field) &&
|
||||
(old_least_field > 0 /* SECOND */ ||
|
||||
new_precis >= MAX_INTERVAL_PRECISION ||
|
||||
new_precis >= old_precis);
|
||||
}
|
||||
if (noop)
|
||||
ret = relabel_to_typmod(source, new_typmod);
|
||||
}
|
||||
|
||||
|
|
|
@ -682,6 +682,24 @@ SELECT interval '1 2:03:04.5678' minute to second(2);
|
|||
1 day 02:03:04.57
|
||||
(1 row)
|
||||
|
||||
-- test casting to restricted precision (bug #14479)
|
||||
SELECT f1, f1::INTERVAL DAY TO MINUTE AS "minutes",
|
||||
(f1 + INTERVAL '1 month')::INTERVAL MONTH::INTERVAL YEAR AS "years"
|
||||
FROM interval_tbl;
|
||||
f1 | minutes | years
|
||||
-----------------+-----------------+----------
|
||||
00:01:00 | 00:01:00 | 00:00:00
|
||||
05:00:00 | 05:00:00 | 00:00:00
|
||||
10 days | 10 days | 00:00:00
|
||||
34 years | 34 years | 34 years
|
||||
3 mons | 3 mons | 00:00:00
|
||||
-00:00:14 | 00:00:00 | 00:00:00
|
||||
1 day 02:03:04 | 1 day 02:03:00 | 00:00:00
|
||||
6 years | 6 years | 6 years
|
||||
5 mons | 5 mons | 00:00:00
|
||||
5 mons 12:00:00 | 5 mons 12:00:00 | 00:00:00
|
||||
(10 rows)
|
||||
|
||||
-- test inputting and outputting SQL standard interval literals
|
||||
SET IntervalStyle TO sql_standard;
|
||||
SELECT interval '0' AS "zero",
|
||||
|
|
|
@ -196,6 +196,11 @@ 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);
|
||||
|
||||
-- test casting to restricted precision (bug #14479)
|
||||
SELECT f1, f1::INTERVAL DAY TO MINUTE AS "minutes",
|
||||
(f1 + INTERVAL '1 month')::INTERVAL MONTH::INTERVAL YEAR AS "years"
|
||||
FROM interval_tbl;
|
||||
|
||||
-- test inputting and outputting SQL standard interval literals
|
||||
SET IntervalStyle TO sql_standard;
|
||||
SELECT interval '0' AS "zero",
|
||||
|
|
Loading…
Reference in New Issue