MYSQL SQL 执行系列1

SQL 语句分发

dispatch_sql_command 是 MySQL 中处理单个 SQL 语句(或一条复合语句中的第一个语句)的核心函数。它位于 dispatch_command 处理 COM_QUERY 命令的路径中,负责将 SQL 文本字符串解析为抽象语法树(AST),进行必要的重写,然后调用执行器(mysql_execute_command)执行该语句。该函数也处理多语句中的第一个语句,并设置 SERVER_MORE_RESULTS_EXISTS 标志以指示还有更多结果。

cpp 复制代码
##sql_parse.cc
/*
  When you modify dispatch_sql_command(), you may need to modify
  mysql_test_parse_for_slave() in this same file.
*/

/**
  Parse an SQL command from a text string and pass the resulting AST to the
  query executor.

  @param thd          Current session.
  @param parser_state Parser state.
*/

void dispatch_sql_command(THD *thd, Parser_state *parser_state) {
  DBUG_TRACE;
  DBUG_PRINT("dispatch_sql_command", ("query: '%s'", thd->query().str));
  statement_id_to_session(thd);
  DBUG_EXECUTE_IF("parser_debug", turn_parser_debug_on(););

  mysql_reset_thd_for_next_command(thd);
  // It is possible that rewritten query may not be empty (in case of
  // multiqueries). So reset it.
  thd->reset_rewritten_query();
  lex_start(thd);

  thd->m_parser_state = parser_state;
  invoke_pre_parse_rewrite_plugins(thd);
  thd->m_parser_state = nullptr;

  // we produce digest if it's not explicitly turned off
  // by setting maximum digest length to zero
  if (get_max_digest_length() != 0)
    parser_state->m_input.m_compute_digest = true;

  LEX *lex = thd->lex;
  const char *found_semicolon = nullptr;

  bool err = thd->get_stmt_da()->is_error();
  size_t qlen = 0;

  if (!err) {
    err = parse_sql(thd, parser_state, nullptr);
    if (!err) err = invoke_post_parse_rewrite_plugins(thd, false);

    found_semicolon = parser_state->m_lip.found_semicolon;
    qlen = found_semicolon ? (found_semicolon - thd->query().str)
                           : thd->query().length;
    /*
      We set thd->query_length correctly to not log several queries, when we
      execute only first. We set it to not see the ';' otherwise it would get
      into binlog and Query_log_event::print() would give ';;' output.
    */

    if (!thd->is_error() && found_semicolon && (ulong)(qlen)) {
      thd->set_query(thd->query().str, qlen - 1);
    }
  }

  DEBUG_SYNC_C("sql_parse_before_rewrite");

  if (!err) {
    /*
      Rewrite the query for logging and for the Performance Schema
      statement tables. (Raw logging happened earlier.)

      Sub-routines of mysql_rewrite_query() should try to only rewrite when
      necessary (e.g. not do password obfuscation when query contains no
      password).

      If rewriting does not happen here, thd->m_rewritten_query is still
      empty from being reset in alloc_query().
    */
    if (thd->rewritten_query().length() == 0) mysql_rewrite_query(thd);

    if (thd->rewritten_query().length()) {
      lex->safe_to_cache_query = false;  // see comments below

      thd->set_query_for_display(thd->rewritten_query().ptr(),
                                 thd->rewritten_query().length());
    } else if (thd->slave_thread) {
      /*
        In the slave, we add the information to pfs.events_statements_history,
        but not to pfs.threads, as that is what the test suite expects.
      */
      MYSQL_SET_STATEMENT_TEXT(thd->m_statement_psi, thd->query().str,
                               thd->query().length);
    } else {
      thd->set_query_for_display(thd->query().str, thd->query().length);
    }

    if (!(opt_general_log_raw || thd->slave_thread)) {
      if (thd->rewritten_query().length())
        query_logger.general_log_write(thd, COM_QUERY,
                                       thd->rewritten_query().ptr(),
                                       thd->rewritten_query().length());
      else {
        query_logger.general_log_write(thd, COM_QUERY, thd->query().str, qlen);
      }
    }
  }

  const bool with_query_attributes = (thd->bind_parameter_values_count > 0);
  MYSQL_NOTIFY_STATEMENT_QUERY_ATTRIBUTES(thd->m_statement_psi,
                                          with_query_attributes);

  DEBUG_SYNC_C("sql_parse_after_rewrite");

  if (!err) {
    thd->m_statement_psi = MYSQL_REFINE_STATEMENT(
        thd->m_statement_psi, sql_statement_info[thd->lex->sql_command].m_key);

    if (mqh_used && thd->get_user_connect() &&
        check_mqh(thd, lex->sql_command)) {
      if (thd->is_classic_protocol())
        thd->get_protocol_classic()->get_net()->error = NET_ERROR_UNSET;
    } else {
      if (!thd->is_error()) {
        /* Actually execute the query */
        if (found_semicolon) {
          lex->safe_to_cache_query = false;
          thd->server_status |= SERVER_MORE_RESULTS_EXISTS;
        }
        lex->set_trg_event_type_for_tables();

        int error [[maybe_unused]];
        if (unlikely(
                (thd->security_context()->password_expired() ||
                 thd->security_context()->is_in_registration_sandbox_mode()) &&
                lex->sql_command != SQLCOM_SET_PASSWORD &&
                lex->sql_command != SQLCOM_ALTER_USER)) {
          if (thd->security_context()->is_in_registration_sandbox_mode())
            my_error(ER_PLUGIN_REQUIRES_REGISTRATION, MYF(0));
          else
            my_error(ER_MUST_CHANGE_PASSWORD, MYF(0));
          error = 1;
        } else {
          resourcegroups::Resource_group *src_res_grp = nullptr;
          resourcegroups::Resource_group *dest_res_grp = nullptr;
          MDL_ticket *ticket = nullptr;
          MDL_ticket *cur_ticket = nullptr;
          auto mgr_ptr = resourcegroups::Resource_group_mgr::instance();
          const bool switched = mgr_ptr->switch_resource_group_if_needed(
              thd, &src_res_grp, &dest_res_grp, &ticket, &cur_ticket);

          error = mysql_execute_command(thd, true);

          if (switched)
            mgr_ptr->restore_original_resource_group(thd, src_res_grp,
                                                     dest_res_grp);
          thd->resource_group_ctx()->m_switch_resource_group_str[0] = '\0';
          if (ticket != nullptr)
            mgr_ptr->release_shared_mdl_for_resource_group(thd, ticket);
          if (cur_ticket != nullptr)
            mgr_ptr->release_shared_mdl_for_resource_group(thd, cur_ticket);
        }
      }
    }
  } else {
    /*
      Log the failed raw query in the Performance Schema. This statement did
      not parse, so there is no way to tell if it may contain a password of not.

      The tradeoff is:
        a) If we do log the query, a user typing by accident a broken query
           containing a password will have the password exposed. This is very
           unlikely, and this behavior can be documented. Remediation is to
           use a new password when retyping the corrected query.

        b) If we do not log the query, finding broken queries in the client
           application will be much more difficult. This is much more likely.

      Considering that broken queries can typically be generated by attempts at
      SQL injection, finding the source of the SQL injection is critical, so the
      design choice is to log the query text of broken queries (a).
    */
    thd->set_query_for_display(thd->query().str, thd->query().length);

    /* Instrument this broken statement as "statement/sql/error" */
    thd->m_statement_psi = MYSQL_REFINE_STATEMENT(
        thd->m_statement_psi, sql_statement_info[SQLCOM_END].m_key);

    assert(thd->is_error());
    DBUG_PRINT("info",
               ("Command aborted. Fatal_error: %d", thd->is_fatal_error()));
  }

  THD_STAGE_INFO(thd, stage_freeing_items);
  sp_cache_enforce_limit(thd->sp_proc_cache, stored_program_cache_size);
  sp_cache_enforce_limit(thd->sp_func_cache, stored_program_cache_size);
  thd->lex->destroy();
  thd->end_statement();
  thd->cleanup_after_query();
  assert(thd->change_list.is_empty());

  DEBUG_SYNC(thd, "query_rewritten");
}

函数签名入参:

  • thd:当前线程的 THD 对象,包含所有会话状态、变量、诊断区域等。

  • parser_state:解析器状态对象,封装了词法分析输入流、解析结果、以及解析过程中的标志(如是否发现分号、是否计算 digest 等)。

初始化准备

cpp 复制代码
statement_id_to_session(thd);
mysql_reset_thd_for_next_command(thd);
thd->reset_rewritten_query();
lex_start(thd);

会话绑定语句ID

cpp 复制代码
void statement_id_to_session(THD *thd) {
  auto sysvar_tracker =
      thd->session_tracker.get_tracker(SESSION_SYSVARS_TRACKER);
  if (sysvar_tracker->is_enabled()) {
    const LEX_CSTRING cs_statement = {STRING_WITH_LEN("statement_id")};
    sysvar_tracker->mark_as_changed(thd, cs_statement);
  }
}

statement_id_to_session 函数的作用是触发会话跟踪器(Session Tracker)标记 statement_id 系统变量为"已更改",以便在后续的 OK 包中将该变量的新值发送给客户端。

该函数在 dispatch_sql_command 的最开始被调用,确保每执行一条新语句前statement_id 都被标记为已更改。当语句执行完毕(无论成功或失败),thd->send_statement_status() 会发送最终的 OK/ERROR 包。如果会话跟踪器启用,则会附带 statement_id 的新值。

MySQL 支持会话状态跟踪 功能(由 session_track_system_variables 等系统变量控制)。当客户端启用该功能后,服务器在执行一条命令(如 COM_QUERY)并返回 OK 包时,会额外携带一个状态变更信息 部分,告知客户端哪些会话变量的值发生了变化。这允许客户端(如 Connector/J)高效地同步会话状态,无需额外发送 SHOW VARIABLES 查询。

statement_id 是 MySQL 内部为每个语句生成的唯一递增标识符 (类似于 thd->statement_id_counter)。用户可以通过 SHOW SESSION STATUS LIKE 'Statement_id' 查看当前会话最后一条语句的 ID。它通常用于性能监控、错误日志关联等场景。

  • 获取系统变量跟踪器SESSION_SYSVARS_TRACKER 负责跟踪系统变量的变化。

  • 检查是否启用:如果客户端未请求跟踪系统变量(或者跟踪器未启用),直接返回,避免无用操作。

  • 标记"已更改" :如果启用,则调用 mark_as_changed,将 statement_id 记录为自上次 OK 包后已经修改的变量。这会导致在发送 OK 包时,服务器自动将该变量的当前值(从 thd->statement_id_counter 获取)包含在会话状态变更信息中。

实际意义

  • 客户端透明同步:客户端(如应用程序或连接池)可以实时获知服务器端当前执行的语句 ID,而无需额外查询。

  • 调试与诊断:开发人员可以更容易地将客户端发起的请求与服务器端的语句执行记录(如慢查询日志、performance_schema)关联起来。

  • 性能优化 :避免客户端因轮询 SHOW STATUS 而增加网络往返。

注意statement_id 并非一个真正的"系统变量",而是一个由服务器内部维护的、可以被跟踪的伪变量。会话跟踪器将其视为一个系统变量名,以便复用通用的变量跟踪机制。

重置前一个命令的状态

cpp 复制代码
void mysql_reset_thd_for_next_command(THD *thd) {
  thd->reset_for_next_command();
}

