MYSQL SQL 执行系列2

复制代码
Sql_cmd_dml::execute 
  |-> Sql_cmd_dml::prepare
    |-> Sql_cmd_select::precheck
    |-> open_tables_for_query
      |-> open_tables 
        |-> open_and_process_table (为 Table_ref 获取 MDL 锁并打开表或视图)
          |-> open_table (对于基表)
    |-> Sql_cmd_select::prepare_inner



Sql_cmd_dml::execute/prepare
  -> open_tables_for_query
    -> open_tables
      -> open_and_process_table
        -> open_table   (对于基表)

所以 open_table 是真正打开一个基表的地方(视图、派生表、临时表有其他处理函数)。

mysql_execute_command

cpp 复制代码
/**
  Execute command saved in thd and lex->sql_command.

  @param thd                       Thread handle
  @param first_level               whether invocation of the
  mysql_execute_command() is a top level query or sub query. At the highest
  level, first_level value is true. Stored procedures can execute sub queries.
  In such cases first_level (recursive mysql_execute_command() call) will be
  false.

  @todo this is workaround. right way will be move invalidating in
    the unlock procedure.
  @todo use check_change_password()

  @retval false       OK
  @retval true        Error
*/

int mysql_execute_command(THD *thd, bool first_level) {
  int res = false;
  LEX *const lex = thd->lex;
  /* first Query_block (have special meaning for many of non-SELECTcommands) */
  Query_block *const query_block = lex->query_block;
  /* first table of first Query_block */
  Table_ref *const first_table = query_block->get_table_list();
  /* list of all tables in query */
  Table_ref *all_tables;
  // keep GTID violation state in order to roll it back on statement failure
  const bool gtid_consistency_violation_state =
      thd->has_gtid_consistency_violation;
  assert(query_block->master_query_expression() == lex->unit);
  DBUG_TRACE;
  /* EXPLAIN OTHER isn't explainable command, but can have describe flag. */
  assert(!lex->is_explain() || is_explainable_query(lex->sql_command) ||
         lex->sql_command == SQLCOM_EXPLAIN_OTHER);

  assert(!thd->m_transactional_ddl.inited() ||
         thd->in_active_multi_stmt_transaction());

  bool early_error_on_rep_command{false};

  CONDITIONAL_SYNC_POINT_FOR_TIMESTAMP("before_execute_command");

  /*
    If there is a CREATE TABLE...START TRANSACTION command which
    is not yet committed or rollbacked, then we should allow only
    BINLOG INSERT, COMMIT or ROLLBACK command.
    TODO: Should we really check name of table when we cable BINLOG INSERT ?
  */
  if (thd->m_transactional_ddl.inited() && lex->sql_command != SQLCOM_COMMIT &&
      lex->sql_command != SQLCOM_ROLLBACK &&
      lex->sql_command != SQLCOM_BINLOG_BASE64_EVENT) {
    my_error(ER_STATEMENT_NOT_ALLOWED_AFTER_START_TRANSACTION, MYF(0));
    binlog_gtid_end_transaction(thd);
    return 1;
  }

  thd->work_part_info = nullptr;

  if (thd->optimizer_switch_flag(OPTIMIZER_SWITCH_SUBQUERY_TO_DERIVED))
    lex->add_statement_options(OPTION_NO_CONST_TABLES);

  /*
    Each statement or replication event which might produce deadlock
    should handle transaction rollback on its own. So by the start of
    the next statement transaction rollback request should be fulfilled
    already.
  */
  assert(!thd->transaction_rollback_request || thd->in_sub_stmt);

  char saved_schema_name_buf[NAME_LEN + 1];
  LEX_STRING saved_schema_name{saved_schema_name_buf,
                               sizeof(saved_schema_name_buf)};
  bool cur_db_changed = false;
  if (lex->is_explain() && lex->explain_format->is_explain_for_schema() &&
      mysql_opt_change_db(thd, lex->explain_format->m_schema_name_for_explain,
                          &saved_schema_name, false, &cur_db_changed)) {
    /* purecov: begin inspected */
    binlog_gtid_end_transaction(thd);
    return 1;
    /* purecov: end */
  }

  /*
    In many cases first table of main Query_block have special meaning =>
    check that it is first table in global list and relink it first in
    queries_tables list if it is necessary (we need such relinking only
    for queries with subqueries in select list, in this case tables of
    subqueries will go to global list first)

    all_tables will differ from first_table only if most upper Query_block
    do not contain tables.

    Because of above in place where should be at least one table in most
    outer Query_block we have following check:
    assert(first_table == all_tables);
    assert(first_table == all_tables && first_table != 0);
  */
  lex->first_lists_tables_same();
  /* should be assigned after making first tables same */
  all_tables = lex->query_tables;
  /* set context for commands which do not use setup_tables */
  query_block->context.resolve_in_table_list_only(
      query_block->get_table_list());

  thd->get_stmt_da()->reset_diagnostics_area();
  if ((thd->lex->keep_diagnostics != DA_KEEP_PARSE_ERROR) &&
      (thd->lex->keep_diagnostics != DA_KEEP_DIAGNOSTICS)) {
    /*
      No parse errors, and it's not a diagnostic statement:
      remove the sql conditions from the DA!
      For diagnostic statements we need to keep the conditions
      around so we can inspec them.
    */
    thd->get_stmt_da()->reset_condition_info(thd);
  }

  if (thd->resource_group_ctx()->m_warn != 0) {
    auto res_grp_name = thd->resource_group_ctx()->m_switch_resource_group_str;
    switch (thd->resource_group_ctx()->m_warn) {
      case WARN_RESOURCE_GROUP_UNSUPPORTED: {
        auto res_grp_mgr = resourcegroups::Resource_group_mgr::instance();
        push_warning_printf(thd, Sql_condition::SL_WARNING,
                            ER_FEATURE_UNSUPPORTED,
                            ER_THD(thd, ER_FEATURE_UNSUPPORTED),
                            "Resource groups", res_grp_mgr->unsupport_reason());
        break;
      }
      case WARN_RESOURCE_GROUP_UNSUPPORTED_HINT:
        push_warning_printf(thd, Sql_condition::SL_WARNING,
                            ER_WARN_UNSUPPORTED_HINT,
                            ER_THD(thd, ER_WARN_UNSUPPORTED_HINT),
                            "Subquery or Stored procedure or Trigger");
        break;
      case WARN_RESOURCE_GROUP_TYPE_MISMATCH: {
        ulonglong pfs_thread_id = 0;
        /*
          Resource group is unsupported with DISABLE_PSI_THREAD.
          The below #ifdef is required for compilation when DISABLE_PSI_THREAD
          is enabled.
        */
#ifdef HAVE_PSI_THREAD_INTERFACE
        pfs_thread_id = PSI_THREAD_CALL(get_current_thread_internal_id)();
#endif  // HAVE_PSI_THREAD_INTERFACE
        push_warning_printf(thd, Sql_condition::SL_WARNING,
                            ER_RESOURCE_GROUP_BIND_FAILED,
                            ER_THD(thd, ER_RESOURCE_GROUP_BIND_FAILED),
                            res_grp_name, pfs_thread_id,
                            "System resource group can't be bound"
                            " with a session thread");
        break;
      }
      case WARN_RESOURCE_GROUP_NOT_EXISTS:
        push_warning_printf(
            thd, Sql_condition::SL_WARNING, ER_RESOURCE_GROUP_NOT_EXISTS,
            ER_THD(thd, ER_RESOURCE_GROUP_NOT_EXISTS), res_grp_name);
        break;
      case WARN_RESOURCE_GROUP_ACCESS_DENIED:
        push_warning_printf(thd, Sql_condition::SL_WARNING,
                            ER_SPECIFIC_ACCESS_DENIED_ERROR,
                            ER_THD(thd, ER_SPECIFIC_ACCESS_DENIED_ERROR),
                            "SUPER OR RESOURCE_GROUP_ADMIN OR "
                            "RESOURCE_GROUP_USER");
    }
    thd->resource_group_ctx()->m_warn = 0;
    res_grp_name[0] = '\0';
  }

  if (unlikely(thd->get_protocol()->has_client_capability(CLIENT_NO_SCHEMA))) {
    push_warning(thd, ER_WARN_DEPRECATED_CLIENT_NO_SCHEMA_OPTION);
  }

  if (unlikely(thd->slave_thread)) {
    if (!check_database_filters(thd, thd->db().str, lex->sql_command)) {
      binlog_gtid_end_transaction(thd);
      return 0;
    }

    if (lex->sql_command == SQLCOM_DROP_TRIGGER) {
      /*
        When dropping a trigger, we need to load its table name
        before checking slave filter rules.
      */
      Table_ref *trigger_table = nullptr;
      (void)get_table_for_trigger(thd, lex->spname->m_db, lex->spname->m_name,
                                  true, &trigger_table);
      if (trigger_table != nullptr) {
        lex->add_to_query_tables(trigger_table);
        all_tables = trigger_table;
      } else {
        /*
          If table name cannot be loaded,
          it means the trigger does not exists possibly because
          CREATE TRIGGER was previously skipped for this trigger
          according to slave filtering rules.
          Returning success without producing any errors in this case.
        */
        binlog_gtid_end_transaction(thd);
        return 0;
      }

      // force searching in slave.cc:tables_ok()
      all_tables->updating = true;
    }

    /*
      For fix of BUG#37051, the master stores the table map for update
      in the Query_log_event, and the value is assigned to
      thd->table_map_for_update before executing the update
      query.

      If thd->table_map_for_update is set, then we are
      replicating from a new master, we can use this value to apply
      filter rules without opening all the tables. However If
      thd->table_map_for_update is not set, then we are
      replicating from an old master, so we just skip this and
      continue with the old method. And of course, the bug would still
      exist for old masters.
    */
    if (lex->sql_command == SQLCOM_UPDATE_MULTI && thd->table_map_for_update) {
      const table_map table_map_for_update = thd->table_map_for_update;
      uint nr = 0;
      Table_ref *table;
      for (table = all_tables; table; table = table->next_global, nr++) {
        if (table_map_for_update & ((table_map)1 << nr))
          table->updating = true;
        else
          table->updating = false;
      }

      if (all_tables_not_ok(thd, all_tables)) {
        /* we warn the slave SQL thread */
        my_error(ER_REPLICA_IGNORED_TABLE, MYF(0));
        binlog_gtid_end_transaction(thd);
        return 0;
      }

      for (table = all_tables; table; table = table->next_global)
        table->updating = true;
    }

    /*
      Check if statement should be skipped because of slave filtering
      rules

      Exceptions are:
      - UPDATE MULTI: For this statement, we want to check the filtering
        rules later in the code
      - SET: we always execute it (Not that many SET commands exists in
        the binary log anyway -- only 4.1 masters write SET statements,
        in 5.0 there are no SET statements in the binary log)
      - DROP TEMPORARY TABLE IF EXISTS: we always execute it (otherwise we
        have stale files on slave caused by exclusion of one tmp table).
    */
    if (!(lex->sql_command == SQLCOM_UPDATE_MULTI) &&
        !(lex->sql_command == SQLCOM_SET_OPTION) &&
        !(lex->sql_command == SQLCOM_DROP_TABLE && lex->drop_temporary &&
          lex->drop_if_exists) &&
        all_tables_not_ok(thd, all_tables)) {
      /* we warn the slave SQL thread */
      my_error(ER_REPLICA_IGNORED_TABLE, MYF(0));
      binlog_gtid_end_transaction(thd);
      return 0;
    }
    /*
       Execute deferred events first
    */
    if (slave_execute_deferred_events(thd)) return -1;

    const int ret = launch_hook_trans_begin(thd, all_tables);
    if (ret) {
      my_error(ret, MYF(0));
      return -1;
    }

  } else {
    const int ret = launch_hook_trans_begin(thd, all_tables);
    if (ret) {
      my_error(ret, MYF(0));
      return -1;
    }

    /*
      When option readonly is set deny operations which change non-temporary
      tables. Except for the replication thread and the 'super' users.
    */
    if (deny_updates_if_read_only_option(thd, all_tables)) {
      err_readonly(thd);
      return -1;
    }
  } /* endif unlikely slave */

  thd->status_var.com_stat[lex->sql_command]++;
  global_aggregated_stats.get_shard(thd->thread_id())
      .com_stat[lex->sql_command]++;

  Opt_trace_start ots(thd, all_tables, lex->sql_command, &lex->var_list,
                      thd->query().str, thd->query().length, nullptr,
                      thd->variables.character_set_client);

  const Opt_trace_object trace_command(&thd->opt_trace);
  const Opt_trace_array trace_command_steps(&thd->opt_trace, "steps");

  if (lex->m_sql_cmd && lex->m_sql_cmd->owner())
    lex->m_sql_cmd->owner()->trace_parameter_types(thd);

  assert(thd->get_transaction()->cannot_safely_rollback(
             Transaction_ctx::STMT) == false);

  switch (gtid_pre_statement_checks(thd)) {
    case GTID_STATEMENT_EXECUTE:
      break;
    case GTID_STATEMENT_CANCEL:
      return -1;
    case GTID_STATEMENT_SKIP:
      my_ok(thd);
      binlog_gtid_end_transaction(thd);
      return 0;
  }

  if (check_and_report_require_row_format_violation(thd) ||
      run_post_replication_filters_actions(thd))
    return -1;

  /*
    End a active transaction so that this command will have it's
    own transaction and will also sync the binary log. If a DDL is
    not run in it's own transaction it may simply never appear on
    the slave in case the outside transaction rolls back.
  */
  if (stmt_causes_implicit_commit(thd, CF_IMPLICIT_COMMIT_BEGIN)) {
    /*
      Note that this should never happen inside of stored functions
      or triggers as all such statements prohibited there.
    */
    assert(!thd->in_sub_stmt);
    /* Statement transaction still should not be started. */
    assert(thd->get_transaction()->is_empty(Transaction_ctx::STMT));

    /*
      Implicit commit is not allowed with an active XA transaction.
      In this case we should not release metadata locks as the XA transaction
      will not be rolled back. Therefore we simply return here.
    */
    if (trans_check_state(thd)) return -1;

    /* Commit the normal transaction if one is active. */
    if (trans_commit_implicit(thd)) return -1;
    /* Release metadata locks acquired in this transaction. */
    thd->mdl_context.release_transactional_locks();
  }

  DEBUG_SYNC(thd, "after_implicit_pre_commit");

  if (gtid_pre_statement_post_implicit_commit_checks(thd)) return -1;

  if (mysql_event_tracking_query_notify(
          thd,
          first_level ? EVENT_TRACKING_QUERY_START
                      : EVENT_TRACKING_QUERY_NESTED_START,
          first_level ? "EVENT_TRACKING_QUERY_START"
                      : "EVENT_TRACKING_QUERY_NESTED_START")) {
    return 1;
  }

#ifndef NDEBUG
  if (lex->sql_command != SQLCOM_SET_OPTION)
    DEBUG_SYNC(thd, "before_execute_sql_command");
#endif

  /*
    Start a new transaction if CREATE TABLE has START TRANSACTION clause.
    Disable binlog so that the BEGIN is not logged in binlog.
   */
  if (lex->create_info && lex->create_info->m_transactional_ddl &&
      !thd->slave_thread) {
    const Disable_binlog_guard binlog_guard(thd);
    if (trans_begin(thd, MYSQL_START_TRANS_OPT_READ_WRITE)) return true;
  }

  /*
    For statements which need this, prevent InnoDB from automatically
    committing InnoDB transaction each time data-dictionary tables are
    closed after being updated.
  */
  const Disable_autocommit_guard autocommit_guard(
      sqlcom_needs_autocommit_off(lex) && !thd->is_plugin_fake_ddl() ? thd
                                                                     : nullptr);

  /*
    Check if we are in a read-only transaction and we're trying to
    execute a statement which should always be disallowed in such cases.

    Note that this check is done after any implicit commits.
  */
  if (thd->tx_read_only &&
      (sql_command_flags[lex->sql_command] & CF_DISALLOW_IN_RO_TRANS)) {
    my_error(ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION, MYF(0));
    goto error;
  }

  /*
    Close tables open by HANDLERs before executing DDL statement
    which is going to affect those tables.

    This should happen before temporary tables are pre-opened as
    otherwise we will get errors about attempt to re-open tables
    if table to be changed is open through HANDLER.

    Note that even although this is done before any privilege
    checks there is no security problem here as closing open
    HANDLER doesn't require any privileges anyway.
  */
  if (sql_command_flags[lex->sql_command] & CF_HA_CLOSE)
    mysql_ha_rm_tables(thd, all_tables);

  /*
    Check that the command is allowed on the PROTOCOL_PLUGIN
  */
  if (thd->get_protocol()->type() == Protocol::PROTOCOL_PLUGIN &&
      !(sql_command_flags[lex->sql_command] & CF_ALLOW_PROTOCOL_PLUGIN)) {
    my_error(ER_PLUGGABLE_PROTOCOL_COMMAND_NOT_SUPPORTED, MYF(0));
    goto error;
  }

  /*
    Open all temporary tables referenced in statement.
    A session has all privileges for any temporary table that it has created,
    however a table must be opened in order to identify it as a temporary table.
  */
  if (sql_command_flags[lex->sql_command] & CF_PREOPEN_TMP_TABLES) {
    if (open_temporary_tables(thd, all_tables)) goto error;
  }

  // Save original info for EXPLAIN FOR CONNECTION
  if (!thd->in_sub_stmt)
    thd->query_plan.set_query_plan(lex->sql_command, lex,
                                   !thd->stmt_arena->is_regular());

  /* Update system variables specified in SET_VAR hints. */
  if (lex->opt_hints_global && lex->opt_hints_global->sys_var_hint)
    lex->opt_hints_global->sys_var_hint->update_vars(thd);

  /* Check if the statement fulfill the requirements on ACL CACHE */
  if (!command_satisfy_acl_cache_requirement(lex->sql_command)) {
    my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--skip-grant-tables");
    goto error;
  }

  DBUG_EXECUTE_IF(
      "force_rollback_in_replica_on_transactional_ddl_commit",
      if (thd->m_transactional_ddl.inited() &&
          thd->lex->sql_command == SQLCOM_COMMIT) {
        lex->sql_command = SQLCOM_ROLLBACK;
      });

  /*
    We do not flag "is DML" (TX_STMT_DML) here as replication expects us to
    test for LOCK TABLE etc. first. To rephrase, we try not to set TX_STMT_DML
    until we have the MDL, and LOCK TABLE could massively delay this.
  */

  DEBUG_SYNC(thd, "execute_command_before_main_switch");

  switch (lex->sql_command) {
    case SQLCOM_PREPARE: {
      mysql_sql_stmt_prepare(thd);
      break;
    }
    case SQLCOM_EXECUTE: {
      mysql_sql_stmt_execute(thd);
      break;
    }
    case SQLCOM_DEALLOCATE_PREPARE: {
      mysql_sql_stmt_close(thd);
      break;
    }

    case SQLCOM_EMPTY_QUERY:
      my_ok(thd);
      break;

    case SQLCOM_HELP:
      res = mysqld_help(thd, lex->help_arg);
      break;

    case SQLCOM_PURGE: {
      Security_context *sctx = thd->security_context();
      if (!sctx->check_access(SUPER_ACL) &&
          !sctx->has_global_grant(STRING_WITH_LEN("BINLOG_ADMIN")).first) {
        my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                 "SUPER or BINLOG_ADMIN");
        goto error;
      }
      /* PURGE BINARY LOGS TO 'file' */
      res = purge_source_logs_to_file(thd, lex->to_log);
      break;
    }
    case SQLCOM_PURGE_BEFORE: {
      Item *it;
      Security_context *sctx = thd->security_context();
      if (!sctx->check_access(SUPER_ACL) &&
          !sctx->has_global_grant(STRING_WITH_LEN("BINLOG_ADMIN")).first) {
        my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                 "SUPER or BINLOG_ADMIN");
        goto error;
      }
      /* PURGE BINARY LOGS BEFORE 'data' */
      it = lex->purge_value_list.head();
      if ((!it->fixed && it->fix_fields(lex->thd, &it)) || it->check_cols(1)) {
        my_error(ER_WRONG_ARGUMENTS, MYF(0), "PURGE LOGS BEFORE");
        goto error;
      }
      it = new Item_func_unix_timestamp(it);
      /*
        it is OK only emulate fix_fieds, because we need only
        value of constant
      */
      it->quick_fix_field();
      const time_t purge_time = static_cast<time_t>(it->val_int());
      if (thd->is_error()) goto error;
      res = purge_source_logs_before_date(thd, purge_time);
      break;
    }
    case SQLCOM_CHANGE_REPLICATION_SOURCE: {
      Security_context *sctx = thd->security_context();
      if (!sctx->check_access(SUPER_ACL) &&
          !sctx->has_global_grant(STRING_WITH_LEN("REPLICATION_SLAVE_ADMIN"))
               .first) {
        my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                 "SUPER or REPLICATION_SLAVE_ADMIN");
        goto error;
      }
      res = change_master_cmd(thd);
      break;
    }
    case SQLCOM_START_GROUP_REPLICATION: {
      Security_context *sctx = thd->security_context();
      if (!sctx->check_access(SUPER_ACL) &&
          !sctx->has_global_grant(STRING_WITH_LEN("GROUP_REPLICATION_ADMIN"))
               .first) {
        my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                 "SUPER or GROUP_REPLICATION_ADMIN");
        goto error;
      }
      if (lex->replica_connection.password && !lex->replica_connection.user) {
        my_error(ER_GROUP_REPLICATION_USER_MANDATORY_MSG, MYF(0));
        goto error;
      }

      /*
        If the client thread has locked tables, a deadlock is possible.
        Assume that
        - the client thread does LOCK TABLE t READ.
        - then the client thread does START GROUP_REPLICATION.
             -try to make the server in super ready only mode
             -acquire MDL lock ownership which will be waiting for
              LOCK on table t to be released.
        To prevent that, refuse START GROUP_REPLICATION if the
        client thread has locked tables
      */
      if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction() ||
          thd->in_sub_stmt) {
        my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0));
        goto error;
      }

      if (thd->variables.gtid_next.type == ASSIGNED_GTID &&
          thd->owned_gtid.sidno > 0) {
        my_error(ER_CANT_EXECUTE_COMMAND_WITH_ASSIGNED_GTID_NEXT, MYF(0));
        early_error_on_rep_command = true;
        goto error;
      }

      if (Clone_handler::is_provisioning()) {
        my_error(ER_GROUP_REPLICATION_COMMAND_FAILURE, MYF(0),
                 "START GROUP_REPLICATION",
                 "This server is being provisioned by CLONE INSTANCE, "
                 "please wait until it is complete.");
        goto error;
      }

      char *error_message = nullptr;
      res = group_replication_start(&error_message, thd);

      // To reduce server dependency, server errors are not used here
      switch (res) {
        case 1:  // GROUP_REPLICATION_CONFIGURATION_ERROR
          my_error(ER_GROUP_REPLICATION_CONFIGURATION, MYF(0));
          goto error;
        case 2:  // GROUP_REPLICATION_ALREADY_RUNNING
          my_error(ER_GROUP_REPLICATION_RUNNING, MYF(0));
          goto error;
        case 3:  // GROUP_REPLICATION_REPLICATION_APPLIER_INIT_ERROR
          my_error(ER_GROUP_REPLICATION_APPLIER_INIT_ERROR, MYF(0));
          goto error;
        case 4:  // GROUP_REPLICATION_COMMUNICATION_LAYER_SESSION_ERROR
          my_error(ER_GROUP_REPLICATION_COMMUNICATION_LAYER_SESSION_ERROR,
                   MYF(0));
          goto error;
        case 5:  // GROUP_REPLICATION_COMMUNICATION_LAYER_JOIN_ERROR
          my_error(ER_GROUP_REPLICATION_COMMUNICATION_LAYER_JOIN_ERROR, MYF(0));
          goto error;
        case 7:  // GROUP_REPLICATION_MAX_GROUP_SIZE
          my_error(ER_GROUP_REPLICATION_MAX_GROUP_SIZE, MYF(0));
          goto error;
        case 8:  // GROUP_REPLICATION_COMMAND_FAILURE
          if (error_message == nullptr) {
            my_error(ER_GROUP_REPLICATION_COMMAND_FAILURE, MYF(0),
                     "START GROUP_REPLICATION",
                     "Please check error log for additional details.");
          } else {
            my_error(ER_GROUP_REPLICATION_COMMAND_FAILURE, MYF(0),
                     "START GROUP_REPLICATION", error_message);
            my_free(error_message);
          }
          goto error;
        case 9:  // GROUP_REPLICATION_SERVICE_MESSAGE_INIT_FAILURE
          my_error(ER_GRP_RPL_MESSAGE_SERVICE_INIT_FAILURE, MYF(0));
          goto error;
        case 10:  // GROUP_REPLICATION_RECOVERY_CHANNEL_STILL_RUNNING
          my_error(ER_GRP_RPL_RECOVERY_CHANNEL_STILL_RUNNING, MYF(0));
          goto error;
      }
      my_ok(thd);
      res = 0;
      break;
    }

    case SQLCOM_STOP_GROUP_REPLICATION: {
      Security_context *sctx = thd->security_context();
      if (!sctx->check_access(SUPER_ACL) &&
          !sctx->has_global_grant(STRING_WITH_LEN("GROUP_REPLICATION_ADMIN"))
               .first) {
        my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                 "SUPER or GROUP_REPLICATION_ADMIN");
        goto error;
      }

      /*
        Please see explanation @SQLCOM_REPLICA_STOP case
        to know the reason for thd->locked_tables_mode in
        the below if condition.
      */
      if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction() ||
          thd->in_sub_stmt) {
        my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0));
        goto error;
      }

      if (thd->variables.gtid_next.type == ASSIGNED_GTID &&
          thd->owned_gtid.sidno > 0) {
        my_error(ER_CANT_EXECUTE_COMMAND_WITH_ASSIGNED_GTID_NEXT, MYF(0));
        early_error_on_rep_command = true;
        goto error;
      }

      char *error_message = nullptr;
      res = group_replication_stop(&error_message);
      if (res == 1)  // GROUP_REPLICATION_CONFIGURATION_ERROR
      {
        my_error(ER_GROUP_REPLICATION_CONFIGURATION, MYF(0));
        goto error;
      }
      if (res == 6)  // GROUP_REPLICATION_APPLIER_THREAD_TIMEOUT
      {
        my_error(ER_GROUP_REPLICATION_STOP_APPLIER_THREAD_TIMEOUT, MYF(0));
        goto error;
      }
      if (res == 8)  // GROUP_REPLICATION_COMMAND_FAILURE
      {
        if (error_message == nullptr) {
          my_error(ER_GROUP_REPLICATION_COMMAND_FAILURE, MYF(0),
                   "STOP GROUP_REPLICATION",
                   "Please check error log for additonal details.");
        } else {
          my_error(ER_GROUP_REPLICATION_COMMAND_FAILURE, MYF(0),
                   "STOP GROUP_REPLICATION", error_message);
          my_free(error_message);
        }
        goto error;
      }
      if (res == 11)  // GROUP_REPLICATION_STOP_WITH_RECOVERY_TIMEOUT
        push_warning(thd, Sql_condition::SL_WARNING,
                     ER_GRP_RPL_RECOVERY_CHANNEL_STILL_RUNNING,
                     ER_THD(thd, ER_GRP_RPL_RECOVERY_CHANNEL_STILL_RUNNING));

      // Allow the command to commit any underlying transaction
      lex->set_was_replication_command_executed();
      thd->set_skip_readonly_check();
      my_ok(thd);
      res = 0;
      break;
    }

    case SQLCOM_REPLICA_START: {
      res = start_slave_cmd(thd);
      break;
    }
    case SQLCOM_REPLICA_STOP: {
      /*
        If the client thread has locked tables, a deadlock is possible.
        Assume that
        - the client thread does LOCK TABLE t READ.
        - then the master updates t.
        - then the SQL slave thread wants to update t,
          so it waits for the client thread because t is locked by it.
        - then the client thread does SLAVE STOP.
          SLAVE STOP waits for the SQL slave thread to terminate its
          update t, which waits for the client thread because t is locked by it.
        To prevent that, refuse SLAVE STOP if the
        client thread has locked tables
      */
      if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction() ||
          thd->global_read_lock.is_acquired()) {
        my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0));
        goto error;
      }

      res = stop_slave_cmd(thd);
      break;
    }
    case SQLCOM_RENAME_TABLE: {
      assert(first_table == all_tables && first_table != nullptr);
      Table_ref *table;
      for (table = first_table; table; table = table->next_local->next_local) {
        if (check_access(thd, ALTER_ACL | DROP_ACL, table->db,
                         &table->grant.privilege, &table->grant.m_internal,
                         false, false) ||
            check_access(thd, INSERT_ACL | CREATE_ACL, table->next_local->db,
                         &table->next_local->grant.privilege,
                         &table->next_local->grant.m_internal, false, false))
          goto error;

        Table_ref old_list = table[0];
        Table_ref new_list = table->next_local[0];
        /*
          It's not clear what the above assignments actually want to
          accomplish. What we do know is that they do *not* want to copy the MDL
          requests, so we overwrite them with uninitialized request.
        */
        old_list.mdl_request = MDL_request();
        new_list.mdl_request = MDL_request();

        if (check_grant(thd, ALTER_ACL | DROP_ACL, &old_list, false, 1,
                        false) ||
            (!test_all_bits(table->next_local->grant.privilege,
                            INSERT_ACL | CREATE_ACL) &&
             check_grant(thd, INSERT_ACL | CREATE_ACL, &new_list, false, 1,
                         false)))
          goto error;
      }

      if (mysql_rename_tables(thd, first_table)) goto error;
      break;
    }
    case SQLCOM_CHECKSUM: {
      assert(first_table == all_tables && first_table != nullptr);
      if (check_table_access(thd, SELECT_ACL, all_tables, false, UINT_MAX,
                             false))
        goto error; /* purecov: inspected */

      res = mysql_checksum_table(thd, first_table, &lex->check_opt);
      break;
    }
    case SQLCOM_REPLACE:
    case SQLCOM_INSERT:
    case SQLCOM_REPLACE_SELECT:
    case SQLCOM_INSERT_SELECT:
    case SQLCOM_DELETE:
    case SQLCOM_DELETE_MULTI:
    case SQLCOM_UPDATE:
    case SQLCOM_UPDATE_MULTI:
    case SQLCOM_CREATE_TABLE:
    case SQLCOM_CREATE_INDEX:
    case SQLCOM_DROP_INDEX:
    case SQLCOM_ASSIGN_TO_KEYCACHE:
    case SQLCOM_PRELOAD_KEYS:
    case SQLCOM_LOAD: {
      assert(first_table == all_tables && first_table != nullptr);
      assert(lex->m_sql_cmd != nullptr);
      res = lex->m_sql_cmd->execute(thd);
      break;
    }
    case SQLCOM_DROP_TABLE: {
      assert(first_table == all_tables && first_table != nullptr);
      if (!lex->drop_temporary) {
        if (check_table_access(thd, DROP_ACL, all_tables, false, UINT_MAX,
                               false))
          goto error; /* purecov: inspected */
      }
      /* DDL and binlog write order are protected by metadata locks. */
      res = mysql_rm_table(thd, first_table, lex->drop_if_exists,
                           lex->drop_temporary);
      /* when dropping temporary tables if @@session_track_state_change is ON
         then send the boolean tracker in the OK packet */
      if (!res && lex->drop_temporary) {
        if (thd->session_tracker.get_tracker(SESSION_STATE_CHANGE_TRACKER)
                ->is_enabled())
          thd->session_tracker.get_tracker(SESSION_STATE_CHANGE_TRACKER)
              ->mark_as_changed(thd, {});
      }
    } break;
    case SQLCOM_CHANGE_DB: {
      const LEX_CSTRING db_str = {query_block->db, strlen(query_block->db)};

      if (!mysql_change_db(thd, db_str, false)) my_ok(thd);

      break;
    }

    case SQLCOM_SET_OPTION: {
      List<set_var_base> *lex_var_list = &lex->var_list;

      if (check_table_access(thd, SELECT_ACL, all_tables, false, UINT_MAX,
                             false))
        goto error;

      if (open_tables_for_query(thd, all_tables, false)) goto error;
      if (!thd->stmt_arena->is_regular() &&
          (thd->stmt_arena->get_state() == Query_arena::STMT_PREPARED ||
           thd->stmt_arena->get_state() == Query_arena::STMT_EXECUTED)) {
        lex->restore_cmd_properties();
        bind_fields(thd->stmt_arena->item_list());
        if (all_tables != nullptr &&
            !thd->stmt_arena->is_stmt_prepare_or_first_stmt_execute() &&
            query_block->check_privileges_for_subqueries(thd))
          return true;
      }
      if (!(res = sql_set_variables(thd, lex_var_list, true)))
        my_ok(thd);
      else {
        /*
          We encountered some sort of error, but no message was sent.
          Send something semi-generic here since we don't know which
          assignment in the list caused the error.
        */
        if (!thd->is_error()) my_error(ER_WRONG_ARGUMENTS, MYF(0), "SET");
        goto error;
      }

#ifndef NDEBUG
      /*
        Makes server crash when executing SET SESSION debug = 'd,crash_now';
        See mysql-test/include/dbug_crash[_all].inc
      */
      const bool force_server_crash_dbug = false;
      DBUG_EXECUTE_IF("crash_now", assert(force_server_crash_dbug););
      DBUG_EXECUTE_IF("crash_now_safe", DBUG_SUICIDE(););
#endif

      break;
    }
    case SQLCOM_SET_PASSWORD: {
      List<set_var_base> *lex_var_list = &lex->var_list;

      assert(lex_var_list->elements == 1);
      assert(all_tables == nullptr);
      Userhostpassword_list generated_passwords;
      if (!(res = sql_set_variables(thd, lex_var_list, false))) {
        List_iterator_fast<set_var_base> it(*lex_var_list);
        set_var_base *var;
        while ((var = it++)) {
          set_var_password *setpasswd = static_cast<set_var_password *>(var);
          if (setpasswd->has_generated_password()) {
            const LEX_USER *user = setpasswd->get_user();
            random_password_info p{
                std::string(user->user.str, user->user.length),
                std::string(user->host.str, user->host.length),
                setpasswd->get_generated_password(), 1};
            generated_passwords.push_back(p);
          }
        }
        if (generated_passwords.size() > 0) {
          if (send_password_result_set(thd, generated_passwords)) goto error;
        }  // end if generated_passwords
        if (generated_passwords.size() == 0) my_ok(thd);
      } else {
        // We encountered some sort of error, but no message was sent.
        if (!thd->is_error())
          my_error(ER_WRONG_ARGUMENTS, MYF(0), "SET PASSWORD");
        goto error;
      }

      break;
    }

    case SQLCOM_UNLOCK_TABLES:
      /*
        It is critical for mysqldump --single-transaction --source-data that
        UNLOCK TABLES does not implicitly commit a connection which has only
        done FLUSH TABLES WITH READ LOCK + BEGIN. If this assumption becomes
        false, mysqldump will not work.
      */
      if (thd->variables.option_bits & OPTION_TABLE_LOCK) {
        /*
          Can we commit safely? If not, return to avoid releasing
          transactional metadata locks.
        */
        if (trans_check_state(thd)) return -1;
        res = trans_commit_implicit(thd);
        thd->locked_tables_list.unlock_locked_tables(thd);
        thd->mdl_context.release_transactional_locks();
        thd->variables.option_bits &= ~(OPTION_TABLE_LOCK);
      }
      if (thd->global_read_lock.is_acquired())
        thd->global_read_lock.unlock_global_read_lock(thd);
      if (res) goto error;
      my_ok(thd);
      break;
    case SQLCOM_LOCK_TABLES:
      /*
        Can we commit safely? If not, return to avoid releasing
        transactional metadata locks.
      */
      if (trans_check_state(thd)) return -1;
      /* We must end the transaction first, regardless of anything */
      res = trans_commit_implicit(thd);
      thd->locked_tables_list.unlock_locked_tables(thd);
      /* Release transactional metadata locks. */
      thd->mdl_context.release_transactional_locks();
      if (res) goto error;

      /*
        Here we have to pre-open temporary tables for LOCK TABLES.

        CF_PREOPEN_TMP_TABLES is not set for this SQL statement simply
        because LOCK TABLES calls close_thread_tables() as a first thing
        (it's called from unlock_locked_tables() above). So even if
        CF_PREOPEN_TMP_TABLES was set and the tables would be pre-opened
        in a usual way, they would have been closed.
      */

      if (open_temporary_tables(thd, all_tables)) goto error;

      if (lock_tables_precheck(thd, all_tables)) goto error;

      thd->variables.option_bits |= OPTION_TABLE_LOCK;

      res = lock_tables_open_and_lock_tables(thd, all_tables);

      if (res) {
        thd->variables.option_bits &= ~(OPTION_TABLE_LOCK);
      } else {
        my_ok(thd);
      }
      break;

    case SQLCOM_IMPORT:
      res = lex->m_sql_cmd->execute(thd);
      break;
    case SQLCOM_CREATE_DB: {
      const char *alias;
      if (!(alias = thd->strmake(lex->name.str, lex->name.length)) ||
          (check_and_convert_db_name(&lex->name, false) !=
           Ident_name_check::OK))
        break;
      if (check_access(thd, CREATE_ACL, lex->name.str, nullptr, nullptr, true,
                       false))
        break;
      /*
        As mysql_create_db() may modify HA_CREATE_INFO structure passed to
        it, we need to use a copy of LEX::create_info to make execution
        prepared statement- safe.
      */
      HA_CREATE_INFO create_info(*lex->create_info);
      res = mysql_create_db(
          thd, (lower_case_table_names == 2 ? alias : lex->name.str),
          &create_info);
      break;
    }
    case SQLCOM_DROP_DB: {
      if (check_and_convert_db_name(&lex->name, false) != Ident_name_check::OK)
        break;
      if (check_access(thd, DROP_ACL, lex->name.str, nullptr, nullptr, true,
                       false))
        break;
      res = mysql_rm_db(thd, to_lex_cstring(lex->name), lex->drop_if_exists);
      break;
    }
    case SQLCOM_ALTER_DB: {
      if (check_and_convert_db_name(&lex->name, false) != Ident_name_check::OK)
        break;
      if (check_access(thd, ALTER_ACL, lex->name.str, nullptr, nullptr, true,
                       false))
        break;
      /*
        As mysql_alter_db() may modify HA_CREATE_INFO structure passed to
        it, we need to use a copy of LEX::create_info to make execution
        prepared statement- safe.
      */
      HA_CREATE_INFO create_info(*lex->create_info);
      res = mysql_alter_db(thd, lex->name.str, &create_info);
      break;
    }
    case SQLCOM_CREATE_EVENT:
    case SQLCOM_ALTER_EVENT:
      do {
        assert(lex->event_parse_data);
        if (lex->table_or_sp_used()) {
          my_error(ER_NOT_SUPPORTED_YET, MYF(0),
                   "Usage of subqueries or stored "
                   "function calls as part of this statement");
          break;
        }

        // Use the hypergraph optimizer if it's enabled.
        lex->set_using_hypergraph_optimizer(
            thd->optimizer_switch_flag(OPTIMIZER_SWITCH_HYPERGRAPH_OPTIMIZER));

        res = sp_process_definer(thd);
        if (res) break;

        switch (lex->sql_command) {
          case SQLCOM_CREATE_EVENT: {
            const bool if_not_exists =
                (lex->create_info->options & HA_LEX_CREATE_IF_NOT_EXISTS);
            res =
                Events::create_event(thd, lex->event_parse_data, if_not_exists);
            break;
          }
          case SQLCOM_ALTER_EVENT: {
            LEX_CSTRING name_lex_str = NULL_CSTR;
            if (lex->spname) {
              name_lex_str.str = lex->spname->m_name.str;
              name_lex_str.length = lex->spname->m_name.length;
            }

            res =
                Events::update_event(thd, lex->event_parse_data,
                                     lex->spname ? &lex->spname->m_db : nullptr,
                                     lex->spname ? &name_lex_str : nullptr);
            break;
          }
          default:
            assert(0);
        }
        DBUG_PRINT("info", ("DDL error code=%d", res));
        if (!res && !thd->killed) my_ok(thd);

      } while (false);
      /* Don't do it, if we are inside a SP */
      if (!thd->sp_runtime_ctx) {
        sp_head::destroy(lex->sphead);
        lex->sphead = nullptr;
      }
      /* lex->cleanup() is called outside, no need to call it here */
      break;
    case SQLCOM_DROP_EVENT: {
      if (!(res = Events::drop_event(thd, lex->spname->m_db,
                                     to_lex_cstring(lex->spname->m_name),
                                     lex->drop_if_exists)))
        my_ok(thd);
      break;
    }
    case SQLCOM_CREATE_FUNCTION:  // UDF function
    {
      if (check_access(thd, INSERT_ACL, "mysql", nullptr, nullptr, true, false))
        break;
      if (!(res = mysql_create_function(
                thd, &lex->udf,
                lex->create_info->options & HA_LEX_CREATE_IF_NOT_EXISTS)))
        my_ok(thd);
      break;
    }
    case SQLCOM_CREATE_USER: {
      if (check_access(thd, INSERT_ACL, "mysql", nullptr, nullptr, true,
                       true) &&
          check_global_access(thd, CREATE_USER_ACL))
        break;
      /* Conditionally writes to binlog */
      const HA_CREATE_INFO create_info(*lex->create_info);
      if (!(res = mysql_create_user(
                thd, lex->users_list,
                create_info.options & HA_LEX_CREATE_IF_NOT_EXISTS, false))) {
        // OK or result set was already sent.
      }

      break;
    }
    case SQLCOM_DROP_USER: {
      if (check_access(thd, DELETE_ACL, "mysql", nullptr, nullptr, true,
                       true) &&
          check_global_access(thd, CREATE_USER_ACL))
        break;
      /* Conditionally writes to binlog */
      if (!(res = mysql_drop_user(thd, lex->users_list, lex->drop_if_exists,
                                  false)))
        my_ok(thd);

      break;
    }
    case SQLCOM_RENAME_USER: {
      if (check_access(thd, UPDATE_ACL, "mysql", nullptr, nullptr, true,
                       true) &&
          check_global_access(thd, CREATE_USER_ACL))
        break;
      /* Conditionally writes to binlog */
      if (!(res = mysql_rename_user(thd, lex->users_list))) my_ok(thd);
      break;
    }
    case SQLCOM_REVOKE_ALL: {
      if (check_access(thd, UPDATE_ACL, "mysql", nullptr, nullptr, true,
                       true) &&
          check_global_access(thd, CREATE_USER_ACL))
        break;

      /* Replicate current user as grantor */
      thd->binlog_invoker();

      /* Conditionally writes to binlog */
      if (!(res = mysql_revoke_all(thd, lex->users_list))) my_ok(thd);
      break;
    }
    case SQLCOM_REVOKE:
    case SQLCOM_GRANT: {
      /* GRANT ... AS preliminery checks */
      if (lex->grant_as.grant_as_used) {
        if ((first_table || query_block->db)) {
          my_error(ER_UNSUPPORTED_USE_OF_GRANT_AS, MYF(0));
          goto error;
        }
      }
      /*
        Skip access check if we're granting a proxy
      */
      if (lex->type != TYPE_ENUM_PROXY) {
        /*
          If there are static grants in the GRANT statement or there are no
          dynamic privileges we perform check_access on GRANT_OPTION based on
          static global privilege level and set the DA accordingly.
        */
        if (lex->grant > 0 || lex->dynamic_privileges.elements == 0) {
          /*
            check_access sets DA error message based on GRANT arguments.
          */
          if (check_access(
                  thd, lex->grant | lex->grant_tot_col | GRANT_ACL,
                  first_table ? first_table->db : query_block->db,
                  first_table ? &first_table->grant.privilege : nullptr,
                  first_table ? &first_table->grant.m_internal : nullptr,
                  first_table ? false : true, false)) {
            goto error;
          }
        }
        /*
          ..else we still call check_access to load internal structures, but
          defer checking of global dynamic GRANT_OPTION to mysql_grant. We still
          ignore checks if this was a grant of a proxy.
        */
        else {
          /*
            check_access will load grant.privilege and grant.m_internal with
            values which are used later during column privilege checking. The
            return value isn't interesting as we'll check for dynamic global
            privileges later.
          */
          check_access(thd, lex->grant | lex->grant_tot_col | GRANT_ACL,
                       first_table ? first_table->db : query_block->db,
                       first_table ? &first_table->grant.privilege : nullptr,
                       first_table ? &first_table->grant.m_internal : nullptr,
                       first_table ? false : true, true);
        }
      }

      /* Replicate current user as grantor */
      thd->binlog_invoker();

      if (thd->security_context()->user().str)  // If not replication
      {
        LEX_USER *user, *tmp_user;
        bool first_user = true;

        List_iterator<LEX_USER> user_list(lex->users_list);
        while ((tmp_user = user_list++)) {
          if (!(user = get_current_user(thd, tmp_user))) goto error;
          if (specialflag & SPECIAL_NO_RESOLVE &&
              hostname_requires_resolving(user->host.str))
            push_warning(thd, Sql_condition::SL_WARNING,
                         ER_WARN_HOSTNAME_WONT_WORK,
                         ER_THD(thd, ER_WARN_HOSTNAME_WONT_WORK));
          // Are we trying to change a password of another user
          assert(user->host.str != nullptr);

          /*
            GRANT/REVOKE PROXY has the target user as a first entry in the list.
           */
          if (lex->type == TYPE_ENUM_PROXY && first_user) {
            first_user = false;
            if (acl_check_proxy_grant_access(thd, user->host.str,
                                             user->user.str,
                                             lex->grant & GRANT_ACL))
              goto error;
          }
        }
      }
      if (first_table) {
        if (lex->dynamic_privileges.elements > 0) {
          if (thd->lex->grant_if_exists) {
            push_warning_printf(thd, Sql_condition::SL_WARNING,
                                ER_ILLEGAL_PRIVILEGE_LEVEL,
                                ER_THD(thd, ER_ILLEGAL_PRIVILEGE_LEVEL),
                                all_tables->table_name);
          } else {
            my_error(ER_ILLEGAL_PRIVILEGE_LEVEL, MYF(0),
                     all_tables->table_name);
            goto error;
          }
        }
        if (lex->type == TYPE_ENUM_PROCEDURE ||
            lex->type == TYPE_ENUM_FUNCTION) {
          uint grants = lex->all_privileges
                            ? (PROC_OP_ACLS) | (lex->grant & GRANT_ACL)
                            : lex->grant;
          if (check_grant_routine(thd, grants | GRANT_ACL, all_tables,
                                  lex->type == TYPE_ENUM_PROCEDURE, false))
            goto error;
          /* Conditionally writes to binlog */
          res = mysql_routine_grant(
              thd, all_tables, lex->type == TYPE_ENUM_PROCEDURE,
              lex->users_list, grants, lex->sql_command == SQLCOM_REVOKE, true,
              lex->all_privileges);
          if (!res) my_ok(thd);
        } else {
          if (check_grant(thd, (lex->grant | lex->grant_tot_col | GRANT_ACL),
                          all_tables, false, UINT_MAX, false))
            goto error;
          /* Conditionally writes to binlog */
          res = mysql_table_grant(
              thd, all_tables, lex->users_list, lex->columns, lex->grant,
              lex->sql_command == SQLCOM_REVOKE, lex->all_privileges);
        }
      } else {
        if (lex->columns.elements ||
            (lex->type && lex->type != TYPE_ENUM_PROXY)) {
          my_error(ER_ILLEGAL_GRANT_FOR_TABLE, MYF(0));
          goto error;
        } else {
          /* Dynamic privileges are allowed only for global grants */
          if (query_block->db && lex->dynamic_privileges.elements > 0) {
            String privs;
            bool comma = false;
            for (const LEX_CSTRING &priv : lex->dynamic_privileges) {
              if (comma) privs.append(",");
              privs.append(priv.str, priv.length);
              comma = true;
            }
            if (thd->lex->grant_if_exists) {
              push_warning_printf(
                  thd, Sql_condition::SL_WARNING, ER_ILLEGAL_PRIVILEGE_LEVEL,
                  ER_THD(thd, ER_ILLEGAL_PRIVILEGE_LEVEL), privs.c_ptr());
            } else {
              my_error(ER_ILLEGAL_PRIVILEGE_LEVEL, MYF(0), privs.c_ptr());
              goto error;
            }
          }
          /* Conditionally writes to binlog */
          res = mysql_grant(
              thd, query_block->db, lex->users_list, lex->grant,
              lex->sql_command == SQLCOM_REVOKE, lex->type == TYPE_ENUM_PROXY,
              lex->dynamic_privileges, lex->all_privileges, &lex->grant_as);
        }
      }
      break;
    }
    case SQLCOM_RESET:
      /*
        RESET commands are never written to the binary log, so we have to
        initialize this variable because RESET shares the same code as FLUSH
      */
      lex->no_write_to_binlog = true;
      if ((lex->type & REFRESH_PERSIST) && (lex->option_type == OPT_PERSIST)) {
        Persisted_variables_cache *pv =
            Persisted_variables_cache::get_instance();
        if (pv)
          if (pv->reset_persisted_variables(thd, lex->name.str,
                                            lex->drop_if_exists))
            goto error;
        my_ok(thd);
        break;
      }
      [[fallthrough]];
    case SQLCOM_FLUSH: {
      int write_to_binlog;
      if (is_reload_request_denied(thd, lex->type)) goto error;

      if (first_table && lex->type & REFRESH_READ_LOCK) {
        /* Check table-level privileges. */
        if (check_table_access(thd, LOCK_TABLES_ACL | SELECT_ACL, all_tables,
                               false, UINT_MAX, false))
          goto error;
        if (flush_tables_with_read_lock(thd, all_tables)) goto error;
        my_ok(thd);
        break;
      } else if (first_table && lex->type & REFRESH_FOR_EXPORT) {
        /* Check table-level privileges. */
        if (check_table_access(thd, LOCK_TABLES_ACL | SELECT_ACL, all_tables,
                               false, UINT_MAX, false))
          goto error;
        if (flush_tables_for_export(thd, all_tables)) goto error;
        my_ok(thd);
        break;
      }

      /*
        handle_reload_request() will tell us if we are allowed to write to the
        binlog or not.
      */
      if (!handle_reload_request(thd, lex->type, first_table,
                                 &write_to_binlog)) {
        /*
          We WANT to write and we CAN write.
          ! we write after unlocking the table.
        */
        /*
          Presumably, RESET and binlog writing doesn't require synchronization
        */

        if (write_to_binlog > 0)  // we should write
        {
          if (!lex->no_write_to_binlog)
            res = write_bin_log(thd, false, thd->query().str,
                                thd->query().length);
        } else if (write_to_binlog < 0) {
          /*
             We should not write, but rather report error because
             handle_reload_request binlog interactions failed
           */
          res = 1;
        }

        if (!res) my_ok(thd);
      }

      break;
    }
    case SQLCOM_KILL: {
      Item *it = lex->kill_value_list.head();

      if (lex->table_or_sp_used()) {
        my_error(ER_NOT_SUPPORTED_YET, MYF(0),
                 "Usage of subqueries or stored "
                 "function calls as part of this statement");
        goto error;
      }

      if ((!it->fixed && it->fix_fields(lex->thd, &it)) || it->check_cols(1)) {
        my_error(ER_SET_CONSTANTS_ONLY, MYF(0));
        goto error;
      }

      const my_thread_id thread_id = static_cast<my_thread_id>(it->val_int());
      if (thd->is_error()) goto error;

      sql_kill(thd, thread_id, lex->type & ONLY_KILL_QUERY);
      break;
    }
    case SQLCOM_SHOW_CREATE_USER: {
      LEX_USER *show_user = get_current_user(thd, lex->grant_user);
      Security_context *sctx = thd->security_context();
      const bool are_both_users_same =
          !strcmp(sctx->priv_user().str, show_user->user.str) &&
          !my_strcasecmp(system_charset_info, show_user->host.str,
                         sctx->priv_host().str);
      if (are_both_users_same || !check_access(thd, SELECT_ACL, "mysql",
                                               nullptr, nullptr, true, false))
        res = mysql_show_create_user(thd, show_user, are_both_users_same);
      break;
    }
    case SQLCOM_BEGIN:
      if (trans_begin(thd, lex->start_transaction_opt)) goto error;
      my_ok(thd);
      break;
    case SQLCOM_COMMIT: {
      assert(thd->lock == nullptr ||
             thd->locked_tables_mode == LTM_LOCK_TABLES);
      const bool tx_chain =
          (lex->tx_chain == TVL_YES ||
           (thd->variables.completion_type == 1 && lex->tx_chain != TVL_NO));
      const bool tx_release =
          (lex->tx_release == TVL_YES ||
           (thd->variables.completion_type == 2 && lex->tx_release != TVL_NO));
      if (trans_commit(thd)) goto error;
      thd->mdl_context.release_transactional_locks();
      /* Begin transaction with the same isolation level. */
      if (tx_chain) {
        if (trans_begin(thd)) goto error;
      } else {
        /* Reset the isolation level and access mode if no chaining
         * transaction.*/
        trans_reset_one_shot_chistics(thd);
      }
      /* Disconnect the current client connection. */
      if (tx_release) thd->killed = THD::KILL_CONNECTION;
      my_ok(thd);
      break;
    }
    case SQLCOM_ROLLBACK: {
      assert(thd->lock == nullptr ||
             thd->locked_tables_mode == LTM_LOCK_TABLES);
      const bool tx_chain =
          (lex->tx_chain == TVL_YES ||
           (thd->variables.completion_type == 1 && lex->tx_chain != TVL_NO));
      const bool tx_release =
          (lex->tx_release == TVL_YES ||
           (thd->variables.completion_type == 2 && lex->tx_release != TVL_NO));
      if (trans_rollback(thd)) goto error;
      thd->mdl_context.release_transactional_locks();
      /* Begin transaction with the same isolation level. */
      if (tx_chain) {
        if (trans_begin(thd)) goto error;
      } else {
        /* Reset the isolation level and access mode if no chaining
         * transaction.*/
        trans_reset_one_shot_chistics(thd);
      }
      /* Disconnect the current client connection. */
      if (tx_release) thd->killed = THD::KILL_CONNECTION;
      my_ok(thd);
      break;
    }
    case SQLCOM_RELEASE_SAVEPOINT:
      if (trans_release_savepoint(thd, lex->ident)) goto error;
      my_ok(thd);
      break;
    case SQLCOM_ROLLBACK_TO_SAVEPOINT:
      if (trans_rollback_to_savepoint(thd, lex->ident)) goto error;
      my_ok(thd);
      break;
    case SQLCOM_SAVEPOINT:
      if (trans_savepoint(thd, lex->ident)) goto error;
      my_ok(thd);
      break;
    case SQLCOM_CREATE_PROCEDURE:
    case SQLCOM_CREATE_SPFUNCTION: {
      uint namelen;
      char *name;

      assert(lex->sphead != nullptr);

      if (!lex->sphead->is_sql()) {
        if (srv_registry == nullptr) {
          my_error(ER_LANGUAGE_COMPONENT_NOT_AVAILABLE, MYF(0));
          goto error;
        }

        my_service<SERVICE_TYPE(external_program_capability_query)>
            lang_service("external_program_capability_query", srv_registry);
        if (lang_service.is_valid()) {
          bool supported = false;
          if (lang_service->get(
                  "supports_language",
                  const_cast<char *>(lex->sp_chistics.language.str),
                  &supported))
            goto error;
          if (!supported) {
            my_error(ER_LANGUAGE_COMPONENT_UNSUPPORTED_LANGUAGE, MYF(0),
                     lex->sp_chistics.language.str);
            goto error;
          }
          my_service<SERVICE_TYPE(external_program_execution)> sp_service(
              "external_program_execution", srv_registry);
          if (!sp_service.is_valid())
            push_warning(thd, ER_LANGUAGE_COMPONENT_NOT_AVAILABLE);
          else if (lex->sphead->init_external_routine(sp_service))
            goto error;
        } else {
          push_warning(thd, ER_LANGUAGE_COMPONENT_NOT_AVAILABLE);
        }
      }

      assert(lex->sphead->m_db.str); /* Must be initialized in the parser */
      /*
        Verify that the database name is allowed, optionally
        lowercase it.
      */
      if (check_and_convert_db_name(&lex->sphead->m_db, false) !=
          Ident_name_check::OK)
        goto error;

      if (check_access(thd, CREATE_PROC_ACL, lex->sphead->m_db.str, nullptr,
                       nullptr, false, false))
        goto error;

      name = lex->sphead->name(&namelen);
      if (lex->sphead->m_type == enum_sp_type::FUNCTION) {
        udf_func *udf = find_udf(name, namelen);
        /*
          Issue a warning if there is an existing loadable function with the
          same name.
        */
        if (udf) {
          push_warning_printf(thd, Sql_condition::SL_NOTE,
                              ER_WARN_SF_UDF_NAME_COLLISION,
                              ER_THD(thd, ER_WARN_SF_UDF_NAME_COLLISION), name);
        }
      }

      if (sp_process_definer(thd)) goto error;

      /*
        Record the CURRENT_USER in binlog. The CURRENT_USER is used on slave to
        grant default privileges when sp_automatic_privileges variable is set.
      */
      thd->binlog_invoker();

      bool sp_already_exists = false;
      if (!(res = sp_create_routine(
                thd, lex->sphead, thd->lex->definer,
                thd->lex->create_info->options & HA_LEX_CREATE_IF_NOT_EXISTS,
                sp_already_exists))) {
        if (!sp_already_exists) {
          /* only add privileges if really necessary */

          Security_context security_context;
          bool restore_backup_context = false;
          Security_context *backup = nullptr;
          /*
            We're going to issue an implicit GRANT statement so we close all
            open tables. We have to keep metadata locks as this ensures that
            this statement is atomic against concurrent FLUSH TABLES WITH READ
            LOCK. Deadlocks which can arise due to fact that this implicit
            statement takes metadata locks should be detected by a deadlock
            detector in MDL subsystem and reported as errors.

            No need to commit/rollback statement transaction, it's not started.

            TODO: Long-term we should either ensure that implicit GRANT
            statement is written into binary log as a separate statement or make
            both creation of routine and implicit GRANT parts of one fully
            atomic statement.
          */
          assert(thd->get_transaction()->is_empty(Transaction_ctx::STMT));
          close_thread_tables(thd);
          /*
            Check if invoker exists on slave, then use invoker privilege to
            insert routine privileges to mysql.procs_priv. If invoker is not
            available then consider using definer.

            Check if the definer exists on slave,
            then use definer privilege to insert routine privileges to
            mysql.procs_priv.

            For current user of SQL thread has GLOBAL_ACL privilege,
            which doesn't any check routine privileges,
            so no routine privilege record  will insert into mysql.procs_priv.
          */

          if (thd->slave_thread) {
            LEX_CSTRING current_user;
            LEX_CSTRING current_host;
            if (thd->has_invoker()) {
              current_host = thd->get_invoker_host();
              current_user = thd->get_invoker_user();
            } else {
              current_host = lex->definer->host;
              current_user = lex->definer->user;
            }
            if (is_acl_user(thd, current_host.str, current_user.str)) {
              security_context.change_security_context(
                  thd, current_user, current_host, thd->lex->sphead->m_db.str,
                  &backup);
              restore_backup_context = true;
            }
          }

          if (sp_automatic_privileges && !opt_noacl &&
              check_routine_access(
                  thd, DEFAULT_CREATE_PROC_ACLS, lex->sphead->m_db.str, name,
                  lex->sql_command == SQLCOM_CREATE_PROCEDURE, true)) {
            if (sp_grant_privileges(
                    thd, lex->sphead->m_db.str, name,
                    lex->sql_command == SQLCOM_CREATE_PROCEDURE))
              push_warning(thd, Sql_condition::SL_WARNING,
                           ER_PROC_AUTO_GRANT_FAIL,
                           ER_THD(thd, ER_PROC_AUTO_GRANT_FAIL));
            thd->clear_error();
          }

          /*
            Restore current user with GLOBAL_ACL privilege of SQL thread
          */
          if (restore_backup_context) {
            assert(thd->slave_thread == 1);
            thd->security_context()->restore_security_context(thd, backup);
          }
        }
        my_ok(thd);
      }
      break; /* break super switch */
    }        /* end case group bracket */

    case SQLCOM_ALTER_PROCEDURE:
    case SQLCOM_ALTER_FUNCTION: {
      if (check_routine_access(thd, ALTER_PROC_ACL, lex->spname->m_db.str,
                               lex->spname->m_name.str,
                               lex->sql_command == SQLCOM_ALTER_PROCEDURE,
                               false))
        goto error;

      const enum_sp_type sp_type = (lex->sql_command == SQLCOM_ALTER_PROCEDURE)
                                       ? enum_sp_type::PROCEDURE
                                       : enum_sp_type::FUNCTION;
      /*
        Note that if you implement the capability of ALTER FUNCTION to
        alter the body of the function, this command should be made to
        follow the restrictions that log-bin-trust-function-creators=0
        already puts on CREATE FUNCTION.
      */
      /* Conditionally writes to binlog */
      res = sp_update_routine(thd, sp_type, lex->spname, &lex->sp_chistics);
      if (res || thd->killed) goto error;

      my_ok(thd);
      break;
    }
    case SQLCOM_DROP_PROCEDURE:
    case SQLCOM_DROP_FUNCTION: {
      if (lex->sql_command == SQLCOM_DROP_FUNCTION &&
          !lex->spname->m_explicit_name) {
        /* DROP FUNCTION <non qualified name> */
        udf_func *udf =
            find_udf(lex->spname->m_name.str, lex->spname->m_name.length);
        if (udf) {
          if (check_access(thd, DELETE_ACL, "mysql", nullptr, nullptr, true,
                           false))
            goto error;

          if (!(res = mysql_drop_function(thd, &lex->spname->m_name))) {
            my_ok(thd);
            break;
          }
          my_error(ER_SP_DROP_FAILED, MYF(0), "FUNCTION (UDF)",
                   lex->spname->m_name.str);
          goto error;
        }

        if (lex->spname->m_db.str == nullptr) {
          if (lex->drop_if_exists) {
            push_warning_printf(thd, Sql_condition::SL_NOTE,
                                ER_SP_DOES_NOT_EXIST,
                                ER_THD(thd, ER_SP_DOES_NOT_EXIST),
                                "FUNCTION (UDF)", lex->spname->m_name.str);
            res = false;
            my_ok(thd);
            break;
          }
          my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "FUNCTION (UDF)",
                   lex->spname->m_name.str);
          goto error;
        }
        /* Fall thought to test for a stored function */
      }

      const char *db = lex->spname->m_db.str;
      char *name = lex->spname->m_name.str;

      if (check_routine_access(thd, ALTER_PROC_ACL, db, name,
                               lex->sql_command == SQLCOM_DROP_PROCEDURE,
                               false))
        goto error;

      const enum_sp_type sp_type = (lex->sql_command == SQLCOM_DROP_PROCEDURE)
                                       ? enum_sp_type::PROCEDURE
                                       : enum_sp_type::FUNCTION;

      /* Conditionally writes to binlog */
      const enum_sp_return_code sp_result =
          sp_drop_routine(thd, sp_type, lex->spname);

      /*
        We're going to issue an implicit REVOKE statement so we close all
        open tables. We have to keep metadata locks as this ensures that
        this statement is atomic against concurrent FLUSH TABLES WITH READ
        LOCK. Deadlocks which can arise due to fact that this implicit
        statement takes metadata locks should be detected by a deadlock
        detector in MDL subsystem and reported as errors.

        No need to commit/rollback statement transaction, it's not started.

        TODO: Long-term we should either ensure that implicit REVOKE statement
              is written into binary log as a separate statement or make both
              dropping of routine and implicit REVOKE parts of one fully atomic
              statement.
      */
      assert(thd->get_transaction()->is_empty(Transaction_ctx::STMT));
      close_thread_tables(thd);

      if (sp_result != SP_DOES_NOT_EXISTS && sp_automatic_privileges &&
          !opt_noacl &&
          sp_revoke_privileges(thd, db, name,
                               lex->sql_command == SQLCOM_DROP_PROCEDURE)) {
        push_warning(thd, Sql_condition::SL_WARNING, ER_PROC_AUTO_REVOKE_FAIL,
                     ER_THD(thd, ER_PROC_AUTO_REVOKE_FAIL));
        /* If this happens, an error should have been reported. */
        goto error;
      }

      res = sp_result;
      switch (sp_result) {
        case SP_OK:
          my_ok(thd);
          break;
        case SP_DOES_NOT_EXISTS:
          if (lex->drop_if_exists) {
            res =
                write_bin_log(thd, true, thd->query().str, thd->query().length);
            push_warning_printf(thd, Sql_condition::SL_NOTE,
                                ER_SP_DOES_NOT_EXIST,
                                ER_THD(thd, ER_SP_DOES_NOT_EXIST),
                                SP_COM_STRING(lex), lex->spname->m_qname.str);
            if (!res) my_ok(thd);
            break;
          }
          my_error(ER_SP_DOES_NOT_EXIST, MYF(0), SP_COM_STRING(lex),
                   lex->spname->m_qname.str);
          goto error;
        default:
          my_error(ER_SP_DROP_FAILED, MYF(0), SP_COM_STRING(lex),
                   lex->spname->m_qname.str);
          goto error;
      }
      break;
    }
    case SQLCOM_CREATE_VIEW: {
      /*
        Note: SQLCOM_CREATE_VIEW also handles 'ALTER VIEW' commands
        as specified through the thd->lex->create_view_mode flag.
      */
      res = mysql_create_view(thd, first_table, thd->lex->create_view_mode);
      break;
    }
    case SQLCOM_DROP_VIEW: {
      if (check_table_access(thd, DROP_ACL, all_tables, false, UINT_MAX, false))
        goto error;
      /* Conditionally writes to binlog. */
      res = mysql_drop_view(thd, first_table);
      break;
    }
    case SQLCOM_CREATE_TRIGGER:
    case SQLCOM_DROP_TRIGGER: {
      /* Conditionally writes to binlog. */
      assert(lex->m_sql_cmd != nullptr);
      static_cast<Sql_cmd_ddl_trigger_common *>(lex->m_sql_cmd)
          ->set_table(all_tables);

      res = lex->m_sql_cmd->execute(thd);
      break;
    }
    case SQLCOM_BINLOG_BASE64_EVENT: {
      mysql_client_binlog_statement(thd);
      break;
    }
    case SQLCOM_ANALYZE:
    case SQLCOM_CHECK:
    case SQLCOM_OPTIMIZE:
    case SQLCOM_REPAIR:
    case SQLCOM_TRUNCATE:
    case SQLCOM_ALTER_TABLE:
    case SQLCOM_HA_OPEN:
    case SQLCOM_HA_READ:
    case SQLCOM_HA_CLOSE:
      assert(first_table == all_tables && first_table != nullptr);
      [[fallthrough]];
    case SQLCOM_CREATE_SERVER:
    case SQLCOM_CREATE_RESOURCE_GROUP:
    case SQLCOM_ALTER_SERVER:
    case SQLCOM_ALTER_RESOURCE_GROUP:
    case SQLCOM_DROP_RESOURCE_GROUP:
    case SQLCOM_DROP_SERVER:
    case SQLCOM_SET_RESOURCE_GROUP:
    case SQLCOM_SIGNAL:
    case SQLCOM_RESIGNAL:
    case SQLCOM_GET_DIAGNOSTICS:
    case SQLCOM_CHANGE_REPLICATION_FILTER:
    case SQLCOM_XA_START:
    case SQLCOM_XA_END:
    case SQLCOM_XA_PREPARE:
    case SQLCOM_XA_COMMIT:
    case SQLCOM_XA_ROLLBACK:
    case SQLCOM_XA_RECOVER:
    case SQLCOM_INSTALL_PLUGIN:
    case SQLCOM_UNINSTALL_PLUGIN:
    case SQLCOM_INSTALL_COMPONENT:
    case SQLCOM_UNINSTALL_COMPONENT:
    case SQLCOM_SHUTDOWN:
    case SQLCOM_ALTER_INSTANCE:
    case SQLCOM_SELECT:
    case SQLCOM_DO:
    case SQLCOM_CALL:
    case SQLCOM_CREATE_ROLE:
    case SQLCOM_DROP_ROLE:
    case SQLCOM_SET_ROLE:
    case SQLCOM_GRANT_ROLE:
    case SQLCOM_REVOKE_ROLE:
    case SQLCOM_ALTER_USER_DEFAULT_ROLE:
    case SQLCOM_SHOW_BINLOG_EVENTS:
    case SQLCOM_SHOW_BINLOGS:
    case SQLCOM_SHOW_CHARSETS:
    case SQLCOM_SHOW_COLLATIONS:
    case SQLCOM_SHOW_CREATE_DB:
    case SQLCOM_SHOW_CREATE_EVENT:
    case SQLCOM_SHOW_CREATE_FUNC:
    case SQLCOM_SHOW_CREATE_PROC:
    case SQLCOM_SHOW_CREATE:
    case SQLCOM_SHOW_CREATE_TRIGGER:
    // case SQLCOM_SHOW_CREATE_USER:
    case SQLCOM_SHOW_DATABASES:
    case SQLCOM_SHOW_ENGINE_LOGS:
    case SQLCOM_SHOW_ENGINE_MUTEX:
    case SQLCOM_SHOW_ENGINE_STATUS:
    case SQLCOM_SHOW_ERRORS:
    case SQLCOM_SHOW_EVENTS:
    case SQLCOM_SHOW_FIELDS:
    case SQLCOM_SHOW_FUNC_CODE:
    case SQLCOM_SHOW_GRANTS:
    case SQLCOM_SHOW_KEYS:
    case SQLCOM_SHOW_BINLOG_STATUS:
    case SQLCOM_SHOW_OPEN_TABLES:
    case SQLCOM_SHOW_PARSE_TREE:
    case SQLCOM_SHOW_PLUGINS:
    case SQLCOM_SHOW_PRIVILEGES:
    case SQLCOM_SHOW_PROC_CODE:
    case SQLCOM_SHOW_PROCESSLIST:
    case SQLCOM_SHOW_PROFILE:
    case SQLCOM_SHOW_PROFILES:
    case SQLCOM_SHOW_RELAYLOG_EVENTS:
    case SQLCOM_SHOW_REPLICAS:
    case SQLCOM_SHOW_REPLICA_STATUS:
    case SQLCOM_SHOW_STATUS:
    case SQLCOM_SHOW_STORAGE_ENGINES:
    case SQLCOM_SHOW_TABLE_STATUS:
    case SQLCOM_SHOW_TABLES:
    case SQLCOM_SHOW_TRIGGERS:
    case SQLCOM_SHOW_STATUS_PROC:
    case SQLCOM_SHOW_STATUS_FUNC:
    case SQLCOM_SHOW_VARIABLES:
    case SQLCOM_SHOW_WARNS:
    case SQLCOM_CLONE:
    case SQLCOM_LOCK_INSTANCE:
    case SQLCOM_UNLOCK_INSTANCE:
    case SQLCOM_ALTER_TABLESPACE:
    case SQLCOM_EXPLAIN_OTHER:
    case SQLCOM_RESTART_SERVER:
    case SQLCOM_CREATE_SRS:
    case SQLCOM_DROP_SRS: {
      assert(lex->m_sql_cmd != nullptr);

      res = lex->m_sql_cmd->execute(thd);

      break;
    }
    case SQLCOM_ALTER_USER: {
      LEX_USER *user, *tmp_user;
      bool changing_own_password = false;
      Security_context *sctx = thd->security_context();
      const bool own_password_expired = sctx->password_expired();
      bool check_permission = true;
      /* track if it is ALTER USER registration step */
      bool finish_reg = false;
      bool init_reg = false;
      bool unregister = false;
      bool is_self = false;

      List_iterator<LEX_USER> user_list(lex->users_list);
      while ((tmp_user = user_list++)) {
        LEX_MFA *tmp_lex_mfa;
        List_iterator<LEX_MFA> mfa_list_it(tmp_user->mfa_list);
        while ((tmp_lex_mfa = mfa_list_it++)) {
          finish_reg |= tmp_lex_mfa->finish_registration;
          init_reg |= tmp_lex_mfa->init_registration;
          unregister |= tmp_lex_mfa->unregister;
        }
        bool update_password_only = false;
        bool second_password = false;

        /* If it is an empty lex_user update it with current user */
        if (!tmp_user->host.str && !tmp_user->user.str) {
          /* set user information as of the current user */
          assert(sctx->priv_host().str);
          tmp_user->host.str = sctx->priv_host().str;
          tmp_user->host.length = strlen(sctx->priv_host().str);
          assert(sctx->user().str);
          tmp_user->user.str = sctx->user().str;
          tmp_user->user.length = strlen(sctx->user().str);
        }
        if (!(user = get_current_user(thd, tmp_user))) goto error;

        /* copy password expire attributes to individual lex user */
        user->alter_status = thd->lex->alter_password;
        /*
          Only self password change is non-privileged operation. To detect the
          same, we find :
          (1) If it is only password change operation
          (2) If this operation is on self
        */
        if (user->first_factor_auth_info.uses_identified_by_clause &&
            !user->first_factor_auth_info.uses_identified_with_clause &&
            !thd->lex->mqh.specified_limits &&
            !user->alter_status.update_account_locked_column &&
            !user->alter_status.update_password_expired_column &&
            !user->alter_status.expire_after_days &&
            user->alter_status.use_default_password_lifetime &&
            (user->alter_status.update_password_require_current ==
             Lex_acl_attrib_udyn::UNCHANGED) &&
            !user->alter_status.update_password_history &&
            !user->alter_status.update_password_reuse_interval &&
            !user->alter_status.update_failed_login_attempts &&
            !user->alter_status.update_password_lock_time &&
            (thd->lex->ssl_type == SSL_TYPE_NOT_SPECIFIED))
          update_password_only = true;

        is_self = !strcmp(sctx->user().length ? sctx->user().str : "",
                          user->user.str) &&
                  !my_strcasecmp(&my_charset_latin1, user->host.str,
                                 sctx->priv_host().str);
        if (finish_reg || init_reg) {
          /* Registration step is allowed only for connecting users */
          if (!is_self) {
            my_error(ER_INVALID_USER_FOR_REGISTRATION, MYF(0), sctx->user().str,
                     sctx->priv_host().str);
            goto error;
          }
        }
        /*
          if user executes ALTER statement to change password only
          for himself then skip access check - Provided preference to
          retain/discard current password was specified.
        */

        if (user->discard_old_password || user->retain_current_password) {
          second_password = true;
        }
        if ((update_password_only || user->discard_old_password || init_reg ||
             finish_reg || unregister) &&
            is_self) {
          changing_own_password = update_password_only;
          if (second_password) {
            if (check_access(thd, UPDATE_ACL, consts::mysql.c_str(), nullptr,
                             nullptr, true, true) &&
                !sctx->check_access(CREATE_USER_ACL, consts::mysql.c_str()) &&
                !(sctx->has_global_grant(
                          STRING_WITH_LEN("APPLICATION_PASSWORD_ADMIN"))
                      .first)) {
              my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0),
                       "CREATE USER or APPLICATION_PASSWORD_ADMIN");
              goto error;
            }
          }
          continue;
        } else if (check_permission) {
          if (check_access(thd, UPDATE_ACL, "mysql", nullptr, nullptr, true,
                           true) &&
              check_global_access(thd, CREATE_USER_ACL))
            goto error;

          check_permission = false;
        }

        if (is_self &&
            (user->first_factor_auth_info.uses_identified_by_clause ||
             user->first_factor_auth_info.uses_identified_with_clause ||
             user->first_factor_auth_info.uses_authentication_string_clause)) {
          changing_own_password = true;
          break;
        }

        if (update_password_only &&
            likely((get_server_state() == SERVER_OPERATING)) &&
            !strcmp(sctx->priv_user().str, "")) {
          my_error(ER_PASSWORD_ANONYMOUS_USER, MYF(0));
          goto error;
        }
      }

      if (unlikely(own_password_expired && !changing_own_password)) {
        my_error(ER_MUST_CHANGE_PASSWORD, MYF(0));
        goto error;
      }
      /* Conditionally writes to binlog */
      res = mysql_alter_user(thd, lex->users_list, lex->drop_if_exists);
      /*
        Iterate over list of MFA methods, check if all auth plugin methods
        which need registration steps have completed, then turn OFF server
        sandbox mode
      */
      tmp_user = lex->users_list[0];
      if (!res && is_self && finish_reg) {
        if (turn_off_sandbox_mode(thd, tmp_user)) return true;
      }
      break;
    }
    default:
      assert(0); /* Impossible */
      my_ok(thd);
      break;
  }
  goto finish;

