HI, Sergei!

On Wed, Jul 29, 2020 at 7:47 PM Sergei Golubchik <serg@mariadb.org> wrote:
Hi, Oleksandr!

On Jul 29, Oleksandr Byelkin wrote:
> revision-id: f854ac4d9e0 (mariadb-10.5.4-20-gf854ac4d9e0)
> parent(s): 6cee9b1953b
> author: Oleksandr Byelkin <sanja@mariadb.com>
> committer: Oleksandr Byelkin <sanja@mariadb.com>
> timestamp: 2020-07-03 13:39:49 +0200
> message:
>
> MDEV-21916: COM_STMT_BULK_EXECUTE with RETURNING insert wrong values
>
> To allocate new net buffer to avoid changing bufer we are reading.

please add a more descriptive comment. I'm still not sure I understood
correctly what the bug was.

OK. But the idea is simple, to avoid reading and writing the same buffer we should allocate a new buffer for writing.
 

> diff --git a/sql/net_serv.cc b/sql/net_serv.cc
> --- a/sql/net_serv.cc
> +++ b/sql/net_serv.cc
> @@ -178,6 +178,14 @@ my_bool my_net_init(NET *net, Vio *vio, void *thd, uint my_flags)
>    DBUG_RETURN(0);
>  }

> +
> +inline void net_reset_new_packet(NET *net)

static
OK 

> +{
> +  net->buff_end= net->buff + net->max_packet;
> +  net->write_pos= net->read_pos= net->buff;
> +}
> +
> +
>  my_bool net_allocate_new_packet(NET *net, void *thd, uint my_flags)
>  {
>    DBUG_ENTER("net_allocate_new_packet");
> @@ -186,11 +194,26 @@ my_bool net_allocate_new_packet(NET *net, void *thd, uint my_flags)
>                                    NET_HEADER_SIZE + COMP_HEADER_SIZE + 1,
>                                    MYF(MY_WME | my_flags))))
>      DBUG_RETURN(1);
> -  net->buff_end=net->buff+net->max_packet;
> -  net->write_pos=net->read_pos = net->buff;
> +  net_reset_new_packet(net);
>    DBUG_RETURN(0);
>  }

> +unsigned char *net_try_allocate_new_packet(NET *net, void *thd, uint my_flags)
> +{
> +  unsigned char * old_buff= net->buff;
> +  if (net_allocate_new_packet(net, thd, my_flags))
> +  {
> +    /*
> +      We failed to allocate a new buffer => return the old buffer and
> +      return an error
> +    */
> +    net->buff= old_buff;
> +    net_reset_new_packet(net);
> +    old_buff= NULL;

wouldn't it be better to make net_allocate_new_packet not to change
net->buff on failure?

because currently you have exactly the same problem in sql_parse.cc (in
10.4) and your patch doesn't fix it.

simply

  if (!(buff= (uchar*) my_malloc(...)))
    DBUG_RETURN(1);
  net->buff= buff;
  net->buff_end= net->buff + net->max_packet;
  net->write_pos= net->read_pos= net->buff;
  DBUG_RETURN(0);


OK, I just tried not to touch it really, just divide it into several functions.
 
> +  }
> +  return old_buff;
> +}
> +

>  void net_end(NET *net)
>  {
> diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc
> --- a/sql/sql_delete.cc
> +++ b/sql/sql_delete.cc
> @@ -685,7 +685,7 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds,
>        !table->prepare_triggers_for_delete_stmt_or_event())
>      will_batch= !table->file->start_bulk_delete();

> -  if (returning)
> +  if (returning && !thd->get_stmt_da()->is_set())

how are result sets for bulk delete returning sent?

Above is just a check for the first cycle. After the first cycle it will be set to DA_EOF_BULK or DA_OK_BULK.
I will add a comment about it.
 