void THD::reset_for_next_command() {
  // TODO: Why on earth is this here?! We should probably fix this
  // function and move it to the proper file. /Matz
  THD *thd = this;
  DBUG_TRACE;
  assert(!thd->sp_runtime_ctx); /* not for substatements of routines */
  assert(!thd->in_sub_stmt);
  thd->reset_item_list();
  /*
    Those two lines below are theoretically unneeded as
    THD::cleanup_after_query() should take care of this already.
  */
  thd->auto_inc_intervals_in_cur_stmt_for_binlog.clear();
  thd->stmt_depends_on_first_successful_insert_id_in_prev_stmt = false;

  thd->query_start_usec_used = false;
  thd->m_is_fatal_error = false;
  thd->time_zone_used = false;
  /*
    Clear the status flag that are expected to be cleared at the
    beginning of each SQL statement.
  */
  thd->server_status &= ~SERVER_STATUS_CLEAR_SET;
  /*
    If in autocommit mode and not in a transaction, reset flag
    that identifies if a transaction has done some operations
    that cannot be safely rolled back.

    If the flag is set an warning message is printed out in
    ha_rollback_trans() saying that some tables couldn't be
    rolled back.
  */
  if (!thd->in_multi_stmt_transaction_mode()) {
    thd->get_transaction()->reset_unsafe_rollback_flags(
        Transaction_ctx::SESSION);
  }
  assert(thd->security_context() == &thd->m_main_security_ctx);
  thd->thread_specific_used = false;

  if (opt_bin_log) {
    thd->user_var_events.clear();
    thd->user_var_events_alloc = thd->mem_root;
  }
  thd->clear_error();
  thd->get_stmt_da()->reset_diagnostics_area();
  thd->get_stmt_da()->reset_statement_cond_count();

  thd->rand_used = false;
  thd->m_sent_row_count = thd->m_examined_row_count = 0;

  thd->reset_current_stmt_binlog_format_row();
  thd->binlog_unsafe_warning_flags = 0;
  thd->binlog_need_explicit_defaults_ts = false;

  thd->commit_error = THD::CE_NONE;
  thd->durability_property = HA_REGULAR_DURABILITY;
  thd->set_trans_pos(nullptr, 0);
  thd->derived_tables_processing = false;
  thd->parsing_system_view = false;

  // Need explicit setting, else demand all privileges to a table.
  thd->want_privilege = ALL_ACCESS;

  thd->reset_skip_readonly_check();
  thd->tx_commit_pending = false;

  DBUG_PRINT("debug", ("is_current_stmt_binlog_format_row(): %d",
                       thd->is_current_stmt_binlog_format_row()));

  /*
    In case we're processing multiple statements we need to checkout a new
    acl access map here as the global acl version might have increased due to
    a grant/revoke or flush.
  */
  thd->security_context()->checkout_access_maps();
#ifndef NDEBUG
  thd->set_tmp_table_seq_id(1);
#endif
}

THD::reset_for_next_command() 是 MySQL 中在每个新 SQL 语句执行前重置线程描述符(THD)状态的关键函数。它确保上一个命令遗留的各种标志、计数、缓存、错误状态等不会影响下一个命令的正确执行。

这个函数通常在 dispatch_sql_command 的最开始(紧接 statement_id_to_session 之后)被调用,也在其他需要执行新命令的地方(如存储过程内某个语句前)被调用。

  1. 重置 Item 列表:释放当前语句中创建的临时 Item 对象(表达式节点),这些对象在上一语句执行后残留,必须清除以释放内存。

    复制代码
    thd->reset_item_list();
  2. 清理二进制日志相关的语句级状态:

    复制代码
    thd->auto_inc_intervals_in_cur_stmt_for_binlog.clear();
    thd->stmt_depends_on_first_successful_insert_id_in_prev_stmt = false;
    1. auto_inc_intervals_in_cur_stmt_for_binlog 记录当前语句中使用过的 AUTO_INCREMENT 区间,用于正确复制。每个新语句需清空。
    2. 标志 stmt_depends_on_first_successful_insert_id_in_prev_stmt 用于某些复制场景(例如 INSERT...SELECT 依赖上一次 INSERT 生成的 LAST_INSERT_ID()),新语句应重置。
  3. 重置时间戳和错误标志

    复制代码
    thd->query_start_usec_used = false;
    thd->m_is_fatal_error = false;
    thd->time_zone_used = false;
    1. query_start_usec_used 指示是否已经将查询开始时间转换为微秒(用于慢日志),新语句重新开始。

    2. m_is_fatal_error 是一个严重错误标志(如内存不足),大多数语句开始前应清除(除非致命错误导致连接关闭,否则不会进入此函数)。

    3. time_zone_used 标记语句是否使用了时区转换(影响 binlog 记录),新语句重新计算。

  4. 重置服务器状态标志:SERVER_STATUS_CLEAR_SET 是一个掩码,定义了那些必须在每个语句开始时清除的状态位,例如 SERVER_STATUS_IN_TRANS(事务是否活跃)、SERVER_STATUS_DB_DROPPED 等。该语句会清除这些位,但保留其他位(如 SERVER_STATUS_AUTOCOMMIT)。

    复制代码
    thd->server_status &= ~SERVER_STATUS_CLEAR_SET;
  5. 重置不安全回滚标志(仅在自动提交模式且不在事务中时)

    复制代码
    if (!thd->in_multi_stmt_transaction_mode()) {
        thd->get_transaction()->reset_unsafe_rollback_flags(Transaction_ctx::SESSION);
    }
    1. 如果一个事务中使用了非事务性表(如 MyISAM)并发生了回滚,这些表无法回滚,因此会设置"不安全回滚"标志。该标志在事务结束后或新语句开始时(如果不在事务中)应清除,以免影响新语句。

    2. in_multi_stmt_transaction_mode() 检查是否处于多语句事务模式(显式 START TRANSACTIONBEGIN,且未提交/回滚)。

  6. 断言安全上下文为主上下文:确保当前安全上下文是 THD 的主安全上下文,而不是某个临时代理上下文。在子语句中可能切换,但顶层语句开始时应该恢复为主上下文。

    复制代码
    assert(thd->security_context() == &thd->m_main_security_ctx);
  7. 清除线程特定使用标志:该标志指示本次语句是否使用了 thread_specific 区域(某些存储引擎使用)。新语句开始时重置。

    复制代码
    thd->thread_specific_used = false;
  8. 清理用户变量事件(如果开启 binlog)

    复制代码
    if (opt_bin_log) {
        thd->user_var_events.clear();
        thd->user_var_events_alloc = thd->mem_root;
    }
    1. user_var_events 记录了语句中设置的用户变量,用于复制。每个语句需要重新记录。

    2. user_var_events_alloc 设置内存分配器为当前的 mem_root,确保事件列表在内存池中分配。

  9. 清除错误和诊断区域

    复制代码
    thd->clear_error();
    thd->get_stmt_da()->reset_diagnostics_area();
    thd->get_stmt_da()->reset_statement_cond_count();
    1. clear_error() 清除 thd->main_da 中的错误状态(错误号、错误信息)。

    2. reset_diagnostics_area() 重置诊断区域(Diagnostics_area)为 OK 状态,清空警告列表。

    3. reset_statement_cond_count() 重置语句条件计数(用于条件处理程序,如 DECLARE CONTINUE HANDLER)。

  10. 重置随机数使用标志:指示语句是否使用了 RAND() 函数。如果是,则复制时需要特殊处理(RAND() 是非确定性的)。新语句重置。

    复制代码
    thd->rand_used = false;
  11. 重置行计数器

    复制代码
    thd->m_sent_row_count = thd->m_examined_row_count = 0;
    1. m_sent_row_count:发送给客户端的行数(仅对 SELECT 类语句有效)。

    2. m_examined_row_count:在执行过程中扫描/检查的行数(用于慢查询日志)。新语句重新开始计数。

  12. 重置二进制日志格式标志

    复制代码
    thd->reset_current_stmt_binlog_format_row();
    thd->binlog_unsafe_warning_flags = 0;
    thd->binlog_need_explicit_defaults_ts = false;
    1. reset_current_stmt_binlog_format_row() 清除当前语句的"强制使用行格式"标志。

    2. binlog_unsafe_warning_flags 记录该语句可能引起的复制不安全警告,每个语句独立。

    3. binlog_need_explicit_defaults_ts 是否需要在二进制日志中显式记录时间戳默认值。

  13. 重置提交错误和持久性属性

    复制代码
    thd->commit_error = THD::CE_NONE;
    thd->durability_property = HA_REGULAR_DURABILITY;
    1. commit_error 记录最近一次提交过程中的错误类型(如 CE_NONE, CE_FLUSH_ERROR)。新语句开始时重置。

    2. durability_property 设置持久性属性(HA_REGULAR_DURABILITYHA_IGNORE_DURABILITY),新语句恢复为常规持久性。

  14. 重置二进制日志位置:清除当前事务在二进制日志中的位置指针(用于复制和 SHOW BINLOG EVENTS)。

    复制代码
    thd->set_trans_pos(nullptr, 0);
  15. 清除派生表处理和系统视图解析标志:这些标志指示是否正在处理派生表(子查询)或解析系统视图,在语句开始时需要清除。

    复制代码
    thd->derived_tables_processing = false;
    thd->parsing_system_view = false;
  16. 重置想要的权限掩码:want_privilege 用于在打开表时请求必要的权限。默认 ALL_ACCESS 表示需要所有权限,但实际上具体语句会按需降低。这里重置为"最严格"是安全的。

    复制代码
    thd->want_privilege = ALL_ACCESS;
  17. 重置只读检查和事务提交挂起标志

    复制代码
    thd->reset_skip_readonly_check();
    thd->tx_commit_pending = false;
    1. skip_readonly_check:临时绕过 read_only 系统变量的检查(用于某些内部操作)。新语句重置。

    2. tx_commit_pending:指示是否有一个提交操作正在等待完成(用于两阶段提交)。新语句重置。

  18. 重新检查访问映射(ACL):全局 ACL(访问控制列表)可能由于 GRANT、REVOKE、FLUSH PRIVILEGES 而改变。在开始新语句前,需要重新加载当前用户的权限映射,以确保权限检查使用最新的授权。

    复制代码
    thd->security_context()->checkout_access_maps();
  19. 调试:重置临时表序列 ID:仅在调试模式下,重置临时表的自增序号,便于测试。

    复制代码
    #ifndef NDEBUG
    thd->set_tmp_table_seq_id(1);
    #endif

重置重写查询缓冲区

cpp 复制代码
  /**
    Reset thd->m_rewritten_query. Protected with the LOCK_thd_query mutex.
   */
  void reset_rewritten_query() {
    if (rewritten_query().length()) {
      String empty;
      swap_rewritten_query(empty);
    }
  }

清空之前的重写查询缓冲区(用于记录经过重写后的 SQL 文本,如密码脱敏)。

初始化LEX

cpp 复制代码
/**
  Call lex_start() before every query that is to be prepared and executed.
  Because of this, it's critical not to do too many things here.  (We already
  do too much)

  The function creates a query_block and a query_block_query_expression object.
  These objects should rather be created by the parser bottom-up.
*/

bool lex_start(THD *thd) {
  DBUG_TRACE;

  LEX *lex = thd->lex;

  lex->thd = thd;
  lex->reset();
  // Initialize the cost model to be used for this query
  thd->init_cost_model();

  const bool status = lex->new_top_level_query();
  assert(lex->current_query_block() == nullptr);
  lex->m_current_query_block = lex->query_block;

  assert(lex->m_IS_table_stats.is_valid() == false);
  assert(lex->m_IS_tablespace_stats.is_valid() == false);

  return status;
}

lex_start 是 MySQL 中为每个新的 SQL 语句准备词法和语法分析环境 的关键函数。它在每次执行 SQL 语句(包括普通查询、预处理语句、存储过程中的语句等)之前被调用,负责初始化 THD 中的 LEX 对象,使其处于一个干净、可用的状态,以便解析器能够从零开始构建新的语法树。

① 绑定当前线程:将 LEX 对象的 thd 指针指向当前线程的 THD 对象。这使得在解析和优化阶段,LEX 内部的代码可以方便地访问线程状态(如系统变量、错误处理、内存分配器等)。