error:
  res = true;

finish:
  /* Restore system variables which were changed by SET_VAR hint. */
  if (lex->opt_hints_global && lex->opt_hints_global->sys_var_hint)
    lex->opt_hints_global->sys_var_hint->restore_vars(thd);

  THD_STAGE_INFO(thd, stage_query_end);

  // Check for receiving a recent kill signal
  if (thd->killed) {
    thd->send_kill_message();
    res = thd->is_error();
  }
  if (res) {
    if (thd->get_reprepare_observer() != nullptr &&
        thd->get_reprepare_observer()->is_invalidated() &&
        thd->get_reprepare_observer()->can_retry())
      thd->skip_gtid_rollback = true;
  } else {
    lex->set_exec_started();
  }

  // Cleanup EXPLAIN info
  if (!thd->in_sub_stmt) {
    if (is_explainable_query(lex->sql_command)) {
      DEBUG_SYNC(thd, "before_reset_query_plan");
      /*
        We want EXPLAIN CONNECTION to work until the explained statement ends,
        thus it is only now that we may fully clean up any unit of this
        statement.
      */
      lex->unit->assert_not_fully_clean();
    }
    thd->query_plan.set_query_plan(SQLCOM_END, nullptr, false);
  }

  assert(!thd->in_active_multi_stmt_transaction() ||
         thd->in_multi_stmt_transaction_mode());

  if (!thd->in_sub_stmt) {
    mysql_event_tracking_query_notify(
        thd,
        first_level ? EVENT_TRACKING_QUERY_STATUS_END
                    : EVENT_TRACKING_QUERY_NESTED_STATUS_END,
        first_level ? "EVENT_TRACKING_QUERY_STATUS_END"
                    : "EVENT_TRACKING_QUERY_NESTED_STATUS_END");

    /* report error issued during command execution */
    if ((thd->is_error() && !early_error_on_rep_command) ||
        (thd->variables.option_bits & OPTION_MASTER_SQL_ERROR))
      trans_rollback_stmt(thd);
    else {
      /* If commit fails, we should be able to reset the OK status. */
      thd->get_stmt_da()->set_overwrite_status(true);
      trans_commit_stmt(thd);
      thd->get_stmt_da()->set_overwrite_status(false);
    }
    /*
      Reset thd killed flag during cleanup so that commands which are
      dispatched using service session API's start with a clean state.
    */
    if (thd->killed == THD::KILL_QUERY || thd->killed == THD::KILL_TIMEOUT) {
      thd->killed = THD::NOT_KILLED;
      thd->reset_query_for_display();
    }
  }

  if (thd->get_command() == COM_STMT_EXECUTE) {
    if (!thd->is_error() && !thd->killed)
      mysql_event_tracking_general_notify(
          thd, AUDIT_EVENT(EVENT_TRACKING_GENERAL_RESULT), 0, nullptr, 0);

    const std::string &cn = Command_names::str_global(thd->get_command());
    mysql_event_tracking_general_notify(
        thd, AUDIT_EVENT(EVENT_TRACKING_GENERAL_STATUS),
        thd->get_stmt_da()->is_error() ? thd->get_stmt_da()->mysql_errno() : 0,
        cn.c_str(), cn.length());
  }

  lex->cleanup(true);

  /* Free tables */
  THD_STAGE_INFO(thd, stage_closing_tables);
  close_thread_tables(thd);

  // Rollback any item transformations made during optimization and execution
  thd->rollback_item_tree_changes();