>    {
>      if (result->send_result_set_metadata(returning->item_list,
>                                  Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
> diff --git a/sql/sql_error.cc b/sql/sql_error.cc
> --- a/sql/sql_error.cc
> +++ b/sql/sql_error.cc
> @@ -380,16 +380,33 @@ Diagnostics_area::set_eof_status(THD *thd)
>    if (unlikely(is_error() || is_disabled()))
>      return;

> -  /*
> -    If inside a stored procedure, do not return the total
> -    number of warnings, since they are not available to the client
> -    anyway.
> -  */
> -  m_statement_warn_count= (thd->spcont ?
> -                           0 :
> -                           current_statement_warn_count());
> +  if (m_status == DA_EOF_BULK)
> +  {
> +    /*
> +      If inside a stored procedure, do not return the total
> +      number of warnings, since they are not available to the client
> +      anyway.
> +    */
> +    if (!thd->spcont)
> +      m_statement_warn_count+= current_statement_warn_count();
> +  }
> +  else
> +  {
> +    /*
> +      If inside a stored procedure, do not return the total
> +      number of warnings, since they are not available to the client
> +      anyway.
> +    */

did you have to repeat the same comment twice?
I will remove 

> +    if (thd->spcont)
> +    {
> +      m_statement_warn_count= 0;
> +      m_affected_rows= 0;

affected rows too? old code didn't do that
made for safety, but you are right maybe it is too much 

> +    }
> +    else
> +      m_statement_warn_count= current_statement_warn_count();
> +    m_status= (is_bulk_op() ? DA_EOF_BULK : DA_EOF);
> +  }

> -  m_status= DA_EOF;
>    DBUG_VOID_RETURN;
>  }

> diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc
> --- a/sql/sql_prepare.cc
> +++ b/sql/sql_prepare.cc
> @@ -4364,24 +4368,37 @@ Prepared_statement::execute_bulk_loop(String *expanded_query,
>    if (state == Query_arena::STMT_ERROR)
>    {
>      my_message(last_errno, last_error, MYF(0));
> -    thd->set_bulk_execution(0);
> -    return TRUE;
> +    goto err;
>    }
>    /* Check for non zero parameter count*/
>    if (param_count == 0)
>    {
>      DBUG_PRINT("error", ("Statement with no parameters for bulk execution."));
>      my_error(ER_UNSUPPORTED_PS, MYF(0));
> -    thd->set_bulk_execution(0);
> -    return TRUE;
> +    goto err;
>    }

>    if (!(sql_command_flags[lex->sql_command] & CF_SP_BULK_SAFE))
>    {
>      DBUG_PRINT("error", ("Command is not supported in bulk execution."));
>      my_error(ER_UNSUPPORTED_PS, MYF(0));
> -    thd->set_bulk_execution(0);
> -    return TRUE;
> +    goto err;
> +  }
> +  /*
> +     Here second buffer for not optimized commands,
> +     optimized commands do it inside thier internal loop.
> +  */
> +  if (!(sql_command_flags[lex->sql_command] & CF_SP_BULK_OPTIMIZED) &&
> +      this->lex->has_returning())
> +  {
> +    // Above check can be true for SELECT in future
> +    DBUG_ASSERT(lex->sql_command != SQLCOM_SELECT);
> +    if ((readbuff=
> +           net_try_allocate_new_packet(&thd->net, thd,
> +                                       MYF(MY_THREAD_SPECIFIC))) == NULL)

sorry, I'm confused. You allocate a buffer here, in
Prepared_statement::execute_bulk_loop, and then again in mysql_insert() ?

there are 2 way to execute "bulk" operation:
1) optimized (it is only INSERT (REPLACE) for now) where all is done inside insert procedure (one table open, one transaction)
2) many calls of the procedure (DELETE/UPDATE) which as output make "picture" of one call (one OK/EOF and maybe result set) (as side effect many time tables opened and it is different transaction)

so it is in fact 2 independent ways and that is why it is in 2 places.
 

> +    {
> +      goto err;
> +    }
>    }

Regards,
Sergei
VP of MariaDB Server Engineering
and security@mariadb.org