复制代码
lex->thd = thd

② 重置 LEX 内部状态

复制代码
lex->reset();
  • LEX 是一个巨大的结构体,存储了解析过程中产生的所有信息:查询块列表、表列表、字段列表、函数调用、排序子句、锁定模式等。

  • reset() 会清除上一个语句残留的所有数据(例如释放之前分配的内存、将指针置空、重置标志位),但保留一些持久化的成员(如存储过程缓存、预编译语句的引用等)。这一步是保证新语句不会受之前语句影响的关键。

③ 初始化成本模型

复制代码
thd->init_cost_model();
  • MySQL 的优化器使用成本模型 来估算不同执行计划的代价(CPU、I/O、内存等)。该模型依赖于系统变量(如 optimizer_cpu_cost_multiplieroptimizer_io_cost_multiplier)以及存储引擎的统计数据。

  • 每个新语句需要重新初始化成本模型,因为会话中的某些成本相关变量可能在语句之间被修改(例如通过 SET 命令)。

④ 创建新的顶级查询块和查询表达式对象

复制代码
const bool status = lex->new_top_level_query();
/**
  Given a LEX object, create a query expression object
  (query_block_query_expression) and a query block object (query_block).

  @return false if successful, true if error
*/

bool LEX::new_top_level_query() {
  DBUG_TRACE;

  // Assure that the LEX does not contain any query expression already
  assert(unit == nullptr && query_block == nullptr);

  // Check for the special situation when using INTO OUTFILE and LOAD DATA.
  assert(result == nullptr);

  query_block = new_query(nullptr);
  if (query_block == nullptr) return true; /* purecov: inspected */

  unit = query_block->master_query_expression();

  return false;
}
  • new_top_level_query()LEX 类的一个成员函数,它创建:

    • 一个新的 Query_block 对象(表示一个 SELECT 或类似操作的基本块)

    • 一个新的 Query_expression 对象(表示一个完整的查询表达式,可能由多个 Query_block 通过 UNION 组合而成)

  • 在 MySQL 的语法树中,最外层总是有一个 Query_expression,其中包含至少一个 Query_block。该函数将这些对象链接起来,并将 lex->query_block 指针指向新创建的 Query_block,将 lex->query_expression 指向新创建的 Query_expression

  • 返回值 status 通常为 false(表示成功),除非内存分配失败。

⑤ 设置当前查询块指针

复制代码
lex->m_current_query_block = lex->query_block;
  • 解析器在处理过程中需要知道当前正在构建的是哪个查询块(例如在解析 SELECT 的子查询时,可能会切换到新的 Query_block)。m_current_query_block 用于跟踪当前活动的查询块,初始时指向顶层的 Query_block

断言检查:

复制代码
assert(lex->m_IS_table_stats.is_valid() == false);
assert(lex->m_IS_tablespace_stats.is_valid() == false);
  • 这些断言用于调试,确保在语句开始前没有残留的 INFORMATION_SCHEMA 表或表空间的统计信息缓存。IS_table_statsIS_tablespace_stats 是内部用于优化 INFORMATION_SCHEMA 查询的结构,每个语句应该从一个干净的状态开始。
new_top_level_query
cpp 复制代码
bool LEX::new_top_level_query() {
  DBUG_TRACE;

  // Assure that the LEX does not contain any query expression already
  assert(unit == nullptr && query_block == nullptr);

  // Check for the special situation when using INTO OUTFILE and LOAD DATA.
  assert(result == nullptr);

  // 创建新的顶级查询块
  query_block = new_query(nullptr);
  if (query_block == nullptr) return true; /* purecov: inspected */

  // 获取该查询块所属的查询表达式
  unit = query_block->master_query_expression();

  return false;
}

LEX::new_top_level_querylex_start 中的作用是:为当前语句创建一个全新的顶级查询表达式(Query_expression)和查询块(Query_block),并初始化 LEX 中的 unitquery_block 指针,为接下来的 SQL 解析提供干净的容器。

new_query
cpp 复制代码
/**
  Create new query_block_query_expression and query_block objects for a query
  block, which can be either a top-level query or a subquery. For the second and
  subsequent query block of a UNION query, use LEX::new_set_operation_query()
  instead.
  Set the new query_block as the current query_block of the LEX object.

  @param curr_query_block    current query block, NULL if an outer-most
                             query block should be created.

  @return new query specification if successful, NULL if error
*/
Query_block *LEX::new_query(Query_block *curr_query_block) {
  DBUG_TRACE;

  Name_resolution_context *outer_context = current_context();

  const enum_parsing_context parsing_place =
      curr_query_block != nullptr ? curr_query_block->parsing_place : CTX_NONE;

  Query_expression *const new_query_expression = create_query_expr_and_block(
      thd, curr_query_block, nullptr, nullptr, parsing_place);
  if (new_query_expression == nullptr) return nullptr;
  Query_block *const new_query_block =
      new_query_expression->first_query_block();

  if (new_query_block->set_context(nullptr)) return nullptr;
  /*
    Assume that a subquery has an outer name resolution context
    (even a non-lateral derived table may have outer references).
    When we come here for a view, it's when we parse the view (in
    open_tables()): we parse it as a standalone query, where parsing_place
    is CTX_NONE, so the outer context is set to nullptr. Then we'll resolve the
    view's query (thus, using no outer context). Later we may merge the
    view's query, but that happens after resolution, so there's no chance that
    a view "looks outside" (uses outer references). An assertion in
    resolve_derived() checks this.
  */
  if (parsing_place == CTX_NONE)  // Outer-most query block
  {
  } else if (parsing_place == CTX_INSERT_UPDATE &&
             curr_query_block->master_query_expression()->is_set_operation()) {
    /*
      Outer references are not allowed for
      subqueries in INSERT ... ON DUPLICATE KEY UPDATE clauses,
      when the outer query expression is a UNION.
    */
    assert(new_query_block->context.outer_context == nullptr);
  } else {
    new_query_block->context.outer_context = outer_context;
  }
  /*
    in subquery is SELECT query and we allow resolution of names in SELECT
    list
  */
  new_query_block->context.resolve_in_select_list = true;
  DBUG_PRINT("outer_field", ("ctx %p <-> SL# %d", &new_query_block->context,
                             new_query_block->select_number));

  return new_query_block;
}

LEX::new_query 是 MySQL 解析器中用于创建一个新的查询块(Query_block)及其所属的查询表达式(Query_expression 的核心函数。它在解析器遇到 SELECT 子句(顶层查询或子查询)时被调用,为当前语句构建新的语法树节点。

函数作用

  • 创建一个新的 Query_expression 对象 :代表一个查询表达式(可以包含一个或多个 Query_block,例如 SELECT ... UNION SELECT ...)。

  • 创建一个新的 Query_block 对象 :作为该 Query_expression 中的第一个(且通常唯一)查询块。

  • 将该查询块设置为 LEX 对象的当前查询块thd->lex->current_query_block()),后续解析出的表、列、条件等会添加到这个查询块中。

  • 建立名称解析上下文 :根据当前解析位置(顶层还是子查询),设置新查询块的 outer_context(外部名称解析上下文),以便在解析时能够引用外部查询中的列(子查询相关)。

  • 不用于 UNION 中后续的查询块 :对于 UNION 中的第二、第三个 SELECT,应使用 LEX::new_set_operation_query 而不是这个函数。

参数含义

  • curr_query_block:当前正在解析的查询块(即调用此函数时的"外层"查询块)。

    • 如果为 nullptr,表示正在创建最外层的顶级查询块。

    • 如果非 nullptr,表示正在创建一个子查询(例如出现在 WHERE 条件中的 SELECT),此时该参数指向包含这个子查询的查询块。

返回值 :成功则返回新创建的 Query_block 指针;失败返回 nullptr(例如内存分配失败)。

  1. 获取当前名称解析上下文和解析位置
复制代码
Name_resolution_context *outer_context = current_context();
const enum_parsing_context parsing_place =
    curr_query_block != nullptr ? curr_query_block->parsing_place : CTX_NONE;
  • current_context() 返回当前活动的名称解析上下文(通常是 curr_query_block 的上下文,如果存在的话)。

  • parsing_place 表示当前正在解析的 SQL 语句位置(如 CTX_NONE 表示顶层,CTX_SELECT 表示在 SELECT 子句中,CTX_INSERT_UPDATE 表示在 ON DUPLICATE KEY UPDATE 子句中),这个信息会传递给新创建的查询块,用于后续检查某些语法限制(例如子查询不能含有 GROUP BY 等)。

  1. 创建 Query_expressionQuery_block 对象
复制代码
Query_expression *const new_query_expression = create_query_expr_and_block(
    thd, curr_query_block, nullptr, nullptr, parsing_place);
if (new_query_expression == nullptr) return nullptr;
Query_block *const new_query_block = new_query_expression->first_query_block();
  • create_query_expr_and_block 是一个内部辅助函数,它会:

    • 分配新的 Query_expression 对象。

    • 分配新的 Query_block 对象,并将其链接到 Query_expression 中。

    • 初始化两个对象的基本属性(如 select_numberparsing_place 等)。

    • 如果 curr_query_block 非空,则将新的查询块添加到其所属查询表达式的子查询列表中(用于后续优化)。

  • 新创建的 Query_expression 的第一个(也是唯一的)查询块就是我们要的 Query_block

  1. 设置名称解析上下文
复制代码
if (new_query_block->set_context(nullptr)) return nullptr;
复制代码
if (parsing_place == CTX_NONE)  // Outer-most query block
{
  // 不做额外操作,外层上下文保持 nullptr
}
else if (parsing_place == CTX_INSERT_UPDATE &&
         curr_query_block->master_query_expression()->is_set_operation()) {
  // 如果当前处于 INSERT ... ON DUPLICATE KEY UPDATE 子句,且外层查询表达式是 UNION,
  // 则不允许子查询引用外部列,因此保持 outer_context 为 nullptr。
  assert(new_query_block->context.outer_context == nullptr);
}
else {
  new_query_block->context.outer_context = outer_context;
}
  • set_context(nullptr) 会初始化 new_query_blockName_resolution_context 成员,分配必要的内存。

  • 对于最外层查询块parsing_place == CTX_NONE),它的 outer_context 保持为空,表示没有外层查询可以引用。

  • 对于子查询块 (例如出现在 WHERE 条件中的 SELECT),通常需要能够引用外层查询的列,因此将当前上下文(outer_context)设置为新查询块的 outer_context。这样,在新查询块中解析列名时,如果在本块中找不到,就会递归地到外层上下文中查找。

  • 有一个特殊例外:当子查询出现在 INSERT ... ON DUPLICATE KEY UPDATE 子句中,并且外层查询表达式是 UNION 时,MySQL 不允许该子查询引用外层列(语法限制),因此不设置外层上下文。

  1. 设置解析时允许在 SELECT 列表中解析名称
复制代码
new_query_block->context.resolve_in_select_list = true;
  • 这个标志表示在解析该查询块时,如果出现一个未限定的列名(例如 SELECT id 中的 id),可以在当前查询块的 SELECT 列表中找到对应的表达式(例如 SELECT id 本身就是一个新定义的列)。这主要用于解析 SELECT 列表中的别名或索引。
  1. 调试打印(可选)
复制代码
DBUG_PRINT("outer_field", ("ctx %p <-> SL# %d", &new_query_block->context,
                           new_query_block->select_number));
  • 仅在调试版本中打印上下文地址和查询块编号。
  1. 返回新查询块
复制代码
return new_query_block;

new_query 与 new_set_operation_query 的区别

  • new_query :创建一个全新的 Query_expression 和其第一个 Query_block。适用于:

    • 顶层查询

    • 子查询

    • UNION 中的第一个查询块

  • new_set_operation_query :在已有的 Query_expression 中添加新的 Query_block(用于 UNIONINTERSECTEXCEPT 中的后续查询块)。它不会创建新的 Query_expression,而是将新的查询块添加到当前查询表达式的链表末尾。