#ifndef NDEBUG
  if (lex->sql_command != SQLCOM_SET_OPTION && !thd->in_sub_stmt)
    DEBUG_SYNC(thd, "execute_command_after_close_tables");
#endif

  if (!thd->in_sub_stmt && thd->transaction_rollback_request) {
    /*
      We are not in sub-statement and transaction rollback was requested by
      one of storage engines (e.g. due to deadlock). Rollback transaction in
      all storage engines including binary log.
    */
    trans_rollback_implicit(thd);
    thd->mdl_context.release_transactional_locks();
  } else if (stmt_causes_implicit_commit(thd, CF_IMPLICIT_COMMIT_END)) {
    /* No transaction control allowed in sub-statements. */
    assert(!thd->in_sub_stmt);
    /* If commit fails, we should be able to reset the OK status. */
    thd->get_stmt_da()->set_overwrite_status(true);
    /* Commit the normal transaction if one is active. */
    trans_commit_implicit(thd);
    thd->get_stmt_da()->set_overwrite_status(false);
    thd->mdl_context.release_transactional_locks();
  } else if (!thd->in_sub_stmt && !thd->in_multi_stmt_transaction_mode()) {
    /*
      - If inside a multi-statement transaction,
      defer the release of metadata locks until the current
      transaction is either committed or rolled back. This prevents
      other statements from modifying the table for the entire
      duration of this transaction.  This provides commit ordering
      and guarantees serializability across multiple transactions.
      - If in autocommit mode, or outside a transactional context,
      automatically release metadata locks of the current statement.
    */
    thd->mdl_context.release_transactional_locks();
  } else if (!thd->in_sub_stmt &&
             (thd->lex->sql_command != SQLCOM_CREATE_TABLE ||
              !thd->lex->create_info->m_transactional_ddl)) {
    thd->mdl_context.release_statement_locks();
  }

  // If the client wishes to have transaction state reported, we add whatever
  // is set on THD to our set here.
  {
    TX_TRACKER_GET(tst);

    if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
      tst->add_trx_state_from_thd(thd);

    // We're done. Clear "is DML" flag.
    tst->clear_trx_state(thd, TX_STMT_DML);
  }

#ifdef HAVE_LSAN_DO_RECOVERABLE_LEAK_CHECK
  // Get incremental leak reports, for easier leak hunting.
  // ./mtr --mem --mysqld='-T 4096' --sanitize main.1st
  // Don't waste time calling leak sanitizer during bootstrap.
  if (!opt_initialize && (test_flags & TEST_DO_QUICK_LEAK_CHECK)) {
    int have_leaks = __lsan_do_recoverable_leak_check();
    if (have_leaks > 0) {
      fprintf(stderr, "LSAN found leaks for Query: %*s\n",
              static_cast<int>(thd->query().length), thd->query().str);
      fflush(stderr);
    }
  }
#endif

#if defined(VALGRIND_DO_QUICK_LEAK_CHECK)
  // Get incremental leak reports, for easier leak hunting.
  // ./mtr --mem --mysqld='-T 4096' --valgrind-mysqld main.1st
  // Note that with multiple connections, the report below may be misleading.
  if (test_flags & TEST_DO_QUICK_LEAK_CHECK) {
    static unsigned long total_leaked_bytes = 0;
    unsigned long leaked = 0;
    unsigned long dubious [[maybe_unused]];
    unsigned long reachable [[maybe_unused]];
    unsigned long suppressed [[maybe_unused]];
    /*
      We could possibly use VALGRIND_DO_CHANGED_LEAK_CHECK here,
      but that is a fairly new addition to the Valgrind api.
      Note: we dont want to check 'reachable' until we have done shutdown,
      and that is handled by the final report anyways.
      We print some extra information, to tell mtr to ignore this report.
    */
    LogErr(INFORMATION_LEVEL, ER_VALGRIND_DO_QUICK_LEAK_CHECK);
    VALGRIND_DO_QUICK_LEAK_CHECK;
    VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed);
    if (leaked > total_leaked_bytes) {
      LogErr(ERROR_LEVEL, ER_VALGRIND_COUNT_LEAKS, leaked - total_leaked_bytes,
             static_cast<int>(thd->query().length), thd->query().str);
    }
    total_leaked_bytes = leaked;
  }
#endif

  if (cur_db_changed &&
      mysql_change_db(thd, to_lex_cstring(saved_schema_name), true)) {
    res = true; /* purecov: inspected */
  }

  if (!res && !thd->is_error()) {      // if statement succeeded
    binlog_gtid_end_transaction(thd);  // finalize GTID life-cycle
    DEBUG_SYNC(thd, "persist_new_state_after_statement_succeeded");
  } else if (!gtid_consistency_violation_state &&    // if the consistency state
             thd->has_gtid_consistency_violation) {  // was set by the failing
                                                     // statement
    gtid_state->end_gtid_violating_transaction(thd);  // just roll it back
    DEBUG_SYNC(thd, "restore_previous_state_after_statement_failed");
  }

  thd->skip_gtid_rollback = false;

  return res || thd->is_error();
}

mysql_execute_command 是 MySQL 服务器中执行所有 SQL 语句 的核心分发函数。它接收已经解析并填充好的 LEX 结构(语法树),根据 lex->sql_command 的值(如 SQLCOM_SELECTSQLCOM_INSERTSQLCOM_CREATE_TABLE 等),调用相应的内部处理函数完成实际的数据操作、元数据变更或管理命令。该函数位于 dispatch_sql_command 之后,是查询执行流程的最终入口。

  • thd:当前线程的 THD 对象,包含所有上下文(变量、打开的表、锁、事务状态等)。

  • first_level :指示当前调用是否为顶层调用(true)还是嵌套调用(例如存储过程内部的语句,false)。影响某些行为(如审计日志记录、语句计数)。

  • 返回值0 表示成功(可能已经发送了结果集或 OK 包),非 0 表示错误(诊断区域中已设置错误信息)。

核心职责 :根据 thd->lex->sql_command 的不同值,调用对应的执行函数(如 mysql_insertmysql_selectmysql_create_table 等),并负责统一的事务边界管理、锁释放、临时表清理、二进制日志写入协调等工作。

  1. 初始化和前置检查
  • 断言和调试同步点。

  • 处理 CREATE TABLE...START TRANSACTION 遗留事务(禁止除 COMMIT/ROLLBACK/BINLOG 外的命令)。

  • 处理 EXPLAIN FOR SCHEMA 导致的临时切换数据库。

  • 调整表列表顺序(first_lists_tables_same)。

  • 重置诊断区域(除非是诊断语句需要保留条件)。

  • 处理资源组警告(如不支持、访问拒绝等)。

  • 处理 CLIENT_NO_SCHEMA 废弃警告。

  • 从库过滤检查(根据复制过滤规则决定是否跳过语句)。

  • 只读模式检查(read_onlysuper_read_only)拒绝修改非临时表的操作。

  • 执行事务开始钩子(launch_hook_trans_begin)。

  • 递增命令计数器(com_stat)。

  • 启动优化器跟踪(Opt_trace_start)。

  1. GTID 预处理
cpp 复制代码
switch (gtid_pre_statement_checks(thd)) {
  case GTID_STATEMENT_EXECUTE: break;
  case GTID_STATEMENT_CANCEL: return -1;
  case GTID_STATEMENT_SKIP: my_ok(thd); return 0;
}

检查 GTID 一致性,决定执行、取消或跳过语句(如从库已经应用过)。

  1. 隐式提交处理

如果语句会导致隐式提交(如 DDL 语句、事务控制语句等),则先提交当前活动的事务,并释放事务性 MDL 锁。参见 stmt_causes_implicit_commit 函数

  1. 特殊准备
  • CREATE TABLE ... START TRANSACTION 开启新事务(禁用 binlog)。

  • 对于需要关闭自动提交的语句(如某些 DDL),临时禁用自动提交。

  • 只读事务中禁止写操作。

  • 关闭 HANDLER 打开的表(如果需要,如 DDL 语句)。

  • 预打开临时表(CF_PREOPEN_TMP_TABLES)。

  • 保存查询计划(用于 EXPLAIN FOR CONNECTION)。

  • 应用 SET_VAR 提示(临时修改系统变量)。

  1. 核心命令分发(switch (lex->sql_command)

根据命令类型,调用具体的执行函数。该 switch 包含上百个 case,覆盖所有 SQL 命令。

典型分类

  • DMLSELECT, INSERT, UPDATE, DELETE, REPLACE, LOAD 等)→ 调用 lex->m_sql_cmd->execute(thd)

  • DDLCREATE TABLE, ALTER TABLE, DROP TABLE, RENAME TABLE, TRUNCATE 等)→ 调用专门的函数(如 mysql_create_table, mysql_alter_table 等)。

  • 事务控制BEGIN, COMMIT, ROLLBACK, SAVEPOINT)→ 直接调用事务函数。

  • 预处理语句PREPARE, EXECUTE, DEALLOCATE)→ 调用 mysql_sql_stmt_prepare 等。

  • 管理命令SHOW, KILL, FLUSH, RESET, SET, GRANT 等)→ 调用对应的处理函数。

  1. 后置处理(finish: 标签)
  • 恢复 SET_VAR 提示修改的变量。

  • 处理 kill 信号。

  • 清理 EXPLAIN 信息。

  • 触发审计事件(EVENT_TRACKING_QUERY_STATUS_END)。

  • 提交或回滚语句事务(trans_commit_stmt / trans_rollback_stmt)。

  • 重置 killed 标志(对于 KILL_QUERY)。

  • 清理 LEX(lex->cleanup(true))。

  • 关闭线程表(close_thread_tables)。

  • 回滚 Item 树变更。

  • 处理事务性 MDL 锁的释放策略(依据是否在事务中、是否自动提交等)。

  • 处理 GTID 一致性标志回滚。

  • 如果切换过数据库,恢复原数据库。

Sql_cmd

cpp 复制代码
/**
  Representation of an SQL command.

  This class is an interface between the parser and the runtime.
  The parser builds the appropriate derived classes of Sql_cmd
  to represent a SQL statement in the parsed tree.
  The execute() method in the derived classes of Sql_cmd contain the runtime
  implementation.
  Note that this interface is used for SQL statements recently implemented,
  the code for older statements tend to load the LEX structure with more
  attributes instead.
  Implement new statements by sub-classing Sql_cmd, as this improves
  code modularity (see the 'big switch' in dispatch_command()), and decreases
  the total size of the LEX structure (therefore saving memory in stored
  programs).
  The recommended name of a derived class of Sql_cmd is Sql_cmd_<derived>.

  Notice that the Sql_cmd class should not be confused with the Statement class.
  Statement is a class that is used to manage an SQL command or a set
  of SQL commands. When the SQL statement text is analyzed, the parser will
  create one or more Sql_cmd objects to represent the actual SQL commands.
*/
class Sql_cmd {
 private:
  Sql_cmd(const Sql_cmd &);   // No copy constructor wanted
  void operator=(Sql_cmd &);  // No assignment operator wanted

 public:
  /**
    @brief Return the command code for this statement
  */
  virtual enum_sql_command sql_command_code() const = 0;

  /**
    @return true if object represents a preparable statement, ie. a query
    that is prepared with a PREPARE statement and executed with an EXECUTE
    statement. False is returned for regular statements (non-preparable
    statements) that are executed directly. Also false if statement is part
    of a stored procedure.
  */
  bool needs_explicit_preparation() const {
    return m_owner != nullptr && !m_part_of_sp;
  }
  /**
    @return true if statement is regular, ie not prepared statement and not
    part of stored procedure.
  */
  bool is_regular() const { return m_owner == nullptr && !m_part_of_sp; }

  /// @return true if this statement is prepared
  bool is_prepared() const { return m_prepared; }

  /**
    Prepare this SQL statement.

    param thd the current thread

    @returns false if success, true if error
  */
  virtual bool prepare(THD *) {
    // Default behavior for a statement is to have no preparation code.
    /* purecov: begin inspected */
    assert(!is_prepared());
    set_prepared();
    return false;
    /* purecov: end */
  }

  /**
    Execute this SQL statement.
    @param thd the current thread.
    @returns false if success, true if error
  */
  virtual bool execute(THD *thd) = 0;

  /**
    Some SQL commands currently require re-preparation on re-execution of a
    prepared statement or stored procedure. For example, a CREATE TABLE command
    containing an index expression.
    @return true if re-preparation is required, false otherwise.
  */
  virtual bool reprepare_on_execute_required() const { return false; }

  /**
    Command-specific reinitialization before execution of prepared statement

    param thd  Current THD.
  */
  virtual void cleanup(THD *) { m_secondary_engine = nullptr; }

  /// Set the owning prepared statement
  void set_owner(Prepared_statement *stmt) {
    assert(!m_part_of_sp);
    m_owner = stmt;
  }

  /// Get the owning prepared statement
  Prepared_statement *owner() const { return m_owner; }

  /**
    Mark statement as part of procedure. Such statements can be executed
    multiple times, the first execute() call will also prepare it.
  */
  void set_as_part_of_sp() {
    assert(!m_part_of_sp && m_owner == nullptr);
    m_part_of_sp = true;
  }
  /// @returns true if statement is part of a stored procedure
  bool is_part_of_sp() const { return m_part_of_sp; }

  /// @return SQL command type (DML, DDL, ... -- "undetermined" by default)
  virtual enum enum_sql_cmd_type sql_cmd_type() const {
    return SQL_CMD_UNDETERMINED;
  }

  /// @return true if implemented as single table plan, DML statement only
  virtual bool is_single_table_plan() const {
    /* purecov: begin inspected */
    assert(sql_cmd_type() == SQL_CMD_DML);
    return false;
    /* purecov: end */
  }

  virtual bool accept(THD *, Select_lex_visitor *) { return false; }

  /**
    Is this statement of a type and on a form that makes it eligible
    for execution in a secondary storage engine?

    @return the name of the secondary storage engine, or nullptr if
    the statement is not eligible for execution in a secondary storage
    engine
  */
  virtual const MYSQL_LEX_CSTRING *eligible_secondary_storage_engine(
      THD *) const {
    return nullptr;
  }

  /** @return true if the operation is BULK LOAD. */
  virtual bool is_bulk_load() const { return false; }

  /**
    Disable use of secondary storage engines in this statement. After
    a call to this function, the statement will not try to use a
    secondary storage engine until it is reprepared.
  */
  void disable_secondary_storage_engine() {
    assert(m_secondary_engine == nullptr);
    m_secondary_engine_enabled = false;
  }

  void enable_secondary_storage_engine() { m_secondary_engine_enabled = true; }

  /**
    Has use of secondary storage engines been disabled for this statement?
  */
  bool secondary_storage_engine_disabled() const {
    return !m_secondary_engine_enabled;
  }

  /**
  Mark the current statement as using a secondary storage engine.
  This function must be called before the statement starts opening
  tables in a secondary engine.
  */
  void use_secondary_storage_engine(const handlerton *hton) {
    assert(m_secondary_engine_enabled);
    m_secondary_engine = hton;
  }

  /**
    Is this statement using a secondary storage engine?
    @note that this is reliable during optimization and afterwards; during
    preparation, if this is an explicit preparation (SQL PREPARE, C API
    PREPARE, and automatic repreparation), it may be false as RAPID tables have
    not yet been opened. Therefore, during preparation, it is safer to test
    THD::secondary_engine_optimization().
  */
  bool using_secondary_storage_engine() const {
    return m_secondary_engine != nullptr;
  }

  /**
    Get the handlerton of the secondary engine that is used for
    executing this statement, or nullptr if a secondary engine is not
    used.
  */
  const handlerton *secondary_engine() const { return m_secondary_engine; }

  void set_optional_transform_prepared(bool value) {
    m_prepared_with_optional_transform = value;
  }

  bool is_optional_transform_prepared() {
    return m_prepared_with_optional_transform;
  }

 protected:
  Sql_cmd() : m_owner(nullptr), m_part_of_sp(false), m_prepared(false) {}

  virtual ~Sql_cmd() {
    /*
      Sql_cmd objects are allocated in thd->mem_root.
      In MySQL, the C++ destructor is never called, the underlying MEM_ROOT is
      simply destroyed instead.
      Do not rely on the destructor for any cleanup.
    */
    assert(false);
  }

  /// Set this statement as prepared
  void set_prepared() { m_prepared = true; }

 private:
  Prepared_statement *m_owner;  /// Owning prepared statement, NULL if non-prep.
  bool m_part_of_sp;            /// True when statement is part of stored proc.
  bool m_prepared;              /// True when statement has been prepared

  /**
    Tells if a secondary storage engine can be used for this
    statement. If it is false, use of a secondary storage engine will
    not be considered for executing this statement.
  */
  bool m_secondary_engine_enabled{true};

  /**
    Keeps track of whether the statement was prepared optional
    transformation.
  */
  bool m_prepared_with_optional_transform{false};

  /**
    The secondary storage engine to use for execution of this
    statement, if any, or nullptr if the primary engine is used.
    This property is reset at the start of each execution.
  */
  const handlerton *m_secondary_engine{nullptr};
};

Sql_cmd 是 MySQL 中表示一个具体 SQL 命令 的抽象基类。它位于解析器(Parser)和执行器(Executor)之间,将解析得到的语法信息封装成可执行的命令对象,从而让执行逻辑与解析细节解耦。从 MySQL 8.0 开始,新实现的语句都倾向于继承 Sql_cmd,而不是继续往 LEX 结构里添加成员。

解析完成后,会调用 LEX::make_sql_cmd(Parse_tree_root *parse_tree),其中 parse_tree 是 Bison 生成的抽象语法树根节点(例如 PT_select)。该函数调用 parse_tree->make_cmd(thd) 创建对应的 Sql_cmd 派生类实例,并赋值给 lex->m_sql_cmd

例如:

  • PT_select::make_cmd()Sql_cmd_select

  • PT_insert::make_cmd()Sql_cmd_insert

  • PT_create_table::make_cmd()Sql_cmd_create_table

之后,在 mysql_execute_command 中,不再使用巨大的 switch (lex->sql_command) 内联实现,而是统一调用:

复制代码
res = lex->m_sql_cmd->execute(thd);

每个 Sql_cmd 派生类实现自己的 execute() 方法,完成具体逻辑。

方法 作用
sql_command_code() 返回 enum_sql_command(如 SQLCOM_SELECT),用于判断命令类型。
prepare(THD *) 准备阶段。对于预处理语句,第一次执行前会调用;对于普通语句,可能在 execute 之前调用一次。默认无操作。
execute(THD *) 执行命令的核心逻辑,纯虚函数,派生类必须实现。
needs_explicit_preparation() 是否为需要显式 PREPARE 的语句。
is_regular() 是否为普通的、非预处理、非存储过程的语句。
set_owner() / owner() 管理所属的 Prepared_statement 对象。
using_secondary_storage_engine() 是否使用了辅助存储引擎(如 HeatWave)。

此外,Sql_cmd 还支持二次准备 (reprepare_on_execute_required)、清理 (cleanup)、优化器接受访问者模式 (accept) 等高级功能。

Sql_cmd_dml

