1
0
mirror of https://github.com/MariaDB/server.git synced 2025-07-27 18:02:13 +03:00

MDEV-17351 Wrong results for GREATEST,TIMESTAMP,ADDTIME with an out-of-range TIME-alike argument

Problems:

Functions LEAST() and GREATEST() in TIME context, as well as functions
TIMESTAMP(a,b) and ADDTIME(a,b), returned confusing results when the
input TIME-alike value in a number or in a string was out of the TIME
supported range.

In case of TIMESTAMP(a,b) and ADDTIME(a,b), the second argument
value could get extra unexpected digits. For example, in:
    ADDTIME('2001-01-01 00:00:00', 10000000)  or
    ADDTIME('2001-01-01 00:00:00', '1000:00:00')
the second argument was converted to '838:59:59.999999'
with six fractional digits, which contradicted "decimals"
previously set to 0 in fix_length_and_dec().
These unexpected fractional digits led to confusing function results.

Changes:
1. GREATEST(), LEAST()

   - fixing Item_func_min_max::get_time_native()
   to respect "decimals" set by fix_length_and_dec().
   If a value of some numeric or string time-alike argument
   goes outside of the TIME range and gets limited to '838:59:59.999999',
   it's now right-truncated to the correct fractional precision.

   - fixing, Type_handler_temporal_result::Item_func_min_max_fix_attributes()
   to take into account arguments' time_precision() or datetime_precision(),
   rather than rely on "decimals" calculated by the generic implementation
   in Type_handler::Item_func_min_max_fix_attributes(). This makes
   GREATEST() and LEAST() return better data types, with the same
   fractional precision with what TIMESTAMP(a,b) and ADDTIME(a,b) return
   for the same arguments, and with DATE(a) and TIMESTAMP(a).

2. Item_func_add_time and Item_func_timestamp

   It was semantically wrong to apply the limit of the TIME data type
   to the argument "b", which plays the role of "INTERVAL DAY TO SECOND" here.
   Changing the code to fetch the argument "b" as INTERVAL rather than as TIME.

   The low level routine calc_time_diff() now gets the interval
   value without limiting to '838:59:59.999999', so in these examples:
     ADDTIME('2001-01-01 00:00:00', 10000000)
     ADDTIME('2001-01-01 00:00:00', '1000:00:00')
   calc_time_diff() gets '1000:00:00' as is.  The SQL function result
   now gets limited to the supported result data type range
   (datetime or time) inside calc_time_diff(), which now calculates
   the return value using the real fractional digits that
   came directly from the arguments (without the effect of limiting
   to the TIME range), so the result does not have any unexpected
   fractional digits any more.

   Detailed changes in TIMESTAMP() and ADDTIME():

   - Adding a new class Interval_DDhhmmssff. It's similar to Time, but:
     * does not try to parse datetime format, as it's not needed for
       functions TIMESTAMP() and ADDTIME().
     * does not cut values to '838:59:59.999999'

     The maximum supported Interval_DDhhmmssff's hard limit is
     'UINT_MAX32:59:59.999999'. The maximum used soft limit is:
     - '87649415:59:59.999999'   (in 'hh:mm:ss.ff' format)
     - '3652058 23:59:59.999999' (in 'DD hh:mm:ss.ff' format)
     which is a difference between:
     - TIMESTAMP'0001-01-01 00:00:00' and
     - TIMESTAMP'9999-12-31 23:59:59.999999'
     (the minimum datetime that supports arithmetic, and the
     maximum possible datetime value).

   - Fixing get_date() methods in the classes related to functions
     ADDTIME(a,b) and TIMESTAMP(a,b) to use the new class Interval_DDhhmmssff
     for fetching data from the second argument, instead of get_date().

   - Fixing fix_length_and_dec() methods in the classes related
     to functions ADDTIME(a,b) and TIMESTAMP(a,b) to use
     Interval_DDhhmmssff::fsp(item) instead of item->time_precision()
     to get the fractional precision of the second argument correctly.

   - Splitting the low level function str_to_time() into smaller pieces
     to reuse the code. Adding a new function str_to_DDhhmmssff(), to
     parse "INTERVAL DAY TO SECOND" values.

   After these changes, functions TIMESTAMP() and ADDTIME()
   return much more predictable results, in terms of fractional
   digits, and in terms of the overall result.

   The full ranges of DATETIME and TIME values are now covered by TIMESTAMP()
   and ADDTIME(), so the following can now be calculated:

    SELECT ADDTIME(TIMESTAMP'0001-01-01 00:00:00', '87649415:59:59.999999');
    -> '9999-12-31 23:59:59.999999'

    SELECT TIMESTAMP(DATE'0001-01-01', '87649415:59:59.999999')
    -> '9999-12-31 23:59:59.999999'

    SELECT ADDTIME(TIME'-838:59:59.999999', '1677:59:59.999998');
    -> '838:59:59.999999'
This commit is contained in:
Alexander Barkov
2018-10-08 13:38:01 +04:00
parent d03581bf3c
commit b639fe2be1
14 changed files with 3494 additions and 77 deletions

View File