cpp 复制代码
Query_expression *LEX::create_query_expr_and_block(
    THD *thd, Query_block *current_query_block, Item *where, Item *having,
    enum_parsing_context ctx) {
  if (current_query_block != nullptr &&
      current_query_block->nest_level >= MAX_SELECT_NESTING) {
    my_error(ER_TOO_HIGH_LEVEL_OF_NESTING_FOR_SELECT, MYF(0));
    return nullptr;
  }

  auto *const new_expression = new (thd->mem_root) Query_expression(ctx);
  if (new_expression == nullptr) return nullptr;

  auto *const new_query_block =
      new (thd->mem_root) Query_block(thd->mem_root, where, having);
  if (new_query_block == nullptr) return nullptr;

  // Link the new query expression below the current query block, if any
  if (current_query_block != nullptr)
    new_expression->include_down(this, current_query_block);

  new_query_block->include_down(this, new_expression);

  new_query_block->parent_lex = this;
  new_query_block->include_in_global(&this->all_query_blocks_list);
  // Set root query_term a priori. It is usually set later by
  // Parse_context::finalize_query_expression. This is necessary since it
  // doesn't always happen during server bootstrap of the dictionary, e.g. in
  // View_metadata_updater_context which creates a top level query whose
  // Query_expression/Query_block is not parsed/contextualized, so we never
  // call finalize_query_expression in the usual way, after contextualization
  // of PT_query_expression.
  new_expression->set_query_term(new_query_block);
  return new_expression;
}

LEX::create_query_expr_and_block 是 MySQL 解析器中创建查询表达式(Query_expression)和查询块(Query_block)对象 的底层实现函数。它负责在内存中分配并初始化这两个对象,建立它们之间的父子关系,并将新查询块加入到 LEX 的全局列表中。该函数通常被 LEX::new_query 调用(用于创建顶级查询或子查询),也被用于某些内部操作(如解析视图定义)。

参数含义

  • thd:当前线程的 THD 对象,用于内存分配。

  • current_query_block:当前正在解析的"外层"查询块。如果为 nullptr,表示正在创建最外层(顶级)查询表达式;如果非空,表示正在创建一个子查询(该子查询属于 current_query_block)。

  • where:可能用于初始化查询块的 WHERE 条件表达式(通常为 nullptr,因为条件是在解析过程中逐步添加的)。这个参数在普通 SELECT 解析中并不使用,但在某些递归调用或内部辅助解析时可能会传入预构建的条件。

  • having:类似 where,用于初始化 HAVING 子句。

  • ctx:枚举 enum_parsing_context,表示当前解析的上下文(例如 CTX_NONE 表示顶层,CTX_SELECT 表示在 SELECT 子句中,CTX_INSERT_UPDATE 表示在 ON DUPLICATE KEY UPDATE 中等)。该值会保存在新创建的 Query_expression 中,用于后续语法检查(例如某些上下文不允许子查询包含某些子句)。

返回值

  • 成功时返回新分配的 Query_expression 指针。

  • 失败(如内存不足或超过嵌套限制)时返回 nullptr,并设置相应的错误信息。

  1. 检查嵌套层级限制
复制代码
if (current_query_block != nullptr &&
    current_query_block->nest_level >= MAX_SELECT_NESTING) {
    my_error(ER_TOO_HIGH_LEVEL_OF_NESTING_FOR_SELECT, MYF(0));
    return nullptr;
}
  • MAX_SELECT_NESTING 通常定义为 63。该宏限制了子查询的最大嵌套深度,防止恶意 SQL 导致栈溢出或性能问题。

  • 如果当前外层查询块的 nest_level 已经达到或超过限制,则拒绝创建更深层的子查询,返回错误。

  1. 创建 Query_expression 对象
复制代码
auto *const new_expression = new (thd->mem_root) Query_expression(ctx);
if (new_expression == nullptr) return nullptr;
  • 使用 thd->mem_root 内存分配器(每个 THD 的内存池)分配一个 Query_expression 对象,并传入 ctx 作为构造参数。

  • Query_expression 代表一个查询表达式,可以是单个 SELECT 或由 UNIONINTERSECTEXCEPT 组合的复合查询。它的 ctx 值被记录,用于后续语义检查(例如限制某些上下文中不允许使用 UNION)。

  1. 创建 Query_block 对象
复制代码
auto *const new_query_block = new (thd->mem_root) Query_block(thd->mem_root, where, having);
if (new_query_block == nullptr) return nullptr;
  • 同样在 mem_root 上分配一个 Query_block 对象,传入两个表达式参数(通常为 nullptr)。Query_block 代表一个具体的查询块(SELECT 的核心)。

  • Query_block 内部会保存指向所属 Query_expression 的指针、表列表、SELECT 列表、条件树等成员。

  1. 将新查询表达式链接到外层查询块(如果存在)
复制代码
if (current_query_block != nullptr)
    new_expression->include_down(this, current_query_block);
  • include_downQuery_expression 的一个方法,用于在语法树中建立父子关系。该调用将新创建的表达式 new_expression 注册为 current_query_block 的子查询。

  • 具体来说,它会将 new_expression 添加到当前查询块的 child_units 列表中,并设置 new_expression->m_parent_query_blockcurrent_query_block。这为后续优化和执行阶段递归处理子查询提供了基础。

  1. 将新查询块链接到新查询表达式
复制代码
new_query_block->include_down(this, new_expression);
  • include_downQuery_block 的方法,用于将自身加入 Query_expression 的查询块链表中。一个 Query_expression 可以包含多个 Query_block(例如 UNION),这里第一个(也是目前唯一的一个)查询块被添加进去。

  • 同时会设置 new_query_block->master_query_expression() 指向 new_expression

  1. 设置 parent_lex 并加入全局列表
复制代码
new_query_block->parent_lex = this;
new_query_block->include_in_global(&this->all_query_blocks_list);
  • parent_lex 指向包含此查询块的 LEX 对象(即 this)。在解析多语句或存储过程时,LEX 可能被复用,这个指针用于快速找到所属的语法上下文。

  • include_in_global 将新查询块添加到 LEX 的全局链表 all_query_blocks_list 中,以便遍历所有查询块(例如用于清理、打印或调试)。

  1. 设置查询表达式的根查询项(Query term)
复制代码
new_expression->set_query_term(new_query_block);
  • set_query_termQuery_expression 的根查询项(query_term())设置为这个新查询块。对于非 UNION 的简单查询,根查询项就是唯一的 Query_block。对于复合查询,根查询项可能是 Query_expression 内部的特殊节点(如 Query_term_union),但在当前函数中只适用于单块查询,所以直接指向 new_query_block
  1. 返回新查询表达式
复制代码
return new_expression;
  • 最终返回新创建的 Query_expression 对象,调用者(通常是 new_query)可以从中提取出 Query_block 并完成进一步的设置(如名称解析上下文)。

create_query_expr_and_block 是一个纯创建函数 ,它只负责在内存中构建两个结构并建立基本链接。但它不设置名称解析上下文outer_context),也不将新查询块设置为 LEX 的当前查询块。这些工作由上层包装函数 LEX::new_query 完成:

调用预解析重写插件

cpp 复制代码
thd->m_parser_state = parser_state;
invoke_pre_parse_rewrite_plugins(thd);
thd->m_parser_state = nullptr;
  • 允许查询重写插件在 SQL 解析之前修改 SQL 文本或中止解析。例如,某些安全或审计插件可能希望在解析前进行检查。

  • m_parser_state 临时设置为当前的 parser_state,以便插件能够访问输入流。

cpp 复制代码
void invoke_pre_parse_rewrite_plugins(THD *thd) {
  Diagnostics_area *plugin_da = thd->get_query_rewrite_plugin_da();
  if (plugin_da == nullptr) return;
  plugin_da->reset_diagnostics_area();
  plugin_da->reset_condition_info(thd);

  Diagnostics_area *da = thd->get_parser_da();
  thd->push_diagnostics_area(plugin_da, false);
  mysql_event_tracking_parse_rewrite_plugin_flag flags =
      EVENT_TRACKING_PARSE_REWRITE_NONE;
  mysql_cstring_with_length rewritten_query = {nullptr, 0};
  mysql_event_tracking_parse_notify(thd,
                                    AUDIT_EVENT(EVENT_TRACKING_PARSE_PREPARSE),
                                    &flags, &rewritten_query);

  /* Do not continue when the plugin set the error state. */
  if (!plugin_da->is_error() &&
      flags & EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN) {
    // It is a rewrite fulltext plugin and we need a rewrite we must have
    // generated a new query then.
    assert(rewritten_query.str != nullptr && rewritten_query.length > 0);
    raise_query_rewritten_note(thd, thd->query().str, rewritten_query.str);
    alloc_query(thd, rewritten_query.str, rewritten_query.length);
    thd->m_parser_state->init(thd, thd->query().str, thd->query().length);
    my_free(const_cast<char *>(rewritten_query.str));
  }

  da->copy_non_errors_from_da(thd, plugin_da);
  thd->pop_diagnostics_area();

  if (plugin_da->is_error()) {
    thd->get_stmt_da()->set_error_status(plugin_da->mysql_errno(),
                                         plugin_da->message_text(),
                                         plugin_da->returned_sqlstate());
    plugin_da->reset_diagnostics_area();
  }
}

invoke_pre_parse_rewrite_plugins 是 MySQL 中实现查询重写(Query Rewrite)插件机制的关键函数。它允许在 SQL 语句正式解析之前,通过插件动态修改原始查询文本,从而实现诸如 SQL 改写、错误纠正、安全过滤等功能。

  1. 准备插件专用的诊断区域

    • 获取 thd->get_query_rewrite_plugin_da(),这个诊断区域专门用于插件返回错误或警告。

    • 清空该诊断区域,重置条件信息。

  2. 临时替换当前诊断区域

    • 调用 thd->push_diagnostics_area(plugin_da, false) 将插件的诊断区域推入栈中,后续插件产生的错误会记录在此区域中,而不是主诊断区域。
  3. 通知插件进行预处理

    • 调用 mysql_event_tracking_parse_notify(thd, AUDIT_EVENT(EVENT_TRACKING_PARSE_PREPARSE), &flags, &rewritten_query)

    • 此函数会遍历所有注册的预解析重写插件,每个插件可以:

      • 修改 flags(例如设置 EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN 表示重写了查询)。

      • rewritten_query 中返回新的查询字符串(如果重写)。

  4. 如果查询被重写

    • 检查 flags 是否包含 EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN,且插件返回了非空的 rewritten_query

    • 发出 ER_QUERY_REWRITTEN 提示(raise_query_rewritten_note),记录原查询和重写后查询。

    • 调用 alloc_query(thd, rewritten_query.str, rewritten_query.length):释放原查询字符串,分配新查询并存入 thd->query()

    • 重新初始化 thd->m_parser_state->init(thd, ...),使词法分析器使用新的查询文本。

    • 释放插件返回的字符串内存(my_free)。

  5. 恢复诊断区域并处理错误

    • 将插件诊断区域中的非错误信息(警告等)复制回之前的诊断区域(da->copy_non_errors_from_da)。

    • 弹出插件诊断区域(thd->pop_diagnostics_area())。

    • 如果插件诊断区域中发生了错误,将该错误设置到主语句诊断区域(thd->get_stmt_da()->set_error_status),并清空插件诊断区域。

启用摘要计算(Digest)

cpp 复制代码
if (get_max_digest_length() != 0)
    parser_state->m_input.m_compute_digest = true;
  • 摘要(digest)是 SQL 语句规范化后的哈希,用于 performance_schema 中的语句统计和慢查询日志中的样本记录。

  • 如果全局变量 max_digest_length 非零(表示启用 digest 收集),则通知词法分析器在解析过程中计算 digest。