cpp 复制代码
class Sql_cmd_dml : public Sql_cmd {
 public:
  /// @return true if data change statement, false if not (SELECT statement)
  virtual bool is_data_change_stmt() const { return true; }

  /**
    Command-specific resolving (doesn't include LEX::prepare())

    @param thd  Current THD.

    @returns false on success, true on error
  */
  bool prepare(THD *thd) override;

  /**
    Execute a DML statement.

    @param thd       thread handler

    @returns false if success, true if error

    @details
      Processing a statement goes through 6 phases (parsing is already done)
       - Prelocking
       - Preparation
       - Locking of tables
       - Optimization
       - Execution or explain
       - Cleanup

      If the statement is already prepared, this step is skipped.

      The queries handled by this function are:

      SELECT
      INSERT ... SELECT
      INSERT ... VALUES
      REPLACE ... SELECT
      REPLACE ... VALUES
      UPDATE (single-table and multi-table)
      DELETE (single-table and multi-table)
      DO

    @todo make this function also handle SET.
   */
  bool execute(THD *thd) override;

  enum enum_sql_cmd_type sql_cmd_type() const override {
    /*
      Somewhat unsurprisingly, anything sub-classed to Sql_cmd_dml
      identifies as DML by default.
    */
    return SQL_CMD_DML;
  }

  virtual bool may_use_cursor() const { return false; }

  bool is_single_table_plan() const override { return false; }

  /// @return the query result associated with a prepared query
  Query_result *query_result() const;

  /// Set query result object for this query statement
  void set_query_result(Query_result *result);

  /// Signal that root result object needs preparing in next execution
  void set_lazy_result() { m_lazy_result = true; }

 protected:
  Sql_cmd_dml()
      : Sql_cmd(),
        lex(nullptr),
        result(nullptr),
        m_empty_query(false),
        m_lazy_result(false) {}

  /// @return true if query is guaranteed to return no data
  /**
    @todo Also check this for the following cases:
          - Empty source for multi-table UPDATE and DELETE.
          - Check empty query expression for INSERT
  */
  bool is_empty_query() const {
    assert(is_prepared());
    return m_empty_query;
  }

  /// Set statement as returning no data
  void set_empty_query() { m_empty_query = true; }

  /**
    Perform a precheck of table privileges for the specific operation.

    @details
    Check that user has some relevant privileges for all tables involved in
    the statement, e.g. SELECT privileges for tables selected from, INSERT
    privileges for tables inserted into, etc. This function will also populate
    Table_ref::grant with all privileges the user has for each table,
    which is later used during checking of column privileges. Note that at
    preparation time, views are not expanded yet. Privilege checking is thus
    rudimentary and must be complemented with later calls to
    Query_block::check_view_privileges().
    The reason to call this function at such an early stage is to be able to
    quickly reject statements for which the user obviously has insufficient
    privileges.
    This function is called before preparing the statement.
    The function must also be complemented with proper privilege checks for all
    involved columns (e.g. check_column_grant_*).
    @see also the function comment of Query_block::prepare().
    During execution of a prepared statement, call check_privileges() instead.

    @param thd thread handler

    @returns false if success, true if false
  */
  virtual bool precheck(THD *thd) = 0;

  /**
    Check privileges on a prepared statement, called at start of execution
    of the statement.

    @details
    Check that user has all relevant privileges to the statement,
    ie. INSERT privilege for columns inserted into, UPDATE privilege
    for columns that are updated, DELETE privilege for tables that are
    deleted from, SELECT privilege for columns that are referenced, etc.

    @param thd thread handler

    @returns false if success, true if false
  */
  virtual bool check_privileges(THD *thd) = 0;

  /**
    Read and check privileges for all tables in a DML statement.

    @param thd thread handler

    @returns false if success, true if false

  */
  bool check_all_table_privileges(THD *thd);

  /**
    Perform the command-specific parts of DML command preparation,
    to be called from prepare()

    @param thd the current thread

    @returns false if success, true if error
  */
  virtual bool prepare_inner(THD *thd) = 0;

  /**
    The inner parts of query optimization and execution.
    Single-table DML operations needs to reimplement this.

    @param thd Thread handler

    @returns false on success, true on error
  */
  virtual bool execute_inner(THD *thd);

  /**
    Restore command properties before execution
    - Bind metadata for tables and fields
    - Restore clauses (e.g ORDER BY, GROUP BY) that were destroyed in
      last optimization.
  */
  virtual bool restore_cmd_properties(THD *thd);

  /// Save command properties, such as prepared query details and table props
  virtual bool save_cmd_properties(THD *thd);

  /**
    Helper function that checks if the command is eligible for secondary engine
    and if that's true returns the name of that eligible secondary storage
    engine.

    @return nullptr if not eligible or the name of the engine otherwise
  */
  const MYSQL_LEX_CSTRING *get_eligible_secondary_engine(THD *thd) const;

 protected:
  LEX *lex;              ///< Pointer to LEX for this statement
  Query_result *result;  ///< Pointer to object for handling of the result
  bool m_empty_query;    ///< True if query will produce no rows
  bool m_lazy_result;    ///< True: prepare query result on next execution
};

Sql_cmd_dml 继承自 Sql_cmd,专门表示 数据修改或查询语句,包括:

  • SELECT

  • INSERT ... SELECT / INSERT ... VALUES

  • REPLACE ... SELECT / REPLACE ... VALUES

  • UPDATE(单表/多表)

  • DELETE(单表/多表)

  • DO

它统一管理 DML 语句的准备 (prepare)、执行 (execute)、权限检查结果集处理等生命周期。

虚函数接口(子类必须实现或重写)

  • is_data_change_stmt():默认返回 true(即 DML 会修改数据),SELECT 子类会重写返回 false

  • precheck(THD*) / check_privileges(THD*):两阶段权限检查。

    • precheck:在准备阶段之前做粗粒度表级权限检查。

    • check_privileges:执行前做细粒度列级权限检查。

  • prepare_inner(THD*):由 prepare() 调用的具体准备逻辑,每个子类必须实现。

  • execute_inner(THD*):具体的执行逻辑(单表 DML 通常重写,多表 DML 使用通用实现)。

公开核心方法:

  • prepare(THD*):解析后的准备阶段,最终调用 prepare_inner

  • execute(THD*):完整执行流程(预锁定 → 准备 → 加锁 → 优化 → 执行 → 清理)。若已准备则跳过准备步骤。

  • sql_cmd_type():返回 SQL_CMD_DML 标识命令类型。

  • may_use_cursor():默认 false,表示该命令是否允许使用游标(例如存储过程中的 SELECT 可能允许)。

  • is_single_table_plan():默认 false,子类可重写指示是否单表计划。

  • query_result() / set_query_result():管理结果集处理对象。

  • set_lazy_result():延迟准备结果集,下次执行时才真正准备。

保护成员与辅助方法:

  • lex:指向语法树结构 LEX

  • result:结果集处理器(如 Query_result_sendQuery_result_insert)。

  • m_empty_query:标记语句保证返回空结果(可被优化器利用)。

  • m_lazy_result:标记结果集延迟准备。

  • is_empty_query():返回是否空查询(仅当已准备时有效)。

  • set_empty_query():由子类在分析阶段设置。

  • check_all_table_privileges(THD*):辅助所有表的权限检查。

  • restore_cmd_properties() / save_cmd_properties():在多次执行(如 prepared statement)时保存/恢复命令属性(如 ORDER BY、GROUP BY 等被优化销毁的 clause)。

  • get_eligible_secondary_engine():判断是否可以下推到 secondary engine(如 HeatWave)。

执行流程概览

execute() 的注释列出了经典六阶段:

  1. Prelocking -- 预加锁(为多语句事务准备)

  2. Preparation -- 调用 prepare() 进行解析后的语义分析、查询重写等

  3. Locking of tables -- 真正加表锁

  4. Optimization -- 优化器生成执行计划

  5. Execution or explain -- 执行或解释计划

  6. Cleanup -- 清理临时资源

设计要点

  • 两阶段权限检查precheck 在视图展开前快速拒绝无权限用户;check_privileges 在展开后做精确列级检查。

  • 支持 prepared statement :通过 save_cmd_properties / restore_cmd_properties 保留多次执行间的状态。

  • 延迟结果集准备m_lazy_result):用于某些场景(如 SELECT ... INTO)在真正执行前不需要创建结果集对象。

  • 空查询优化 :若确定无数据(如 WHERE FALSE),可跳过后续执行。

具体的子类:

  • Sql_cmd_select(SELECT 语句)

  • Sql_cmd_insert(INSERT/REPLACE)

  • Sql_cmd_update(UPDATE)

  • Sql_cmd_delete(DELETE)

Sql_cmd_select

cpp 复制代码
class Sql_cmd_select : public Sql_cmd_dml {
 public:
  explicit Sql_cmd_select(Query_result *result_arg) : Sql_cmd_dml() {
    result = result_arg;
  }

  enum_sql_command sql_command_code() const override { return SQLCOM_SELECT; }

  bool is_data_change_stmt() const override { return false; }

  bool accept(THD *thd, Select_lex_visitor *visitor) override;

  const MYSQL_LEX_CSTRING *eligible_secondary_storage_engine(
      THD *thd) const override;

 protected:
  bool may_use_cursor() const override { return true; }
  bool precheck(THD *thd) override;
  bool check_privileges(THD *thd) override;
  bool prepare_inner(THD *thd) override;
};

execute

cpp 复制代码
bool Sql_cmd_dml::execute(THD *thd) {
  DBUG_TRACE;

  lex = thd->lex;

  Query_expression *const unit = lex->unit;

  bool statement_timer_armed = false;
  bool error_handler_active = false;

  Ignore_error_handler ignore_handler;
  Strict_error_handler strict_handler;

  // If statement is preparable, it must be prepared
  assert(owner() == nullptr || is_prepared());
  // If statement is regular, it must be unprepared
  assert(!is_regular() || !is_prepared());
  // If statement is part of SP, it can be both prepared and unprepared.

  // If a timer is applicable to statement, then set it.
  if (is_timer_applicable_to_statement(thd))
    statement_timer_armed = set_statement_timer(thd);

  if (is_data_change_stmt()) {
    // Push ignore / strict error handler
    if (lex->is_ignore()) {
      thd->push_internal_handler(&ignore_handler);
      error_handler_active = true;
      /*
        UPDATE IGNORE can be unsafe. We therefore use row based
        logging if mixed or row based logging is available.
        TODO: Check if the order of the output of the select statement is
        deterministic. Waiting for BUG#42415
      */
      if (lex->sql_command == SQLCOM_UPDATE)
        lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_UPDATE_IGNORE);
    } else if (thd->is_strict_mode()) {
      thd->push_internal_handler(&strict_handler);
      error_handler_active = true;
    }
  }

  if (!is_prepared()) {
    if (prepare(thd)) goto err;
  } else {
    /*
      Prepared statement, open tables referenced in statement and check
      privileges for it.
    */
    cleanup(thd);
    if (open_tables_for_query(thd, lex->query_tables, 0)) goto err;
#ifndef NDEBUG
    if (sql_command_code() == SQLCOM_SELECT)
      DEBUG_SYNC(thd, "after_table_open");
#endif
    // Use the hypergraph optimizer for the SELECT statement, if enabled.
    const bool need_hypergraph_optimizer =
        thd->optimizer_switch_flag(OPTIMIZER_SWITCH_HYPERGRAPH_OPTIMIZER);

    if (need_hypergraph_optimizer != lex->using_hypergraph_optimizer() &&
        ask_to_reprepare(thd)) {
      goto err;
    }
    assert(need_hypergraph_optimizer == lex->using_hypergraph_optimizer());

    // Bind table and field information
    if (restore_cmd_properties(thd)) goto err;
    if (check_privileges(thd)) goto err;

    if (m_lazy_result) {
      const Prepared_stmt_arena_holder ps_arena_holder(thd);

      if (result->prepare(thd, *unit->get_unit_column_types(), unit)) goto err;
      m_lazy_result = false;
    }
  }

  if (validate_use_secondary_engine(lex)) goto err;

  lex->set_exec_started();

  DBUG_EXECUTE_IF("use_attachable_trx",
                  thd->begin_attachable_ro_transaction(););

  THD_STAGE_INFO(thd, stage_init);

  thd->clear_current_query_costs();

  // Replication may require extra check of data change statements
  if (is_data_change_stmt() && run_before_dml_hook(thd)) goto err;

  // Revertable changes are not supported during preparation
  assert(thd->change_list.is_empty());

  assert(!lex->is_query_tables_locked());
  /*
    Locking of tables is done after preparation but before optimization.
    This allows to do better partition pruning and avoid locking unused
    partitions. As a consequence, in such a case, prepare stage can rely only
    on metadata about tables used and not data from them.
  */
  if (!is_empty_query()) {
    if (lock_tables(thd, lex->query_tables, lex->table_count, 0)) goto err;
  }

  // Perform statement-specific execution
  if (execute_inner(thd)) goto err;

  // Count the number of statements offloaded to a secondary storage engine.
  if (using_secondary_storage_engine() && lex->unit->is_executed()) {
    ++thd->status_var.secondary_engine_execution_count;
    global_aggregated_stats.get_shard(thd->thread_id())
        .secondary_engine_execution_count++;
  }

  assert(!thd->is_error());

  // Pop ignore / strict error handler
  if (error_handler_active) thd->pop_internal_handler();

  THD_STAGE_INFO(thd, stage_end);

  // Do partial cleanup (preserve plans for EXPLAIN).
  lex->cleanup(false);
  lex->clear_values_map();
  lex->set_secondary_engine_execution_context(nullptr);

  // Perform statement-specific cleanup for Query_result
  if (result != nullptr) result->cleanup();

  thd->save_current_query_costs();

  thd->update_previous_found_rows();

  DBUG_EXECUTE_IF("use_attachable_trx", thd->end_attachable_transaction(););

  if (statement_timer_armed && thd->timer) reset_statement_timer(thd);

  /*
    This sync point is normally right before thd->query_plan is reset, so
    EXPLAIN FOR CONNECTION can catch the plan. It is copied here as
    after unprepare() EXPLAIN considers the query as "not ready".
    @todo remove in WL#6570 when unprepare() is gone.
  */
  DEBUG_SYNC(thd, "before_reset_query_plan");

  return false;

err:
  assert(thd->is_error() || thd->killed);
  DBUG_PRINT("info", ("report_error: %d", thd->is_error()));
  THD_STAGE_INFO(thd, stage_end);

  lex->cleanup(false);
  lex->clear_values_map();
  lex->set_secondary_engine_execution_context(nullptr);

  // Abort and cleanup the result set (if it has been prepared).
  if (result != nullptr) {
    result->abort_result_set(thd);
    result->cleanup();
  }
  if (error_handler_active) thd->pop_internal_handler();

  if (statement_timer_armed && thd->timer) reset_statement_timer(thd);

  /*
    There are situations where we want to know the cost of a query that
    has failed during execution, e.g because of a timeout.
  */
  thd->save_current_query_costs();

  DBUG_EXECUTE_IF("use_attachable_trx", thd->end_attachable_transaction(););

  return thd->is_error();
}

Sql_cmd_dml::execute 函数是 MySQL 中所有 DML 语句(包括 SELECT)执行的核心入口。下面逐步分析它的主要逻辑和执行阶段。

复制代码
execute
  ├─ is_prepared? (分支)
  ├─ prepare (若未准备)
  │    ├─ precheck (权限检查)
  │    ├─ open_tables_for_query (获取 MDL)
  │    └─ prepare_inner (语义分析、派生表展开等)
  ├─ open_tables_for_query (已准备时)
  ├─ restore_cmd_properties
  ├─ check_privileges
  ├─ lock_tables (获取存储引擎表锁)
  └─ execute_inner          <--- 我们刚刚分析的这个函数
        ├─ optimize
        ├─ (二级引擎判断)
        └─ execute / explain
  1. 初始化和定时器设置
复制代码
lex = thd->lex;
Query_expression *const unit = lex->unit;
bool statement_timer_armed = false;
bool error_handler_active = false;
// 错误处理器(IGNORE / STRICT)
Ignore_error_handler ignore_handler;
Strict_error_handler strict_handler;

if (is_timer_applicable_to_statement(thd))
    statement_timer_armed = set_statement_timer(thd);
  • 获取当前线程的词法结构 lex 和查询表达式 unit

  • 若语句支持执行时间限制(如 MAX_EXECUTION_TIME),则启动计时器。

  1. 为数据修改语句安装错误处理器
复制代码
if (is_data_change_stmt()) {
    if (lex->is_ignore()) {
        thd->push_internal_handler(&ignore_handler);
        error_handler_active = true;
        // UPDATE IGNORE 标记为不安全,强制行格式日志
        if (lex->sql_command == SQLCOM_UPDATE)
            lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_UPDATE_IGNORE);
    } else if (thd->is_strict_mode()) {
        thd->push_internal_handler(&strict_handler);
        error_handler_active = true;
    }
}
  • 对于 INSERT/UPDATE/DELETE 等数据修改语句:

    • 若语句带有 IGNORE 关键字,压入 Ignore_error_handler,将错误转换为警告。

    • 否则若线程处于严格 SQL 模式,压入 Strict_error_handler,将警告转换为错误。

  • SELECTis_data_change_stmt() 返回 false)不安装这些处理器。

  1. 准备阶段(Prepare)
复制代码
if (!is_prepared()) {
    if (prepare(thd)) goto err;
} else {
    // 已准备好的预处理语句(prepared statement)
    cleanup(thd);
    if (open_tables_for_query(thd, lex->query_tables, 0)) goto err;
    // ... 检查优化器开关变化
    if (need_hypergraph_optimizer != lex->using_hypergraph_optimizer() &&
        ask_to_reprepare(thd)) {
        goto err;
    }
    // 绑定元数据、恢复子句、检查权限、延迟结果集准备等
    if (restore_cmd_properties(thd)) goto err;
    if (check_privileges(thd)) goto err;
    if (m_lazy_result) { ... result->prepare(...); m_lazy_result = false; }
}
  • 首次执行 :调用 prepare(thd) 完成语义分析、查询重写、分区裁剪等(注意这里还没有加表锁,只用了元数据)。

  • 再次执行(预编译语句)

    • 调用 cleanup(thd) 清理上次执行的状态。

    • open_tables_for_query 打开表(打开表并获取 MDL 锁)。

    • 如果优化器开关改变(例如启用了超图优化器),可能需要重新准备(ask_to_reprepare)。

    • restore_cmd_properties 恢复上次执行中被优化的子句(如 ORDER BY、GROUP BY)。

    • check_privileges 做细粒度权限检查。

    • m_lazy_result 为真,延迟准备 Query_result(例如 SELECT ... INTO 场景)。

  1. 二级存储引擎校验
复制代码
if (validate_use_secondary_engine(lex)) goto err;
  • 检查是否可以将查询下推到二级引擎(如 HeatWave),若不允许则回退或报错。
  1. 可附加事务(Attachable Transaction)支持(调试用)
复制代码
DBUG_EXECUTE_IF("use_attachable_trx",
                thd->begin_attachable_ro_transaction(););
  • 仅在 debug 模式下用于模拟可附加只读事务。
  1. 执行前钩子(复制相关)
复制代码
if (is_data_change_stmt() && run_before_dml_hook(thd)) goto err;
  • 复制过滤可能需要额外检查(例如 binlog_check_filter)。
  1. 锁表
复制代码
if (!is_empty_query()) {
    if (lock_tables(thd, lex->query_tables, lex->table_count, 0)) goto err;
}
  • 关键点:锁表发生在准备之后、优化之前。这样分区裁剪已经完成,可以避免锁住不需要的分区。

  • 如果 is_empty_query() 为真(如 SELECT WHERE FALSE),则跳过锁表,直接返回空结果。

  1. 执行内部逻辑
复制代码
if (execute_inner(thd)) goto err;
  • 多态核心execute_inner 是虚函数,由子类实现(如 Sql_cmd_select::execute_inner 会调用优化器和执行器)。

  • 对于单表 DML,Sql_cmd_dml::execute_inner 有默认实现;多表 DML 通常重写。

  1. 统计与计数
复制代码
if (using_secondary_storage_engine() && lex->unit->is_executed()) {
    ++thd->status_var.secondary_engine_execution_count;
    // 全局聚合统计
}
  • 记录被下推到二级引擎的语句数量。
  1. 清理与后处理
复制代码
// 弹出错误处理器
if (error_handler_active) thd->pop_internal_handler();
THD_STAGE_INFO(thd, stage_end);
// 部分清理(保留执行计划用于 EXPLAIN)
lex->cleanup(false);
lex->clear_values_map();
lex->set_secondary_engine_execution_context(nullptr);
if (result != nullptr) result->cleanup();
thd->save_current_query_costs();
thd->update_previous_found_rows();
if (statement_timer_armed && thd->timer) reset_statement_timer(thd);
  • 注意 lex->cleanup(false) 不会完全销毁计划,后续 EXPLAIN FOR CONNECTION 仍可读取。

  • 保存查询成本信息(save_current_query_costs),即使失败也保存(如超时)。

  • 更新 found_rows(用于 SQL_CALC_FOUND_ROWS 或类似)。

  1. 错误处理标签 err
复制代码
err:
  assert(thd->is_error() || thd->killed);
  // 相同顺序的清理,但调用 abort_result_set
  lex->cleanup(false);
  if (result != nullptr) {
      result->abort_result_set(thd);
      result->cleanup();
  }
  if (error_handler_active) thd->pop_internal_handler();
  if (statement_timer_armed && thd->timer) reset_statement_timer(thd);
  thd->save_current_query_costs();
  return thd->is_error();
  • 无论成功还是失败,都保证清理资源。

  • 失败时额外调用 abort_result_set 通知结果集提前终止(如发送部分结果后出错)。

preprare

cpp 复制代码
bool Sql_cmd_dml::prepare(THD *thd) {
  DBUG_TRACE;

  bool error_handler_active = false;

  Ignore_error_handler ignore_handler;
  Strict_error_handler strict_handler;

  // @todo: Move this to constructor?
  lex = thd->lex;

  // Parser may have assigned a specific query result handler
  result = lex->result;

  assert(!is_prepared());

  assert(!lex->unit->is_prepared() && !lex->unit->is_optimized() &&
         !lex->unit->is_executed());
  /*
    Constant folding could cause warnings during preparation. Make
    sure they are promoted to errors when strict mode is enabled.
  */
  if (is_data_change_stmt() && needs_explicit_preparation()) {
    // Push ignore / strict error handler
    if (lex->is_ignore()) {
      thd->push_internal_handler(&ignore_handler);
      error_handler_active = true;
    } else if (thd->is_strict_mode()) {
      thd->push_internal_handler(&strict_handler);
      error_handler_active = true;
    }
  }

  // Perform a coarse statement-specific privilege check.
  if (precheck(thd)) goto err;

  // Trigger out_of_memory condition inside open_tables_for_query()
  DBUG_EXECUTE_IF("sql_cmd_dml_prepare__out_of_memory",
                  DBUG_SET("+d,simulate_out_of_memory"););
  /*
    Open tables and expand views.
    During prepare of query (not as part of an execute), acquire only
    S metadata locks instead of SW locks to be compatible with concurrent
    LOCK TABLES WRITE and global read lock.
  */
  if (open_tables_for_query(
          thd, lex->query_tables,
          needs_explicit_preparation() ? MYSQL_OPEN_FORCE_SHARED_MDL : 0)) {
    if (thd->is_error())  // @todo - dictionary code should be fixed
      goto err;
    if (error_handler_active) thd->pop_internal_handler();
    lex->cleanup(false);
    return true;
  }
  DEBUG_SYNC(thd, "after_open_tables");
#ifndef NDEBUG
  if (sql_command_code() == SQLCOM_SELECT) DEBUG_SYNC(thd, "after_table_open");
#endif

  lex->set_using_hypergraph_optimizer(
      thd->optimizer_switch_flag(OPTIMIZER_SWITCH_HYPERGRAPH_OPTIMIZER));

  if (thd->lex->validate_use_in_old_optimizer()) {
    return true;
  }

  if (lex->set_var_list.elements && resolve_var_assignments(thd, lex))
    goto err; /* purecov: inspected */

  {
    const Prepare_error_tracker tracker(thd);
    const Prepared_stmt_arena_holder ps_arena_holder(thd);
    const Enable_derived_merge_guard derived_merge_guard(
        thd, is_show_cmd_using_system_view(thd));

    if (prepare_inner(thd)) goto err;
    if (needs_explicit_preparation() && result != nullptr) {
      result->cleanup();
    }
    if (!is_regular()) {
      if (save_cmd_properties(thd)) goto err;
    }
    if (needs_explicit_preparation()) {
      lex->set_secondary_engine_execution_context(nullptr);
    }
    set_prepared();
  }

  // Pop ignore / strict error handler
  if (error_handler_active) thd->pop_internal_handler();

  // Revertable changes are not supported during preparation
  assert(thd->change_list.is_empty());

  return false;

err:
  assert(thd->is_error());
  DBUG_PRINT("info", ("report_error: %d", thd->is_error()));

  lex->set_secondary_engine_execution_context(nullptr);

  if (error_handler_active) thd->pop_internal_handler();

  if (result != nullptr) result->cleanup();

  lex->cleanup(false);

  return true;
}

Sql_cmd_dml::prepare 函数,它负责 DML 语句的准备阶段 (解析后的语义分析、权限预检、表打开、视图展开、查询重写等),并且补充说明 Sql_cmd_select 中剩余的两个简单方法。

  1. 初始化 & 错误处理器
复制代码
lex = thd->lex;
result = lex->result;  // 解析器可能已经指定了结果处理器
assert(!is_prepared());
// 确保查询表达式未处于 prepared/optimized/executed 状态
assert(!lex->unit->is_prepared() && ...);
  • thd 获取词法结构 lex,并继承解析阶段可能赋值的 result

  • 断言语句尚未准备。

  1. 为数据修改语句安装错误处理器(准备阶段)
复制代码
if (is_data_change_stmt() && needs_explicit_preparation()) {
    if (lex->is_ignore())
        thd->push_internal_handler(&ignore_handler);
    else if (thd->is_strict_mode())
        thd->push_internal_handler(&strict_handler);
    error_handler_active = true;
}
  • 注意 :这里与 execute 中的错误处理器安装类似,但多了一个 needs_explicit_preparation() 条件。

    • needs_explicit_preparation() 通常返回 true(普通语句),对于某些已优化过的内部语句可能返回 false
  • 目的:准备阶段中常量折叠等操作可能产生警告,需要根据 IGNORE 或严格模式正确处理。

  1. 粗粒度权限预检(precheck
复制代码
if (precheck(thd)) goto err;
  • 调用子类实现的 precheck(例如 Sql_cmd_select::precheck)。

  • 检查用户是否具备涉及表的基本权限(如 SELECT 权限),视图尚未展开,只做表级检查,以便快速拒绝无权限用户。

  1. 打开表并展开视图
复制代码
if (open_tables_for_query(
        thd, lex->query_tables,
        needs_explicit_preparation() ? MYSQL_OPEN_FORCE_SHARED_MDL : 0)) {
    ...
}
  • open_tables_for_query 打开语句中涉及的所有表,同时展开视图(将视图引用替换为视图定义的基础表)。

  • 关键标志 MYSQL_OPEN_FORCE_SHARED_MDL:准备阶段只请求 共享元数据锁(S MDL) ,而不是写锁(SW),以避免与并发的 LOCK TABLES WRITE 或全局读锁冲突。这允许准备过程不阻塞写操作。

  • 若打开失败,跳转错误处理。

  1. 优化器开关同步
复制代码
lex->set_using_hypergraph_optimizer(
    thd->optimizer_switch_flag(OPTIMIZER_SWITCH_HYPERGRAPH_OPTIMIZER));
if (thd->lex->validate_use_in_old_optimizer()) {
    return true;
}
  • 记录当前是否使用超图优化器(实验性)。

  • 验证语句在旧优化器中是否合法(某些特性可能要求必须使用超图优化器)。

  1. 解析变量赋值(SET 语句的一部分)
复制代码
if (lex->set_var_list.elements && resolve_var_assignments(thd, lex))
    goto err;
  • 对于包含用户变量赋值的语句(如 SELECT @a := 1),解析变量名和表达式。
  1. 调用 prepare_inner(核心准备逻辑)
复制代码
{
    const Prepare_error_tracker tracker(thd);
    const Prepared_stmt_arena_holder ps_arena_holder(thd);
    const Enable_derived_merge_guard derived_merge_guard(
        thd, is_show_cmd_using_system_view(thd));

    if (prepare_inner(thd)) goto err;
    if (needs_explicit_preparation() && result != nullptr) {
        result->cleanup();
    }
    if (!is_regular()) {
        if (save_cmd_properties(thd)) goto err;
    }
    if (needs_explicit_preparation()) {
        lex->set_secondary_engine_execution_context(nullptr);
    }
    set_prepared();
}
  • prepare_inner 是虚函数,由子类实现(如 Sql_cmd_select::prepare_inner 会调用 Query_expression::prepare 进行查询块的解析、派生表处理、查询重写等)。

  • 作用域内的 RAII 辅助对象:

    • Prepare_error_tracker:跟踪准备过程中的错误。

    • Prepared_stmt_arena_holder:为预处理语句管理内存分配器。

    • Enable_derived_merge_guard:临时启用或禁用派生表合并(针对系统视图)。

  • 结果集清理 :准备完成后,调用 result->cleanup() 重置结果处理器(因为准备阶段可能已经在 result 中积累了一些临时状态)。

  • 保存命令属性 :如果当前语句是预处理语句!is_regular()),调用 save_cmd_properties(thd) 保存子句信息(如 ORDER BY、GROUP BY 列表),以便后续多次执行时恢复。

  • 清空二级引擎上下文(准备阶段不涉及实际二级引擎执行)。

  • set_prepared() 标记状态为已准备。

  1. 清理并返回
复制代码
if (error_handler_active) thd->pop_internal_handler();
assert(thd->change_list.is_empty());
return false;
  • 弹出错误处理器,断言没有未提交的 revertible 变更(准备阶段不应有数据变更)。

  • 成功返回。

  1. 错误标签 err
复制代码
err:
  lex->set_secondary_engine_execution_context(nullptr);
  if (error_handler_active) thd->pop_internal_handler();
  if (result != nullptr) result->cleanup();
  lex->cleanup(false);
  return true;
  • 清理已打开的表、释放内存,并返回错误。

Sql_cmd_select::precheck

cpp 复制代码
bool Sql_cmd_select::precheck(THD *thd) {
  // 1. 检查是否需要 FILE 权限(SELECT ... INTO OUTFILE)
  const bool check_file_acl =
      (lex->result != nullptr && lex->result->needs_file_privilege());
  // 2. 检查所有表的 SELECT_ACL
  Table_ref *tables = lex->query_tables;
  bool res;
  if (tables)
    res = check_table_access(thd, SELECT_ACL, tables, false, UINT_MAX, false);
  else
    res = check_access(thd, SELECT_ACL, any_db, nullptr, nullptr, false, false);
  // 3. 检查锁定子句(FOR UPDATE / LOCK IN SHARE MODE)的权限
  return res || check_locking_clause_access(thd, Global_tables_list(tables));
}

核心要点

  • 仅需 SELECT_ACL 权限(读表),不需要写权限。

  • 如果语句包含 INTO OUTFILE,额外要求 FILE_ACL(全局权限)。

  • 如果查询中使用了 FOR UPDATELOCK IN SHARE MODE,需要额外的锁定权限(由 check_locking_clause_access 实现)。

  1. 计算是否需要检查 FILE 权限
复制代码
const bool check_file_acl =
    (lex->result != nullptr && lex->result->needs_file_privilege());
  • lex :指向当前语句的 LEX 结构,包含解析后的全部信息(查询树、表列表、结果集目标等)。它是 THD 线程对象的一个成员。

  • lex->result :指向一个 Query_result 对象,该对象决定了查询结果的去向。例如:

    • 普通 SELECTQuery_result_send(发送给客户端)

    • SELECT ... INTO OUTFILEQuery_result_export

    • SELECT ... INTO DUMPFILEQuery_result_dump

    • SELECT ... INTO @varQuery_result_to_var

  • needs_file_privilege()Query_result 类的虚函数,对于需要写入服务器文件系统的结果类型(如 INTO OUTFILEINTO DUMPFILE),返回 true;否则返回 false

  • 结论check_file_acltrue 当且仅当当前 SELECT 语句的结果需要写入文件(即 SELECT ... INTO OUTFILE/DUMPFILE)。这意味着语句需要 FILE_ACL 全局权限。

  1. 获取表链表
复制代码
Table_ref *tables = lex->query_tables;
  • lex->query_tables :当前语句中所有直接或间接引用的表、视图、派生表的链表头。在 precheck 阶段,该链表已经由解析器初步填充,但视图尚未展开(即视图仍然作为 Table_ref 节点存在,其底层表还未加入链表)。
  1. 检查 FILE 权限(如果需要)