@ -154,6 +154,12 @@ VDec_op::VDec_op(Item_func_hybrid_field_type *item)
}
date_mode_t Temporal::sql_mode_for_dates(THD *thd)
{
return ::sql_mode_for_dates(thd);
}
bool Dec_ptr::to_datetime_with_warn(THD *thd, MYSQL_TIME *to,
date_mode_t fuzzydate, Item *item)
{
@ -176,9 +182,9 @@ my_decimal *Temporal::bad_to_decimal(my_decimal *to) const
}
Temporal_hybrid::Temporal_hybrid(THD *thd, Item *item)
Temporal_hybrid::Temporal_hybrid(THD *thd, Item *item, date_mode_t fuzzydate)
{
if (item->get_date(thd, this, sql_mode_for_dates(thd)))
if (item->get_date(thd, this, fuzzydate))
time_type= MYSQL_TIMESTAMP_NONE;
}
@ -331,6 +337,117 @@ VYear_op::VYear_op(Item_func_hybrid_field_type *item)
{ }
const LEX_CSTRING Interval_DDhhmmssff::m_type_name=
{STRING_WITH_LEN("INTERVAL DAY TO SECOND")};
Interval_DDhhmmssff::Interval_DDhhmmssff(THD *thd, MYSQL_TIME_STATUS *st,
bool push_warnings,
Item *item, ulong max_hour)
{
my_time_status_init(st);
switch (item->cmp_type()) {
case ROW_RESULT:
DBUG_ASSERT(0);
time_type= MYSQL_TIMESTAMP_NONE;
break;
case TIME_RESULT:
{
if (item->get_date(thd, this, TIME_TIME_ONLY))
time_type= MYSQL_TIMESTAMP_NONE;
else if (time_type != MYSQL_TIMESTAMP_TIME)
{
st->warnings|= MYSQL_TIME_WARN_OUT_OF_RANGE;
push_warning_wrong_or_truncated_value(thd, ErrConvTime(this),
st->warnings);
time_type= MYSQL_TIMESTAMP_NONE;
}
break;
}
case INT_RESULT:
case REAL_RESULT:
case DECIMAL_RESULT:
case STRING_RESULT:
{
StringBuffer<STRING_BUFFER_USUAL_SIZE> tmp;
String *str= item->val_str(&tmp);
if (!str)
time_type= MYSQL_TIMESTAMP_NONE;
else if (str_to_DDhhmmssff(st, str->ptr(), str->length(), str->charset(),
UINT_MAX32))
{
if (push_warnings)
thd->push_warning_wrong_value(Sql_condition::WARN_LEVEL_WARN,
m_type_name.str,
ErrConvString(str).ptr());
time_type= MYSQL_TIMESTAMP_NONE;
}
else
{
if (hour > max_hour)
{
st->warnings|= MYSQL_TIME_WARN_OUT_OF_RANGE;
time_type= MYSQL_TIMESTAMP_NONE;
}
// Warn if hour or nanosecond truncation happened
if (push_warnings)
push_warning_wrong_or_truncated_value(thd, ErrConvString(str),
st->warnings);
}
}
break;
}
DBUG_ASSERT(is_valid_value_slow());
}
void
Interval_DDhhmmssff::push_warning_wrong_or_truncated_value(THD *thd,
const ErrConv &str,
int warnings)
{
if (warnings & MYSQL_TIME_WARN_OUT_OF_RANGE)
{
thd->push_warning_wrong_value(Sql_condition::WARN_LEVEL_WARN,
m_type_name.str, str.ptr());
}
else if (MYSQL_TIME_WARN_HAVE_WARNINGS(warnings))
{
thd->push_warning_truncated_wrong_value(Sql_condition::WARN_LEVEL_WARN,
m_type_name.str, str.ptr());
}
else if (MYSQL_TIME_WARN_HAVE_NOTES(warnings))
{
thd->push_warning_truncated_wrong_value(Sql_condition::WARN_LEVEL_NOTE,
m_type_name.str, str.ptr());
}
}
uint Interval_DDhhmmssff::fsp(THD *thd, Item *item)
{
MYSQL_TIME_STATUS st;
switch (item->cmp_type()) {
case INT_RESULT:
case TIME_RESULT:
return item->decimals;
case REAL_RESULT:
case DECIMAL_RESULT:
return MY_MIN(item->decimals, TIME_SECOND_PART_DIGITS);
case ROW_RESULT:
DBUG_ASSERT(0);
return 0;
case STRING_RESULT:
break;
}
if (!item->const_item() || item->is_expensive())
return TIME_SECOND_PART_DIGITS;
Interval_DDhhmmssff it(thd, &st, false/*no warnings*/, item, UINT_MAX32);
return it.is_valid_interval_DDhhmmssff() ? st.precision :
TIME_SECOND_PART_DIGITS;
}
void Time::make_from_item(THD *thd, int *warn, Item *item, const Options opt)
{
*warn= 0;
@ -3487,6 +3604,15 @@ bool Type_handler_temporal_result::
{
bool rc= Type_handler::Item_func_min_max_fix_attributes(thd, func,
items, nitems);
bool is_time= func->field_type() == MYSQL_TYPE_TIME;
func->decimals= 0;
for (uint i= 0; i < nitems; i++)
{
uint deci= is_time ? items[i]->time_precision(thd) :
items[i]->datetime_precision(thd);
set_if_bigger(func->decimals, deci);
}
if (rc || func->maybe_null)
return rc;
/*