解析 SQL

cpp 复制代码
err = parse_sql(thd, parser_state, nullptr);
if (!err) err = invoke_post_parse_rewrite_plugins(thd, false);
  • parse_sql 是实际调用词法/语法分析器的函数,它会填充 thd->lex 结构,建立语法树。如果解析失败(语法错误),errtrue,并在诊断区域设置错误。

  • 解析成功后,调用后解析重写插件(例如,某些重写规则可能基于 AST 修改 LEX 结构)。

SQL 解析

cpp 复制代码
bool parse_sql(THD *thd, Parser_state *parser_state,
               Object_creation_ctx *creation_ctx) {
  DBUG_TRACE;
  bool ret_value;
  assert(thd->m_parser_state == nullptr);
  // TODO fix to allow parsing gcol exprs after main query.
  //  assert(thd->lex->m_sql_cmd == NULL);

  /* Backup creation context. */

  Object_creation_ctx *backup_ctx = nullptr;

  if (creation_ctx) backup_ctx = creation_ctx->set_n_backup(thd);

  /* Set parser state. */

  thd->m_parser_state = parser_state;

  parser_state->m_digest_psi = nullptr;
  parser_state->m_lip.m_digest = nullptr;

  /*
    Partial parsers (GRAMMAR_SELECTOR_*) are not supposed to compute digests.
  */
  assert(!parser_state->m_lip.is_partial_parser() ||
         !parser_state->m_input.m_has_digest);

  /*
    Only consider statements that are supposed to have a digest,
    like top level queries.
  */
  if (parser_state->m_input.m_has_digest) {
    /*
      For these statements,
      see if the digest computation is required.
    */
    if (thd->m_digest != nullptr) {
      /* Start Digest */
      parser_state->m_digest_psi = MYSQL_DIGEST_START(thd->m_statement_psi);

      if (parser_state->m_input.m_compute_digest ||
          (parser_state->m_digest_psi != nullptr)) {
        /*
          If either:
          - the caller wants to compute a digest
          - the performance schema wants to compute a digest
          set the digest listener in the lexer.
        */
        parser_state->m_lip.m_digest = thd->m_digest;
        parser_state->m_lip.m_digest->m_digest_storage.m_charset_number =
            thd->charset()->number;
      }
    }
  }

  /* Parse the query. */

  /*
    Use a temporary DA while parsing. We don't know until after parsing
    whether the current command is a diagnostic statement, in which case
    we'll need to have the previous DA around to answer questions about it.
  */
  Diagnostics_area *parser_da = thd->get_parser_da();
  Diagnostics_area *da = thd->get_stmt_da();

  Parser_oom_handler poomh;
  // Note that we may be called recursively here, on INFORMATION_SCHEMA queries.

  thd->mem_root->set_max_capacity(thd->variables.parser_max_mem_size);
  thd->mem_root->set_error_for_capacity_exceeded(true);
  thd->push_internal_handler(&poomh);

  thd->push_diagnostics_area(parser_da, false);

  const bool mysql_parse_status = thd->sql_parser();

  thd->pop_internal_handler();
  thd->mem_root->set_max_capacity(0);
  thd->mem_root->set_error_for_capacity_exceeded(false);
  /*
    Unwind diagnostics area.

    If any issues occurred during parsing, they will become
    the sole conditions for the current statement.

    Otherwise, if we have a diagnostic statement on our hands,
    we'll preserve the previous diagnostics area here so we
    can answer questions about it.  This specifically means
    that repeatedly asking about a DA won't clear it.

    Otherwise, it's a regular command with no issues during
    parsing, so we'll just clear the DA in preparation for
    the processing of this command.
  */

  if (parser_da->current_statement_cond_count() != 0) {
    /*
      Error/warning during parsing: top DA should contain parse error(s)!  Any
      pre-existing conditions will be replaced. The exception is diagnostics
      statements, in which case we wish to keep the errors so they can be sent
      to the client.
    */
    if (thd->lex->sql_command != SQLCOM_SHOW_WARNS &&
        thd->lex->sql_command != SQLCOM_GET_DIAGNOSTICS)
      da->reset_condition_info(thd);

    /*
      We need to put any errors in the DA as well as the condition list.
    */
    if (parser_da->is_error() && !da->is_error()) {
      da->set_error_status(parser_da->mysql_errno(), parser_da->message_text(),
                           parser_da->returned_sqlstate());
    }

    da->copy_sql_conditions_from_da(thd, parser_da);

    parser_da->reset_diagnostics_area();
    parser_da->reset_condition_info(thd);

    /*
      Do not clear the condition list when starting execution as it
      now contains not the results of the previous executions, but
      a non-zero number of errors/warnings thrown during parsing!
    */
    thd->lex->keep_diagnostics = DA_KEEP_PARSE_ERROR;
  }

  thd->pop_diagnostics_area();

  /*
    Check that if THD::sql_parser() failed either thd->is_error() is set, or an
    internal error handler is set.

    The assert will not catch a situation where parsing fails without an
    error reported if an error handler exists. The problem is that the
    error handler might have intercepted the error, so thd->is_error() is
    not set. However, there is no way to be 100% sure here (the error
    handler might be for other errors than parsing one).
  */

  assert(!mysql_parse_status || (mysql_parse_status && thd->is_error()) ||
         (mysql_parse_status && thd->get_internal_handler()));

  /* Reset parser state. */

  thd->m_parser_state = nullptr;

  /* Restore creation context. */

  if (creation_ctx) creation_ctx->restore_env(thd, backup_ctx);

  /* That's it. */

  ret_value = mysql_parse_status;

  if ((ret_value == 0) && (parser_state->m_digest_psi != nullptr)) {
    /*
      On parsing success, record the digest in the performance schema.
    */
    assert(thd->m_digest != nullptr);
    MYSQL_DIGEST_END(parser_state->m_digest_psi,
                     &thd->m_digest->m_digest_storage);
  }

  return ret_value;
}

parse_sql 是 MySQL 中将 SQL 文本字符串转换为**抽象语法树(AST)**的核心函数。它封装了词法分析(Lexer)和语法分析(Parser)的完整过程,并负责管理解析过程中的上下文、摘要(Digest)以及性能监控。

  • 输入 :一个 SQL 字符串(位于 parser_state->m_input 中),以及可选的对象创建上下文creation_ctx,用于确定创建存储过程/函数等对象的默认字符集、数据库等)。

  • 输出 :填充 thd->lex 结构(语法树),并可能生成语句摘要(Digest)存储在 thd->m_digest 中。

  • 返回值false 表示解析成功,true 表示解析失败(语法错误或内存不足等)。

parse_sqldispatch_sql_command 中完成 SQL 解析的关键步骤,位于 invoke_pre_parse_rewrite_plugins 之后、invoke_post_parse_rewrite_plugins 之前。

参数详解:

|----------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数 | 类型 | 作用 |
| thd | THD * | 当前线程描述符,包含会话状态、系统变量、内存分配器、LEX 对象等。 |
| parser_state | Parser_state * | 封装了解析器所需的所有状态: * m_input:原始 SQL 字符串、长度、是否计算 digest 等标志。 * m_lip:词法输入流(Lex_input_stream),负责从字符串中逐个读取字符、识别 token。 * m_digest_psi:用于性能模式的 digest 事件句柄。 * found_semicolon:解析过程中发现的分号位置(用于多语句分割)。 |
| creation_ctx | Object_creation_ctx * | * 当解析的 SQL 语句是 CREATE PROCEDURE/FUNCTION/TRIGGER/EVENT 时,需要知道创建对象的默认数据库、字符集等。 * 该参数允许调用者临时覆盖当前线程的创建上下文,确保存储过程/函数的内容在正确的环境下解析(例如,CREATE PROCEDURE db.p() 中的过程体应该以 db 为默认数据库,而不是当前线程的 db)。 * 如果为 nullptr,表示使用当前线程已有的上下文。 |

详细步骤:

  1. 断言与初始化
复制代码
assert(thd->m_parser_state == nullptr);
  • 断言 thd->m_parser_state 当前为 nullptr,确保当前线程没有残留的解析器状态(parse_sql 不支持重入,但在某些特殊场景如 INFORMATION_SCHEMA 查询中会递归调用,函数后面的注释提到了这一点,但此断言确保顶层调用干净)。

2.备份并临时设置创建上下文

复制代码
Object_creation_ctx *backup_ctx = nullptr;
if (creation_ctx) backup_ctx = creation_ctx->set_n_backup(thd);
  • Object_creation_ctx 用于定义创建存储对象(过程、函数、触发器、事件)时的默认数据库、字符集等环境。

  • 当解析 CREATE PROCEDURE 等语句时,过程体应该使用存储过程自己的默认数据库(通常是 db_name.proc_name 中的 db_name),而不是当前会话的默认数据库。

  • set_n_backup 方法会将当前线程的创建上下文替换为 creation_ctx,并返回旧的上下文以便后续恢复。

3.设置解析器状态

  • parser_state 存入 THD 中,这样词法/语法分析器中的代码(如 MYSQLlex)就能通过 thd->m_parser_state 访问输入流和标志。

  • 清空 parser_state 中摘要相关的字段,因为每个语句都需要独立的摘要会话。

4.摘要(Digest)初始化

复制代码
if (parser_state->m_input.m_has_digest) {
    if (thd->m_digest != nullptr) {
        parser_state->m_digest_psi = MYSQL_DIGEST_START(thd->m_statement_psi);
        if (parser_state->m_input.m_compute_digest ||
            (parser_state->m_digest_psi != nullptr)) {
            parser_state->m_lip.m_digest = thd->m_digest;
            parser_state->m_lip.m_digest->m_digest_storage.m_charset_number =
                thd->charset()->number;
        }
    }
}
  • m_has_digesttrue 表示这是一个需要生成摘要的顶层语句(例如 COM_QUERY 中的 SQL)。

  • 如果上层已经分配了 thd->m_digest(在 dispatch_sql_command 中设置),则通过 MYSQL_DIGEST_START 通知 Performance Schema 开始记录。

  • 如果调用者要求计算摘要(m_compute_digest)或者 Performance Schema 需要(m_digest_psi 非空),则将 thd->m_digest 挂到词法分析器的 m_lip.m_digest 上,这样每次识别一个 token 时就能记录其信息,最终构建出语句的规范化摘要。

  • 同时设置摘要存储的字符集号,用于正确的大小写转换和字符串比较。

5.准备诊断区域和内存处理程序

复制代码
Diagnostics_area *parser_da = thd->get_parser_da();
Diagnostics_area *da = thd->get_stmt_da();
Parser_oom_handler poomh;
thd->mem_root->set_max_capacity(thd->variables.parser_max_mem_size);
thd->mem_root->set_error_for_capacity_exceeded(true);
thd->push_internal_handler(&poomh);
thd->push_diagnostics_area(parser_da, false);
  • parser_da :专门用于解析过程的诊断区域。因为解析后可能需要区分"解析错误"与"执行错误",并且诊断语句(如 SHOW WARNINGS)需要保留之前的诊断信息,所以使用独立的 DA 来收集解析期间的警告/错误。

  • da :当前语句的主诊断区域(通常指向 thd->m_stmt_da)。

  • 内存超限处理

    • 设置当前内存根(mem_root)的最大容量为 parser_max_mem_size(系统变量,限制解析器可分配的内存)。

    • set_error_for_capacity_exceeded(true) 表示当超出容量时自动触发内存错误(而不是静默失败)。

    • Parser_oom_handler 是一个内部错误处理程序,用于在内存耗尽时进行清理并设置合适的错误消息。

    • push_internal_handler(&poomh) 将该处理程序压入栈,以便在解析过程中捕获 OOM 异常。

  • 诊断区域压栈 :将 parser_da 推入诊断区域栈,并设置 false 表示不强制覆盖已有的错误。后续解析过程中产生的所有警告/错误都会记录到 parser_da 中,而不会影响 da