复制代码
if (check_file_acl && check_global_access(thd, FILE_ACL)) return true;
  • check_global_access(thd, FILE_ACL) :检查当前用户是否拥有 FILE_ACL 全局权限(即 FILE 权限)。该权限允许使用 LOAD DATA INFILESELECT ... INTO OUTFILE 等操作。

  • 逻辑 :如果需要文件写入(check_file_acltrue),但用户没有 FILE_ACL 权限,则函数返回 true(表示预检查失败)。失败后,上层 prepare 会报错并终止。

  1. 检查表的 SELECT 权限
复制代码
bool res;
if (tables)
    res = check_table_access(thd, SELECT_ACL, tables, false, UINT_MAX, false);
else
    res = check_access(thd, SELECT_ACL, any_db, nullptr, nullptr, false, false);
  • check_table_access 函数:检查用户对一组表是否具有指定的权限。

    • 参数1:thd 线程句柄。

    • 参数2:SELECT_ACL,表示需要 SELECT 权限。

    • 参数3:tables,需要检查的 Table_ref 链表。

    • 参数4:false,表示不检查列权限(因为是表级检查)。

    • 参数5:UINT_MAX,表示不限制列数。

    • 参数6:false,表示不检查是否所有表都是临时表。

    • 返回值:true 表示有权限错误,false 表示通过。

  • 如果 tables 非空 :调用 check_table_access 检查用户对语句中出现的所有表(包括视图占位符)是否拥有 SELECT_ACL。注意,此时视图中展开的底层表尚未加入链表,因此视图本身会被当作一个表来检查,而视图的 SELECT 权限定义不同(通常需要 SHOW VIEW 权限,但这里仍用 SELECT_ACL?实际上 check_table_access 内部会识别视图并做相应处理,此处不展开)。

  • 如果 tables 为空 :意味着 SELECT 语句没有 FROM 子句(例如 SELECT 1)。此时调用 check_access

    • 参数1:thd

    • 参数2:SELECT_ACL(所需权限)

    • 参数3:any_db(一个特殊的常量,表示"任意数据库")

    • 参数4、5:nullptr(无具体数据库和表名)

    • 参数6:false

    • 参数7:false

    • 作用:执行一个"空检查",通常总是返回 false(因为不涉及任何具体表),但有一个重要的副作用:它会增加 Performance Schema 中 stage/sql/checking permissions 的计数器,以保持与旧版本的兼容性。这是注释中提到的。

  1. 检查锁定子句的权限
复制代码
return res || check_locking_clause_access(thd, Global_tables_list(tables));
  • Global_tables_list(tables) :将 tables 链表包装成一个 Global_tables_list 对象,该对象可以遍历所有表(包括通过视图间接引用的表,虽然此时视图未展开,但锁定子句只关心直接命名的表)。

  • check_locking_clause_access :检查 FOR UPDATELOCK IN SHARE MODE 子句的权限。如果语句中使用了这些锁定子句,可能需要额外的权限(例如,对于某些系统表或视图可能不允许锁定)。该函数返回 true 表示权限不足。

  • 返回值res || check_locking_clause_access(...)。如果前面任何一步返回 true,则整个 precheck 返回 true(失败);否则返回 false(通过)。

Sql_cmd_select::precheck 的核心职责:

  1. 如果是 SELECT ... INTO OUTFILE,检查 FILE_ACL 全局权限。

  2. 检查用户对查询中涉及的所有表是否拥有 SELECT_ACL 权限(表级)。

  3. 如果查询包含锁定子句(FOR UPDATE / LOCK IN SHARE MODE),检查该子句的合法性和权限。

  4. 对于无表的 SELECT(如 SELECT 1),调用 check_access 以维持性能计数器兼容性,但不会实际拒绝。

该方法在 Sql_cmd_dml::prepare 的早期被调用,一旦返回 trueprepare 将立即失败并报错,不会继续打开表和展开视图。这实现了"快速失败"的优化。

Sql_cmd_select::prepare_inner

cpp 复制代码
bool Sql_cmd_select::prepare_inner(THD *thd) {
  if (lex->is_explain()) {
    /*
      Always use Query_result_send for EXPLAIN, even if it's an EXPLAIN for
      SELECT ... INTO OUTFILE: a user application should be able to prepend
      EXPLAIN to any query and receive output for it, even if the query itself
      redirects the output.
    */
    result = new (thd->mem_root) Query_result_send();
    if (result == nullptr) return true; /* purecov: inspected */
  } else {
    if (result == nullptr) {
      if (sql_command_code() == SQLCOM_SELECT)
        result = new (thd->mem_root) Query_result_send();
      else if (sql_command_code() == SQLCOM_DO)
        result = new (thd->mem_root) Query_result_do();
      else  // Currently assumed to be a SHOW command
        result = new (thd->mem_root) Query_result_send();

      if (result == nullptr) return true; /* purecov: inspected */
    }
  }

  Query_expression *const unit = lex->unit;
  Query_block *parameters = unit->global_parameters();
  if (!parameters->has_limit()) {
    parameters->m_use_select_limit = true;
  }

  if (unit->is_simple()) {
    Query_block *const select = unit->first_query_block();
    select->context.resolve_in_select_list = true;
    select->set_query_result(result);
    unit->set_query_result(result);
    // Unlock the table as soon as possible, so don't set SELECT_NO_UNLOCK.
    select->make_active_options(0, 0);

    if (select->prepare(thd, nullptr)) return true;

    unit->set_prepared();
  } else {
    // If we have multiple query blocks, don't unlock and re-lock
    // tables between each each of them.
    if (unit->prepare(thd, result, nullptr, SELECT_NO_UNLOCK, 0)) return true;
  }

  return false;
}

Sql_cmd_select::prepare_inner,这是 SELECT 命令准备阶段的核心实现。它为 SELECT 语句(包括普通 SELECT、DO、SHOW 命令以及 EXPLAIN)完成:

  • 结果处理器(Query_result)的创建或覆盖

  • 查询单元(Query_expression)的准备(语义分析、派生表处理、查询重写等)

  1. 处理 EXPLAIN 语句的结果集
复制代码
if (lex->is_explain()) {
    // Always use Query_result_send for EXPLAIN
    result = new (thd->mem_root) Query_result_send();
    if (result == nullptr) return true;
}
  • 对于 EXPLAIN SELECT ... 语句,强制使用 Query_result_send 作为结果处理器。

  • 这意味着即使原始查询是 SELECT ... INTO OUTFILE,加上 EXPLAIN 后也会将执行计划直接返回给客户端,而不是写入文件。这是合理的设计:用户期望看到执行计划,而不是被重定向。

  1. 根据语句类型创建默认结果处理器
复制代码
else {
    if (result == nullptr) {
        if (sql_command_code() == SQLCOM_SELECT)
            result = new (thd->mem_root) Query_result_send();
        else if (sql_command_code() == SQLCOM_DO)
            result = new (thd->mem_root) Query_result_do();
        else  // Currently assumed to be a SHOW command
            result = new (thd->mem_root) Query_result_send();
        if (result == nullptr) return true;
    }
}
  • 如果上层未指定 result(例如解析器没有设置),则根据命令类型创建:

    • SQLCOM_SELECTQuery_result_send(将结果集发送给客户端)

    • SQLCOM_DOQuery_result_do(DO 语句不返回结果集,只是执行表达式)

    • 其他(如 SHOW) → Query_result_send(SHOW 命令也返回结果集给客户端)

  1. 设置全局参数的 limit 行为
复制代码
Query_expression *const unit = lex->unit;
Query_block *parameters = unit->global_parameters();
if (!parameters->has_limit()) {
    parameters->m_use_select_limit = true;
}
  • global_parameters() 返回最外层查询块的参数。

  • 如果查询没有显式 LIMIT 子句,则设置 m_use_select_limit = true,表示后续可能使用会话变量 sql_select_limit 来隐式限制返回行数(除非语句包含 SQL_CALC_FOUND_ROWS 或属于子查询等特殊情况)。

  1. 区分简单查询与复合查询,分别准备
复制代码
if (unit->is_simple()) {
    Query_block *const select = unit->first_query_block();
    select->context.resolve_in_select_list = true;
    select->set_query_result(result);
    unit->set_query_result(result);
    // Unlock the table as soon as possible, so don't set SELECT_NO_UNLOCK.
    select->make_active_options(0, 0);
    if (select->prepare(thd, nullptr)) return true;
    unit->set_prepared();
} else {
    // If we have multiple query blocks, don't unlock and re-lock
    // tables between each each of them.
    if (unit->prepare(thd, result, nullptr, SELECT_NO_UNLOCK, 0)) return true;
}

简单查询(无 UNION / INTERSECT / EXCEPT)

  • 获取第一个(也是唯一一个)查询块 select

  • context.resolve_in_select_list = true:允许在 SELECT 列表中解析列引用时,将未限定的列名与 SELECT 列表中的别名匹配(非标准 SQL 但 MySQL 支持)。

  • result 同时设置给查询块和整个查询单元。

  • make_active_options(0, 0):不设置 SELECT_NO_UNLOCK 标志,意味着执行完查询块后可以尽早解锁表(例如在发送完数据后),提升并发性。

  • 调用 select->prepare(thd, nullptr) 对该查询块进行准备。

  • 最后标记 unit 为已准备。

复合查询(UNION 等)

  • 调用 unit->prepare(thd, result, nullptr, SELECT_NO_UNLOCK, 0)

  • 传入 SELECT_NO_UNLOCK 标志,表示不在各个查询块之间解锁表。因为复合查询需要一次执行所有块,中间解锁可能导致不一致或需要重新加锁,影响性能。

  • unit->prepare 内部会递归准备所有查询块,处理临时表、合并等。

  1. 返回值
  • 成功返回 false,失败返回 true(此时 thd->is_error() 已设置)。

与基类的关系

  • Sql_cmd_dml::prepare 负责总体流程(权限预检、打开表、错误处理器等),然后调用 prepare_inner

  • Sql_cmd_select::prepare_inner 仅关注 SELECT 特定的准备逻辑,利用 Query_expressionQuery_block 的层次结构完成具体工作。

