/* Copyright (c) 2015, 2023, Oracle and/or its affiliates.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is also distributed with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have included with MySQL.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA */

#include "rpl_trx_boundary_parser.h"

#include "log.h"           // sql_print_warning
#include "log_event.h"     // Log_event


#ifndef NDEBUG
/* Event parser state names */
static const char *event_parser_state_names[]= {
  "None",
  "GTID",
  "DDL",
  "DML",
  "Error"
};
#endif

/*
  -----------------------------------------
  Transaction_boundary_parser class methods
  -----------------------------------------
*/

/**
   Reset the transaction boundary parser.

   This method initialize the boundary parser state.
*/
void Transaction_boundary_parser::reset()
{
  DBUG_ENTER("Transaction_boundary_parser::reset");
  DBUG_PRINT("info", ("transaction boundary parser is changing state "
                      "from '%s' to '%s'",
                      event_parser_state_names[current_parser_state],
                      event_parser_state_names[EVENT_PARSER_NONE]));
  current_parser_state= EVENT_PARSER_NONE;
  DBUG_VOID_RETURN;
}

/**
   Feed the transaction boundary parser with a Log_event of any type,
   serialized into a char* buffer.

   @param buf            Pointer to the event buffer.
   @param length         The size of the event buffer.
   @param fd_event       The description event of the master which logged
                         the event.
   @param throw_warnings If the function should throw warning messages while
                         updating the boundary parser state.
                         While initializing the Relay_log_info the
                         relay log is scanned backwards and this could
                         generate false warnings. So, in this case, we
                         don't want to throw warnings.

   @return  false if the transaction boundary parser accepted the event.
            true if the transaction boundary parser didn't accepted the event.
*/
bool Transaction_boundary_parser::feed_event(const char *buf, size_t length,
                                             const Format_description_log_event
                                             *fd_event,
                                             bool throw_warnings)
{
  DBUG_ENTER("Transaction_boundary_parser::feed_event");
  enum_event_boundary_type event_boundary_type=
    get_event_boundary_type(buf, length, fd_event, throw_warnings);
  DBUG_RETURN(update_state(event_boundary_type, throw_warnings));
}