6.执行真正的解析

复制代码
const bool mysql_parse_status = thd->sql_parser();
  • thd->sql_parser() 是一个包装函数,最终调用 Bison 生成的 yyparse(thd)。该函数会使用当前设置的 thd->m_parser_state 中的词法分析器逐词读取 token,并根据语法规则创建语法树节点填充到 thd->lex 中。

  • 如果解析过程中遇到语法错误或内存不足,会通过 my_error 在诊断区域中设置错误,并且函数返回 true

7.恢复内存限制并弹出内部处理程序

复制代码
thd->pop_internal_handler();
thd->mem_root->set_max_capacity(0);
thd->mem_root->set_error_for_capacity_exceeded(false);
  • 解析完成后立即移除 OOM 处理程序,恢复内存根的容量限制为 0(表示无限制),并关闭容量超限报错。这样可以避免后续执行阶段受到解析器内存限制的影响。

8.处理解析过程中产生的诊断信息,这个段落处理解析过程中产生的警告/错误 ,并将其转移到主诊断区域(da):

复制代码
if (parser_da->current_statement_cond_count() != 0) {
    if (thd->lex->sql_command != SQLCOM_SHOW_WARNS &&
        thd->lex->sql_command != SQLCOM_GET_DIAGNOSTICS)
        da->reset_condition_info(thd);
    if (parser_da->is_error() && !da->is_error()) {
        da->set_error_status(parser_da->mysql_errno(), parser_da->message_text(),
                             parser_da->returned_sqlstate());
    }
    da->copy_sql_conditions_from_da(thd, parser_da);
    parser_da->reset_diagnostics_area();
    parser_da->reset_condition_info(thd);
    thd->lex->keep_diagnostics = DA_KEEP_PARSE_ERROR;
}
thd->pop_diagnostics_area();
  • 条件计数非零:表示解析过程中至少有一个警告或错误。

  • 对诊断语句的特殊处理 :如果当前命令是 SHOW WARNINGSGET DIAGNOSTICS,则不清空 主诊断区域的现有条件,因为这些命令需要读取上一条语句的诊断信息。否则,先重置 da 的条件列表。

  • 转移错误状态 :如果 parser_da 中有错误(例如语法错误),且 da 中没有错误,则将错误信息复制到 da

  • 转移所有条件 :将 parser_da 中的所有警告/错误条件复制到 da 中(copy_sql_conditions_from_da)。

  • 清空 parser_da:重置该专用诊断区域,以便下次使用。

  • 设置标志lex->keep_diagnostics = DA_KEEP_PARSE_ERROR 指示后续执行阶段不应该清除这些解析期间产生的条件(因为它们现在已经是当前语句的合法诊断信息)。

  • 弹出诊断区域 :将 parser_da 从诊断区域栈中移除,恢复之前的状态。

9.解析失败后的断言检查

复制代码
assert(!mysql_parse_status || (mysql_parse_status && thd->is_error()) ||
       (mysql_parse_status && thd->get_internal_handler()));
  • 如果解析失败(mysql_parse_statustrue),则要么 thd->is_error()true(错误已记录),要么有一个内部错误处理程序(例如 OOM 处理程序)已经处理了错误。这是一个防御性断言,确保错误状态一致。

10.重置解析器状态并恢复创建上下文

复制代码
thd->m_parser_state = nullptr;
if (creation_ctx) creation_ctx->restore_env(thd, backup_ctx);
  • 解除 thd->m_parser_state 的引用,避免误用。

  • 如果之前切换了创建上下文,则恢复回原来的上下文(backup_ctx)。

11.处理摘要结束

复制代码
ret_value = mysql_parse_status;
if ((ret_value == 0) && (parser_state->m_digest_psi != nullptr)) {
    assert(thd->m_digest != nullptr);
    MYSQL_DIGEST_END(parser_state->m_digest_psi,
                     &thd->m_digest->m_digest_storage);
}
return ret_value;
  • 如果解析成功(ret_value == 0)并且摘要记录已经启动(m_digest_psi 非空),则调用 MYSQL_DIGEST_END 完成摘要记录,将最终生成的摘要存储到 thd->m_digest->m_digest_storage 中。Performance Schema 随后可以使用该摘要进行语句聚合统计。

  • 返回解析状态。

真正执行解析的是THD对象的sql_parser函数,起具体实现如下:

cpp 复制代码
/**
  Call parser to transform statement into a parse tree.
  Then, transform the parse tree further into an AST, ready for resolving.
*/
bool THD::sql_parser() {
  /*
    SQL parser function generated by YACC from sql_yacc.yy.

    In the case of success returns 0, and THD::is_error() is false.
    Otherwise returns 1, or THD::>is_error() is true.

    The second (output) parameter "root" returns the new parse tree.
    It is undefined (unchanged) on error. If "root" is NULL on success,
    then the parser has already called lex->make_sql_cmd() internally.
  */
  extern int my_sql_parser_parse(class THD * thd,
                                 class Parse_tree_root * *root);

  Parse_tree_root *root = nullptr;
  if (my_sql_parser_parse(this, &root) || is_error()) {
    /*
      Restore the original LEX if it was replaced when parsing
      a stored procedure. We must ensure that a parsing error
      does not leave any side effects in the THD.
    */
    cleanup_after_parse_error();
    return true;
  }
  if (root != nullptr && lex->make_sql_cmd(root)) {
    return true;
  }
  return false;
}



  Uses parse_tree to instantiate an Sql_cmd object and assigns it to the Lex.

  @param parse_tree The parse tree.

  @returns false on success, true on error.
*/
bool LEX::make_sql_cmd(Parse_tree_root *parse_tree) {
  if (!will_contextualize) return false;

  m_sql_cmd = parse_tree->make_cmd(thd);
  if (m_sql_cmd == nullptr) return true;

  assert(m_sql_cmd->sql_command_code() == sql_command);

  return false;
}

当用户执行一条 SQL 命令(如 COM_QUERY)时,dispatch_command 会获取完整的 SQL 字符串,并调用 dispatch_sql_command。该函数会初始化解析状态(Parser_state),然后调用 parse_sqlparse_sql 准备工作完成后,会调用 THD::sql_parser,最终触发 my_sql_parser_parse 执行核心的语法分析工作。

MySQL 使用 Bison 作为语法分析器的生成器。sql_yacc.yy 文件包含了 上下文无关文法 的规则、语义动作(用 C++ 代码片段表示)、以及一些预处理指令。Bison 读取这个文件,生成两个主要文件(通常名为 sql_yacc.ccsql_yacc.h):

  • sql_yacc.cc :包含 my_sql_parser_parse 函数的实现,以及所有语法规则对应的状态转换表、移位/归约处理代码。

  • sql_yacc.h :包含 token 编号的定义(如 SELECT_SYMINSERT_SYM 等)以及 YYSTYPE 等类型定义。

sql_yacc.yy 文件的开头,你可以看到:

cpp 复制代码
%define api.prefix {my_sql_parser_}

这条指令告诉 Bison:生成的函数名、token 前缀等都使用 my_sql_parser_ 作为前缀。因此,默认的 yyparse 会被命名为 my_sql_parser_parse;默认的 yylex 会被命名为 my_sql_parser_lex;token 常量也会以 my_sql_parser_ 开头(但在 MySQL 中,token 常量的实际定义位置有所不同,这里不做展开)。

在生成的 sql_yacc.cc 中,函数声明大致如下:

复制代码
int my_sql_parser_parse(
    class THD *YYTHD,
    class Parse_tree_root **parse_tree
);
  • YYTHD:当前线程的句柄(THD 对象),包含了词法分析器状态、内存环境、客户端协议等信息。

  • parse_tree:输出参数,指向解析成功后的语法树根节点(Parse_tree_root 的子类对象)。

这对应于 sql_yacc.yy 中的 %parse-param 指令:

复制代码
%parse-param { class THD *YYTHD }
%parse-param { class Parse_tree_root **parse_tree }

同时,词法分析器(my_sql_parser_lex)的调用也通过 %lex-param 传递了 THD

复制代码
%lex-param { class THD *YYTHD }

这样,解析器在每次需要获取下一个 token 时,都会调用 my_sql_parser_lex(&yylval, &yylloc, YYTHD),其中 yylval 用于存储 token 的语义值(如数字、字符串、关键字代码等),yylloc 用于记录位置信息。

my_sql_parser_parse 内部结构概览

Bison 生成的解析器是一个 基于 LALR(1) 算法的确定性下推自动机。生成的代码主要包含:

  1. 状态转移表yydefactyydefgotoyypgotoyypactyypgoto 等数组),用于根据当前状态和当前 token 决定是移位(shift)还是归约(reduce)。

  2. 归约动作代码 :每个产生式对应的语义动作被翻译成 C++ 代码块,插入到 case 分支中。

  3. 栈管理:使用两个栈(状态栈和值栈)来模拟下推自动机。

  4. 错误恢复机制 :通过 yyerrlab 等标签处理语法错误,并尝试通过 yyclearinyyerrok 等机制恢复。

简化后的执行流程:

cpp 复制代码
int my_sql_parser_parse(THD *YYTHD, Parse_tree_root **parse_tree) {
    // 初始化栈、状态等
    yystate = 0;
    yyvsp = yyss;
    yyval = ...;
    for (;;) {
        // 根据当前状态 yystate 判断是否需要读入下一个 token
        if (yynext) {
            yychar = my_sql_parser_lex(&yylval, &yylloc, YYTHD);
        }
        // 查表:yydefact[yystate] 等决定动作
        if (shift) {
            // 将 token 和状态压栈,更新 yystate
        } else if (reduce) {
            // 根据规则号执行对应的语义动作(在 switch 中)
            // 弹出右部符号,压入左部符号,更新 yystate
        } else if (accept) {
            return 0;
        } else {
            // 语法错误处理
        }
    }
}

语义动作与抽象语法树(AST)的构建

sql_yacc.yy 中,每个产生式后面都跟有花括号包围的 C++ 代码,这些代码就是语义动作。例如,对于 select_stmt 产生式:

cpp 复制代码
select_stmt:
          query_expression
          {
            $$ = NEW_PTN PT_select_stmt(@$, $1);
          }
        | query_expression locking_clause_list
        ...

这些动作会创建 Parse_tree_node 的子类对象(如 PT_select_stmt),并将它们通过 $$ 向上传递。最终整个 SQL 语句的根节点通过 %parse-param 中的 parse_tree 参数返回给调用者。

Bison 生成的代码将每个产生式对应的动作代码复制到 case 分支中,并使用 YYSTYPE 类型的临时变量 yyval 作为 $$。例如:

cpp 复制代码
case 123:
    // 对应产生式: select_stmt -> query_expression
    (yyval.node) = NEW_PTN PT_select_stmt(@$, (yyvsp[0].node));
    break;

与词法分析器的交互

词法分析器由 sql_lex.cc 中的 MYSQLlex(实际是 my_sql_parser_lex)实现。当解析器需要下一个 token 时,它调用:

cpp 复制代码
yychar = my_sql_parser_lex(&yylval, &yylloc, YYTHD);
  • yylval 是一个联合体(或 C++ 变体类型),用于存储 token 的语义值。例如,当识别到 IDENT 时,yylval 中会填充一个 LEX_STRING;当识别到 NUM 时,会填充一个数字字符串。

  • yylloc 用于记录 token 在原始 SQL 文本中的起始和结束位置,用于错误报告和某些语句的重写。

  • YYTHD 包含了词法分析器需要的状态(如当前正在扫描的 SQL 字符串指针、字符集转换标志等)。实际上,my_sql_parser_lex 内部会调用 YYLIP->yylex() 来实际扫描字符。