在实际调用中(如 open_table_get_mdl_lock 传入的是 ot_ctx->get_timeout(),通常是 thd->variables.lock_wait_timeout,默认值 >0,所以走阻塞路径)。

    1. 初始化超时时间
    复制代码
    set_timespec(&abs_timeout, lock_wait_timeout);
    1. 尝试无等待获取锁
    复制代码
    if (try_acquire_lock_impl(mdl_request, &ticket)) return true;
    if (mdl_request->ticket) {
        // 立即成功,返回
        return false;
    }
    • try_acquire_lock_impl 内部会尝试无阻塞地获取锁。它检查:

      • 当前上下文是否已经持有更强的锁(通过 find_ticket) → 直接复用,设置 mdl_request->ticket 并返回成功。

      • 否则,尝试通过 Fast PathMDL_lock::try_acquire_lock)获取,这个路径使用原子位图操作,适用于低冲突锁类型(如 MDL_SHARED 等)。如果成功,也直接返回。

    • 如果以上都失败,则 ticket 会被分配并加入 MDL_lock 的等待队列之前(但还没有真正加入等待队列)。此时 mdl_request->ticket 仍然为 nullptr,表示需要等待。

    1. 准备等待:needs_notification 对于 MDL_SHARED_WRITE 和 MDL_SHARED_READ 通常返回 false(只有需要升级或特殊锁才需要),所以一般不会调用 notify_conflicting_locks。
    复制代码
    lock = ticket->m_lock;   // 指向 MDL_lock 对象
    lock->m_waiting.add_ticket(ticket);   // 将 ticket 加入等待队列(此时持有着 lock->m_rwlock 写锁)
    m_wait.reset_status();   // 清空等待状态标志
    if (lock->needs_notification(ticket))
        lock->notify_conflicting_locks(this);   // 通知可能被阻塞的锁(如升级场景)
    mysql_prlock_unlock(&lock->m_rwlock);   // 释放 MDL_lock 的读写锁
    1. 注册等待并可能进行死锁检测
    复制代码
    will_wait_for(ticket);   // 将此 ticket 记录为当前上下文正在等待的锁(用于死锁检测)
    • 4.1 死锁检测优化(针对 ACL_CACHE 锁)

    • 代码中有一段特殊的优化:对于 MDL_key::ACL_CACHE 命名空间、MDL_SHARED 锁,且不是复制应用器线程,会采用延迟死锁检测(先等1秒,再检测)。这可以避免在高度竞争 ACL 缓存锁时频繁进行昂贵的死锁检测。

    • 对于普通表锁(MDL_key::TABLE),直接调用 find_deadlock() 进行死锁检测。

    • find_deadlock() 会遍历所有等待关系(通过 m_waiting_forMDL_lock::m_granted 队列中的上下文),使用深度优先搜索检测是否存在循环等待。若发现死锁,会选择牺牲一个事务(通常是权重较低的那个)。

    1. 进入等待循环
    复制代码
    if (lock->needs_notification(ticket) || lock->needs_connection_check()) {
        // 某些锁需要分段等待(如 ACL_CACHE)
        // 在1秒内反复尝试通知并检查连接状态
    } else {
        // 普通锁:直接调用 timed_wait 等待
        wait_status = m_wait.timed_wait(m_owner, &abs_timeout, true,
                                        mdl_request->key.get_wait_state_name());
    }
    • m_wait.timed_wait 是核心等待函数,它使用条件变量等待,直到:

      • 锁被授予(GRANTED

      • 超时(TIMEOUT

      • 被死锁检测选为牺牲品(VICTIM

      • 查询被杀死(KILLED

    1. 等待结束后的处理
    • 6.1 等待失败(超时、死锁、被杀)

      复制代码
      if (wait_status != MDL_wait::GRANTED) {
          lock->remove_ticket(this, m_pins, &MDL_lock::m_waiting, ticket);   // 从等待队列移除
          if (ticket->m_hton_notified) { ... }   // 通知存储引擎(如果有)
          MDL_ticket::destroy(ticket);           // 销毁 ticket
          // 根据 wait_status 报告相应错误
          return true;
      }
    • 6.2 等待成功

      复制代码
      assert(wait_status == MDL_wait::GRANTED);
      m_ticket_store.push_front(mdl_request->duration, ticket);   // 按持续时间存入上下文队列
      mdl_request->ticket = ticket;   // 关联 request 到 ticket
      mysql_mdl_set_status(ticket->m_psi, MDL_ticket::GRANTED);   // 性能监控标记
      return false;
      • m_ticket_store 是一个按持续时间(MDL_STATEMENTMDL_TRANSACTION)组织的链表,便于在语句或事务结束时释放对应的锁。

      • ticket 是最终代表"已授予锁"的对象,它包含了锁的元信息以及指向 MDL_lock 的指针。

execute_inner

cpp 复制代码
/**
  Execute a DML statement.
  This is the default implementation for a DML statement and uses a
  nested-loop join processor per outer-most query block.
  The implementation is split in two: One for query expressions containing
  a single query block and one for query expressions containing multiple
  query blocks combined with UNION.
*/

bool Sql_cmd_dml::execute_inner(THD *thd) {
  Query_expression *unit = lex->unit;

  if (unit->optimize(thd, /*materialize_destination=*/nullptr,
                     /*create_iterators=*/true, /*finalize_access_paths=*/true))
    return true;

  // Calculate the current statement cost.
  accumulate_statement_cost(lex);

  // Perform secondary engine optimizations, if needed.
  if (optimize_secondary_engine(thd)) return true;

  // We know by now that execution will complete (successful or with error)
  lex->set_exec_completed();
  if (lex->is_explain()) {
    for (Table_ref *ref = lex->query_tables; ref != nullptr;
         ref = ref->next_global) {
      if (ref->table != nullptr && ref->table->file != nullptr) {
        handlerton *hton = ref->table->file->ht;
        if (hton->external_engine_explain_check != nullptr) {
          if (hton->external_engine_explain_check(thd)) return true;
        }
      }
    }

    if (explain_query(thd, thd, unit)) return true; /* purecov: inspected */
  } else {
    if (unit->execute(thd)) return true;

    notify_plugins_after_select(thd, lex->m_sql_cmd);
  }

  return false;
}

execute_innerSql_cmd_dml 的默认实现,适用于大多数 DML 语句(SELECT、INSERT、UPDATE、DELETE 等)。它主要完成两件大事:

  1. 优化查询表达式Query_expression::optimize

  2. 执行查询表达式Query_expression::execute 或对于 EXPLAIN 输出执行计划)

注意:execute_inner 被调用时,已经确保了所有表都已打开并加锁(MDL 锁和存储引擎锁),且语句已经准备就绪(语法树、字段绑定等)。

  1. unit->optimize(...)
  • 这是查询优化的核心入口。

  • 对于 SELECT/UNION,它会生成执行计划(访问路径 AccessPath),进行基于成本的优化、连接重排序、下推条件、选择索引等。

  • 参数 create_iterators = true 表示在优化完成后直接构建执行迭代器(Iterator)树,这样后续 execute 可以直接使用迭代器执行,避免重复构建。

  • finalize_access_paths = true 表示完成访问路径的最终调整(如添加必要的排序、聚合操作等)。

优化阶段也可能物化派生表、CTE 等临时结果集。

  1. accumulate_statement_cost(lex)
  • 将优化器计算出的查询成本累加到当前线程的统计信息中,可用于 SHOW STATUS 或 Performance Schema。
  1. optimize_secondary_engine(thd)
  • 如果启用了二级存储引擎(例如 HeatWave),则尝试将执行计划下推到该引擎执行。

  • 这可能会完全替代 InnoDB 执行,或者回退到主引擎。

  • 该函数会检查二级引擎是否可用,并调用其优化器接口。

  1. lex->set_exec_completed()
  • 设置标志,表示语句至少已经走过了优化阶段,开始执行(或准备执行)。主要用于内部调试或状态管理。
  1. EXPLAIN 与普通执行的分支

5.1 EXPLAIN 分支

  • 遍历语句中涉及的所有表,对每个表的存储引擎调用 external_engine_explain_check(如果实现了)。某些存储引擎(如二级引擎)可能需要在 EXPLAIN 时提供额外的信息。

  • 调用 explain_query(thd, thd, unit),该函数会根据优化器生成的 AccessPath 输出执行计划(文本、JSON 或树格式),直接发送给客户端,而不实际读取用户数据。

5.2 普通执行分支

  • 调用 unit->execute(thd) 真正执行查询。

    • 对于 SELECT:迭代器会逐行读取数据并发送给客户端。

    • 对于 INSERT/UPDATE/DELETE:会调用相应的修改逻辑(如 handler::write_row, update_row, delete_row)。

  • 执行完成后,调用 notify_plugins_after_select 通知任何注册的插件(例如审计插件)语句已执行。

Sql_cmd_dml::execute_inner 的主要职责:

  • 驱动查询优化 :调用 unit->optimize 生成执行计划。

  • 尝试二级引擎加速:如果可能,将查询交给专用引擎。

  • 执行或解释 :对于普通 DML,调用 unit->execute;对于 EXPLAIN,输出执行计划。

它完美地将优化器与执行器解耦,并通过二级引擎接口支持了最新的加速方案(如 HeatWave)。这个函数是 MySQL 查询处理流水线中从"准备好"到"真正干活"的关键转折点。

lock_tables

cpp 复制代码
/**
  Lock all tables in a list.

  @param  thd           Thread handler
  @param  tables        Tables to lock
  @param  count         Number of opened tables
  @param  flags         Options (see mysql_lock_tables() for details)

  You can't call lock_tables() while holding thr_lock locks, as
  this would break the dead-lock-free handling thr_lock gives us.
  You must always get all needed locks at once.

  If the query for which we are calling this function is marked as
  requiring prelocking, this function will change
  locked_tables_mode to LTM_PRELOCKED.

  @retval false         Success.
  @retval true          A lock wait timeout, deadlock or out of memory.
*/

bool lock_tables(THD *thd, Table_ref *tables, uint count, uint flags) {
  Table_ref *table;

  DBUG_TRACE;
  /*
    We can't meet statement requiring prelocking if we already
    in prelocked mode.
  */
  assert(thd->locked_tables_mode <= LTM_LOCK_TABLES ||
         !thd->lex->requires_prelocking());

  /*
    lock_tables() should not be called if this statement has
    already locked its tables.
  */
  assert(thd->lex->lock_tables_state == Query_tables_list::LTS_NOT_LOCKED);

  if (!tables && !thd->lex->requires_prelocking()) {
    /*
      Even though we are not really locking any tables mark this
      statement as one that has locked its tables, so we won't
      call this function second time for the same execution of
      the same statement.
    */
    thd->lex->lock_tables_state = Query_tables_list::LTS_LOCKED;
    const int ret = thd->decide_logging_format(tables);
    return ret;
  }

  /*
    Check for thd->locked_tables_mode to avoid a redundant
    and harmful attempt to lock the already locked tables again.
    Checking for thd->lock is not enough in some situations. For example,
    if a stored function contains
    "drop table t3; create temporary t3 ..; insert into t3 ...;"
    thd->lock may be 0 after drop tables, whereas locked_tables_mode
    is still on. In this situation an attempt to lock temporary
    table t3 will lead to a memory leak.
  */
  if (!thd->locked_tables_mode) {
    assert(thd->lock == nullptr);  // You must lock everything at once
    TABLE **start, **ptr;

    if (!(ptr = start = (TABLE **)thd->alloc(sizeof(TABLE *) * count)))
      return true;
    for (table = tables; table; table = table->next_global) {
      if (!table->is_placeholder() &&
          /*
            Do not call handler::store_lock()/external_lock() for temporary
            tables from prelocking list.

            Prelocking algorithm does not add element for a table to the
            prelocking list if it finds that the routine that uses the table can
            create it as a temporary during its execution. Note that such
            routine actually can use existing temporary table if its CREATE
            TEMPORARY TABLE has IF NOT EXISTS clause. For such tables we rely on
            calls to handler::start_stmt() done by routine's substatement when
            it accesses the table to inform storage engine about table
            participation in transaction and type of operation carried out,
            instead of calls to handler::store_lock()/external_lock() done at
            prelocking stage.

            In cases when statement uses two routines one of which can create
            temporary table and modifies it, while another only reads from this
            table, storage engine might be confused about real operation type
            performed by the whole statement. Calls to
            handler::store_lock()/external_lock() done at prelocking stage will
            inform SE only about read part, while information about modification
            will be delayed until handler::start_stmt() call during execution of
            the routine doing modification. InnoDB considers this breaking of
            promise about operation type and fails on assertion.

            To avoid this problem we try to handle both the cases when temporary
            table can be created by routine and the case when it is created
            outside of routine and only accessed by it, uniformly. We don't call
            handler::store_lock()/external_lock() for temporary tables used by
            routines at prelocking stage and rely on calls to
            handler::start_stmt(), which happen during substatement execution,
            to pass correct information about operation type instead.
          */
          !(table->prelocking_placeholder &&
            table->table->s->tmp_table != NO_TMP_TABLE)) {
        *(ptr++) = table->table;
      }
    }

    DEBUG_SYNC(thd, "before_lock_tables_takes_lock");

    if (!(thd->lock =
              mysql_lock_tables(thd, start, (uint)(ptr - start), flags)))
      return true;

    DEBUG_SYNC(thd, "after_lock_tables_takes_lock");

    if (thd->lex->requires_prelocking() &&
        thd->lex->sql_command != SQLCOM_LOCK_TABLES) {
      Table_ref *first_not_own = thd->lex->first_not_own_table();
      /*
        We just have done implicit LOCK TABLES, and now we have
        to emulate first open_and_lock_tables() after it.

        When open_and_lock_tables() is called for a single table out of
        a table list, the 'next_global' chain is temporarily broken. We
        may not find 'first_not_own' before the end of the "list".
        Look for example at those places where open_n_lock_single_table()
        is called. That function implements the temporary breaking of
        a table list for opening a single table.
      */
      for (table = tables; table && table != first_not_own;
           table = table->next_global) {
        if (!table->is_placeholder()) {
          table->table->query_id = thd->query_id;
          if (check_lock_and_start_stmt(thd, thd->lex, table)) {
            mysql_unlock_tables(thd, thd->lock);
            thd->lock = nullptr;
            return true;
          }
        }
      }
      /*
        Let us mark all tables which don't belong to the statement itself,
        and was marked as occupied during open_tables() as free for reuse.
      */
      mark_real_tables_as_free_for_reuse(first_not_own);
      DBUG_PRINT("info", ("locked_tables_mode= LTM_PRELOCKED"));
      thd->enter_locked_tables_mode(LTM_PRELOCKED);
    }
  } else {
    /*
      When we implicitly open DD tables used by a IS query in LOCK TABLE mode,
      we do not go through mysql_lock_tables(), which sets lock type to use
      by SE. Here, we request SE to use read lock for these implicitly opened
      DD tables using ha_external_lock().

      TODO: In PRELOCKED under LOCKED TABLE mode, if sub-statement is a IS
            query then for DD table ha_external_lock is called more than once.
            This works for now as in this mode each sub-statement gets its own
            brand new TABLE instances for each table.
            Allocating a brand new TABLE instances for each sub-statement is
            a resources wastage. Once this issue is fixed, following code
            should be adjusted to not to call ha_external_lock in sub-statement
            mode (similar to how code in close_thread_table() behaves).
    */
    if (in_LTM(thd)) {
      for (table = tables; table; table = table->next_global) {
        TABLE *tbl = table->table;
        if (tbl && belongs_to_dd_table(table)) {
          assert(tbl->file->get_lock_type() == F_UNLCK);
          tbl->file->init_table_handle_for_HANDLER();
          tbl->file->ha_external_lock(thd, F_RDLCK);
        }
      }
    }

    Table_ref *first_not_own = thd->lex->first_not_own_table();
    /*
      When open_and_lock_tables() is called for a single table out of
      a table list, the 'next_global' chain is temporarily broken. We
      may not find 'first_not_own' before the end of the "list".
      Look for example at those places where open_n_lock_single_table()
      is called. That function implements the temporary breaking of
      a table list for opening a single table.
    */
    for (table = tables; table && table != first_not_own;
         table = table->next_global) {
      if (table->is_placeholder()) continue;

      /*
        In a stored function or trigger we should ensure that we won't change
        a table that is already used by the calling statement.
      */
      if (thd->locked_tables_mode >= LTM_PRELOCKED &&
          table->lock_descriptor().type >= TL_WRITE_ALLOW_WRITE) {
        for (TABLE *opentab = thd->open_tables; opentab;
             opentab = opentab->next) {
          if (table->table->s == opentab->s && opentab->query_id &&
              table->table->query_id != opentab->query_id) {
            my_error(ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG, MYF(0),
                     table->table->s->table_name.str);
            return true;
          }
        }
      }

      if (check_lock_and_start_stmt(thd, thd->lex, table)) {
        return true;
      }
    }
    /*
      If we are under explicit LOCK TABLES and our statement requires
      prelocking, we should mark all "additional" tables as free for use
      and enter prelocked mode.
    */
    if (thd->lex->requires_prelocking()) {
      mark_real_tables_as_free_for_reuse(first_not_own);
      DBUG_PRINT("info",
                 ("thd->locked_tables_mode= LTM_PRELOCKED_UNDER_LOCK_TABLES"));
      thd->locked_tables_mode = LTM_PRELOCKED_UNDER_LOCK_TABLES;
    }
  }

  /*
    Mark the statement as having tables locked. For purposes
    of Query_tables_list::lock_tables_state we treat any
    statement which passes through lock_tables() as such.
  */
  thd->lex->lock_tables_state = Query_tables_list::LTS_LOCKED;

  const int ret = thd->decide_logging_format(tables);
  return ret;
}
  • 调用时机 :在 Sql_cmd_dml::execute 中,经过 open_tables(获取 MDL 锁)后,若 !is_empty_query() 则调用 lock_tables

  • 主要任务

    1. 为已经打开的 TABLE 对象(通过 mysql_lock_tables)获取存储引擎表锁(例如 MyISAM 的表锁,或 InnoDB 的意向锁)。

    2. 对于需要预锁定的语句(lex->requires_prelocking()),进入预锁定模式(LTM_PRELOCKED),并调用 check_lock_and_start_stmt 通知存储引擎每个表的操作类型。

    3. 处理显式 LOCK TABLES 模式下的子语句(如存储过程、触发器中的表)。

    4. 标记语句已锁定(LTS_LOCKED)并决定二进制日志格式。

重要前提 :调用此函数时,所有需要用到的表都已经通过 open_tables 打开并获得了 MDL 锁,且 tables 链表包含了所有需要锁定的表(包括预锁定添加的)。

  1. 前置断言与状态检查
复制代码
assert(thd->locked_tables_mode <= LTM_LOCK_TABLES ||
       !thd->lex->requires_prelocking());
assert(thd->lex->lock_tables_state == Query_tables_list::LTS_NOT_LOCKED);
  • 第一条断言:如果已经处于预锁定模式(LTM_PRELOCKED)或更高的模式,则语句不应再要求预锁定(因为已经处于预锁定中)。

  • 第二条断言:确保当前语句尚未锁定表(避免重复调用)。

  1. 特殊情况:无表且不需要预锁定
复制代码
if (!tables && !thd->lex->requires_prelocking()) {
    thd->lex->lock_tables_state = Query_tables_list::LTS_LOCKED;
    const int ret = thd->decide_logging_format(tables);
    return ret;
}
  • 对于没有表的语句(如 SELECT 1),无需锁表,但依然要标记为已锁定,并调用 decide_logging_format(决定 binlog 格式)。

  • 直接返回,不进行后续锁操作。

  1. 核心分支:未处于锁定模式(!thd->locked_tables_mode

这是最常见的执行路径(普通 DML,不在 LOCK TABLES 或预锁定模式下)。

3.1 构建表指针数组

复制代码
if (!(ptr = start = (TABLE **)thd->alloc(sizeof(TABLE *) * count)))
    return true;
for (table = tables; table; table = table->next_global) {
    if (!table->is_placeholder() &&
        !(table->prelocking_placeholder &&
          table->table->s->tmp_table != NO_TMP_TABLE)) {
        *(ptr++) = table->table;
    }
}
  • 遍历 tables 链表,收集需要锁定的 TABLE* 指针放入数组。

  • 排除:

    • 占位符表(is_placeholder(),如派生表、视图等无实际 TABLE 对象)。

    • 属于预锁定占位符且是临时表的表(注释中解释了复杂原因:为避免存储引擎混淆操作类型,临时表的锁推迟到 start_stmt 中处理)。

3.2 调用 mysql_lock_tables 获取存储引擎锁

复制代码
if (!(thd->lock = mysql_lock_tables(thd, start, (uint)(ptr - start), flags)))
    return true;
  • mysql_lock_tables 是存储引擎层的锁接口,它会调用每个表的 handler::store_lock()handler::external_lock(),获取实际的表锁(对于 InnoDB 主要是意向锁)。

  • 返回的 thd->lock 是一个 MYSQL_LOCK 结构,包含所有获取的锁,用于后续释放。

3.3 如果需要预锁定,进入预锁定模式

复制代码
if (thd->lex->requires_prelocking() &&
    thd->lex->sql_command != SQLCOM_LOCK_TABLES) {
    Table_ref *first_not_own = thd->lex->first_not_own_table();
    for (table = tables; table && table != first_not_own;
         table = table->next_global) {
        if (!table->is_placeholder()) {
            table->table->query_id = thd->query_id;
            if (check_lock_and_start_stmt(thd, thd->lex, table)) {
                mysql_unlock_tables(thd, thd->lock);
                thd->lock = nullptr;
                return true;
            }
        }
    }
    mark_real_tables_as_free_for_reuse(first_not_own);
    thd->enter_locked_tables_mode(LTM_PRELOCKED);
}
  • 触发条件 :语句要求预锁定(例如因为触发了触发器、调用了存储函数等)且不是 LOCK TABLES 命令本身。

  • first_not_own:表链表中第一个不属于当前语句"自有"的表(即通过预锁定添加的表)。自有表是 SQL 语句显式引用的表。

  • 遍历自有表 :对每个非占位符表,设置 query_id 为当前语句 ID(标记该表正在被使用),并调用 check_lock_and_start_stmt

    • check_lock_and_start_stmt 内部会调用 handler::start_stmt,通知存储引擎该表在当前语句中的操作类型(读/写),以便引擎正确设置锁或事务状态。
  • mark_real_tables_as_free_for_reuse :将 first_not_own 开始的所有表标记为可重用(因为它们属于预锁定添加的表,在当前语句中只使用一次,执行完后可释放)。

  • thd->enter_locked_tables_mode(LTM_PRELOCKED):将线程的锁模式切换为"预锁定模式"。在此模式下,后续子语句(如触发器内的 SQL)不会再尝试锁表,而是直接使用已锁定的表。

  1. 分支:已经处于锁定模式(else

该分支处理在显式 LOCK TABLES 或预锁定模式下执行子语句的情况。

4.1 处理 DD 表在 LOCK TABLES 下的隐式打开

复制代码
if (in_LTM(thd)) {
    for (table = tables; table; table = table->next_global) {
        TABLE *tbl = table->table;
        if (tbl && belongs_to_dd_table(table)) {
            assert(tbl->file->get_lock_type() == F_UNLCK);
            tbl->file->init_table_handle_for_HANDLER();
            tbl->file->ha_external_lock(thd, F_RDLCK);
        }
    }
}
  • LOCK TABLES 模式下,如果语句需要访问数据字典表(如 I_S 视图底层表),这些表可能没有被显式锁定。这里为它们单独获取读锁(F_RDLCK),确保可以读取。

4.2 遍历自有表,调用 check_lock_and_start_stmt

复制代码
for (table = tables; table && table != first_not_own;
     table = table->next_global) {
    if (table->is_placeholder()) continue;

    // 冲突检测:在存储函数/触发器中,不允许修改已被外层语句使用的表
    if (thd->locked_tables_mode >= LTM_PRELOCKED &&
        table->lock_descriptor().type >= TL_WRITE_ALLOW_WRITE) {
        for (TABLE *opentab = thd->open_tables; opentab;
             opentab = opentab->next) {
            if (table->table->s == opentab->s && opentab->query_id &&
                table->table->query_id != opentab->query_id) {
                my_error(ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG, MYF(0),
                         table->table->s->table_name.str);
                return true;
            }
        }
    }

    if (check_lock_and_start_stmt(thd, thd->lex, table)) {
        return true;
    }
}
  • 冲突检测:在预锁定模式或更高模式下,如果当前表请求写锁,并且该表已经被外层语句打开且正在使用(query_id 不同),则报错,防止存储函数/触发器修改外层正在使用的表。

  • 调用 check_lock_and_start_stmt 通知存储引擎操作类型(此时因为已经持有锁,start_stmt 主要用于设置事务状态)。

4.3 如果需要预锁定,进入 LTM_PRELOCKED_UNDER_LOCK_TABLES

复制代码
if (thd->lex->requires_prelocking()) {
    mark_real_tables_as_free_for_reuse(first_not_own);
    thd->locked_tables_mode = LTM_PRELOCKED_UNDER_LOCK_TABLES;
}
  • 在显式 LOCK TABLES 模式下执行一个需要预锁定的语句(例如包含触发器的 INSERT),则切换到一个特殊模式:LTM_PRELOCKED_UNDER_LOCK_TABLES,表示在 LOCK TABLES 之上又叠加了预锁定。

open_tables_for_query

cpp 复制代码
/**
  Open all tables for a query or statement, in list started by "tables"

  @param       thd      thread handler
  @param       tables   list of tables for open
  @param       flags    bitmap of flags to modify how the tables will be open:
                        MYSQL_LOCK_IGNORE_FLUSH - open table even if someone has
                        done a flush on it.

  @retval false - ok
  @retval true  - error

  @note
    This is to be used on prepare stage when you don't read any
    data from the tables.

  @note
    Updates Query_tables_list::table_count as side-effect.
*/

bool open_tables_for_query(THD *thd, Table_ref *tables, uint flags) {
  DML_prelocking_strategy prelocking_strategy;
  MDL_savepoint mdl_savepoint = thd->mdl_context.mdl_savepoint();
  DBUG_TRACE;

  assert(tables == thd->lex->query_tables);

  if (open_tables(thd, &tables, &thd->lex->table_count, flags,
                  &prelocking_strategy))
    goto end;

  if (open_secondary_engine_tables(thd, flags)) goto end;

  if (thd->secondary_engine_optimization() ==
          Secondary_engine_optimization::PRIMARY_TENTATIVELY &&
      has_external_table(thd->lex)) {
    /* Avoid materializing parts of result in primary engine
     * during the PRIMARY_TENTATIVELY optimization phase
     * if there are external tables since this can
     * take a long time compared to the execution of the query
     * in the secondary engine and it's wasted work if we end up
     * executing the query in the secondary engine. */
    thd->lex->add_statement_options(OPTION_NO_CONST_TABLES |
                                    OPTION_NO_SUBQUERY_DURING_OPTIMIZATION);
  }

  return false;
end:
  /*
    No need to commit/rollback the statement transaction: it's
    either not started or we're filling in an INFORMATION_SCHEMA
    table on the fly, and thus mustn't manipulate with the
    transaction of the enclosing statement.
  */
  assert(thd->get_transaction()->is_empty(Transaction_ctx::STMT) ||
         (thd->state_flags & Open_tables_state::BACKUPS_AVAIL) ||
         thd->in_sub_stmt);
  close_thread_tables(thd);
  /* Don't keep locks for a failed statement. */
  thd->mdl_context.rollback_to_savepoint(mdl_savepoint);

  return true; /* purecov: inspected */
}

预加锁(Prelocking)机制,其核心实现就在 open_tables_for_query 这个函数中。在 Sql_cmd_dml::execute 的执行路径里,无论是准备阶段(prepare)还是后续执行阶段(对于已准备好的预编译语句),都会调用 open_tables_for_query 来完成表依赖的收集和元数据锁的获取。

  1. 预锁定策略 DML_prelocking_strategy,这是一个策略类,专门为 DML 语句设计,它会自动发现语句所依赖的额外对象
  • 存储函数:如果查询中调用了存储函数,该函数内部访问的表也会被加入依赖列表。

  • 触发器 :对于 INSERT/UPDATE/DELETE,关联的触发器所依赖的表也会被预锁定。

  • 视图:视图会被递归展开,将其底层表纳入依赖。

  • 外键:当执行涉及外键约束的修改操作时,被引用的父表也会被加入。

  1. open_tables 递归处理,open_tables 会遍历你传入的表链表 tables,并在 prelocking_strategy 的指导下:
  • 为每个表获取元数据锁(MDL,初次为共享锁 MDL_SHARED)。

  • 如果遇到视图,则展开并将底层表插入链表。

  • 如果发现存储函数/触发器,则将其依赖的表动态添加到链表中继续处理。

  • 重复直到所有直接/间接依赖的表都被打开并加锁。

这个过程的最终结果就是:thd->lex->query_tables 链表被扩展为包含语句所有可能用到的全部表(不仅仅是语法上显式写出的那些),并且这些表都已经获得了合适的 MDL 锁。这就是你所说的"预加锁"。

调用情况一:首次执行(未准备)

cpp 复制代码
// Sql_cmd_dml::execute 中
if (!is_prepared()) {
    if (prepare(thd)) goto err;
}

Sql_cmd_dml::prepare 中会调用:

cpp 复制代码
// Sql_cmd_dml::prepare 中
if (open_tables_for_query(
        thd, lex->query_tables,
        needs_explicit_preparation() ? MYSQL_OPEN_FORCE_SHARED_MDL : 0)) {
    ...
}
  • 此时传入标志 MYSQL_OPEN_FORCE_SHARED_MDL,确保只获取共享元数据锁,不影响并发的写操作(因为准备阶段不读取数据)。

调用情况二:已准备好的预编译语句再次执行

cpp 复制代码
// Sql_cmd_dml::execute 中
} else {
    // Prepared statement, open tables referenced in statement and check privileges for it.
    cleanup(thd);
    if (open_tables_for_query(thd, lex->query_tables, 0)) goto err;
    ...
}
  • 此时 flags 为 0,但仍然会通过预锁定策略收集所有依赖表,并获取必要的 MDL 锁(根据语句类型可能升级为写锁)。这就是每次执行前的"预加锁"阶段。

open_tables_for_query vs lock_tables 的区别

  • open_tables_for_query 负责获取 MDL 锁,这是表级元数据锁,防止 DDL 并发。

  • lock_tables 负责获取表锁 (例如 read lock / write lock),用于存储引擎层面的并发控制。

两者层次不同,但"预加锁"这个词通常指代前者------即提前分析语句依赖,确保所有需要的表都在 MDL 的保护下被打开,从而避免在执行过程中由于发现新依赖表而重新打开(这会导致死锁或性能问题)。

open_tables

cpp 复制代码
/**
  Open all tables in list

  @param[in]     thd      Thread context.
  @param[in,out] start    List of tables to be open (it can be adjusted for
                          statement that uses tables only implicitly, e.g.
                          for "SELECT f1()").
  @param[out]    counter  Number of tables which were open.
  @param[in]     flags    Bitmap of flags to modify how the tables will be
                          open, see open_table() description for details.
  @param[in]     prelocking_strategy  Strategy which specifies how prelocking
                                      algorithm should work for this statement.

  @note
    Unless we are already in prelocked mode and prelocking strategy prescribes
    so this function will also precache all SP/SFs explicitly or implicitly
    (via views and triggers) used by the query and add tables needed for their
    execution to table list. Statement that uses SFs, invokes triggers or
    requires foreign key checks will be marked as requiring prelocking.
    Prelocked mode will be enabled for such query during lock_tables() call.

    If query for which we are opening tables is already marked as requiring
    prelocking it won't do such precaching and will simply reuse table list
    which is already built.

  @retval  false  Success.
  @retval  true   Error, reported.
*/

bool open_tables(THD *thd, Table_ref **start, uint *counter, uint flags,
                 Prelocking_strategy *prelocking_strategy) {
  /*
    We use pointers to "next_global" member in the last processed
    Table_ref element and to the "next" member in the last processed
    Sroutine_hash_entry element as iterators over, correspondingly, the table
    list and stored routines list which stay valid and allow to continue
    iteration when new elements are added to the tail of the lists.
  */
  // 指向链表尾部的指针,用于追加新依赖
  Table_ref **table_to_open;
  TABLE *old_table;
  Sroutine_hash_entry **sroutine_to_open;
  Table_ref *tables;
  Open_table_context ot_ctx(thd, flags);
  bool error = false;
  bool some_routine_modifies_data = false;
  bool has_prelocking_list;
  DBUG_TRACE;
  bool audit_notified = false;

restart:
  /*
    Close HANDLER tables which are marked for flush or against which there
    are pending exclusive metadata locks. This is needed both in order to
    avoid deadlocks and to have a point during statement execution at
    which such HANDLERs are closed even if they don't create problems for
    the current session (i.e. to avoid having a DDL blocked by HANDLERs
    opened for a long time).
  */
  if (!thd->handler_tables_hash.empty()) mysql_ha_flush(thd);

  has_prelocking_list = thd->lex->requires_prelocking();
  table_to_open = start;
  old_table = *table_to_open ? (*table_to_open)->table : nullptr;
  sroutine_to_open = &thd->lex->sroutines_list.first;
  *counter = 0;

  if (!(thd->state_flags & Open_tables_state::SYSTEM_TABLES))
    THD_STAGE_INFO(thd, stage_opening_tables);

  /*
    If we are executing LOCK TABLES statement or a DDL statement
    (in non-LOCK TABLES mode) we might have to acquire upgradable
    semi-exclusive metadata locks (SNW or SNRW) on some of the
    tables to be opened.
    When executing CREATE TABLE .. If NOT EXISTS .. SELECT, the
    table may not yet exist, in which case we acquire an exclusive
    lock.
    We acquire all such locks at once here as doing this in one
    by one fashion may lead to deadlocks or starvation. Later when
    we will be opening corresponding table pre-acquired metadata
    lock will be reused (thanks to the fact that in recursive case
    metadata locks are acquired without waiting).
  */
  if (!(flags & (MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_OPEN_FORCE_SHARED_MDL |
                 MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL))) {
    if (thd->locked_tables_mode) {
      /*
        Under LOCK TABLES, we can't acquire new locks, so we instead
        need to check if appropriate locks were pre-acquired.
      */
      Table_ref *end_table = thd->lex->first_not_own_table();
      if (open_tables_check_upgradable_mdl(thd, *start, end_table) ||
          acquire_backup_lock_in_lock_tables_mode(thd, *start, end_table)) {
        error = true;
        goto err;
      }
    } else {
      Table_ref *table;
      if (lock_table_names(thd, *start, thd->lex->first_not_own_table(),
                           ot_ctx.get_timeout(), flags)) {
        error = true;
        goto err;
      }
      for (table = *start; table && table != thd->lex->first_not_own_table();
           table = table->next_global) {
        if (table->mdl_request.is_ddl_or_lock_tables_lock_request() ||
            table->open_strategy == Table_ref::OPEN_FOR_CREATE)
          table->mdl_request.ticket = nullptr;
      }
    }
  }

  /*
    Perform steps of prelocking algorithm until there are unprocessed
    elements in prelocking list/set.
  */
  // 主循环:只要表链表或例程链表中还有未处理的元素,就继续
  while (*table_to_open ||
         (thd->locked_tables_mode <= LTM_LOCK_TABLES && *sroutine_to_open)) {
    /*
      For every table in the list of tables to open, try to find or open
      a table.
    */
    // 第一阶段:处理所有待打开的表(包括原始表和新增的表)
    for (tables = *table_to_open; tables;
         table_to_open = &tables->next_global, tables = tables->next_global) {
      old_table = (*table_to_open)->table;

      // 调用 open_and_process_table,它会:
      // 1. 为表获取 MDL 锁
      // 2. 根据 prelocking_strategy,检查是否需要展开视图、添加触发器依赖的表、
      //    添加外键依赖的表等。
      //    这些新依赖的表会被追加到 query_tables 链表的末尾。
      error = open_and_process_table(thd, thd->lex, tables, counter,
                                     prelocking_strategy, has_prelocking_list,
                                     &ot_ctx);

      if (error) {
        if (ot_ctx.can_recover_from_failed_open()) {
          /*
            We have met exclusive metadata lock or old version of table.
            Now we have to close all tables and release metadata locks.
            We also have to throw away set of prelocked tables (and thus
            close tables from this set that were open by now) since it
            is possible that one of tables which determined its content
            was changed.

            Instead of implementing complex/non-robust logic mentioned
            above we simply close and then reopen all tables.

            We have to save pointer to table list element for table which we
            have failed to open since closing tables can trigger removal of
            elements from the table list (if MERGE tables are involved),
          */
          close_tables_for_reopen(thd, start, ot_ctx.start_of_statement_svp());

          /*
            Here we rely on the fact that 'tables' still points to the valid
            Table_ref element. Although currently this assumption is valid
            it may change in future.
          */
          if (ot_ctx.recover_from_failed_open()) goto err;

          /* Re-open temporary tables after close_tables_for_reopen(). */
          if (open_temporary_tables(thd, *start)) goto err;

          error = false;
          goto restart;
        }
        goto err;
      }

      DEBUG_SYNC(thd, "open_tables_after_open_and_process_table");
    }

    /*
      Iterate through set of tables and generate table access audit events.
    */
    if (!audit_notified &&
        mysql_event_tracking_table_access_notify(thd, *start)) {
      error = true;
      goto err;
    }

    /*
      Event is not generated in the next loop. It may contain duplicated
      table entries as well as new tables discovered for stored procedures.
      Events for these tables will be generated during the queries of these
      stored procedures.
    */
    audit_notified = true;

    /*
      If we are not already in prelocked mode and extended table list is
      not yet built for our statement we need to cache routines it uses
      and build the prelocking list for it.
      If we are not in prelocked mode but have built the extended table
      list, we still need to call open_and_process_routine() to take
      MDL locks on the routines.
    */

    // 第二阶段:处理所有待处理的存储例程(存储函数/过程)
    if (thd->locked_tables_mode <= LTM_LOCK_TABLES) {
      bool routine_modifies_data;
      /*
        Process elements of the prelocking set which are present there
        since parsing stage or were added to it by invocations of
        Prelocking_strategy methods in the above loop over tables.

        For example, if element is a routine, cache it and then,
        if prelocking strategy prescribes so, add tables it uses to the
        table list and routines it might invoke to the prelocking set.
      */
      for (Sroutine_hash_entry *rt = *sroutine_to_open; rt;
           sroutine_to_open = &rt->next, rt = rt->next) {
        bool need_prelocking = false;
        Table_ref **save_query_tables_last = thd->lex->query_tables_last;

        // 调用 open_and_process_routine,它会:
        // 1. 缓存例程定义,获取 MDL 锁
        // 2. 根据 prelocking_strategy,如果例程中包含 SQL 语句,会将其依赖的表和
        //    子例程追加到对应的链表中(通过修改 thd->lex->query_tables_last
        //    和 sroutines_list)。
        // 3. 若需要预锁定且尚未标记,则调用 mark_as_requiring_prelocking。
        error = open_and_process_routine(
            thd, thd->lex, rt, prelocking_strategy, has_prelocking_list,
            &ot_ctx, &need_prelocking, &routine_modifies_data);

        if (need_prelocking && !thd->lex->requires_prelocking())
          thd->lex->mark_as_requiring_prelocking(save_query_tables_last);

        if (need_prelocking && !*start) *start = thd->lex->query_tables;

        if (error) {
          if (ot_ctx.can_recover_from_failed_open()) {
            close_tables_for_reopen(thd, start,
                                    ot_ctx.start_of_statement_svp());
            if (ot_ctx.recover_from_failed_open()) goto err;

            /* Re-open temporary tables after close_tables_for_reopen(). */
            if (open_temporary_tables(thd, *start)) goto err;

            error = false;
            goto restart;
          }
          /*
            Serious error during reading stored routines from mysql.proc table.
            Something is wrong with the table or its contents, and an error has
            been emitted; we must abort.
          */
          goto err;
        }

        // Remember if any of SF modifies data.
        some_routine_modifies_data |= routine_modifies_data;
      }
    }
  }

  /* Accessing data in XA_IDLE or XA_PREPARED is not allowed. */
  if (*start &&
      (thd->get_transaction()->xid_state()->check_xa_idle_or_prepared(true) ||
       thd->get_transaction()->xid_state()->xa_trans_rolled_back()))
    return true;

  /*
   If some routine is modifying the table then the statement is not read only.
   If timer is enabled then resetting the timer in this case.
  */
  if (thd->timer && some_routine_modifies_data) {
    reset_statement_timer(thd);
    push_warning(thd, Sql_condition::SL_NOTE, ER_NON_RO_SELECT_DISABLE_TIMER,
                 ER_THD(thd, ER_NON_RO_SELECT_DISABLE_TIMER));
  }

  /*
    After successful open of all tables, including MERGE parents and
    children, attach the children to their parents. At end of statement,
    the children are detached. Attaching and detaching are always done,
    even under LOCK TABLES.

    We also convert all TL_WRITE_DEFAULT and TL_READ_DEFAULT locks to
    appropriate "real" lock types to be used for locking and to be passed
    to storage engine.
  */
  for (tables = *start; tables; tables = tables->next_global) {
    TABLE *tbl = tables->table;

    /*
      NOTE: temporary merge tables should be processed here too, because
      a temporary merge table can be based on non-temporary tables.
    */

    /* Schema tables may not have a TABLE object here. */
    if (tbl && tbl->file && tbl->file->ht->db_type == DB_TYPE_MRG_MYISAM) {
      /* MERGE tables need to access parent and child TABLE_LISTs. */
      assert(tbl->pos_in_table_list == tables);
      if (tbl->db_stat && tbl->file->ha_extra(HA_EXTRA_ATTACH_CHILDREN)) {
        error = true;
        goto err;
      }
    }

    /*
      Access to ACL table in a SELECT ... LOCK IN SHARE MODE are required
      to skip acquiring row locks. So, we use TL_READ_DEFAULT lock on ACL
      tables. This allows concurrent ACL DDL's.

      Do not request SE to skip row lock if 'flags' has
      MYSQL_OPEN_FORCE_SHARED_MDL, which indicates that this is PREPARE
      phase. It is OK to do so since during this phase no rows will be read
      anyway. And by doing this we avoid generation of extra warnings.
      EXECUTION phase will request SE to skip row locks if necessary.
    */
    bool issue_warning_on_skipping_row_lock = false;
    if (tables->lock_descriptor().type == TL_READ_WITH_SHARED_LOCKS &&
        !(flags & MYSQL_OPEN_FORCE_SHARED_MDL) &&
        is_acl_table_in_non_LTM(tables, thd->locked_tables_mode)) {
      tables->set_lock({TL_READ_DEFAULT, THR_DEFAULT});
      issue_warning_on_skipping_row_lock = true;
    }

    /* Set appropriate TABLE::lock_type. */
    if (tbl && tables->lock_descriptor().type != TL_UNLOCK &&
        !thd->locked_tables_mode) {
      if (tables->lock_descriptor().type == TL_WRITE_DEFAULT)
        tbl->reginfo.lock_type = thd->update_lock_default;
      else if (tables->lock_descriptor().type == TL_WRITE_CONCURRENT_DEFAULT)
        tables->table->reginfo.lock_type = thd->insert_lock_default;
      else if (tables->lock_descriptor().type == TL_READ_DEFAULT)
        tbl->reginfo.lock_type = read_lock_type_for_table(
            thd, thd->lex, tables, some_routine_modifies_data);
      else
        tbl->reginfo.lock_type = tables->lock_descriptor().type;
    }

    /*
      SELECT using a I_S system view with 'FOR UPDATE' and
      'LOCK IN SHARED MODE' clause is not allowed.
    */
    if (tables->is_system_view &&
        tables->lock_descriptor().type == TL_READ_WITH_SHARED_LOCKS) {
      my_error(ER_IS_QUERY_INVALID_CLAUSE, MYF(0), "LOCK IN SHARE MODE");
      error = true;
      goto err;
    }

    // Setup lock type for DD tables used under I_S view.
    if (set_non_locking_read_for_IS_view(thd, tables)) {
      error = true;
      goto err;
    }

    /**
      Setup lock type for read requests for ACL table in SQL statements.

      Do not request SE to skip row lock if 'flags' has
      MYSQL_OPEN_FORCE_SHARED_MDL, which indicates that this is PREPARE
      phase. It is OK to do so since during this phase no rows will be read
      anyway. And by doing this we avoid generation of extra warnings.
      EXECUTION phase will request SE to skip row locks if necessary.
    */
    if (!(flags & MYSQL_OPEN_FORCE_SHARED_MDL) &&
        set_non_locking_read_for_ACL_table(
            thd, tables, issue_warning_on_skipping_row_lock)) {
      error = true;
      goto err;
    }

  }  // End of for(;;)

err:
  // If a new TABLE was introduced, it's garbage, don't link to it:
  if (error && *table_to_open && old_table != (*table_to_open)->table) {
    (*table_to_open)->table = nullptr;
  }
  DBUG_PRINT("open_tables", ("returning: %d", (int)error));
  return error;
}

预加锁的核心任务是:对于一个 SQL 语句,找出它直接和间接依赖的所有表、存储过程/函数、触发器,并为它们获取元数据锁(MDL)。

open_tables 中,这是通过迭代扩展表链表和存储例程链表完成的,直到没有新的依赖被发现。

关键数据结构与标志

  • thd->lex->query_tables:表链表的头指针,会在过程中被动态扩展。

  • thd->lex->sroutines_list:存储例程(存储函数、过程)链表的头指针。

  • thd->lex->requires_prelocking() :返回 true 表示该语句已经标记为"需要预锁定模式",此时不会重复发现依赖(因为已经做过一次)。

  • Prelocking_strategy :多态策略类,针对不同命令(DML、DDL、SHOW 等)定义如何响应发现的依赖。对于 DML 语句,传入的是 DML_prelocking_strategy,它的行为是:

    • 对表:检查是否是视图(递归展开)、是否有触发器、是否需要外键检查等,并将依赖的表或例程加入对应链表。

    • 对例程:缓存例程定义,如果其中包含 SQL 语句,则将其依赖的表和例程也加入链表中。

预锁定算法流程示意

  1. 初始状态query_tables 只包含语法解析得到的直接引用的表;sroutines_list 包含语法解析得到的直接调用的存储函数(如果有)。

  2. 循环开始table_to_open 指向 query_tables 的第一个元素,sroutine_to_open 指向 sroutines_list 的第一个元素。

  3. 处理表 :依次打开每个表。对于每个表,prelocking_strategy 检查:

    • 如果表是视图 → 递归展开,将视图定义中的底层表追加到 query_tables 末尾。

    • 如果表有触发器(对于 INSERT/UPDATE/DELETE 语句)→ 将触发器引用的表和执行的例程追加到相应链表。

    • 如果有外键约束(如被引用表)→ 将相关表追加到链表。

  4. 处理例程 :依次打开每个存储例程。prelocking_strategy 会分析例程体中的 SQL 语句,将其依赖的表和例程追加到链表末尾(通过 query_tables_last 指针)。

  5. 继续循环 :因为链表被扩展了,table_to_opensroutine_to_open 会继续指向新添加的元素,直到所有新增的表和例程都处理完毕,并且没有新的依赖加入。

  6. 结束 :此时所有直接或间接依赖的表都已在 query_tables 链表中,并且每个表都获得了合适的 MDL 锁(通常是 MDL_SHARED 或更强)。

  7. 局部变量与初始化

复制代码
Table_ref **table_to_open;          // 指向下一个要处理的表元素的指针
TABLE *old_table;                   // 用于错误恢复时判断是否分配了新 TABLE
Sroutine_hash_entry **sroutine_to_open; // 指向下一个存储例程
Table_ref *tables;
Open_table_context ot_ctx(thd, flags); // 上下文(超时、恢复标志等)
bool error = false;
bool some_routine_modifies_data = false; // 是否有存储例程修改了数据
bool has_prelocking_list;                // 语句是否已经标记需要预锁定
bool audit_notified = false;             // 是否已触发过表访问审计

Open_table_context 封装了打开表时需要的临时信息,包括超时、是否可重试、MDL 锁的保存点等。

  1. restart 标签与 HANDLER 表刷新
复制代码
restart:
  if (!thd->handler_tables_hash.empty()) mysql_ha_flush(thd);
  • 关闭所有被标记为需要刷新的 HANDLER 打开的表。目的是避免死锁,并确保即使当前会话未使用这些 HANDLER,也能及时关闭,防止 DDL 被长时间阻塞。

  • 使用 restart 标签是为了支持错误恢复:当遇到可恢复的错误(如 MDL 锁冲突或表版本过期)时,会关闭所有已打开的表,回滚到保存点,然后 goto restart 重新开始。

  1. 初始化迭代器与标志
复制代码
has_prelocking_list = thd->lex->requires_prelocking();
table_to_open = start;
old_table = *table_to_open ? (*table_to_open)->table : nullptr;
sroutine_to_open = &thd->lex->sroutines_list.first;
*counter = 0;
  • has_prelocking_list:如果语句之前已经标记为需要预锁定(例如在第一次准备时已经扩展过依赖),则不再重复扩展(只打开表和获取锁)。这用于预处理语句的多次执行。

  • sroutines_list:存储语句中显式或隐式调用的存储例程链表。解析器在遇到存储函数调用时会填充该链表。

  1. 全局 MDL 锁预获取(针对 LOCK TABLES 或 DDL)
复制代码
if (!(flags & (MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_OPEN_FORCE_SHARED_MDL |
               MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL))) {
    if (thd->locked_tables_mode) {
        // 在 LOCK TABLES 模式下,不能获取新锁,只检查已有锁是否足够
        Table_ref *end_table = thd->lex->first_not_own_table();
        if (open_tables_check_upgradable_mdl(thd, *start, end_table) ||
            acquire_backup_lock_in_lock_tables_mode(thd, *start, end_table)) {
            error = true; goto err;
        }
    } else {
        // 非 LOCK TABLES 模式,一次性获取所有表的高级别锁(如 DDL 需要的 SNW/SRNW)
        if (lock_table_names(thd, *start, thd->lex->first_not_own_table(),
                             ot_ctx.get_timeout(), flags)) {
            error = true; goto err;
        }
        // 对于 DDL 或 CREATE TABLE 类型的表,清空 ticket(后续会在 open_table 中重新获取)
        for (table = *start; table && table != thd->lex->first_not_own_table();
             table = table->next_global) {
            if (table->mdl_request.is_ddl_or_lock_tables_lock_request() ||
                table->open_strategy == Table_ref::OPEN_FOR_CREATE)
                table->mdl_request.ticket = nullptr;
        }
    }
}
  • 目的 :对于 LOCK TABLES 语句或 DDL 语句,需要在打开表之前先获取一些高级别的 MDL 锁(如 MDL_SHARED_NO_WRITE),以避免死锁或饥饿。

  • first_not_own_table():返回表链表中第一个不属于当前语句"自有"的表。自有表是语句显式引用的表,而非通过预锁定添加的表。

  • lock_table_names :一次性获取从 *startfirst_not_own_table() 之间所有表的高级别 MDL 锁。

  • 对于普通 DML(flags 包含 MYSQL_OPEN_FORCE_SHARED_MDL 或准备阶段),此段不执行。

  1. 主循环:扩展预锁定集合
复制代码
while (*table_to_open ||
       (thd->locked_tables_mode <= LTM_LOCK_TABLES && *sroutine_to_open)) {

循环条件:只要还有未处理的表(*table_to_open 非空)或未处理的存储例程(*sroutine_to_open 非空且不在 LOCK TABLES 模式下),就继续。

  • 5.1 处理表链表中的剩余表

    • open_and_process_table 负责:

      • 获取表的 MDL 锁(调用 open_table_get_mdl_lock)。

      • 打开表(open_tableopen_temporary_table)。

      • 如果是视图,则展开视图,将底层表追加到 thd->lex->query_tables 尾部。

      • 根据 prelocking_strategy 检查触发器、外键等依赖,并将依赖的表追加到链表尾部。

    • 可恢复错误 :如果遇到 MDL 锁冲突或表版本过旧,并且 ot_ctx.can_recover_from_failed_open() 为真,则执行恢复:

      • close_tables_for_reopen:关闭所有已打开的表,回滚到保存点。

      • 重新打开临时表,然后 goto restart 重试整个过程。

  • 5.2 审计事件通知(仅一次):在处理完初始表后,触发一次表访问审计事件。后续通过预锁定添加的表不再触发,因为它们的访问事件会在各自的上下文中单独处理。

    复制代码
    if (!audit_notified && mysql_event_tracking_table_access_notify(thd, *start)) {
        error = true; goto err;
    }
    audit_notified = true;
  • 5.3 处理存储例程链表

    复制代码
    if (thd->locked_tables_mode <= LTM_LOCK_TABLES) {
        for (Sroutine_hash_entry *rt = *sroutine_to_open; rt;
             sroutine_to_open = &rt->next, rt = rt->next) {
            bool need_prelocking = false;
            Table_ref **save_query_tables_last = thd->lex->query_tables_last;
            error = open_and_process_routine(
                thd, thd->lex, rt, prelocking_strategy, has_prelocking_list,
                &ot_ctx, &need_prelocking, &routine_modifies_data);
            if (need_prelocking && !thd->lex->requires_prelocking())
                thd->lex->mark_as_requiring_prelocking(save_query_tables_last);
            if (need_prelocking && !*start) *start = thd->lex->query_tables;
            // 错误处理...
            some_routine_modifies_data |= routine_modifies_data;
        }
    }
    • open_and_process_routine 负责:

      • 加载存储例程的定义(如果尚未加载),获取 MDL 锁。

      • 分析例程体中的 SQL 语句,将其依赖的表和子例程追加到 thd->lex->query_tablesthd->lex->sroutines_list 中。

      • 如果添加了依赖,则设置 need_prelocking = true,并调用 mark_as_requiring_prelocking 将语句标记为需要预锁定模式。

    • 注意:即使 has_prelocking_listtrue,仍然会调用 open_and_process_routine,因为需要获取例程的 MDL 锁(但不一定会扩展依赖)。

  1. 主循环结束后的检查
复制代码
if (*start &&
    (thd->get_transaction()->xid_state()->check_xa_idle_or_prepared(true) ||
     thd->get_transaction()->xid_state()->xa_trans_rolled_back()))
    return true;
  • 在 XA 事务的 IDLE 或 PREPARED 状态下不允许访问数据,否则报错。
复制代码
if (thd->timer && some_routine_modifies_data) {
    reset_statement_timer(thd);
    push_warning(thd, Sql_condition::SL_NOTE, ER_NON_RO_SELECT_DISABLE_TIMER, ...);
}
  • 如果语句本应是只读(如 SELECT),但调用的存储函数中包含了数据修改操作,则禁用语句执行时间限制(因为修改数据可能耗时较长)。
  1. 后处理循环:附加 MERGE 子表、转换锁类型、特殊表处理
复制代码
for (tables = *start; tables; tables = tables->next_global) {
    TABLE *tbl = tables->table;
    // 1. 处理 MERGE 表:将子表 attach 到父表
    if (tbl && tbl->file && tbl->file->ht->db_type == DB_TYPE_MRG_MYISAM) {
        if (tbl->db_stat && tbl->file->ha_extra(HA_EXTRA_ATTACH_CHILDREN)) {
            error = true; goto err;
        }
    }

    // 2. 对 ACL 表,如果请求的是共享锁(TL_READ_WITH_SHARED_LOCKS),降级为普通读锁(TL_READ_DEFAULT)
    bool issue_warning_on_skipping_row_lock = false;
    if (tables->lock_descriptor().type == TL_READ_WITH_SHARED_LOCKS &&
        !(flags & MYSQL_OPEN_FORCE_SHARED_MDL) &&
        is_acl_table_in_non_LTM(tables, thd->locked_tables_mode)) {
        tables->set_lock({TL_READ_DEFAULT, THR_DEFAULT});
        issue_warning_on_skipping_row_lock = true;
    }

    // 3. 将默认锁类型转换为实际锁类型,存入 TABLE::reginfo.lock_type
    if (tbl && tables->lock_descriptor().type != TL_UNLOCK &&
        !thd->locked_tables_mode) {
        if (tables->lock_descriptor().type == TL_WRITE_DEFAULT)
            tbl->reginfo.lock_type = thd->update_lock_default;
        else if (tables->lock_descriptor().type == TL_WRITE_CONCURRENT_DEFAULT)
            tables->table->reginfo.lock_type = thd->insert_lock_default;
        else if (tables->lock_descriptor().type == TL_READ_DEFAULT)
            tbl->reginfo.lock_type = read_lock_type_for_table(
                thd, thd->lex, tables, some_routine_modifies_data);
        else
            tbl->reginfo.lock_type = tables->lock_descriptor().type;
    }

    // 4. 禁止在系统视图上使用 FOR UPDATE / LOCK IN SHARE MODE
    if (tables->is_system_view &&
        tables->lock_descriptor().type == TL_READ_WITH_SHARED_LOCKS) {
        my_error(ER_IS_QUERY_INVALID_CLAUSE, MYF(0), "LOCK IN SHARE MODE");
        error = true; goto err;
    }

    // 5. 对 I_S 视图下的 DD 表设置非锁定读
    if (set_non_locking_read_for_IS_view(thd, tables)) {
        error = true; goto err;
    }

    // 6. 对 ACL 表设置非锁定读(跳过行锁)
    if (!(flags & MYSQL_OPEN_FORCE_SHARED_MDL) &&
        set_non_locking_read_for_ACL_table(
            thd, tables, issue_warning_on_skipping_row_lock)) {
        error = true; goto err;
    }
}
  • 锁类型转换 :解析器设置的默认锁类型(TL_READ_DEFAULTTL_WRITE_DEFAULT 等)在这里被转换为具体的存储引擎锁类型(如 TL_READTL_WRITETL_WRITE_LOW_PRIORITY),并存入 TABLE::reginfo.lock_type。真正向存储引擎请求锁是在后续的 lock_tables 函数中,通过 handler::ha_external_lock(thd, lock_type) 完成。

  • 特殊表处理:ACL 表、系统视图、I_S 表等会调整锁类型以避免行锁或禁止某些锁定子句。

  1. 错误处理
复制代码
err:
  if (error && *table_to_open && old_table != (*table_to_open)->table) {
    (*table_to_open)->table = nullptr;
  }
  return error;
  • 如果发生错误且当前表元素是新分配的 TABLE 对象(old_table != (*table_to_open)->table),则将其置为 nullptr,避免残留指针。

open_tables 实现了预锁定算法的核心循环,通过两个迭代器逐步扩展依赖集合,并在此过程中获取 MDL 锁、打开表、处理存储例程。后处理阶段完成锁类型转换和特殊表的调整。该函数是整个 MySQL 查询处理中"打开表"和"预锁定"的关键环节,为后续的优化和执行奠定了坚实的基础。

open_and_process_table
cpp 复制代码
/**
  Handle table list element by obtaining metadata lock, opening table or view
  and, if prelocking strategy prescribes so, extending the prelocking set with
  tables and routines used by it.

  @param[in]     thd                  Thread context.
  @param[in]     lex                  LEX structure for statement.
  @param[in]     tables               Table list element to be processed.
  @param[in,out] counter              Number of tables which are open.
  @param[in]     prelocking_strategy  Strategy which specifies how the
                                      prelocking set should be extended
                                      when table or view is processed.
  @param[in]     has_prelocking_list  Indicates that prelocking set/list for
                                      this statement has already been built.
  @param[in]     ot_ctx               Context used to recover from a failed
                                      open_table() attempt.

  @retval  false  Success.
  @retval  true   Error, reported unless there is a chance to recover from it.
*/

static bool open_and_process_table(THD *thd, LEX *lex, Table_ref *const tables,
                                   uint *counter,
                                   Prelocking_strategy *prelocking_strategy,
                                   bool has_prelocking_list,
                                   Open_table_context *ot_ctx) {
  bool error = false;
  bool safe_to_ignore_table = false;
  DBUG_TRACE;
  DEBUG_SYNC(thd, "open_and_process_table");

  /*
    Ignore placeholders for unnamed derived tables, as they are fully resolved
    by the optimizer.
  */
  if (tables->is_derived() || tables->is_table_function() ||
      tables->is_recursive_reference())
    goto end;

  assert(!tables->common_table_expr());

  /*
    If this Table_ref object is a placeholder for an information_schema
    table, create a temporary table to represent the information_schema
    table in the query. Do not fill it yet - will be filled during
    execution.
  */
  if (tables->schema_table) {
    /*
      Since we no longer set Table_ref::schema_table/table for table
      list elements representing mergeable view, we can't meet a table
      list element which represent information_schema table and a view
      at the same time. Otherwise, acquiring metadata lock om the view
      would have been necessary.
    */
    assert(!tables->is_view());

    if (!mysql_schema_table(thd, lex, tables) &&
        !check_and_update_table_version(thd, tables, tables->table->s)) {
      goto end;
    }
    error = true;
    goto end;
  }
  DBUG_PRINT("tcache", ("opening table: '%s'.'%s'  item: %p", tables->db,
                        tables->table_name, tables));

  (*counter)++;

  /*
    Not a placeholder so this must be a base/temporary table or view.
    Open it:
  */

  /*
    A Table_ref object may have an associated open TABLE object
    (Table_ref::table is not NULL) if it represents a pre-opened temporary
    table, or is a materialized view. (Derived tables are not handled here).
  */

  assert(tables->table == nullptr || is_temporary_table(tables) ||
         (tables->is_view() && tables->uses_materialization()));

  /*
    OT_TEMPORARY_ONLY means that we are in CREATE TEMPORARY TABLE statement.
    Also such table list element can't correspond to prelocking placeholder
    or to underlying table of merge table.
    So existing temporary table should have been preopened by this moment
    and we can simply continue without trying to open temporary or base table.
  */
  assert(tables->open_type != OT_TEMPORARY_ONLY ||
         (tables->open_strategy && !tables->prelocking_placeholder &&
          tables->parent_l == nullptr));

  if (tables->open_type == OT_TEMPORARY_ONLY || is_temporary_table(tables)) {
    // Already "open", no action required
  } else if (tables->prelocking_placeholder) {
    /*
      For the tables added by the pre-locking code, attempt to open
      the table but fail silently if the table does not exist.
      The real failure will occur when/if a statement attempts to use
      that table.
    */
    No_such_table_error_handler no_such_table_handler;
    thd->push_internal_handler(&no_such_table_handler);

    /*
      We're opening a table from the prelocking list.

      Since this table list element might have been added after pre-opening
      of temporary tables we have to try to open temporary table for it.

      We can't simply skip this table list element and postpone opening of
      temporary tabletill the execution of substatement for several reasons:
      - Temporary table can be a MERGE table with base underlying tables,
        so its underlying tables has to be properly open and locked at
        prelocking stage.
      - Temporary table can be a MERGE table and we might be in PREPARE
        phase for a prepared statement. In this case it is important to call
        HA_ATTACH_CHILDREN for all merge children.
        This is necessary because merge children remember "TABLE_SHARE ref type"
        and "TABLE_SHARE def version" in the HA_ATTACH_CHILDREN operation.
        If HA_ATTACH_CHILDREN is not called, these attributes are not set.
        Then, during the first EXECUTE, those attributes need to be updated.
        That would cause statement re-preparing (because changing those
        attributes during EXECUTE is caught by THD::m_reprepare_observers).
        The problem is that since those attributes are not set in merge
        children, another round of PREPARE will not help.
    */
    error = open_temporary_table(thd, tables);

    if (!error && !tables->table) error = open_table(thd, tables, ot_ctx);

    thd->pop_internal_handler();
    safe_to_ignore_table = no_such_table_handler.safely_trapped_errors();
  } else if (tables->parent_l && (thd->open_options & HA_OPEN_FOR_REPAIR)) {
    /*
      Also fail silently for underlying tables of a MERGE table if this
      table is opened for CHECK/REPAIR TABLE statement. This is needed
      to provide complete list of problematic underlying tables in
      CHECK/REPAIR TABLE output.
    */
    Repair_mrg_table_error_handler repair_mrg_table_handler;
    thd->push_internal_handler(&repair_mrg_table_handler);

    error = open_temporary_table(thd, tables);
    if (!error && !tables->table) error = open_table(thd, tables, ot_ctx);

    thd->pop_internal_handler();
    safe_to_ignore_table = repair_mrg_table_handler.safely_trapped_errors();
  } else {
    if (tables->parent_l) {
      /*
        Even if we are opening table not from the prelocking list we
        still might need to look for a temporary table if this table
        list element corresponds to underlying table of a merge table.
      */
      error = open_temporary_table(thd, tables);
    }

    if (!error && (tables->is_view() || tables->table == nullptr))
      error = open_table(thd, tables, ot_ctx);
  }

  if (error) {
    if (!ot_ctx->can_recover_from_failed_open() && safe_to_ignore_table) {
      DBUG_PRINT("info", ("open_table: ignoring table '%s'.'%s'", tables->db,
                          tables->alias));
      error = false;
    }
    goto end;
  }

  // Do specific processing for a view, and skip actions that apply to tables

  if (tables->is_view()) {
    // Views do not count as tables
    (*counter)--;

    /*
      tables->next_global list consists of two parts:
      1) Query tables and underlying tables of views.
      2) Tables used by all stored routines that this statement invokes on
         execution.
      We need to know where the bound between these two parts is. If we've
      just opened a view, which was the last table in part #1, and it
      has added its base tables after itself, adjust the boundary pointer
      accordingly.
    */
    if (lex->query_tables_own_last == &(tables->next_global) &&
        tables->view_query()->query_tables)
      lex->query_tables_own_last = tables->view_query()->query_tables_last;
    /*
      Let us free memory used by 'sroutines' hash here since we never
      call destructor for this LEX.
    */
    tables->view_query()->sroutines.reset();
    goto process_view_routines;
  }

  /*
    Special types of open can succeed but still don't set
    Table_ref::table to anything.
  */
  if (tables->open_strategy && !tables->table) goto end;

  /*
    If we are not already in prelocked mode and extended table list is not
    yet built we might have to build the prelocking set for this statement.

    Since currently no prelocking strategy prescribes doing anything for
    tables which are only read, we do below checks only if table is going
    to be changed.
  */
  if (thd->locked_tables_mode <= LTM_LOCK_TABLES && !has_prelocking_list &&
      tables->lock_descriptor().type >= TL_WRITE_ALLOW_WRITE) {
    bool need_prelocking = false;
    Table_ref **save_query_tables_last = lex->query_tables_last;
    /*
      Extend statement's table list and the prelocking set with
      tables and routines according to the current prelocking
      strategy.

      For example, for DML statements we need to add tables and routines
      used by triggers which are going to be invoked for this element of
      table list and also add tables required for handling of foreign keys.
    */
    error =
        prelocking_strategy->handle_table(thd, lex, tables, &need_prelocking);

    if (need_prelocking && !lex->requires_prelocking())
      lex->mark_as_requiring_prelocking(save_query_tables_last);

    if (error) goto end;
  }

  /* Check and update metadata version of a base table. */
  error = check_and_update_table_version(thd, tables, tables->table->s);
  if (error) goto end;
  /*
    After opening a MERGE table add the children to the query list of
    tables, so that they are opened too.
    Note that placeholders don't have the handler open.
  */
  /* MERGE tables need to access parent and child TABLE_LISTs. */
  assert(tables->table->pos_in_table_list == tables);
  /* Non-MERGE tables ignore this call. */
  if (tables->table->db_stat &&
      tables->table->file->ha_extra(HA_EXTRA_ADD_CHILDREN_LIST)) {
    error = true;
    goto end;
  }

process_view_routines:
  assert((tables->is_view() &&
          (tables->uses_materialization() || tables->table == nullptr)) ||
         (!tables->is_view()));

  /*
    Again we may need cache all routines used by this view and add
    tables used by them to table list.
  */
  if (tables->is_view() && thd->locked_tables_mode <= LTM_LOCK_TABLES &&
      !has_prelocking_list) {
    bool need_prelocking = false;
    Table_ref **save_query_tables_last = lex->query_tables_last;

    error =
        prelocking_strategy->handle_view(thd, lex, tables, &need_prelocking);

    if (need_prelocking && !lex->requires_prelocking())
      lex->mark_as_requiring_prelocking(save_query_tables_last);

    if (error) goto end;
  }

end:
  return error;
}

open_and_process_table 函数是 open_tables 主循环中处理单个表项的核心实现。它负责:

  • 为某个 Table_ref 获取 MDL 锁,并打开表或视图。

  • 根据 Prelocking_strategy(对于 DML 就是 DML_prelocking_strategy扩展依赖 :将触发器引用的表、外键关联的表、视图的底层表、存储例程等加入 thd->lex 的链表。

  • 若发现新的依赖,可能将语句标记为需要预锁定模式requires_prelocking())。

1.跳过不需要处理的节点

复制代码
if (tables->is_derived() || tables->is_table_function() ||
    tables->is_recursive_reference())
    goto end;
  • 派生表(derived table)、表函数、递归 CTE 引用在这里不打开,由优化器后续处理。
复制代码
if (tables->schema_table) {   // INFORMATION_SCHEMA 表
    // 创建临时表但不填充,直接跳转到 end
    ...
    goto end;
}
  • I_S 表不在此时打开和填充。

2.打开表或视图

  • 情况分支,代码根据 open_typeprelocking_placeholderparent_l 等标志决定如何打开:

    • OT_TEMPORARY_ONLY 或已经是临时表:直接跳过(表已"打开")。

    • prelocking_placeholder == true :该表是预锁定算法自动添加的依赖表。

      压入 No_such_table_error_handler 静默处理"表不存在"错误,尝试打开临时表,失败则尝试打开基表。

      如果是因为表不存在而失败,且 safe_to_ignore_table 为真,则忽略错误(后续真正用到时会报错)。

    • parent_l 非空且正在执行 CHECK/REPAIR :使用 Repair_mrg_table_error_handler 静默处理 MERGE 引擎的底层表。

    • 普通表:先尝试打开临时表(如果该表可能被临时表覆盖),再打开基表。

  • 临时表和 MERGE 表的特殊处理:注意对 MERGE 表底层表也需要打开临时表,因为临时表可能遮盖基表,且为了后续 HA_EXTRA_ATTACH_CHILDREN 正确工作。

3.视图的特殊处理

复制代码
if (tables->is_view()) {
    (*counter)--;   // 视图不计入表计数
    // 调整 query_tables_own_last 边界,将视图的底层表加入链表尾部
    if (lex->query_tables_own_last == &(tables->next_global) &&
        tables->view_query()->query_tables)
        lex->query_tables_own_last = tables->view_query()->query_tables_last;
    // 释放视图的 routines 哈希
    tables->view_query()->sroutines.reset();
    goto process_view_routines;
}
  • 打开视图后,视图的底层表被追加到表链表尾部。query_tables_own_last 指针记录了"最初语句显式引用的表"的末尾,视图底层表会插入在这个边界之前。

  • 随后跳转到 process_view_routines,进一步处理视图中引用的存储例程。

4.核心:根据策略扩展预锁定集合

  • 对于可修改的表(如 UPDATE/DELETE 的目标表)

    复制代码
    if (thd->locked_tables_mode <= LTM_LOCK_TABLES && !has_prelocking_list &&
        tables->lock_descriptor().type >= TL_WRITE_ALLOW_WRITE) {
        bool need_prelocking = false;
        Table_ref **save_query_tables_last = lex->query_tables_last;
        error = prelocking_strategy->handle_table(thd, lex, tables, &need_prelocking);
        if (need_prelocking && !lex->requires_prelocking())
            lex->mark_as_requiring_prelocking(save_query_tables_last);
        if (error) goto end;
    }
    • 条件 :未处于预锁定模式、尚未构建预锁定列表、且当前表将被修改(锁类型 >= TL_WRITE_ALLOW_WRITE)。

    • 调用策略的 handle_table :对于 DML_prelocking_strategy,它会:

      • 检查表是否有 INSERT/UPDATE/DELETE 触发器 → 将触发器内部引用的表和存储例程加入链表。

      • 检查表是否有外键约束(例如被引用表) → 将父表加入链表。

      • 若添加了新依赖,need_prelocking 被设为 true

    • 如果 need_prelocking 为真且语句尚未标记,则调用 mark_as_requiring_prelocking,保存当前链表尾指针,以便后续进入预锁定模式。

  • 对于视图(handle_view

    复制代码
    process_view_routines:
        if (tables->is_view() && thd->locked_tables_mode <= LTM_LOCK_TABLES &&
            !has_prelocking_list) {
            bool need_prelocking = false;
            Table_ref **save_query_tables_last = lex->query_tables_last;
            error = prelocking_strategy->handle_view(thd, lex, tables, &need_prelocking);
            if (need_prelocking && !lex->requires_prelocking())
                lex->mark_as_requiring_prelocking(save_query_tables_last);
            if (error) goto end;
        }
    • 收集视图定义中用到的存储函数,加入 sroutines_list

    • 如果视图的查询中包含了数据修改(例如可更新视图的 INSTEAD OF 触发器),也可能扩展依赖。

    • 同样可能标记需要预锁定。

5.后续处理

复制代码
// 检查并更新表的元数据版本
error = check_and_update_table_version(thd, tables, tables->table->s);
// 如果是 MERGE 表,将其子表添加到查询表链表(递归打开子表)
if (tables->table->db_stat && 
    tables->table->file->ha_extra(HA_EXTRA_ADD_CHILDREN_LIST)) {
    error = true;
    goto end;
}
  • 确保打开的表版本与已缓存的 share 一致。

  • 对 MERGE 表,通知存储引擎将子表加入链表(后续 open_and_process_table 会递归处理它们)。

open_table
cpp 复制代码
/**
  Open a base table.

  @param thd            Thread context.
  @param table_list     Open first table in list.
  @param ot_ctx         Context with flags which modify how open works
                        and which is used to recover from a failed
                        open_table() attempt.
                        Some examples of flags:
                        MYSQL_OPEN_IGNORE_FLUSH - Open table even if
                        someone has done a flush. No version number
                        checking is done.
                        MYSQL_OPEN_HAS_MDL_LOCK - instead of acquiring
                        metadata locks rely on that caller already has
                        appropriate ones.

  Uses a cache of open tables to find a TABLE instance not in use.

  If Table_ref::open_strategy is set to OPEN_IF_EXISTS, the table is
  opened only if it exists. If the open strategy is OPEN_STUB, the
  underlying table is never opened. In both cases, metadata locks are
  always taken according to the lock strategy.

  @retval true  Open failed. "action" parameter may contain type of action
                needed to remedy problem before retrying again.
  @retval false Success. Members of Table_ref structure are filled
  properly (e.g.  Table_ref::table is set for real tables and
                Table_ref::view is set for views).
*/

bool open_table(THD *thd, Table_ref *table_list, Open_table_context *ot_ctx) {
  TABLE *table = nullptr;
  TABLE_SHARE *share = nullptr;
  const char *key;
  size_t key_length;
  const char *alias = table_list->alias;
  uint flags = ot_ctx->get_flags();
  MDL_ticket *mdl_ticket = nullptr;
  int error = 0;

  DBUG_TRACE;

  // Temporary tables and derived tables are not allowed:
  assert(!is_temporary_table(table_list) && !table_list->is_derived());

  /*
    The table must not be opened already. The table can be pre-opened for
    some statements if it is a temporary table.

    open_temporary_table() must be used to open temporary tables.
    A derived table cannot be opened with this.
  */
  assert(table_list->is_view() || table_list->table == nullptr);

  /* an open table operation needs a lot of the stack space */
  if (check_stack_overrun(thd, STACK_MIN_SIZE_FOR_OPEN, (uchar *)&alias))
    return true;

  // New DD- In current_thd->is_strict_mode() mode we call open_table
  // on new DD tables like mysql.tables/* when CREATE fails and we
  // try to abort the operation and invoke quick_rm_table().
  // Currently, we ignore deleting table in strict mode. Need to fix this.
  // TODO.

  DBUG_EXECUTE_IF("kill_query_on_open_table_from_tz_find", {
    /*
      When on calling my_tz_find the following
      tables are opened in specified order: time_zone_name,
      time_zone, time_zone_transition_type,
      time_zone_transition. Emulate killing a query
      on opening the second table in the list.
    */
    if (!strcmp("time_zone", table_list->table_name))
      thd->killed = THD::KILL_QUERY;
  });

  if (!(flags & MYSQL_OPEN_IGNORE_KILLED) && thd->killed) return true;

  /*
    Check if we're trying to take a write lock in a read only transaction.

    Note that we allow write locks on log tables as otherwise logging
    to general/slow log would be disabled in read only transactions.
  */
  if (table_list->mdl_request.is_write_lock_request() &&
      (thd->tx_read_only && !thd->is_cmd_skip_transaction_read_only()) &&
      !(flags & (MYSQL_LOCK_LOG_TABLE | MYSQL_OPEN_HAS_MDL_LOCK))) {
    my_error(ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION, MYF(0));
    return true;
  }

  /*
    FLUSH TABLES is ignored for DD, I_S and P_S tables/views.
    Hence setting MYSQL_OPEN_IGNORE_FLUSH flag.
  */
  if (table_list->is_system_view || belongs_to_dd_table(table_list) ||
      belongs_to_p_s(table_list))
    flags |= MYSQL_OPEN_IGNORE_FLUSH;

  key_length = get_table_def_key(table_list, &key);

  // If a table in a secondary storage engine has been requested,
  // adjust the key to refer to the secondary table.
  std::string secondary_key;
  if ((flags & MYSQL_OPEN_SECONDARY_ENGINE) != 0) {
    secondary_key = create_table_def_key_secondary(
        table_list->get_db_name(), table_list->get_table_name());
    key = secondary_key.data();
    key_length = secondary_key.length();
  }

  /*
    If we're in pre-locked or LOCK TABLES mode, let's try to find the
    requested table in the list of pre-opened and locked tables. If the
    table is not there, return an error - we can't open not pre-opened
    tables in pre-locked/LOCK TABLES mode.

    There is a special case where we allow opening not pre-opened tables
    in LOCK TABLES mode for new DD tables. The reason is as following.
    With new DD, IS system views need to be accessible in LOCK TABLE
    mode without user explicitly calling LOCK TABLE on IS view or its
    underlying DD tables. This is required to keep the old behavior the
    MySQL server had without new DD.

    In case user executes IS system view under LOCK TABLE mode
    (LTM and not prelocking), then MySQL server implicitly opens system
    view and related DD tables. Such DD tables are then implicitly closed
    upon end of statement execution.

    Our goal is to hide DD tables from users, so there is no possibility of
    explicit locking DD table using LOCK TABLE. In case user does LOCK TABLE
    on IS system view explicitly, MySQL server throws a error.

    TODO: move this block into a separate function.
  */
  if (thd->locked_tables_mode && !(flags & MYSQL_OPEN_GET_NEW_TABLE) &&
      !(in_LTM(thd) &&
        (table_list->is_system_view || belongs_to_dd_table(table_list) ||
         belongs_to_p_s(table_list)))) {  // Using table locks
    TABLE *best_table = nullptr;
    int best_distance = INT_MIN;
    for (table = thd->open_tables; table; table = table->next) {
      if (table->s->table_cache_key.length == key_length &&
          !memcmp(table->s->table_cache_key.str, key, key_length)) {
        if (!my_strcasecmp(system_charset_info, table->alias, alias) &&
            table->query_id != thd->query_id && /* skip tables already used */
            (thd->locked_tables_mode == LTM_LOCK_TABLES ||
             table->query_id == 0)) {
          const int distance = ((int)table->reginfo.lock_type -
                                (int)table_list->lock_descriptor().type);

          /*
            Find a table that either has the exact lock type requested,
            or has the best suitable lock. In case there is no locked
            table that has an equal or higher lock than requested,
            we us the closest matching lock to be able to produce an error
            message about wrong lock mode on the table. The best_table
            is changed if bd < 0 <= d or bd < d < 0 or 0 <= d < bd.

            distance <  0 - No suitable lock found
            distance >  0 - we have lock mode higher then we require
            distance == 0 - we have lock mode exactly which we need
          */
          if ((best_distance < 0 && distance > best_distance) ||
              (distance >= 0 && distance < best_distance)) {
            best_distance = distance;
            best_table = table;
            if (best_distance == 0) {
              /*
                We have found a perfect match and can finish iterating
                through open tables list. Check for table use conflict
                between calling statement and SP/trigger is done in
                lock_tables().
              */
              break;
            }
          }
        }
      }
    }
    if (best_table) {
      table = best_table;
      table->query_id = thd->query_id;
      DBUG_PRINT("info", ("Using locked table"));
      goto reset;
    }
    /*
      Is this table a view and not a base table?
      (it is work around to allow to open view with locked tables,
      real fix will be made after definition cache will be made)

      Since opening of view which was not explicitly locked by LOCK
      TABLES breaks metadata locking protocol (potentially can lead
      to deadlocks) it should be disallowed.
    */
    if (thd->mdl_context.owns_equal_or_stronger_lock(
            MDL_key::TABLE, table_list->db, table_list->table_name,
            MDL_SHARED)) {
      /*
        Note that we can't be 100% sure that it is a view since it's
        possible that we either simply have not found unused TABLE
        instance in THD::open_tables list or were unable to open table
        during prelocking process (in this case in theory we still
        should hold shared metadata lock on it).
      */
      const dd::cache::Dictionary_client::Auto_releaser releaser(
          thd->dd_client());
      const dd::View *view = nullptr;
      if (!thd->dd_client()->acquire(table_list->db, table_list->table_name,
                                     &view) &&
          view != nullptr) {
        /*
          If parent_l of the table_list is non null then a merge table
          has this view as child table, which is not supported.
        */
        if (table_list->parent_l) {
          my_error(ER_WRONG_MRG_TABLE, MYF(0));
          return true;
        }

        /*
          In the case of a CREATE, add a dummy LEX object to
          indicate the presence of a view amd skip processing the
          existing view.
        */
        if (table_list->open_strategy == Table_ref::OPEN_FOR_CREATE)
          return add_view_place_holder(thd, table_list);

        if (!tdc_open_view(thd, table_list, key, key_length)) {
          assert(table_list->is_view());
          return false;  // VIEW
        }
      }
    }
    /*
      No table in the locked tables list. In case of explicit LOCK TABLES
      this can happen if a user did not include the table into the list.
      In case of pre-locked mode locked tables list is generated automatically,
      so we may only end up here if the table did not exist when
      locked tables list was created.
    */
    if (thd->locked_tables_mode == LTM_PRELOCKED)
      my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db, table_list->alias);
    else
      my_error(ER_TABLE_NOT_LOCKED, MYF(0), alias);
    return true;
  }

  // Non pre-locked/LOCK TABLES mode, and not using secondary storage engine.
  // This is the normal use case.

  if ((flags & (MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_OPEN_SECONDARY_ENGINE)) == 0) {
    /*
      We are not under LOCK TABLES and going to acquire write-lock/
      modify the base table. We need to acquire protection against
      global read lock until end of this statement in order to have
      this statement blocked by active FLUSH TABLES WITH READ LOCK.

      We don't block acquire this protection under LOCK TABLES as
      such protection already acquired at LOCK TABLES time and
      not released until UNLOCK TABLES.

      We don't block statements which modify only temporary tables
      as these tables are not preserved by backup by any form of
      backup which uses FLUSH TABLES WITH READ LOCK.

      TODO: The fact that we sometimes acquire protection against
            GRL only when we encounter table to be write-locked
            slightly increases probability of deadlock.
            This problem will be solved once Alik pushes his
            temporary table refactoring patch and we can start
            pre-acquiring metadata locks at the beginning of
            open_tables() call.
    */
    if (table_list->mdl_request.is_write_lock_request() &&
        !(flags &
          (MYSQL_OPEN_IGNORE_GLOBAL_READ_LOCK | MYSQL_OPEN_FORCE_SHARED_MDL |
           MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL |
           MYSQL_OPEN_SKIP_SCOPED_MDL_LOCK)) &&
        !ot_ctx->has_protection_against_grl()) {
      MDL_request protection_request;
      MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

      if (thd->global_read_lock.can_acquire_protection()) return true;

      MDL_REQUEST_INIT(&protection_request, MDL_key::GLOBAL, "", "",
                       MDL_INTENTION_EXCLUSIVE, MDL_STATEMENT);

      /*
        Install error handler which if possible will convert deadlock error
        into request to back-off and restart process of opening tables.

        Prefer this context as a victim in a deadlock when such a deadlock
        can be easily handled by back-off and retry.
      */
      thd->push_internal_handler(&mdl_deadlock_handler);
      thd->mdl_context.set_force_dml_deadlock_weight(ot_ctx->can_back_off());

      bool result = thd->mdl_context.acquire_lock(&protection_request,
                                                  ot_ctx->get_timeout());

      /*
        Unlike in other places where we acquire protection against global read
        lock, the read_only state is not checked here since we check its state
        later in mysql_lock_tables()
      */

      thd->mdl_context.set_force_dml_deadlock_weight(false);
      thd->pop_internal_handler();

      if (result) return true;

      ot_ctx->set_has_protection_against_grl();
    }

    if (open_table_get_mdl_lock(thd, ot_ctx, table_list, flags, &mdl_ticket) ||
        mdl_ticket == nullptr) {
      DEBUG_SYNC(thd, "before_open_table_wait_refresh");
      return true;
    }
    DEBUG_SYNC(thd, "after_open_table_mdl_shared");
  } else {
    /*
      Grab reference to the MDL lock ticket that was acquired
      by the caller.
    */
    mdl_ticket = table_list->mdl_request.ticket;
  }

  if (table_list->open_strategy == Table_ref::OPEN_IF_EXISTS ||
      table_list->open_strategy == Table_ref::OPEN_FOR_CREATE) {
    bool exists;

    if (check_if_table_exists(thd, table_list, &exists)) return true;

    /*
      If the table does not exist then upgrade the lock to the EXCLUSIVE MDL
      lock.
    */
    if (!exists) {
      if (table_list->open_strategy == Table_ref::OPEN_FOR_CREATE &&
          !(flags & (MYSQL_OPEN_FORCE_SHARED_MDL |
                     MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL))) {
        MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

        thd->push_internal_handler(&mdl_deadlock_handler);

        DEBUG_SYNC(thd, "before_upgrading_lock_from_S_to_X_for_create_table");
        bool wait_result = thd->mdl_context.upgrade_shared_lock(
            table_list->mdl_request.ticket, MDL_EXCLUSIVE,
            thd->variables.lock_wait_timeout);

        thd->pop_internal_handler();
        DEBUG_SYNC(thd, "after_upgrading_lock_from_S_to_X_for_create_table");

        /* Deadlock or timeout occurred while upgrading the lock. */
        if (wait_result) return true;
      }

      return false;
    }

    /* Table exists. Let us try to open it. */
  } else if (table_list->open_strategy == Table_ref::OPEN_STUB)
    return false;

retry_share : {
  Table_cache *tc = table_cache_manager.get_cache(thd);

  tc->lock();

  /*
    Try to get unused TABLE object or at least pointer to
    TABLE_SHARE from the table cache.
  */
  if (!table_list->is_view())
    table = tc->get_table(thd, key, key_length, &share);

  if (table) {
    /* We have found an unused TABLE object. */

    if (!(flags & MYSQL_OPEN_IGNORE_FLUSH)) {
      /*
        TABLE_SHARE::version can only be initialised while holding the
        LOCK_open and in this case no one has a reference to the share
        object, if a reference exists to the share object it is necessary
        to lock both LOCK_open AND all table caches in order to update
        TABLE_SHARE::version. The same locks are required to increment
        refresh_version global variable.

        As result it is safe to compare TABLE_SHARE::version and
        refresh_version values while having only lock on the table
        cache for this thread.

        Table cache should not contain any unused TABLE objects with
        old versions.
      */
      assert(!share->has_old_version());

      /*
        Still some of already opened might become outdated (e.g. due to
        concurrent table flush). So we need to compare version of opened
        tables with version of TABLE object we just have got.
      */
      if (thd->open_tables &&
          thd->open_tables->s->version() != share->version()) {
        tc->release_table(thd, table);
        tc->unlock();
        (void)ot_ctx->request_backoff_action(
            Open_table_context::OT_REOPEN_TABLES, nullptr);
        return true;
      }
    }
    tc->unlock();

    /* Call rebind_psi outside of the critical section. */
    assert(table->file != nullptr);
    table->file->rebind_psi();
    table->file->ha_extra(HA_EXTRA_RESET_STATE);

    thd->status_var.table_open_cache_hits++;
    global_aggregated_stats.get_shard(thd->thread_id()).table_open_cache_hits++;
    goto table_found;
  } else if (share) {
    /*
      We weren't able to get an unused TABLE object. Still we have
      found TABLE_SHARE for it. So let us try to create new TABLE
      for it. We start by incrementing share's reference count and
      checking its version.
    */
    mysql_mutex_lock(&LOCK_open);
    tc->unlock();
    share->increment_ref_count();
    goto share_found;
  } else {
    /*
      We have not found neither TABLE nor TABLE_SHARE object in
      table cache (this means that there are no TABLE objects for
      it in it).
      Let us try to get TABLE_SHARE from table definition cache or
      from disk and then to create TABLE object for it.
    */
    tc->unlock();
  }
}

  mysql_mutex_lock(&LOCK_open);

  if (!(share = get_table_share_with_discover(
            thd, table_list, key, key_length,
            flags & MYSQL_OPEN_SECONDARY_ENGINE, &error))) {
    mysql_mutex_unlock(&LOCK_open);
    /*
      If thd->is_error() is not set, we either need discover
      (error == 7), or the error was silenced by the prelocking
      handler (error == 0), in which case we should skip this
      table.
    */
    if (error == 7 && !thd->is_error()) {
      (void)ot_ctx->request_backoff_action(Open_table_context::OT_DISCOVER,
                                           table_list);
    }
    return true;
  }

  /*
    If a view is anticipated or the TABLE_SHARE object is a view, perform
    a version check for it without creating a TABLE object.

    Note that there is no need to call TABLE_SHARE::has_old_version() as we
    do for regular tables, because view shares are always up to date.
  */
  if (table_list->is_view() || share->is_view) {
    bool view_open_result = true;
    /*
      If parent_l of the table_list is non null then a merge table
      has this view as child table, which is not supported.
    */
    if (table_list->parent_l) my_error(ER_WRONG_MRG_TABLE, MYF(0));
    /*
      Validate metadata version: in particular, that a view is opened when
      it is expected, or that a table is opened when it is expected.
    */
    else if (check_and_update_table_version(thd, table_list, share))
      ;
    else if (table_list->open_strategy == Table_ref::OPEN_FOR_CREATE) {
      /*
        Skip reading the view definition if the open is for a table to be
        created. This scenario will happen only when there exists a view and
        the current CREATE TABLE request is with the same name.
      */
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);

      /*
        The LEX object is used by the executor and other parts of the
        code to detect the presence of a view. As this is
        OPEN_FOR_CREATE we skip the call to open_and_read_view(),
        which creates the LEX object, and create a dummy LEX object.

        For SP and PS, LEX objects are created at the time of
        statement prepare and open_table() is called for every execute
        after that. Skip creation of LEX objects if it is already
        present.
      */
      if (!table_list->is_view()) return add_view_place_holder(thd, table_list);
      return false;
    } else {
      /*
        Read definition of existing view.
      */
      view_open_result = open_and_read_view(thd, share, table_list);
    }

    /* TODO: Don't free this */
    release_table_share(share);
    mysql_mutex_unlock(&LOCK_open);

    if (view_open_result) return true;

    if (parse_view_definition(thd, table_list)) return true;

    assert(table_list->is_view());

    return false;
  }

share_found:
  if (!(flags & MYSQL_OPEN_IGNORE_FLUSH)) {
    if (share->has_old_version()) {
      /*
        We already have an MDL lock. But we have encountered an old
        version of table in the table definition cache which is possible
        when someone changes the table version directly in the cache
        without acquiring a metadata lock (e.g. this can happen during
        "rolling" FLUSH TABLE(S)).
        Release our reference to share, wait until old version of
        share goes away and then try to get new version of table share.
      */
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);

      MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);
      bool wait_result;

      thd->push_internal_handler(&mdl_deadlock_handler);

      /*
        In case of deadlock we would like this thread to be preferred as
        a deadlock victim when this deadlock can be nicely handled by
        back-off and retry. We still have a few weird cases, like
        FLUSH TABLES <table-list> WITH READ LOCK, where we use strong
        metadata locks and open_tables() is called with some metadata
        locks pre-acquired. In these cases we still want to use DDL
        deadlock weight.
      */
      const uint deadlock_weight =
          ot_ctx->can_back_off() ? MDL_wait_for_subgraph::DEADLOCK_WEIGHT_DML
                                 : mdl_ticket->get_deadlock_weight();

      wait_result =
          tdc_wait_for_old_version(thd, table_list->db, table_list->table_name,
                                   ot_ctx->get_timeout(), deadlock_weight);

      thd->pop_internal_handler();

      if (wait_result) return true;

      DEBUG_SYNC(thd, "open_table_before_retry");
      goto retry_share;
    }

    if (thd->open_tables &&
        thd->open_tables->s->version() != share->version()) {
      /*
        If the version changes while we're opening the tables,
        we have to back off, close all the tables opened-so-far,
        and try to reopen them. Note: refresh_version is currently
        changed only during FLUSH TABLES.
      */
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);
      (void)ot_ctx->request_backoff_action(Open_table_context::OT_REOPEN_TABLES,
                                           nullptr);
      return true;
    }
  }

  mysql_mutex_unlock(&LOCK_open);

  DEBUG_SYNC(thd, "open_table_found_share");

  {
    const dd::cache::Dictionary_client::Auto_releaser releaser(
        thd->dd_client());
    const dd::Table *table_def = nullptr;
    if (!(flags & MYSQL_OPEN_NO_NEW_TABLE_IN_SE) &&
        thd->dd_client()->acquire(share->db.str, share->table_name.str,
                                  &table_def)) {
      // Error is reported by the dictionary subsystem.
      goto err_lock;
    }

    if (table_def && table_def->hidden() == dd::Abstract_table::HT_HIDDEN_SE) {
      my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db,
               table_list->table_name);
      goto err_lock;
    }

    /* make a new table */
    if (!(table = (TABLE *)my_malloc(key_memory_TABLE, sizeof(*table),
                                     MYF(MY_WME))))
      goto err_lock;

    error = open_table_from_share(
        thd, share, alias,
        ((flags & MYSQL_OPEN_NO_NEW_TABLE_IN_SE)
             ? 0
             : ((uint)(HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | HA_GET_INDEX |
                       HA_TRY_READ_ONLY))),
        EXTRA_RECORD, thd->open_options, table, false, table_def);

    if (error) {
      ::destroy_at(table);
      my_free(table);

      if (error == 7)
        (void)ot_ctx->request_backoff_action(Open_table_context::OT_DISCOVER,
                                             table_list);
      else if (error == 8)
        (void)ot_ctx->request_backoff_action(
            Open_table_context::OT_FIX_ROW_TYPE, table_list);
      else if (share->crashed)
        (void)ot_ctx->request_backoff_action(Open_table_context::OT_REPAIR,
                                             table_list);
      goto err_lock;
    } else if (share->crashed) {
      switch (thd->lex->sql_command) {
        case SQLCOM_ALTER_TABLE:
        case SQLCOM_REPAIR:
        case SQLCOM_CHECK:
        case SQLCOM_SHOW_CREATE:
          break;
        default:
          closefrm(table, false);
          ::destroy_at(table);
          my_free(table);
          my_error(ER_CRASHED_ON_USAGE, MYF(0), share->table_name.str);
          goto err_lock;
      }
    }

    if (open_table_entry_fini(thd, share, table_def, table)) {
      closefrm(table, false);
      ::destroy_at(table);
      my_free(table);
      goto err_lock;
    }
  }
  {
    /* Add new TABLE object to table cache for this connection. */
    Table_cache *tc = table_cache_manager.get_cache(thd);

    tc->lock();

    if (tc->add_used_table(thd, table)) {
      tc->unlock();
      goto err_lock;
    }
    tc->unlock();
  }
  thd->status_var.table_open_cache_misses++;
  global_aggregated_stats.get_shard(thd->thread_id()).table_open_cache_misses++;

table_found:
  table->mdl_ticket = mdl_ticket;

  table->next = thd->open_tables; /* Link into simple list */
  thd->set_open_tables(table);

  table->reginfo.lock_type = TL_READ; /* Assume read */

reset:
  table->reset();
  table->set_created();
  /*
    Check that there is no reference to a condition from an earlier query
    (cf. Bug#58553).
  */
  assert(table->file->pushed_cond == nullptr);

  // Table is not a derived table and not a non-updatable view:
  table_list->set_updatable();
  table_list->set_insertable();

  table_list->table = table;

  /*
    Position for each partition in the bitmap is read from the Handler_share
    instance of the table. In MYSQL_OPEN_NO_NEW_TABLE_IN_SE mode, table is not
    opened in the SE and Handler_share instance for it is not created. Hence
    skipping partitions bitmap setting in the MYSQL_OPEN_NO_NEW_TABLE_IN_SE
    mode.
  */
  if (!(flags & MYSQL_OPEN_NO_NEW_TABLE_IN_SE)) {
    if (table->part_info) {
      /* Set all [named] partitions as used. */
      if (table->part_info->set_partition_bitmaps(table_list)) return true;
    } else if (table_list->partition_names) {
      /* Don't allow PARTITION () clause on a nonpartitioned table */
      my_error(ER_PARTITION_CLAUSE_ON_NONPARTITIONED, MYF(0));
      return true;
    }
  }

  table->init(thd, table_list);

  /* Request a read lock for implicitly opened P_S tables. */
  if (in_LTM(thd) && table_list->table->file->get_lock_type() == F_UNLCK &&
      belongs_to_p_s(table_list)) {
    table_list->table->file->ha_external_lock(thd, F_RDLCK);
  }

  return false;

err_lock:
  mysql_mutex_lock(&LOCK_open);
  release_table_share(share);
  mysql_mutex_unlock(&LOCK_open);

  return true;
}

