Skip to content
  • Guilhem Bichot's avatar
    c54748a1
    Bug#25320909 ASSERTION `THD->LEX->CURRENT_SELECT() == UNIT->FIRST_SELECT()' WITH DELETE · c54748a1
    Guilhem Bichot authored
    Introduced by WL#3634 (recursive CTE).
    Before WL3634, in unit::execute(), if one of the query blocks'
    join::exec() returned an error, unit::execute() would return
    immediately, but if the fake_select_lex's join::exec() returned an
    error then we would first restore current_select() to the value it had
    when the function started, and then return.
    In WL3634, I moved the fake_select_lex's join::exec() into the same
    loop as the query blocks' join::exec() because it may be executed
    several times. Doing this, I automatically removed the "exceptional"
    restoration of current_select(), judging that it was anyway
    unnecessary: "as a failure of join::exec() means a real problem, the
    statement is going to end in error very soon, so why restore?".
    This introduced the bug. Scenario is:
    DELETE IGNORE ... WHERE a!=(subq);
    where "subq" contains a UNION which returns more than one row.
    DELETE evaluates the scalar subq. The subq's fake_select_lex's JOIN
    sends its result to a Query_result_scalar_subquery which job is to
    fill the Item_singlerow_subselect with the JOIN's result. However if
    this item is already filled (i.e. the JOIN is finding a 2nd result
    row) the result object signals my_error(ER_SUBQUERY_1_ROW) then
    returns "error":
     bool Query_result_scalar_subquery::send_data(List<Item> &items)
     {
       Item_singlerow_subselect *it= (Item_singlerow_subselect *)item;
       if (it->assigned())  <<< already assigned a value
       {
        my_error(ER_SUBQUERY_NO_1_ROW, MYF(0));
        DBUG_RETURN(true);  <<<<<<< error
    
    There is IGNORE so my_error() only pushes a warning. Due to the
    returned "true" value, subquery's unit::execute() terminates, but as
    it's the fake_select_lex's JOIN's which failed, current_select() is
    restored() before terminating. The termination also means the 3rd row
    won't be read (that's efficient). The the subselect_union_engine gets
    the "true" return code, then the Item gets it too:
    
    longlong Item_singlerow_subselect::val_int()
    {
      DBUG_ASSERT(fixed == 1);
      if (!no_rows && !exec() && !value->null_value)
      {
        null_value= FALSE;
        return value->val_int();
      }
      else
      { <<<<<< here as exec()==true
        reset();
        return 0;
      }
    }
    
    reset() fills the Item with NULL and then "return 0" makes the error
    disappear. From the point of view of the outer statement, everything
    is said to be ok. So the DELETE uses the NULL value to evaluate its
    WHERE, then goes and delete the row in:
    which has this assertion:
     `thd->lex->current_select() == unit->first_select()'.
    Which passes.
    After the changes of WL#3634, as current_select() isn't restored, the
    assertion fails.
    
    Fix: restore current_select()
    On-the-side fix: in sql_union.cc:
           if (set_limit(thd, fake_select_lex))
    -        DBUG_RETURN(status);
    +        DBUG_RETURN(true);
    because "status" may not be true at this point.
    c54748a1
    Bug#25320909 ASSERTION `THD->LEX->CURRENT_SELECT() == UNIT->FIRST_SELECT()' WITH DELETE
    Guilhem Bichot authored
    Introduced by WL#3634 (recursive CTE).
    Before WL3634, in unit::execute(), if one of the query blocks'
    join::exec() returned an error, unit::execute() would return
    immediately, but if the fake_select_lex's join::exec() returned an
    error then we would first restore current_select() to the value it had
    when the function started, and then return.
    In WL3634, I moved the fake_select_lex's join::exec() into the same
    loop as the query blocks' join::exec() because it may be executed
    several times. Doing this, I automatically removed the "exceptional"
    restoration of current_select(), judging that it was anyway
    unnecessary: "as a failure of join::exec() means a real problem, the
    statement is going to end in error very soon, so why restore?".
    This introduced the bug. Scenario is:
    DELETE IGNORE ... WHERE a!=(subq);
    where "subq" contains a UNION which returns more than one row.
    DELETE evaluates the scalar subq. The subq's fake_select_lex's JOIN
    sends its result to a Query_result_scalar_subquery which job is to
    fill the Item_singlerow_subselect with the JOIN's result. However if
    this item is already filled (i.e. the JOIN is finding a 2nd result
    row) the result object signals my_error(ER_SUBQUERY_1_ROW) then
    returns "error":
     bool Query_result_scalar_subquery::send_data(List<Item> &items)
     {
       Item_singlerow_subselect *it= (Item_singlerow_subselect *)item;
       if (it->assigned())  <<< already assigned a value
       {
        my_error(ER_SUBQUERY_NO_1_ROW, MYF(0));
        DBUG_RETURN(true);  <<<<<<< error
    
    There is IGNORE so my_error() only pushes a warning. Due to the
    returned "true" value, subquery's unit::execute() terminates, but as
    it's the fake_select_lex's JOIN's which failed, current_select() is
    restored() before terminating. The termination also means the 3rd row
    won't be read (that's efficient). The the subselect_union_engine gets
    the "true" return code, then the Item gets it too:
    
    longlong Item_singlerow_subselect::val_int()
    {
      DBUG_ASSERT(fixed == 1);
      if (!no_rows && !exec() && !value->null_value)
      {
        null_value= FALSE;
        return value->val_int();
      }
      else
      { <<<<<< here as exec()==true
        reset();
        return 0;
      }
    }
    
    reset() fills the Item with NULL and then "return 0" makes the error
    disappear. From the point of view of the outer statement, everything
    is said to be ok. So the DELETE uses the NULL value to evaluate its
    WHERE, then goes and delete the row in:
    which has this assertion:
     `thd->lex->current_select() == unit->first_select()'.
    Which passes.
    After the changes of WL#3634, as current_select() isn't restored, the
    assertion fails.
    
    Fix: restore current_select()
    On-the-side fix: in sql_union.cc:
           if (set_limit(thd, fake_select_lex))
    -        DBUG_RETURN(status);
    +        DBUG_RETURN(true);
    because "status" may not be true at this point.
Loading