MySQL 的词法分析器是手工编写的(不是 Flex 生成),位于 sql/sql_lex.cc 中的 MYSQLlex 函数。为了与 Bison 生成的解析器配合,MySQL 在 sql_lex.h 中定义了 YY_DECL 宏,将 yylex 映射到 my_sql_parser_lex

错误报告与 syntax_error 机制

当解析器遇到不符合语法的 token 时,Bison 生成的代码会调用用户定义的错误报告函数。在 sql_yacc.yy 中,通过 %error-verboseyyerror 函数定义来指定。MySQL 定义了 my_sql_parser_error 函数(在你的文件末尾附近),该函数根据错误信息调用 thd->syntax_error_at(location) 等方法来生成具体的错误消息。

cpp 复制代码
static void my_sql_parser_error(YYLTYPE *location,
                                THD *thd, Parse_tree_root **,
                                const char *s) {
    if (strcmp(s, "syntax error") == 0) {
        thd->syntax_error_at(*location);
    } else if (strcmp(s, "memory exhausted") == 0) {
        my_error(ER_DA_OOM, MYF(0));
    } else {
        // ...
    }
}

这个函数会在解析器内部通过 yyerror 回调被调用(在 Bison 生成的代码中,当遇到无法处理的 token 且未进行错误恢复时会调用它)。

SQL 重写

cpp 复制代码
bool invoke_post_parse_rewrite_plugins(THD *thd, bool is_prepared) {
  Diagnostics_area *plugin_da = thd->get_query_rewrite_plugin_da();
  plugin_da->reset_diagnostics_area();
  plugin_da->reset_condition_info(thd);

  Diagnostics_area *stmt_da = thd->get_stmt_da();

  /*
    We save the value of keep_diagnostics here as it gets reset by
    push_diagnostics_area(), see below for use.
  */
  bool keeping_diagnostics = thd->lex->keep_diagnostics == DA_KEEP_PARSE_ERROR;

  thd->push_diagnostics_area(plugin_da, false);

  {
    /*
       We have to call a function in rules_table_service.cc, or the service
       won't be visible to plugins.
    */
#ifndef NDEBUG
    int dummy =
#endif
        rules_table_service::
            dummy_function_to_ensure_we_are_linked_into_the_server();
    assert(dummy == 1);

#ifndef NDEBUG
    dummy =
#endif
        ssl_wrapper_service::
            dummy_function_to_ensure_we_are_linked_into_the_server();
    assert(dummy == 1);
  }

  mysql_event_tracking_parse_rewrite_plugin_flag flags =
      is_prepared ? EVENT_TRACKING_PARSE_REWRITE_IS_PREPARED_STATEMENT
                  : EVENT_TRACKING_PARSE_REWRITE_NONE;
  bool err = false;
  const char *original_query = thd->query().str;
  mysql_event_tracking_parse_notify(
      thd, AUDIT_EVENT(EVENT_TRACKING_PARSE_POSTPARSE), &flags, nullptr);

  if (flags & EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN) {
    raise_query_rewritten_note(thd, original_query, thd->query().str);
    thd->lex->safe_to_cache_query = false;
  }

  if (plugin_da->current_statement_cond_count() != 0) {
    /*
      A plugin raised at least one condition. At this point these are in the
      plugin DA, and we should copy them to the statement DA. But before we do
      that, we may have to clear it as this DA may contain conditions from the
      previous statement. We have to clear it *unless* the statement is a
      diagnostics statement, in which case we keep everything: conditions from
      previous statements, parser conditions and plugin conditions. If this is
      not a diagnostics statement, parse_sql() has already cleared the
      statement DA, copied the parser conditions to the statement DA and set
      DA_KEEP_PARSE_ERROR. So we arrive at the below condition for telling us
      when to clear the statement DA.
    */
    if (thd->lex->sql_command != SQLCOM_SHOW_WARNS && !keeping_diagnostics)
      stmt_da->reset_condition_info(thd);

    /* We need to put any errors in the DA as well as the condition list. */
    if (plugin_da->is_error())
      stmt_da->set_error_status(plugin_da->mysql_errno(),
                                plugin_da->message_text(),
                                plugin_da->returned_sqlstate());

    stmt_da->copy_sql_conditions_from_da(thd, plugin_da);

    /*
      Do not clear the condition list when starting execution as it now
      contains not the results of the previous executions, but a non-zero
      number of errors/warnings thrown during parsing or plugin execution.
    */
    thd->lex->keep_diagnostics = DA_KEEP_PARSE_ERROR;
  }

  thd->pop_diagnostics_area();

  return err;
}

invoke_post_parse_rewrite_plugins 是 MySQL 中查询重写插件 框架的一部分,它在 SQL 解析完成之后、执行之前 被调用,允许插件对已经生成的语法树(LEX 结构)进行修改或执行其他附加操作。与预解析重写插件(invoke_pre_parse_rewrite_plugins)不同的是,后解析重写插件操作的是已经构建好的抽象语法树(AST),而不是原始文本。

  • 参数

    • thd:当前线程的 THD 对象。

    • is_prepared:指示当前正在处理的语句是否是预处理语句(COM_STMT_PREPARECOM_STMT_EXECUTE 路径),该信息会通过 flags 传递给插件。

  • 返回值 :总是 false(当前实现中 err 初始化为 false 且从未被修改为 true),但未来可能扩展为表示插件是否要求中止执行。

调用时机

  • dispatch_sql_command 中,成功调用 parse_sql 解析 SQL 并生成语法树之后,在 invoke_post_parse_rewrite_plugins 被调用。

  • 对于预处理语句,is_prepared 会设置为 true,使得插件能够区分普通查询和 PREPARE 语句。

  1. 准备插件专用的诊断区域
复制代码
Diagnostics_area *plugin_da = thd->get_query_rewrite_plugin_da();
plugin_da->reset_diagnostics_area();
plugin_da->reset_condition_info(thd);
  • 获取预先分配好的、用于查询重写插件的诊断区域(每个 THD 一个)。

  • 清空该区域,确保插件产生的警告/错误不会与之前的残留混淆。

  1. 保存"是否保留解析错误"标志

bool keeping_diagnostics = thd->lex->keep_diagnostics == DA_KEEP_PARSE_ERROR;

  • lex->keep_diagnostics 标志指示当前语句是否应该保留解析阶段产生的诊断信息(例如,对于 SHOW WARNINGS 语句,需要保留上一条语句的条件列表)。该标志在 parse_sql 中可能被设置为 DA_KEEP_PARSE_ERROR

  • 在后续处理插件产生的条件时,需要参考该标志来决定是否清空主诊断区域的条件。

  1. 临时切换诊断区域
复制代码
thd->push_diagnostics_area(plugin_da, false);
  • plugin_da 推入诊断区域栈,后续插件产生的任何警告/错误都会记录到 plugin_da 中,而不是主诊断区域(stmt_da)。

  • 参数 false 表示如果插件 DA 中已经有错误,不会强制覆盖。

  1. 确保服务函数被链接(仅调试)
复制代码
#ifndef NDEBUG
    int dummy = rules_table_service::dummy_function_to_ensure_we_are_linked_into_the_server();
    assert(dummy == 1);
    dummy = ssl_wrapper_service::dummy_function_to_ensure_we_are_linked_into_the_server();
    assert(dummy == 1);
#endif
  • 这是一个调试技巧:调用某些服务中的空函数,确保链接器将这些服务的目标文件包含进最终二进制,否则插件可能因为找不到符号而无法加载。在生产版本中会被优化掉。
  1. 通知插件后解析事件
复制代码
mysql_event_tracking_parse_rewrite_plugin_flag flags =
    is_prepared ? EVENT_TRACKING_PARSE_REWRITE_IS_PREPARED_STATEMENT
                : EVENT_TRACKING_PARSE_REWRITE_NONE;
mysql_event_tracking_parse_notify(
    thd, AUDIT_EVENT(EVENT_TRACKING_PARSE_POSTPARSE), &flags, nullptr);
  • mysql_event_tracking_parse_notify 是一个插件通知接口,它会遍历所有已注册的查询重写插件,并调用它们的回调函数。

  • 事件类型为 EVENT_TRACKING_PARSE_POSTPARSE,表示解析已完成。

  • 输入输出参数 flags:插件可以修改该标志,例如设置 EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN 来表示查询已被重写。

  • 最后一个参数 nullptr 是一个保留的 rewritten_query 输出,对于后解析阶段,插件通常直接修改 thd->lex 而不是返回新文本,所以不需要输出新查询字符串。

  • is_prepared 作为入参传递给插件,使得插件可以对 PREPARE 语句(准备阶段)和执行阶段(EXECUTE)采取不同的行为。例如,插件可能希望在准备阶段就对语句结构进行改写,而不是每次执行时重复。

  1. 处理"查询重写"标志
复制代码
if (flags & EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN) {
    raise_query_rewritten_note(thd, original_query, thd->query().str);
    thd->lex->safe_to_cache_query = false;
}
  • 如果插件在回调中设置了"已重写"标志,则:

    • 调用 raise_query_rewritten_note 发出一个 ER_QUERY_REWRITTEN 警告(或信息),记录原查询和当前查询字符串(注意:当前 thd->query() 可能已经被插件在更早的阶段修改过,或者在重写时插件没有修改文本而是修改了语法树,所以这里只是作为日志记录)。

    • 设置 lex->safe_to_cache_query = false,表示该语句的结果不可缓存(因为重写可能导致查询语义变化,或者重写包含非确定性元素)。

  1. 处理插件产生的诊断条件
复制代码
if (plugin_da->current_statement_cond_count() != 0) {
    if (thd->lex->sql_command != SQLCOM_SHOW_WARNS && !keeping_diagnostics)
        stmt_da->reset_condition_info(thd);
    if (plugin_da->is_error())
        stmt_da->set_error_status(...);
    stmt_da->copy_sql_conditions_from_da(thd, plugin_da);
    thd->lex->keep_diagnostics = DA_KEEP_PARSE_ERROR;
}
  • 如果插件产生了一个或多个警告/错误(条件计数非零):

    • 决定是否需要清空主诊断区域 (stmt_da) 的条件列表。清空的条件是:当前不是 SHOW WARNINGS 语句,并且 keeping_diagnosticsfalse(即没有要求保留解析错误)。这是因为 SHOW WARNINGS 需要保留之前的条件,而普通语句应该用新的条件替换旧条件。

    • 如果插件 DA 中有一个错误状态,则将该错误复制到主 DA 中。

    • 将插件 DA 中的所有条件(警告、错误等)复制到主 DA 中。

    • 设置 lex->keep_diagnostics = DA_KEEP_PARSE_ERROR,指示后续执行阶段不要清除这些诊断信息(因为现在它们已经是当前语句有效的一部分)。

  1. 恢复诊断区域
复制代码
hd->pop_diagnostics_area();
return false;
  • 弹出插件专用的诊断区域,恢复之前的诊断区域(通常是主语句诊断区域)。

  • 函数返回 false 表示执行成功(当前设计中没有让插件中止执行的机制,但未来可能通过返回值或 flags 扩展)。

查询重写插件可以在两个阶段工作:

  • 预解析:修改原始 SQL 文本,影响词法/语法分析。

  • 后解析 :修改已经生成的语法树(例如添加、删除或修改 SELECT 列表、WHERE 条件、表名等),或进行额外的语义检查。

invoke_post_parse_rewrite_plugins 提供了后解析阶段的钩子,插件可以通过注册的回调函数访问 thd->lex,并对其进行任意修改。这为实现以下功能提供了基础:

  • 自动添加审计列 :例如在 SELECT 语句中自动追加 user_id 过滤条件。

  • 行级安全:根据当前用户动态修改查询条件。

  • 查询优化辅助:为特定查询模式添加索引提示。

  • 流量控制:根据语句类型或内容决定是否允许执行。