/**
   Get the boundary type for a given Log_event of any type,
   serialized into a char* buffer, based on event parser logic.

   @param buf               Pointer to the event buffer.
   @param length            The size of the event buffer.
   @param description_event The description event of the master which logged
                            the event.
   @param throw_warnings    If the function should throw warnings getting the
                            event boundary type.
                            Please see comments on this at feed_event().

   @return  the transaction boundary type of the event.
*/
Transaction_boundary_parser::enum_event_boundary_type
Transaction_boundary_parser::get_event_boundary_type(
  const char *buf, size_t length, const Format_description_log_event *fd_event,
  bool throw_warnings)
{
  DBUG_ENTER("Transaction_boundary_parser::get_event_boundary_type");

  Log_event_type event_type;
  enum_event_boundary_type boundary_type= EVENT_BOUNDARY_TYPE_ERROR;
  uint header_size= fd_event->common_header_len;

  /* Error if the event content is smaller than header size for the format */
  if (length < header_size)
    goto end;

  event_type= (Log_event_type)buf[EVENT_TYPE_OFFSET];
  DBUG_PRINT("info",("trx boundary parser was fed with an event of type %s",
                     Log_event::get_type_str(event_type)));

  switch (event_type)
  {
    case binary_log::GTID_LOG_EVENT:
    case binary_log::ANONYMOUS_GTID_LOG_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_GTID;
      break;

    /*
      There are four types of queries that we have to deal with: BEGIN, COMMIT,
      ROLLBACK and the rest.
    */
    case binary_log::QUERY_EVENT:
    {
      char *query= NULL;
      size_t qlen= 0;
      /* Get the query to let us check for BEGIN/COMMIT/ROLLBACK */
      qlen= Query_log_event::get_query(buf, length, fd_event, &query);
      if (qlen == 0)
      {
        assert(query == NULL);
        boundary_type= EVENT_BOUNDARY_TYPE_ERROR;
        break;
      }

      /*
        BEGIN is always the begin of a DML transaction.
      */
      if (!strncmp(query, "BEGIN", qlen) ||
          !strncmp(query, STRING_WITH_LEN("XA START")))
        boundary_type= EVENT_BOUNDARY_TYPE_BEGIN_TRX;
      /*
        COMMIT and ROLLBACK are always the end of a transaction.
      */
      else if (!strncmp(query, "COMMIT", qlen) ||
               (!native_strncasecmp(query, STRING_WITH_LEN("ROLLBACK")) &&
                native_strncasecmp(query, STRING_WITH_LEN("ROLLBACK TO "))))
        boundary_type= EVENT_BOUNDARY_TYPE_END_TRX;
      /*
        XA ROLLBACK is always the end of a XA transaction.
      */
      else if (!native_strncasecmp(query, STRING_WITH_LEN("XA ROLLBACK")))
        boundary_type= EVENT_BOUNDARY_TYPE_END_XA_TRX;
      /*
        If the query is not (BEGIN | XA START | COMMIT | [XA] ROLLBACK), it can
        be considered an ordinary statement.
      */
      else
        boundary_type= EVENT_BOUNDARY_TYPE_STATEMENT;

      break;
    }

    /*
      XID events are always the end of a transaction.
    */
    case binary_log::XID_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_END_TRX;
      break;
    /*
      XA_prepare event ends XA-prepared group of events (prepared XA transaction).
    */
    case binary_log::XA_PREPARE_LOG_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_END_TRX;
      break;

    /*
      Intvar, Rand and User_var events are always considered as pre-statements.
    */
    case binary_log::INTVAR_EVENT:
    case binary_log::RAND_EVENT:
    case binary_log::USER_VAR_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_PRE_STATEMENT;
      break;

    /*
      The following event types are always considered as statements
      because they will always be wrapped between BEGIN/COMMIT.
    */
    case binary_log::EXECUTE_LOAD_QUERY_EVENT:
    case binary_log::TABLE_MAP_EVENT:
    case binary_log::APPEND_BLOCK_EVENT:
    case binary_log::BEGIN_LOAD_QUERY_EVENT:
    case binary_log::ROWS_QUERY_LOG_EVENT:
    case binary_log::WRITE_ROWS_EVENT:
    case binary_log::UPDATE_ROWS_EVENT:
    case binary_log::DELETE_ROWS_EVENT:
    case binary_log::WRITE_ROWS_EVENT_V1:
    case binary_log::UPDATE_ROWS_EVENT_V1:
    case binary_log::DELETE_ROWS_EVENT_V1:
    case binary_log::PRE_GA_WRITE_ROWS_EVENT:
    case binary_log::PRE_GA_DELETE_ROWS_EVENT:
    case binary_log::PRE_GA_UPDATE_ROWS_EVENT:
    case binary_log::VIEW_CHANGE_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_STATEMENT;
      break;

    /*
      Incident events have their own boundary type.
    */
    case binary_log::INCIDENT_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_INCIDENT;
      break;

    /*
      Rotate, Format_description and Heartbeat should be ignored.
      Also, any other kind of event not listed in the "cases" above
      will be ignored.
    */
    case binary_log::ROTATE_EVENT:
    case binary_log::FORMAT_DESCRIPTION_EVENT:
    case binary_log::HEARTBEAT_LOG_EVENT:
    case binary_log::PREVIOUS_GTIDS_LOG_EVENT:
    case binary_log::START_EVENT_V3:
    case binary_log::STOP_EVENT:
    case binary_log::LOAD_EVENT:
    case binary_log::SLAVE_EVENT:
    case binary_log::CREATE_FILE_EVENT:
    case binary_log::DELETE_FILE_EVENT:
    case binary_log::NEW_LOAD_EVENT:
    case binary_log::EXEC_LOAD_EVENT:
    case binary_log::TRANSACTION_CONTEXT_EVENT:
      boundary_type= EVENT_BOUNDARY_TYPE_IGNORE;
      break;

    /*
      If the event is none of above supported event types, this is probably
      an event type unsupported by this server version. So, we must check if
      this event is ignorable or not.
    */
    default:
      if (uint2korr(buf + FLAGS_OFFSET) & LOG_EVENT_IGNORABLE_F)
        boundary_type= EVENT_BOUNDARY_TYPE_IGNORE;
      else
      {
        boundary_type= EVENT_BOUNDARY_TYPE_ERROR;
        if (throw_warnings)
          sql_print_warning(
            "Unsupported non-ignorable event fed into the "
            "event stream.");
      }
  } /* End of switch(event_type) */

end:
  DBUG_RETURN(boundary_type);
}

