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 之后)被调用,也在其他需要执行新命令的地方(如存储过程内某个语句前)被调用。
-
重置 Item 列表:释放当前语句中创建的临时
Item对象(表达式节点),这些对象在上一语句执行后残留,必须清除以释放内存。thd->reset_item_list(); -
清理二进制日志相关的语句级状态:
thd->auto_inc_intervals_in_cur_stmt_for_binlog.clear(); thd->stmt_depends_on_first_successful_insert_id_in_prev_stmt = false;- auto_inc_intervals_in_cur_stmt_for_binlog 记录当前语句中使用过的 AUTO_INCREMENT 区间,用于正确复制。每个新语句需清空。
- 标志 stmt_depends_on_first_successful_insert_id_in_prev_stmt 用于某些复制场景(例如 INSERT...SELECT 依赖上一次 INSERT 生成的 LAST_INSERT_ID()),新语句应重置。
-
重置时间戳和错误标志
thd->query_start_usec_used = false; thd->m_is_fatal_error = false; thd->time_zone_used = false;-
query_start_usec_used指示是否已经将查询开始时间转换为微秒(用于慢日志),新语句重新开始。 -
m_is_fatal_error是一个严重错误标志(如内存不足),大多数语句开始前应清除(除非致命错误导致连接关闭,否则不会进入此函数)。 -
time_zone_used标记语句是否使用了时区转换(影响 binlog 记录),新语句重新计算。
-
-
重置服务器状态标志:SERVER_STATUS_CLEAR_SET 是一个掩码,定义了那些必须在每个语句开始时清除的状态位,例如 SERVER_STATUS_IN_TRANS(事务是否活跃)、SERVER_STATUS_DB_DROPPED 等。该语句会清除这些位,但保留其他位(如 SERVER_STATUS_AUTOCOMMIT)。
thd->server_status &= ~SERVER_STATUS_CLEAR_SET; -
重置不安全回滚标志(仅在自动提交模式且不在事务中时)
if (!thd->in_multi_stmt_transaction_mode()) { thd->get_transaction()->reset_unsafe_rollback_flags(Transaction_ctx::SESSION); }-
如果一个事务中使用了非事务性表(如 MyISAM)并发生了回滚,这些表无法回滚,因此会设置"不安全回滚"标志。该标志在事务结束后或新语句开始时(如果不在事务中)应清除,以免影响新语句。
-
in_multi_stmt_transaction_mode()检查是否处于多语句事务模式(显式START TRANSACTION或BEGIN,且未提交/回滚)。
-
-
断言安全上下文为主上下文:确保当前安全上下文是 THD 的主安全上下文,而不是某个临时代理上下文。在子语句中可能切换,但顶层语句开始时应该恢复为主上下文。
assert(thd->security_context() == &thd->m_main_security_ctx); -
清除线程特定使用标志:该标志指示本次语句是否使用了 thread_specific 区域(某些存储引擎使用)。新语句开始时重置。
thd->thread_specific_used = false; -
清理用户变量事件(如果开启 binlog)
if (opt_bin_log) { thd->user_var_events.clear(); thd->user_var_events_alloc = thd->mem_root; }-
user_var_events记录了语句中设置的用户变量,用于复制。每个语句需要重新记录。 -
user_var_events_alloc设置内存分配器为当前的mem_root,确保事件列表在内存池中分配。
-
-
清除错误和诊断区域
thd->clear_error(); thd->get_stmt_da()->reset_diagnostics_area(); thd->get_stmt_da()->reset_statement_cond_count();-
clear_error()清除thd->main_da中的错误状态(错误号、错误信息)。 -
reset_diagnostics_area()重置诊断区域(Diagnostics_area)为 OK 状态,清空警告列表。 -
reset_statement_cond_count()重置语句条件计数(用于条件处理程序,如DECLARE CONTINUE HANDLER)。
-
-
重置随机数使用标志:指示语句是否使用了 RAND() 函数。如果是,则复制时需要特殊处理(RAND() 是非确定性的)。新语句重置。
thd->rand_used = false; -
重置行计数器
thd->m_sent_row_count = thd->m_examined_row_count = 0;-
m_sent_row_count:发送给客户端的行数(仅对SELECT类语句有效)。 -
m_examined_row_count:在执行过程中扫描/检查的行数(用于慢查询日志)。新语句重新开始计数。
-
-
重置二进制日志格式标志
thd->reset_current_stmt_binlog_format_row(); thd->binlog_unsafe_warning_flags = 0; thd->binlog_need_explicit_defaults_ts = false;-
reset_current_stmt_binlog_format_row()清除当前语句的"强制使用行格式"标志。 -
binlog_unsafe_warning_flags记录该语句可能引起的复制不安全警告,每个语句独立。 -
binlog_need_explicit_defaults_ts是否需要在二进制日志中显式记录时间戳默认值。
-
-
重置提交错误和持久性属性
thd->commit_error = THD::CE_NONE; thd->durability_property = HA_REGULAR_DURABILITY;-
commit_error记录最近一次提交过程中的错误类型(如CE_NONE,CE_FLUSH_ERROR)。新语句开始时重置。 -
durability_property设置持久性属性(HA_REGULAR_DURABILITY或HA_IGNORE_DURABILITY),新语句恢复为常规持久性。
-
-
重置二进制日志位置:清除当前事务在二进制日志中的位置指针(用于复制和 SHOW BINLOG EVENTS)。
thd->set_trans_pos(nullptr, 0); -
清除派生表处理和系统视图解析标志:这些标志指示是否正在处理派生表(子查询)或解析系统视图,在语句开始时需要清除。
thd->derived_tables_processing = false; thd->parsing_system_view = false; -
重置想要的权限掩码:want_privilege 用于在打开表时请求必要的权限。默认 ALL_ACCESS 表示需要所有权限,但实际上具体语句会按需降低。这里重置为"最严格"是安全的。
thd->want_privilege = ALL_ACCESS; -
重置只读检查和事务提交挂起标志
thd->reset_skip_readonly_check(); thd->tx_commit_pending = false;-
skip_readonly_check:临时绕过read_only系统变量的检查(用于某些内部操作)。新语句重置。 -
tx_commit_pending:指示是否有一个提交操作正在等待完成(用于两阶段提交)。新语句重置。
-
-
重新检查访问映射(ACL):全局 ACL(访问控制列表)可能由于 GRANT、REVOKE、FLUSH PRIVILEGES 而改变。在开始新语句前,需要重新加载当前用户的权限映射,以确保权限检查使用最新的授权。
thd->security_context()->checkout_access_maps(); -
调试:重置临时表序列 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_multiplier、optimizer_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_stats和IS_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_query 在 lex_start 中的作用是:为当前语句创建一个全新的顶级查询表达式(Query_expression)和查询块(Query_block),并初始化 LEX 中的 unit 和 query_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(例如内存分配失败)。
- 获取当前名称解析上下文和解析位置
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等)。
- 创建
Query_expression和Query_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_number、parsing_place等)。 -
如果
curr_query_block非空,则将新的查询块添加到其所属查询表达式的子查询列表中(用于后续优化)。
-
-
新创建的
Query_expression的第一个(也是唯一的)查询块就是我们要的Query_block。
- 设置名称解析上下文
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_block的Name_resolution_context成员,分配必要的内存。 -
对于最外层查询块 (
parsing_place == CTX_NONE),它的outer_context保持为空,表示没有外层查询可以引用。 -
对于子查询块 (例如出现在
WHERE条件中的SELECT),通常需要能够引用外层查询的列,因此将当前上下文(outer_context)设置为新查询块的outer_context。这样,在新查询块中解析列名时,如果在本块中找不到,就会递归地到外层上下文中查找。 -
有一个特殊例外:当子查询出现在
INSERT ... ON DUPLICATE KEY UPDATE子句中,并且外层查询表达式是UNION时,MySQL 不允许该子查询引用外层列(语法限制),因此不设置外层上下文。
- 设置解析时允许在 SELECT 列表中解析名称
new_query_block->context.resolve_in_select_list = true;
- 这个标志表示在解析该查询块时,如果出现一个未限定的列名(例如
SELECT id中的id),可以在当前查询块的SELECT列表中找到对应的表达式(例如SELECT id本身就是一个新定义的列)。这主要用于解析SELECT列表中的别名或索引。
- 调试打印(可选)
DBUG_PRINT("outer_field", ("ctx %p <-> SL# %d", &new_query_block->context,
new_query_block->select_number));
- 仅在调试版本中打印上下文地址和查询块编号。
- 返回新查询块
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(用于UNION、INTERSECT、EXCEPT中的后续查询块)。它不会创建新的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,并设置相应的错误信息。
- 检查嵌套层级限制
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已经达到或超过限制,则拒绝创建更深层的子查询,返回错误。
- 创建
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或由UNION、INTERSECT、EXCEPT组合的复合查询。它的ctx值被记录,用于后续语义检查(例如限制某些上下文中不允许使用UNION)。
- 创建
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列表、条件树等成员。
- 将新查询表达式链接到外层查询块(如果存在)
if (current_query_block != nullptr)
new_expression->include_down(this, current_query_block);
-
include_down是Query_expression的一个方法,用于在语法树中建立父子关系。该调用将新创建的表达式new_expression注册为current_query_block的子查询。 -
具体来说,它会将
new_expression添加到当前查询块的child_units列表中,并设置new_expression->m_parent_query_block为current_query_block。这为后续优化和执行阶段递归处理子查询提供了基础。
- 将新查询块链接到新查询表达式
new_query_block->include_down(this, new_expression);
-
include_down是Query_block的方法,用于将自身加入Query_expression的查询块链表中。一个Query_expression可以包含多个Query_block(例如UNION),这里第一个(也是目前唯一的一个)查询块被添加进去。 -
同时会设置
new_query_block->master_query_expression()指向new_expression。
- 设置
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中,以便遍历所有查询块(例如用于清理、打印或调试)。
- 设置查询表达式的根查询项(Query term)
new_expression->set_query_term(new_query_block);
set_query_term将Query_expression的根查询项(query_term())设置为这个新查询块。对于非UNION的简单查询,根查询项就是唯一的Query_block。对于复合查询,根查询项可能是Query_expression内部的特殊节点(如Query_term_union),但在当前函数中只适用于单块查询,所以直接指向new_query_block。
- 返回新查询表达式
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 改写、错误纠正、安全过滤等功能。
-
准备插件专用的诊断区域
-
获取
thd->get_query_rewrite_plugin_da(),这个诊断区域专门用于插件返回错误或警告。 -
清空该诊断区域,重置条件信息。
-
-
临时替换当前诊断区域
- 调用
thd->push_diagnostics_area(plugin_da, false)将插件的诊断区域推入栈中,后续插件产生的错误会记录在此区域中,而不是主诊断区域。
- 调用
-
通知插件进行预处理
-
调用
mysql_event_tracking_parse_notify(thd, AUDIT_EVENT(EVENT_TRACKING_PARSE_PREPARSE), &flags, &rewritten_query)。 -
此函数会遍历所有注册的预解析重写插件,每个插件可以:
-
修改
flags(例如设置EVENT_TRACKING_PARSE_REWRITE_QUERY_REWRITTEN表示重写了查询)。 -
在
rewritten_query中返回新的查询字符串(如果重写)。
-
-
-
如果查询被重写
-
检查
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)。
-
-
恢复诊断区域并处理错误
-
将插件诊断区域中的非错误信息(警告等)复制回之前的诊断区域(
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结构,建立语法树。如果解析失败(语法错误),err为true,并在诊断区域设置错误。 -
解析成功后,调用后解析重写插件(例如,某些重写规则可能基于 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_sql 是 dispatch_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,表示使用当前线程已有的上下文。 |
详细步骤:
- 断言与初始化
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_digest为true表示这是一个需要生成摘要的顶层语句(例如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 WARNINGS或GET 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_status为true),则要么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_sql。parse_sql 准备工作完成后,会调用 THD::sql_parser,最终触发 my_sql_parser_parse 执行核心的语法分析工作。
MySQL 使用 Bison 作为语法分析器的生成器。sql_yacc.yy 文件包含了 上下文无关文法 的规则、语义动作(用 C++ 代码片段表示)、以及一些预处理指令。Bison 读取这个文件,生成两个主要文件(通常名为 sql_yacc.cc 和 sql_yacc.h):
-
sql_yacc.cc:包含my_sql_parser_parse函数的实现,以及所有语法规则对应的状态转换表、移位/归约处理代码。 -
sql_yacc.h:包含 token 编号的定义(如SELECT_SYM、INSERT_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) 算法的确定性下推自动机。生成的代码主要包含:
-
状态转移表 (
yydefact、yydefgoto、yypgoto、yypact、yypgoto等数组),用于根据当前状态和当前 token 决定是移位(shift)还是归约(reduce)。 -
归约动作代码 :每个产生式对应的语义动作被翻译成 C++ 代码块,插入到
case分支中。 -
栈管理:使用两个栈(状态栈和值栈)来模拟下推自动机。
-
错误恢复机制 :通过
yyerrlab等标签处理语法错误,并尝试通过yyclearin、yyerrok等机制恢复。
简化后的执行流程:
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-verbose 或 yyerror 函数定义来指定。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_PREPARE或COM_STMT_EXECUTE路径),该信息会通过flags传递给插件。
-
-
返回值 :总是
false(当前实现中err初始化为false且从未被修改为true),但未来可能扩展为表示插件是否要求中止执行。
调用时机:
-
在
dispatch_sql_command中,成功调用parse_sql解析 SQL 并生成语法树之后,在invoke_post_parse_rewrite_plugins被调用。 -
对于预处理语句,
is_prepared会设置为true,使得插件能够区分普通查询和 PREPARE 语句。
- 准备插件专用的诊断区域
Diagnostics_area *plugin_da = thd->get_query_rewrite_plugin_da();
plugin_da->reset_diagnostics_area();
plugin_da->reset_condition_info(thd);
-
获取预先分配好的、用于查询重写插件的诊断区域(每个 THD 一个)。
-
清空该区域,确保插件产生的警告/错误不会与之前的残留混淆。
- 保存"是否保留解析错误"标志
bool keeping_diagnostics = thd->lex->keep_diagnostics == DA_KEEP_PARSE_ERROR;
-
lex->keep_diagnostics标志指示当前语句是否应该保留解析阶段产生的诊断信息(例如,对于SHOW WARNINGS语句,需要保留上一条语句的条件列表)。该标志在parse_sql中可能被设置为DA_KEEP_PARSE_ERROR。 -
在后续处理插件产生的条件时,需要参考该标志来决定是否清空主诊断区域的条件。
- 临时切换诊断区域
thd->push_diagnostics_area(plugin_da, false);
-
将
plugin_da推入诊断区域栈,后续插件产生的任何警告/错误都会记录到plugin_da中,而不是主诊断区域(stmt_da)。 -
参数
false表示如果插件 DA 中已经有错误,不会强制覆盖。
- 确保服务函数被链接(仅调试)
#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
- 这是一个调试技巧:调用某些服务中的空函数,确保链接器将这些服务的目标文件包含进最终二进制,否则插件可能因为找不到符号而无法加载。在生产版本中会被优化掉。
- 通知插件后解析事件
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)采取不同的行为。例如,插件可能希望在准备阶段就对语句结构进行改写,而不是每次执行时重复。
- 处理"查询重写"标志
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,表示该语句的结果不可缓存(因为重写可能导致查询语义变化,或者重写包含非确定性元素)。
-
- 处理插件产生的诊断条件
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_diagnostics为false(即没有要求保留解析错误)。这是因为SHOW WARNINGS需要保留之前的条件,而普通语句应该用新的条件替换旧条件。 -
如果插件 DA 中有一个错误状态,则将该错误复制到主 DA 中。
-
将插件 DA 中的所有条件(警告、错误等)复制到主 DA 中。
-
设置
lex->keep_diagnostics = DA_KEEP_PARSE_ERROR,指示后续执行阶段不要清除这些诊断信息(因为现在它们已经是当前语句有效的一部分)。
-
- 恢复诊断区域
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 USER或SET 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:一个包装器对象,用于传递重写所需的额外参数(如某些密码重写策略需要知道目标存储引擎等),当前版本中可能未使用或仅用于扩展。
- 断言与前置检查
assert(thd);
assert(thd->rewritten_query().length() == 0);
-
确保
thd有效。 -
确保当前语句还没有被重写过(
rewritten_query长度为 0)。这防止了同一个语句被重复重写(例如,由于某些路径错误地多次调用该函数)。
- 判断是否需要重写
if (thd->lex->contains_plaintext_password ||
thd->lex->is_rewrite_required()) {
// 执行重写
}
-
contains_plaintext_password:在词法/语法分析阶段,如果解析器检测到 SQL 语句中包含明文密码(例如IDENTIFIED BY 'password'子句),这个标志会被设置为true。 -
is_rewrite_required():这是一个更通用的标志,指示该语句需要某种形式的重写(可能不仅仅是密码混淆)。例如,某些插件可能要求对所有语句进行重写,或者语句含有需要转义的特殊字符。
如果任一条件为真,则调用 rewrite_query 执行实际的重写操作。
- 执行重写
String rlb;
rewrite_query(thd, type, params, rlb);
-
rewrite_query是一个内部函数,根据type和thd->lex中的信息生成重写后的查询字符串,存入rlb。 -
具体重写规则由
type决定:-
对于
TEXTLOG:将IDENTIFIED BY 'plaintext'替换为IDENTIFIED BY '********';同时可能处理SET PASSWORD = 'plaintext'、ALTER USER等语句中的密码部分。 -
对于
BINLOG:可能需要更细致的处理,确保复制的一致性,但通常也会混淆密码。 -
对于
STDOUT:通常与TEXTLOG类似。
-
- 保存重写后的查询
if (rlb.length() > 0) thd->swap_rewritten_query(rlb);
-
如果重写生成了非空字符串,则调用
swap_rewritten_query将thd->m_rewritten_query与rlb交换。这样thd就持有了重写后的查询,而原来的rlb被清空(或持有旧值),随后rlb超出作用域自动释放。 -
thd->m_rewritten_query是一个String对象,专门用于存储重写后的查询文本,供日志模块使用。
mysql_rewrite_query 在 dispatch_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 PROCESSLIST和performance_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 PASSWORD或ALTER 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 锁(但不是事务锁)、重置临时表等。