处理分号和多语句

cpp 复制代码
found_semicolon = parser_state->m_lip.found_semicolon;
qlen = found_semicolon ? (found_semicolon - thd->query().str) : thd->query().length;
if (!thd->is_error() && found_semicolon && (ulong)(qlen)) {
    thd->set_query(thd->query().str, qlen - 1);
}
  • 词法分析器会记录在输入流中发现第一个分号的位置(found_semicolon)。如果存在分号,说明客户端发送了多条 SQL 语句(例如 SELECT 1; SELECT 2)。

  • qlen 计算第一个语句的长度(从原始查询开始到分号的位置)。然后,将 thd->query() 截断为第一个语句的长度(不包含分号),以便后续只处理第一条语句。注意:原始查询字符串本身不会被修改,只是改变了 THD 中记录的查询长度。

  • 这样做是为了让 binlog 和通用日志只记录实际执行的语句,而不包含多余的分号和空白。

查询重写(用于日志和 Performance Schema)

cpp 复制代码
if (thd->rewritten_query().length() == 0) mysql_rewrite_query(thd);
  • mysql_rewrite_query 会检查是否需要重写原始查询。最常见的用途是密码脱敏 :将 CREATE USERSET PASSWORD 语句中的密码字面量替换为 '********',以避免明文密码出现在日志中。

  • 如果重写发生,重写后的查询会保存在 thd->rewritten_query() 中,并设置 lex->safe_to_cache_query = false(因为重写后的语句可能不适合缓存,例如包含变化的密码)。

  • 然后,将用于显示的查询(thd->set_query_for_display)设置为重写后的版本;否则使用原始查询。

cpp 复制代码
/**
  Provides the default interface to rewrite the SQL statements to
  to obfuscate passwords.

  The query aimed to be rewritten in the usual log files
  (i.e. General, slow query and audit log) uses default value of
  type which is Consumer_type::TEXTLOG

   Side-effects:

   - thd->m_rewritten_query will contain a rewritten query,
     or be cleared if no rewriting took place.
     LOCK_thd_query will be temporarily acquired to make that change.

   @note Keep in mind that these side-effects will only happen when
         calling this top-level function, but not when calling
         individual sub-functions directly!

  @param thd        The THD to rewrite for.
  @param type       Purpose of rewriting the query
                     Consumer_type::TEXTLOG
                      To rewrite the query either for general, slow query
                      and audit log.
                     Consumer_type::BINLOG
                      To rewrite the query for binlogs.
                     Consumer_type::STDOUT
                      To rewrite the query for standard output.
  @param params     Wrapper object of parameters in case needed by a SQL
                      rewriter.
*/
void mysql_rewrite_query(THD *thd, Consumer_type type,
                         const Rewrite_params *params) {
  String rlb;

  DBUG_TRACE;
  assert(thd);

  // We should not come through here twice for the same query.
  assert(thd->rewritten_query().length() == 0);

  if (thd->lex->contains_plaintext_password ||
      thd->lex->is_rewrite_required()) {
    rewrite_query(thd, type, params, rlb);
    if (rlb.length() > 0) thd->swap_rewritten_query(rlb);
    // The previous rewritten query is in rlb now, which now goes out of scope.
  }
}

mysql_rewrite_query 是 MySQL 中用于重写 SQL 语句以隐藏明文密码 的核心函数。它在将查询写入通用日志、慢查询日志或审计日志之前被调用,将诸如 CREATE USER ... IDENTIFIED BY 'secret' 中的密码替换为 '********',防止敏感信息泄露。

  • thd :当前线程的 THD 对象,包含语句上下文、LEX 结构、重写后的查询缓冲区等。

  • type :指定重写查询的目的(消费类型)。定义于 enum Consumer_type

    • TEXTLOG:用于文本日志(通用日志、慢查询日志、审计日志),这是最常见的用途。

    • BINLOG:用于二进制日志(某些场景下需要混淆密码)。

    • STDOUT:用于标准输出(例如 mysql 客户端程序的 --verbose 输出)。

  • params:一个包装器对象,用于传递重写所需的额外参数(如某些密码重写策略需要知道目标存储引擎等),当前版本中可能未使用或仅用于扩展。

  1. 断言与前置检查
复制代码
assert(thd);
assert(thd->rewritten_query().length() == 0);
  • 确保 thd 有效。

  • 确保当前语句还没有被重写过(rewritten_query 长度为 0)。这防止了同一个语句被重复重写(例如,由于某些路径错误地多次调用该函数)。

  1. 判断是否需要重写
复制代码
if (thd->lex->contains_plaintext_password ||
    thd->lex->is_rewrite_required()) {
    // 执行重写
}
  • contains_plaintext_password:在词法/语法分析阶段,如果解析器检测到 SQL 语句中包含明文密码(例如 IDENTIFIED BY 'password' 子句),这个标志会被设置为 true

  • is_rewrite_required():这是一个更通用的标志,指示该语句需要某种形式的重写(可能不仅仅是密码混淆)。例如,某些插件可能要求对所有语句进行重写,或者语句含有需要转义的特殊字符。

如果任一条件为真,则调用 rewrite_query 执行实际的重写操作。

  1. 执行重写
复制代码
String rlb;
rewrite_query(thd, type, params, rlb);
  • rewrite_query 是一个内部函数,根据 typethd->lex 中的信息生成重写后的查询字符串,存入 rlb

  • 具体重写规则由 type 决定:

    • 对于 TEXTLOG:将 IDENTIFIED BY 'plaintext' 替换为 IDENTIFIED BY '********';同时可能处理 SET PASSWORD = 'plaintext'ALTER USER 等语句中的密码部分。

    • 对于 BINLOG:可能需要更细致的处理,确保复制的一致性,但通常也会混淆密码。

    • 对于 STDOUT:通常与 TEXTLOG 类似。

  1. 保存重写后的查询
复制代码
if (rlb.length() > 0) thd->swap_rewritten_query(rlb);
  • 如果重写生成了非空字符串,则调用 swap_rewritten_querythd->m_rewritten_queryrlb 交换。这样 thd 就持有了重写后的查询,而原来的 rlb 被清空(或持有旧值),随后 rlb 超出作用域自动释放。

  • thd->m_rewritten_query 是一个 String 对象,专门用于存储重写后的查询文本,供日志模块使用。

mysql_rewrite_querydispatch_sql_command 中被调用,位于 解析成功之后、执行之前,具体位置:

cpp 复制代码
// 解析成功
if (thd->rewritten_query().length() == 0) 
    mysql_rewrite_query(thd);

if (thd->rewritten_query().length()) {
    thd->set_query_for_display(thd->rewritten_query().ptr(),
                               thd->rewritten_query().length());
} else {
    thd->set_query_for_display(thd->query().str, thd->query().length());
}
// 然后记录通用日志
  • 如果重写成功,thd->rewritten_query() 非空,则后续的通用日志写入(query_logger.general_log_write)会使用重写后的查询;否则使用原始查询。

  • 同时,thd->set_query_for_display 也会使用重写后的版本,供 SHOW PROCESSLISTperformance_schema 等展示使用。

注意事项

  • 该函数仅重写包含明文密码 的语句,对于使用哈希值(如 IDENTIFIED WITH mysql_native_password AS 'hash')的语句不会重写,因为哈希值不算敏感信息(虽然也应小心)。

  • 如果某些存储过程或预处理语句中动态构建包含密码的 SQL,重写机制可能无法捕获,因为这些语句可能在执行阶段才拼接字符串,解析器无法提前标记 contains_plaintext_password。对于这类情况,需要依赖其他安全措施(如避免在日志中记录动态 SQL)。

  • 在 MySQL 8.0 中,默认情况下通用日志和慢查询日志可能默认关闭,开启时才会调用此函数。

记录通用日志

cpp 复制代码
if (!(opt_general_log_raw || thd->slave_thread)) {
    if (thd->rewritten_query().length())
        query_logger.general_log_write(thd, COM_QUERY,
                                       thd->rewritten_query().ptr(),
                                       thd->rewritten_query().length());
    else {
        query_logger.general_log_write(thd, COM_QUERY, thd->query().str, qlen);
    }
}
  • 除非设置了 opt_general_log_raw(记录原始未修改的查询)或者当前是复制线程,否则通用日志会写入重写后的查询(如果存在)或第一个语句的文本。

  • 这确保了密码信息不会出现在日志中。

语句类型细化与资源检查

cpp 复制代码
thd->m_statement_psi = MYSQL_REFINE_STATEMENT(
    thd->m_statement_psi, sql_statement_info[thd->lex->sql_command].m_key);
  • 根据解析得到的 sql_command(如 SQLCOM_SELECT, SQLCOM_INSERT 等),细化 P_S 语句事件的类型,便于更精确的统计。
cpp 复制代码
if (mqh_used && thd->get_user_connect() &&
    check_mqh(thd, lex->sql_command)) {
    // 如果超过了资源限制(max_questions, max_updates 等),则跳过执行
}
  • 检查用户资源限制(如果启用 max_user_connections 或每个小时的查询数限制)。

执行前密码过期检查

cpp 复制代码
if (unlikely(
    (thd->security_context()->password_expired() ||
     thd->security_context()->is_in_registration_sandbox_mode()) &&
    lex->sql_command != SQLCOM_SET_PASSWORD &&
    lex->sql_command != SQLCOM_ALTER_USER)) {
    // 报错 ER_MUST_CHANGE_PASSWORD 或 ER_PLUGIN_REQUIRES_REGISTRATION
}
  • 如果用户密码已过期,只允许执行 SET PASSWORDALTER USER 语句更改密码;其他语句会被拒绝。同样,对于插件注册沙盒模式,也只允许注册相关的操作。

资源组切换(如果必要)

cpp 复制代码
const bool switched = mgr_ptr->switch_resource_group_if_needed(
    thd, &src_res_grp, &dest_res_grp, &ticket, &cur_ticket);
  • 根据语句类型或会话设置,可能需要在执行前切换到不同的资源组(CPU 核心、IO 优先级等)。执行完毕后,通过 restore_original_resource_group 恢复。

核心执行:mysql_execute_command

见系列2文章。

清理与结束

cpp 复制代码
thd->lex->destroy();
thd->end_statement();
thd->cleanup_after_query();
  • lex->destroy():释放语法树中分配的内存(表列表、字段列表等),但保留 LEX 对象本身供下次复用。

  • thd->end_statement():关闭打开的游标、发送最后一个结果集的 EOF 包(如果有)、更新 slow log 状态等。

  • cleanup_after_query():关闭语句中打开的表、释放 MDL 锁(但不是事务锁)、重置临时表等。

相关推荐
REDcker2 小时前
Android 15 16KB 内存页适配详解
android·开发语言·python
Kapaseker2 小时前
Android 开放这扇门,正在被 Google 从里面反锁
android·kotlin
Java成神之路-2 小时前
彻底分清:垂直分库、水平分库、垂直分表、水平分表(附 Mycat 完整配置)
mysql
帅次2 小时前
Android 高级工程师面试:网络与数据层 近1年高频追问 18 题
android·网络·okhttp·面试·retrofit·gilde
迷枫7122 小时前
简单sql与dm功能实验记录
数据库·sql·oracle
三少爷的鞋3 小时前
Android 架构这些年,其实一直在消灭碎片化异步
android
zhanghaofaowhrql3 小时前
Spring Data JPA Repository 详解:从基础到高级用法
java·数据库·sql
techdashen4 小时前
Uber 如何让 MySQL Group Replication 在大规模生产环境中可用:自动化、故障转移与一致性保障
数据库·mysql·自动化
木木子228 小时前
# 密码强度检测深度解析:正则表达式实时分析、多维度评分算法与可视化反馈
mysql·算法·华为·正则表达式·harmonyos