/**
   Update the boundary parser state based on a given boundary type.

   @param event_boundary_type The event boundary type of the event used to
                              fed the boundary parser.
   @param throw_warnings      If the function should throw warnings while
                              updating the boundary parser state.
                              Please see comments on this at feed_event().

   @return  false State updated successfully.
            true  There was an error updating the state.
*/
bool Transaction_boundary_parser::update_state(
  enum_event_boundary_type event_boundary_type, bool throw_warnings)
{
  DBUG_ENTER("Transaction_boundary_parser::update_state");

  enum_event_parser_state new_parser_state= EVENT_PARSER_NONE;

  bool error= false;

  switch (event_boundary_type)
  {
  /*
    GTIDs are always the start of a transaction stream.
  */
  case EVENT_BOUNDARY_TYPE_GTID:
    /* In any case, we will update the state to GTID */
    new_parser_state= EVENT_PARSER_GTID;
    /* The following switch is mostly to differentiate the warning messages */
    switch(current_parser_state) {
    case EVENT_PARSER_GTID:
    case EVENT_PARSER_DDL:
    case EVENT_PARSER_DML:
      if (throw_warnings)
        sql_print_warning(
          "GTID_LOG_EVENT or ANONYMOUS_GTID_LOG_EVENT "
          "is not expected in an event stream %s.",
          current_parser_state == EVENT_PARSER_GTID ?
            "after a GTID_LOG_EVENT or an ANONYMOUS_GTID_LOG_EVENT" :
            current_parser_state == EVENT_PARSER_DDL ?
              "in the middle of a DDL" :
              "in the middle of a DML"); /* EVENT_PARSER_DML */
      error= true;
      break;
    case EVENT_PARSER_ERROR: /* we probably threw a warning before */
      error= true;
      /* FALL THROUGH */
    case EVENT_PARSER_NONE:
      break;
    }
    break;

  /*
    There are four types of queries that we have to deal with: BEGIN, COMMIT,
    ROLLBACK and the rest.
  */
  case EVENT_BOUNDARY_TYPE_BEGIN_TRX:
    /* In any case, we will update the state to DML */
    new_parser_state= EVENT_PARSER_DML;
    /* The following switch is mostly to differentiate the warning messages */
    switch(current_parser_state) {
    case EVENT_PARSER_DDL:
    case EVENT_PARSER_DML:
      if (throw_warnings)
        sql_print_warning(
          "QUERY(BEGIN) is not expected in an event stream "
          "in the middle of a %s.",
          current_parser_state == EVENT_PARSER_DDL ? "DDL" : "DML");
      error= true;
      break;
    case EVENT_PARSER_ERROR: /* we probably threw a warning before */
      error= true;
      /* FALL THROUGH */
    case EVENT_PARSER_NONE:
    case EVENT_PARSER_GTID:
      break;
    }
    break;

  case EVENT_BOUNDARY_TYPE_END_TRX:
    /* In any case, we will update the state to NONE */
    new_parser_state= EVENT_PARSER_NONE;
    /* The following switch is mostly to differentiate the warning messages */
    switch(current_parser_state) {
    case EVENT_PARSER_NONE:
    case EVENT_PARSER_GTID:
    case EVENT_PARSER_DDL:
      if (throw_warnings)
        sql_print_warning(
          "QUERY(COMMIT or ROLLBACK) or "
          "XID_LOG_EVENT is not expected "
          "in an event stream %s.",
          current_parser_state == EVENT_PARSER_NONE ? "outside a transaction" :
          current_parser_state == EVENT_PARSER_GTID ? "after a GTID_LOG_EVENT" :
          "in the middle of a DDL"); /* EVENT_PARSER_DDL */
      error= true;
      break;
    case EVENT_PARSER_ERROR: /* we probably threw a warning before */
      error= true;
      /* FALL THROUGH */
    case EVENT_PARSER_DML:
      break;
    }
    break;

  case EVENT_BOUNDARY_TYPE_END_XA_TRX:
    /* In any case, we will update the state to NONE */
    new_parser_state= EVENT_PARSER_NONE;
    /* The following switch is mostly to differentiate the warning messages */
    switch(current_parser_state) {
      case EVENT_PARSER_NONE:
      case EVENT_PARSER_DDL:
        if (throw_warnings)
          sql_print_warning(
              "QUERY(XA ROLLBACK) is "
              "not expected in an event stream %s.",
              current_parser_state == EVENT_PARSER_NONE ? "outside a transaction" :
              "in the middle of a DDL"); /* EVENT_PARSER_DDL */
        error= true;
        break;
      case EVENT_PARSER_ERROR: /* we probably threw a warning before */
        error= true;
        /* FALL THROUGH */
      case EVENT_PARSER_DML:
      /* XA ROLLBACK can appear after a GTID event */
      case EVENT_PARSER_GTID:
        break;
    }
    break;

  case EVENT_BOUNDARY_TYPE_STATEMENT:
    switch(current_parser_state) {
    case EVENT_PARSER_NONE:
      new_parser_state= EVENT_PARSER_NONE;
      break;
    case EVENT_PARSER_GTID:
    case EVENT_PARSER_DDL:
      new_parser_state= EVENT_PARSER_NONE;
      break;
    case EVENT_PARSER_DML:
      new_parser_state= current_parser_state;
      break;
    case EVENT_PARSER_ERROR: /* we probably threw a warning before */
      error= true;
      break;
    }
    break;

  /*
    Intvar, Rand and User_var events might be inside of a transaction stream if
    any Intvar, Rand and User_var was fed before, if BEGIN was fed before or if
    GTID was fed before.
    In the case of no GTID, no BEGIN and no previous Intvar, Rand or User_var
    it will be considered the start of a transaction stream.
  */
  case EVENT_BOUNDARY_TYPE_PRE_STATEMENT:
    switch(current_parser_state) {
    case EVENT_PARSER_NONE:
    case EVENT_PARSER_GTID:
      new_parser_state= EVENT_PARSER_DDL;
      break;
    case EVENT_PARSER_DDL:
    case EVENT_PARSER_DML:
      new_parser_state= current_parser_state;
      break;
    case EVENT_PARSER_ERROR: /* we probably threw a warning before */
      error= true;
      break;
    }
    break;

  /*
    Incident events can happen without a GTID (before BUG#19594845 fix) or
    with its own GTID in order to be skipped. In any case, it should always
    mark "the end" of a transaction.
  */
  case EVENT_BOUNDARY_TYPE_INCIDENT:
    /* In any case, we will update the state to NONE */
    new_parser_state= EVENT_PARSER_NONE;
    break;

  /*
    Rotate, Format_description and Heartbeat should be ignored.
    The rotate might be fake, like when the IO thread receives from dump thread
    Previous_gtid and Heartbeat events due to reconnection/auto positioning.
  */
  case EVENT_BOUNDARY_TYPE_IGNORE:
    new_parser_state= current_parser_state;
    break;

  case EVENT_BOUNDARY_TYPE_ERROR:
    error= true;
    new_parser_state= EVENT_PARSER_ERROR;
    break;
  }

  DBUG_PRINT("info", ("transaction boundary parser is changing state "
                      "from '%s' to '%s'",
                      event_parser_state_names[current_parser_state],
                      event_parser_state_names[new_parser_state]));
  current_parser_state= new_parser_state;

  DBUG_RETURN(error);
}