open_table 的核心任务是:

  • 为一个 Table_ref 对象(代表一个基表或视图)获取 MDL 锁(如果需要)。

  • 从表缓存(Table Cache)中获得或创建 TABLE 对象(用于后续数据访问)。

  • 处理 LOCK TABLES 模式 下的表复用。

  • 处理 视图 的打开(如果是视图则走另一条路径)。

  • 处理 CREATE TABLE ... IF NOT EXISTSOPEN_STUB 等特殊策略。

  • 处理 表版本变更 导致的 retry / back off 逻辑。

复制代码
Sql_cmd_dml::execute/prepare
  -> open_tables_for_query
    -> open_tables
      -> open_and_process_table
        -> open_table   (对于基表)

所以 open_table 是真正打开一个基表的地方(视图、派生表、临时表有其他处理函数)。

再次强调:open_table 全程没有获取任何存储引擎锁(如 InnoDB 的意向锁或行锁)。它只负责:

  • MDL 锁(服务器层)

  • 打开表文件,读取元数据,建立 TABLE 对象

  • TABLE 挂入 thd->open_tables 链表

真正的存储引擎锁(lock_tables 中调用 handler::ha_external_lock)要等到执行阶段才获取。

open_table 的全貌

阶段 关键动作
前置检查 只读事务拒绝写锁;特殊表设置 IGNORE_FLUSH
LOCK TABLES 分支 从已锁列表中找表;找不到则尝试视图;否则报错
常规分支 获取 GRL 保护;获取 MDL 表锁
策略分支 OPEN_IF_EXISTS / OPEN_FOR_CREATE 处理表不存在情况
表缓存访问 查找 TABLE/TABLE_SHARE;版本检查;必要时重试
视图处理 打开视图定义,创建 LEX 对象
创建 TABLE 从 share 实例化,加入缓存
完成 关联 MDL ticket,重置状态,返回
  1. 断言和基础检查
复制代码
assert(!is_temporary_table(table_list) && !table_list->is_derived());
assert(table_list->is_view() || table_list->table == nullptr);
if (check_stack_overrun(...)) return true;
  • 临时表和派生表不在这里打开(有专门路径)。

  • 表不能已经打开(除非是视图,视图可以没有 TABLE 对象)。

  • 栈空间保护。

  1. 处理只读事务中的写锁请求
复制代码
if (table_list->mdl_request.is_write_lock_request() &&
    (thd->tx_read_only && !thd->is_cmd_skip_transaction_read_only()) &&
    !(flags & (MYSQL_LOCK_LOG_TABLE | MYSQL_OPEN_HAS_MDL_LOCK))) {
    my_error(ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION, MYF(0));
    return true;
}
  • 如果当前事务只读,又想获取写 MDL 锁,则拒绝。

  • 例外:日志表(general/slow log)允许写;或者调用者已经持有 MDL 锁(MYSQL_OPEN_HAS_MDL_LOCK)。

  1. 特殊表设置 IGNORE_FLUSH 标志
复制代码
if (table_list->is_system_view || belongs_to_dd_table(table_list) || belongs_to_p_s(table_list))
    flags |= MYSQL_OPEN_IGNORE_FLUSH;
  • 数据字典表、I_S 表、P_S 表不受 FLUSH TABLES 影响,因此跳过版本检查。
  1. 生成缓存 key
复制代码
key_length = get_table_def_key(table_list, &key);
if ((flags & MYSQL_OPEN_SECONDARY_ENGINE) != 0) {
    secondary_key = create_table_def_key_secondary(...);
    key = secondary_key.data();
    key_length = secondary_key.length();
}
  • get_table_def_key 生成 "db/table_name" 字符串。

  • 如果要求打开二级引擎中的表,key 会改变(例如加后缀),用于在表缓存中区分不同引擎的表。

5.LOCK TABLES / 预锁定模式下的特殊处理(核心分支)

复制代码
if (thd->locked_tables_mode && !(flags & MYSQL_OPEN_GET_NEW_TABLE) &&
    !(in_LTM(thd) && (table_list->is_system_view || belongs_to_dd_table(table_list) ||
         belongs_to_p_s(table_list)))) {
    // 在 thd->open_tables 链表中查找已经打开并锁定的表
}
  • 条件:处于 LOCK TABLES 模式或预锁定模式(thd->locked_tables_mode 非 0),且没有要求获取新表(MYSQL_OPEN_GET_NEW_TABLE),且不是特殊系统表(I_S/DD/P_S)------这些允许隐式打开。

  • 目的:复用已经锁定好的表,不允许打开不在锁定列表中的表。

查找算法:

复制代码
TABLE *best_table = nullptr;
int best_distance = INT_MIN;
for (table = thd->open_tables; table; table = table->next) {
    // 匹配 cache key 和 alias
    // 确保表未被当前语句使用(query_id != thd->query_id)
    // 计算请求锁类型与已有锁类型的距离
}
  • best_distance 逻辑:

    • 若已有锁强度 >= 请求锁强度,distance >= 0,越小越匹配(0 完全匹配)。

    • 若已有锁强度 < 请求锁强度,distance < 0,越大越接近。

  • 找到 best_table 后直接 goto reset,复用该 TABLE 对象,不再重新打开。

如果没找到表,但已经拥有 MDL_SHARED 锁,尝试作为视图打开

复制代码
if (thd->mdl_context.owns_equal_or_stronger_lock(MDL_key::TABLE, ... , MDL_SHARED)) {
    // 尝试从 DD 中获取 view 对象
    if (view exists) {
        if (OPEN_FOR_CREATE) return add_view_place_holder(...);
        if (!tdc_open_view(...)) {
            // 成功打开视图
            return false;
        }
    }
}
  • 这处理了:虽然不在锁表列表中,但已经因为其他原因(例如预锁定)持有 MDL 锁,且对象实际是一个视图。此时可以打开视图。

最后报错

复制代码
if (thd->locked_tables_mode == LTM_PRELOCKED)
    my_error(ER_NO_SUCH_TABLE, ...);
else
    my_error(ER_TABLE_NOT_LOCKED, ...);
return true;
  • 在预锁定模式下提示表不存在,在显式 LOCK TABLES 下提示表未被锁定。

6.常规模式(非 LOCK TABLES)下的 MDL 锁获取

复制代码
if ((flags & (MYSQL_OPEN_HAS_MDL_LOCK | MYSQL_OPEN_SECONDARY_ENGINE)) == 0) {
    // 普通路径:需要自己获取 MDL 锁
}

防止全局读锁(GRL)的保护

复制代码
if (table_list->mdl_request.is_write_lock_request() && ... && !ot_ctx->has_protection_against_grl()) {
    MDL_request protection_request;
    MDL_REQUEST_INIT(&protection_request, MDL_key::GLOBAL, "", "",
                     MDL_INTENTION_EXCLUSIVE, MDL_STATEMENT);
    thd->mdl_context.acquire_lock(&protection_request, ot_ctx->get_timeout());
    ot_ctx->set_has_protection_against_grl();
}
  • 对于写锁请求,先获取一个 MDL_INTENTION_EXCLUSIVE 全局锁 。这个锁与 FLUSH TABLES WITH READ LOCK 持有的 MDL_SHARED 全局锁互斥,从而保证当前语句不会被 GRL 阻塞。

  • 这个保护每个语句只需要获取一次(通过 ot_ctx 记录),避免重复获取。

获取表的 MDL 锁

复制代码
if (open_table_get_mdl_lock(thd, ot_ctx, table_list, flags, &mdl_ticket) || mdl_ticket == nullptr) {
    return true;
}
  • open_table_get_mdl_lock 内部调用 MDL_context::acquire_lock,将 table_list->mdl_request 转换成实际的 MDL_ticket

  • 这对应前面讨论的 MDL 锁获取过程。

如果调用者已经持有锁

复制代码
} else {
    mdl_ticket = table_list->mdl_request.ticket;
}
  • MYSQL_OPEN_HAS_MDL_LOCK 标志存在时,直接使用外部传入的 ticket,不重复获取。

7.处理 OPEN_IF_EXISTSOPEN_FOR_CREATE 策略

复制代码
if (table_list->open_strategy == Table_ref::OPEN_IF_EXISTS ||
    table_list->open_strategy == Table_ref::OPEN_FOR_CREATE) {
    bool exists;
    if (check_if_table_exists(thd, table_list, &exists)) return true;
    if (!exists) {
        if (table_list->open_strategy == Table_ref::OPEN_FOR_CREATE && ...) {
            // 升级 MDL 锁到 EXCLUSIVE
            thd->mdl_context.upgrade_shared_lock(..., MDL_EXCLUSIVE, ...);
        }
        return false; // 表不存在,不继续打开
    }
    // 表存在,继续下面的正常打开流程
}
  • 对于 CREATE TABLE IF NOT EXISTSOPEN_IF_EXISTS,先检查表是否存在。若不存在且是 CREATE,升级锁到 X 锁后直接返回(表示创建新表)。

  • 若存在,继续打开表(用于后续可能的 SELECT 等)。

    复制代码
    } else if (table_list->open_strategy == Table_ref::OPEN_STUB) {
        return false; // 不需要打开,只占位
    }
  • OPEN_STUB 用于某些语句(如 LOCK TABLES 中的表),无需真正打开。

8.从表缓存中获取或创建 TABLE 对象

8.1 查找缓存:每个线程有自己的表缓存,get_table 尝试返回一个未使用的 TABLE 对象 ,同时可能带回 TABLE_SHARE。:

复制代码
Table_cache *tc = table_cache_manager.get_cache(thd);
tc->lock();
if (!table_list->is_view())
    table = tc->get_table(thd, key, key_length, &share);
  • 情况 A:找到未使用的 TABLE:如果版本检查失败,要求上层重新打开所有表(OT_REOPEN_TABLES)。

    复制代码
    if (table) {
        if (!(flags & MYSQL_OPEN_IGNORE_FLUSH)) {
            // 检查版本
            if (thd->open_tables && thd->open_tables->s->version() != share->version()) {
                // 版本不匹配,需要重试
                tc->release_table(thd, table);
                tc->unlock();
                ot_ctx->request_backoff_action(OT_REOPEN_TABLES, nullptr);
                return true;
            }
        }
        tc->unlock();
        // 重绑定性能监控,重置状态
        goto table_found;
    }
  • 情况 B:只找到 TABLE_SHARE(无可用 TABLE):转到 share_found 标签,创建新的 TABLE 对象。

    复制代码
    else if (share) {
        mysql_mutex_lock(&LOCK_open);
        tc->unlock();
        share->increment_ref_count();
        goto share_found;
    }
  • 情况 C:什么都没找到

    复制代码
    else {
        tc->unlock();
    }
    // 继续往下,从磁盘获取 share

8.2 从磁盘获取 TABLE_SHARE

复制代码
mysql_mutex_lock(&LOCK_open);
if (!(share = get_table_share_with_discover(...))) {
    mysql_mutex_unlock(&LOCK_open);
    if (error == 7 && !thd->is_error()) {
        ot_ctx->request_backoff_action(OT_DISCOVER, table_list);
    }
    return true;
}
  • get_table_share_with_discover 会从数据字典或旧 .frm 文件读取表定义,并创建 TABLE_SHARE

  • 如果表不存在且错误码 7,请求发现(discover)机制(用于引擎发现表)。

9.处理视图(VIEW)路径

复制代码
if (table_list->is_view() || share->is_view) {
    // 检查 parent_l(不允许 MERGE 表包含视图)
    // 版本检查
    // 如果是 OPEN_FOR_CREATE,添加视图占位符返回
    // 否则调用 open_and_read_view 和 parse_view_definition
    // 释放 share,返回
}
  • 视图不创建 TABLE 对象,而是构建 LEX 结构供优化器使用。

  • 注意 share 仍然被释放,因为视图不需要长期缓存。

10.常规表:创建新的 TABLE 对象(share_found 标签)

10.1 再次检查版本(针对旧版本 share)

复制代码
if (share->has_old_version()) {
    // 等待旧版本消失
    tdc_wait_for_old_version(...);
    goto retry_share;
}
if (thd->open_tables && thd->open_tables->s->version() != share->version()) {
    // 版本变化,要求重新打开所有表
    ot_ctx->request_backoff_action(OT_REOPEN_TABLES, nullptr);
    return true;
}
  • has_old_version 表示该 TABLE_SHARE 已被标记为过时(例如被 FLUSH TABLES 刷新),需要等待新的 share 加载。

10.2. 从 TABLE_SHARE 实例化 TABLE

复制代码
// 分配 TABLE 内存
table = (TABLE*)my_malloc(...);
// 调用 open_table_from_share 填充结构
error = open_table_from_share(thd, share, alias, ...);
// 处理错误(DISCOVER, FIX_ROW_TYPE, REPAIR)
  • open_table_from_share 会打开存储引擎文件、初始化索引等。

10.3. 将新 TABLE 加入线程的缓存

复制代码
Table_cache *tc = table_cache_manager.get_cache(thd);
tc->lock();
tc->add_used_table(thd, table);
tc->unlock();
  • 此时 TABLE 被标记为"正在使用",后续可能被其他语句复用。

11.table_found 标签:完成表对象关联

复制代码
table_found:
  table->mdl_ticket = mdl_ticket;          // 关联 MDL ticket
  table->next = thd->open_tables;          // 加入线程的 open_tables 链表
  thd->set_open_tables(table);
  table->reginfo.lock_type = TL_READ;       // 临时设为读,后续 lock_tables 会改
reset:
  table->reset();                          // 重置状态
  // 设置 table_list 为可更新、可插入
  table_list->table = table;
  // 处理分区位图
  // 调用 table->init()
  // 特殊处理 P_S 表(提前加读锁)
  return false;
  • table->mdl_ticketTABLE 对象知道它对应的 MDL 锁,以便在关闭时释放。

12.错误处理 err_lock

复制代码
err_lock:
  mysql_mutex_lock(&LOCK_open);
  release_table_share(share);
  mysql_mutex_unlock(&LOCK_open);
  return true;
  • 释放之前增加的 share 引用计数,避免泄漏。
open_table_get_mdl_lock
复制代码
11

open_table_get_mdl_lock 是 MySQL 在打开表(open_table)过程中用于获取元数据锁(MDL)的核心函数。它负责:

  • 根据调用标志(flags)和会话变量(low_priority_updates调整 原本请求的 MDL 锁类型(例如覆盖为 MDL_SHAREDMDL_SHARED_HIGH_PRIO,或将普通写锁降级为 MDL_SHARED_WRITE_LOW_PRIO)。

  • 尝试获取 MDL 锁:可以等待 (默认)或非阻塞尝试MYSQL_OPEN_FAIL_ON_MDL_CONFLICT 标志)。

  • 处理死锁:通过安装内部死锁处理器(MDL_deadlock_handler)支持回退和重试(back-off and retry)。

  • 输出获取到的 MDL 票据(mdl_ticket)供后续使用。

函数原型:

复制代码
static bool open_table_get_mdl_lock(THD *thd, Open_table_context *ot_ctx,
                                    Table_ref *table_list, uint flags,
                                    MDL_ticket **mdl_ticket)

参数:

  • thd:当前线程(会话)上下文。

  • ot_ctx:打开表上下文,用于记录回退状态、超时等。

  • table_list:表列表元素,其中 mdl_request 成员包含了原本请求的 MDL 锁(由解析器或视图展开时设定),lock_type 用于判断是否使用低优先级写锁。

  • flags:可以包含 MYSQL_OPEN_FORCE_SHARED_MDLMYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDLMYSQL_OPEN_FAIL_ON_MDL_CONFLICT 等。

  • mdl_ticket:输出参数,成功获取锁后指向 MDL 票据;否则为 NULL

返回值:

  • true:发生了错误(如死锁且无法恢复、内存错误等)。

  • false:没有错误,但可能因冲突未获得锁(需检查 mdl_ticket 是否为 NULL)。

1 获取原始 MDL 请求指针

c

复制代码
MDL_request *mdl_request = &table_list->mdl_request;
MDL_request new_mdl_request;
  • mdl_request 指向原本在 Table_ref 中已经初始化的 MDL 请求结构。

  • new_mdl_request 用于在需要覆盖锁类型时构造新的请求。

2 根据标志覆盖 MDL 锁类型(强制共享锁或高优先级共享锁)

c

复制代码
if (flags &
    (MYSQL_OPEN_FORCE_SHARED_MDL | MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL)) {
    assert(!(flags & MYSQL_OPEN_FORCE_SHARED_MDL) ||
           !(flags & MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL));

    MDL_REQUEST_INIT_BY_KEY(&new_mdl_request, &mdl_request->key,
                            (flags & MYSQL_OPEN_FORCE_SHARED_MDL)
                                ? MDL_SHARED
                                : MDL_SHARED_HIGH_PRIO,
                            MDL_TRANSACTION);
    mdl_request = &new_mdl_request;
}

场景与目的:

  • MYSQL_OPEN_FORCE_SHARED_MDL

    用于 PREPARE 语句。在解析阶段,PREPARE 可能会根据语句类型(如 SELECTUPDATE)设置类型敏感的 MDL 锁(例如 MDL_SHARED_WRITE)。但当真正执行 PREPARE 时,我们只需要一个简单的共享锁MDL_SHARED),以避免与并发的 LOCK TABLES WRITE 冲突。覆盖后可以允许 PREPARELOCK TABLES WRITE 并发执行。

  • MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL

    用于 INFORMATION_SCHEMA 查询。打开 I_S 表时,原本可能持有类型敏感的共享锁(如视图展开带来的),但为了避免不必要的等待和警告ER_WARN_I_S_SKIPPED_TABLE),改为请求高优先级共享锁(MDL_SHARED_HIGH_PRIO)。这种锁可以绕过一些等待队列,快速获得。

  • 两个标志互斥,断言保证不会同时设置。

3 低优先级更新模式(low_priority_updates)处理

c

复制代码
else if (thd->variables.low_priority_updates &&
         mdl_request->type == MDL_SHARED_WRITE &&
         (table_list->lock_descriptor().type == TL_WRITE_DEFAULT ||
          table_list->lock_descriptor().type ==
              TL_WRITE_CONCURRENT_DEFAULT)) {
    MDL_REQUEST_INIT_BY_KEY(&new_mdl_request, &mdl_request->key,
                            MDL_SHARED_WRITE_LOW_PRIO, MDL_TRANSACTION);
    mdl_request = &new_mdl_request;
}
  • 当会话变量 low_priority_updatesON 时,MySQL 会降低写操作的优先级,以避免写操作阻塞读操作。

  • 条件判断:

    • 原始请求的 MDL 类型是 MDL_SHARED_WRITE(即普通的 DML 写锁)。

    • 表锁描述符中的类型是 TL_WRITE_DEFAULTTL_WRITE_CONCURRENT_DEFAULT,表示用户没有显式指定 LOW_PRIORITYHIGH_PRIORITY

  • 满足条件时,将锁类型替换为 MDL_SHARED_WRITE_LOW_PRIO (低优先级写锁)。这种锁在与 MDL_SHARED_READ_ONLY(例如 LOCK TABLES READ)冲突时,会让写锁等待,从而实现读优先。

注意 :如果语句中显式使用了 LOW_PRIORITYHIGH_PRIORITY,则不会覆盖。

4 非阻塞获取(用于 I_S 表,避免死锁)

c

复制代码
if (flags & MYSQL_OPEN_FAIL_ON_MDL_CONFLICT) {
    if (thd->mdl_context.try_acquire_lock(mdl_request)) return true;
    if (mdl_request->ticket == nullptr) {
        my_error(ER_WARN_I_S_SKIPPED_TABLE, MYF(0), mdl_request->key.db_name(),
                 mdl_request->key.name());
        return true;
    }
}
  • 该标志通常用于 INFORMATION_SCHEMA 查询:当打开一个表用于获取 I_S 数据时,如果该表可能已经被其他会话以冲突的锁模式锁定(如 LOCK TABLES WRITE),那么等待可能导致不可被 MDL 死锁检测器捕捉的死锁(例如与 InnoDB 行锁混合的循环等待)。

  • 使用 try_acquire_lock 非阻塞尝试获取锁:

    • 如果立即获得锁,mdl_request->ticket 非空,继续。

    • 如果存在冲突,mdl_request->ticketNULL,则发出警告 ER_WARN_I_S_SKIPPED_TABLE 并跳过该表(不等待)。

    • 如果发生错误(如内存不足),返回 true

5 正常阻塞获取(支持死锁回退)

c

复制代码
else {
    MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

    thd->push_internal_handler(&mdl_deadlock_handler);
    thd->mdl_context.set_force_dml_deadlock_weight(ot_ctx->can_back_off());

    bool result =
        thd->mdl_context.acquire_lock(mdl_request, ot_ctx->get_timeout());

    thd->mdl_context.set_force_dml_deadlock_weight(false);
    thd->pop_internal_handler();

    if (result && !ot_ctx->can_recover_from_failed_open()) return true;
}

这是最常见的情况:正常打开表(如执行 SELECTINSERT 等)。

关键点:

  • 安装死锁处理器 MDL_deadlock_handler

    acquire_lock 检测到死锁时,会调用内部错误处理器。MDL_deadlock_handler 会检查是否可以从死锁中恢复(通过回退当前打开表的操作 并重试)。如果可以,它会设置 ot_ctx 的状态,并让 acquire_lock 返回失败(result = true),但错误被抑制。

  • 设置 DML 死锁权重

    set_force_dml_deadlock_weight(ot_ctx->can_back_off()) 的作用是:当 can_back_off()true 时,强制 MDL 死锁检测器将当前请求视为 DML 类型的锁(具有较低的死锁权重),从而在死锁时倾向于选择当前事务作为牺牲品(victim)。这样做的原因是:

    • 如果当前打开表操作(例如 LOCK TABLES 语句)与一个 DDL 语句发生死锁,我们更希望 LOCK TABLES 被回退并重试(对用户透明),而不是让 DDL 失败。

    • 但是在某些场景(如 FLUSH TABLES ... WITH READ LOCK)已经预先持有一些强锁,无法安全回退,此时 can_back_off() 返回 false,就不强制使用 DML 权重,从而允许 DDL 可能成为受害者。

  • 获取锁

    thd->mdl_context.acquire_lock(mdl_request, timeout) 是核心 MDL 获取函数。如果锁可用,立即返回成功(result = false);如果锁冲突,会等待直到超时或被死锁检测器中断。

  • 清理和错误处理

    恢复死锁权重标志,弹出内部处理器。

    如果 resulttrue(获取锁失败,可能是死锁或超时)且无法从失败的打开中恢复!ot_ctx->can_recover_from_failed_open()),则返回 true 表示错误。否则,如果能够恢复(例如通过回退并稍后重试),则函数返回 false,但 mdl_ticket 可能为 NULL,上层会处理重试逻辑。

6 输出 MDL 票据

c

复制代码
*mdl_ticket = mdl_request->ticket;
return false;
  • 无论通过哪种路径(非阻塞或阻塞),只要没有错误且获得了锁,mdl_request->ticket 就是有效的 MDL 票据;如果未获得锁(如冲突跳过),则为 NULL

  • 将票据返回给调用者(open_table)。

相关推荐
北冥you鱼1 小时前
Go 语言初始化 MySQL 数据库实战指南:从零到生产环境部署
数据库·mysql·golang
琢森2 小时前
服务器崩溃后,我是怎么用一条备份脚本救回全部数据的
mysql
宠友信息2 小时前
MySQL 租户数据隔离在即时通讯源码后端中的落地方式
java·数据库·spring boot·redis·websocket·mysql
wear工程师3 小时前
MySQL 索引失效别再背八股:看懂 EXPLAIN,面试官追问也不慌
mysql·面试
逆向编程4 小时前
MySQL 负载均衡完整入门实战教程
mysql·adb·负载均衡
tant1an5 小时前
MySQL面试题(一)
数据库·mysql
红糖奶茶5 小时前
MySQL 8.0的自增主键持久化特性
数据库·mysql·adb
xieliyu.7 小时前
MySQL 六大基础约束详解:NOT NULL/DEFAULT/UNIQUE/ 主键 / 外键 / CHECK
android·数据库·sql·mysql
Anokata7 小时前
MYSQL SQL 执行系列1
android·sql·mysql