Skip to content

第17章 MySQL 驱动:COM_QUERY 与 COM_STMT 的二元选择

"MySQL's protocol is Postgres's protocol in a different dialect— the moves are the same, the spelling differs." —— 对比读过两家协议的人的观察

本章要点

  • sqlx-mysql(约 9195 行)是 sqlx-postgres 的一半体量——协议细节少(无 COPY / LISTEN / advisory lock 等扩展) + 类型系统简单(MySQL 内建类型比 Postgres 少很多)。
  • MySQL 协议两大命令COM_QUERY(0x03,文本查询)vs COM_STMT_PREPARE/EXECUTE/CLOSE(0x16/0x17/0x19,二进制 prepared)。sqlx 默认走 COM_STMT——对等 Postgres 的 Extended Query。
  • Handshake V10sqlx-mysql/src/connection/establish.rs)—— server 先发 Handshake、client 回 HandshakeResponse41 + auth 数据、然后 Auth 子协议(mysql_native_password / caching_sha2_password)。和 Postgres 的 Startup-first 相反。
  • Capabilities 位图——64 位 flag 协商 client 和 server 的能力(SSL、plugin auth、multi-statement、deprecated EOF、session track 等)。双方取交集
  • caching_sha2_password 是 MySQL 8+ 的默认认证——比 mysql_native_password 安全(SHA-256 + 盐 + 多轮迭代)。sqlx-mysql/src/connection/auth.rs 实现两种 + SHA256 公钥交换逻辑。
  • MySqlArguments 三件套(第 6 章 §6.5)—— values + types + null_bitmap。MySQL binary protocol 的 null 用独立位图而非值内 -1 长度。
  • MySqlQueryResultlast_insert_id(Postgres 的 QueryResult 没有)——AUTO_INCREMENT 列值由这个字段返回。用户代码里 let id = result.last_insert_id() 经典用法。
  • status_flagsconnection/mod.rs:40)—— server 在每个响应里带状态位(SERVER_STATUS_IN_TRANS / SERVER_STATUS_AUTOCOMMIT / SERVER_MORE_RESULTS_EXISTS 等)—— sqlx 缓存这些用于事务状态跟踪。
  • MariaDB 兼容——sqlx-mysql 同时支持 MySQL 5.7/8.0 和 MariaDB 10+——协议兼容但版本字符串和某些 capability 不同。URL scheme mariadb://mysql:// 都走同一驱动。

17.1 问题引入:MySQL 协议的定位

读完第 16 章 Postgres 驱动,你对 wire protocol 有了完整心智模型。MySQL 协议结构上很类似——握手、认证、查询、结果集——但每一步细节都不同。如果我重复讲一遍每个消息,你会觉得啰嗦。所以本章以对比为主线——讲 MySQL 哪里和 Postgres 相同、哪里不同、为什么不同。

MySQL 协议的几条宏观特征

  • 更老(1995)——比 Postgres 更早、结构反映当时 C/S 模型的典型做法。
  • 更简单——MySQL 内建类型少(数值 / 字符串 / 日期时间 + 一些 geometry)、协议扩展少(无 LISTEN/NOTIFY、无 COPY 等价物)。
  • 协议版本多——v9(1998)/ v10(2006+)——现在都用 v10。
  • 认证演进快——从 mysql_old_password(不安全)到 mysql_native_password(SHA1)到 caching_sha2_password(SHA256,MySQL 8 默认)。
  • 方言多——MySQL vs MariaDB 协议兼容但 SQL 和 capability 略有分叉。

sqlx-mysql 的9195 行代码对比 sqlx-postgres 的 19841 行——体量一半。差距主要在:

  • 无 COPY 协议(Postgres 有,MySQL 没有等价批量协议)。
  • 无 LISTEN/NOTIFY
  • 无 advisory lock 等 app-level 锁
  • 类型系统简单——types/ 目录更小。

核心的握手 + Extended Query 等价路径和 Postgres 对应——本章讲清楚这些差异就够。

17.2 MySQL 协议概览

MySQL 协议的包结构(和 Postgres 不同):

+-----------------+----------+---------+
| 长度 (3 字节)   | 序号     | 载荷    |
| u24 小端        | u8       | N 字节  |
+-----------------+----------+---------+

差异点

  • 3 字节长度(u24)——最大单包 16MB。超过要分片。Postgres 是 4 字节 u32。
  • 1 字节序号——每个包有递增序号、从 0 开始、每次新查询重置。Postgres 没有序号。
  • 无类型字节——消息类型通过第一字节载荷上下文识别。Postgres 每个消息都有明确的类型字节。

序号的作用——让 client 和 server 能确认消息按顺序收到、没乱序。这是 MySQL 设计时(1990 年代)考虑不可靠网络的产物。现代 TCP 已经保证顺序——序号变得冗余但协议保留。

MySQL 的序号 + 分片机制让大数据包(>16MB)能跨多个物理包传——sqlx-mysql 的 io/ 子目录实现这个拼接。

17.3 连接握手:Handshake V10

和 Postgres 相反——MySQL 是 server first:client 连 TCP 后、server 先发 Handshake

Handshake V10 (server → client):
  - protocol_version (u8 = 10)
  - server_version (null-terminated string)
  - thread_id (u32, MySQL 内部 session ID)
  - auth_plugin_data_part_1 (8 字节盐)
  - filler (1 字节 0x00)
  - capability_flags_lower (u16)
  - character_set (u8)
  - status_flags (u16)
  - capability_flags_upper (u16)
  - auth_plugin_data_len (u8)
  - reserved (10 字节 0x00)
  - auth_plugin_data_part_2 (>= 13 字节)
  - auth_plugin_name (null-terminated)

Client 接收后解析 server 发的能力 + 盐,然后决定怎么回复。

HandshakeResponse41(client → server):

  - capability_flags (u32)
  - max_packet_size (u32, 一般 16MB)
  - character_set (u8, 客户端选的字符集)
  - reserved (23 字节)
  - username (null-terminated)
  - auth_response_length (LENENC)
  - auth_response (auth_response_length 字节,即密码 hash)
  - database (null-terminated, 可选)
  - auth_plugin_name (null-terminated)
  - connect_attrs (LENENC key-value list, 可选)

对比 Postgres:Postgres 是 client 先发 Startup、server 根据 pg_hba.conf 决定认证方式;MySQL 是 server 先发 Handshake 暴露自己的 capabilities、client 从中选能接受的。两种哲学——"client 协商" vs "server 声明 + client 同意"

17.3.1 Capabilities 位图

Capabilities 是 64 位(其中 Handshake 只有低 32 位、HandshakeResponse41 也是 32 位;扩展 capability 是 MySQL 5.7+ 的 32 位 extra)。常见位:

含义
CLIENT_LONG_PASSWORD(0x01)早期 SHA1 密码
CLIENT_PROTOCOL_41(0x200)用协议 v4.1 而非老 v3.23
CLIENT_SSL(0x800)SSL 支持
CLIENT_PLUGIN_AUTH(0x80000)支持 auth plugin 协商
CLIENT_CONNECT_WITH_DB(0x08)初始 db 在 handshake 里指定
CLIENT_MULTI_STATEMENTS(0x10000)支持一个包多条 SQL
CLIENT_DEPRECATE_EOF(0x1000000)用 OK 包替代 EOF 包

Client 和 server 取交集——双方都支持才启用。sqlx 要的能力在 sqlx-mysql/src/protocol/capabilities.rs 声明。

17.4 认证:caching_sha2_password 详解

MySQL 8 的默认认证插件caching_sha2_password——相比旧的 mysql_native_password(SHA1)更安全。

mysql_native_password 握手(老):

scramble = SHA1(password) XOR SHA1(nonce + SHA1(SHA1(password)))

客户端发 scramble、server 验证。

caching_sha2_password 握手(新):

  1. Client 发 SHA256(SHA256(password)) XOR SHA256(nonce + SHA256(SHA256(password)))
  2. Server 检查缓存(就是"caching"的来源)——如果密码在 server 的 caching_sha2_password 缓存里、验证通过。
  3. 缓存 miss——server 回 "full auth"(0x04)——client 必须用明文密码 + SSL公钥加密再发。

sqlx-mysql 的 connection/auth.rs 实现所有这些——包括:

  • 发第一轮 scramble。
  • 处理 "full auth" fallback(SSL 下发明文、非 SSL 下通过公钥交换发密码)。
  • 处理公钥请求(AuthMoreData 带公钥、client 用公钥 RSA 加密密码)。

完整的 caching_sha2 握手最多 4 条消息往返——比 Postgres 的 SCRAM 更复杂(SCRAM 固定 4 条)。sqlx-mysql 的这部分约 200 行代码——用 sha2 + rsa + rand crate 实现。

Rust client 实现 RSA 加密密码听起来复杂、但 rsa crate 提供高层 API——sqlx 的代码约 30-50 行搞定 "公钥 + 密码 + nonce → 加密字节"。

17.5 COM_QUERY:文本查询

MySQL 的 simple query 等价物COM_QUERY

Client → Server: COM_QUERY (0x03) + sql 字符串
Server → Client: (按查询类型响应)
  - UPDATE/INSERT/DELETE/DDL: OK Packet
  - SELECT: ResultSet (ColumnCount + ColumnDefinition × N + EOF + Row × M + EOF)
  - 错误: ERR Packet

ResultSet 的结构

[ColumnCount (LENENC integer)]
[ColumnDefinition 1] ... [ColumnDefinition N]
[EOF packet]           (老版本) 或 没有(新版本 CLIENT_DEPRECATE_EOF)
[Row 1] ... [Row M]    每行是 LENENC-string × N
[EOF / OK packet]

COM_QUERY 结果用文本格式——每列值是字符串。Row 是 LENENC-string × N——每列值带长度前缀。

NULL 表示0xfb 单字节标记(不是 LENENC)。这是 MySQL 文本协议特色。

sqlx-mysql 在无参数查询时走 COM_QUERY——executor.rs:152-156 的分支:

rust
} else {
    // https://dev.mysql.com/doc/internals/en/com-query.html
    self.inner.stream.send_packet(Query(sql)).await?;
    (Arc::default(), MySqlValueFormat::Text, true)
}

场景:无参数 SQLSELECT 1 / DDL / 管理命令)——COM_QUERY 简洁、一包搞定。

17.6 COM_STMT:二进制预处理

MySQL 的 Extended Query 等价COM_STMT 三件套

17.6.1 COM_STMT_PREPARE (0x16)

Client → Server: 0x16 + sql
Server → Client: OK:
  - statement_id (u32)
  - num_columns (u16)
  - num_params (u16)
  - warnings (u16)
  - ColumnDefinition × num_params
  - EOF (如果 !DEPRECATE_EOF)
  - ColumnDefinition × num_columns
  - EOF (如果 !DEPRECATE_EOF)

一条消息发 SQL、一组响应包拿回 statement_id + 参数定义 + 列定义。类似 Postgres 的 Parse + ParameterDescription + RowDescription 合并。

17.6.2 COM_STMT_EXECUTE (0x17)

Client → Server: 0x17:
  - statement_id (u32)
  - flags (u8, CURSOR_TYPE 等)
  - iteration_count (u32 = 1)
  - null_bitmap (bytes, (num_params+7)/8)
  - new_params_bound_flag (u8 = 1)
  - param_types (u16 × num_params)
  - param_values (binary × num_params)

核心是 null_bitmap + 参数值——每参数按binary 协议编码。

类型和值的分离:每个参数先发类型 (u16)、然后集中发值(按类型紧凑格式)。整数是 4/8 字节小端、字符串是 LENENC-string、日期时间是特殊二进制格式。

Response 和 COM_QUERY 的 ResultSet 类似,但Row 是 binary format(不同于 COM_QUERY 的 text format):

Binary Row:
  - null_bitmap (bytes, (num_fields+9)/8)
  - field values (类型特定 binary)

Binary row 比 text row 更紧凑——整数 4 字节而不是 "123456" 6 字节——但只有 COM_STMT_EXECUTE 触发的响应才是 binary row(COM_QUERY 始终 text)。

17.6.3 COM_STMT_CLOSE (0x19)

Client → Server: 0x19 + statement_id
(无响应)

没有响应包——单向通知 server 释放 statement。sqlx 在 statement cache 淘汰时发这条(类似 Postgres 的 Close::Statement)。

17.7 sqlx-mysql 的 run 方法

sqlx-mysql/src/connection/executor.rs:102-190 的 run 方法——和 Postgres 的 run 类似结构:

rust
pub(crate) async fn run<'e, 'c: 'e, 'q: 'e>(
    &'c mut self, sql: &'q str, arguments: Option<MySqlArguments>, persistent: bool,
) -> Result<impl Stream<...>, Error>
{
    self.inner.stream.wait_until_ready().await?;
    self.inner.stream.waiting.push_back(Waiting::Result);

    Ok(try_stream! {
        let (column_names, format, needs_metadata) = if let Some(args) = arguments {
            // COM_STMT_PREPARE + COM_STMT_EXECUTE 路径
            let (id, metadata) = self.get_or_prepare_statement(sql).await?;
            self.inner.stream.send_packet(StatementExecute { statement: id, arguments: &args }).await?;

            if !persistent {
                self.inner.stream.send_packet(StmtClose { statement: id }).await?;
            }

            (metadata.column_names, MySqlValueFormat::Binary, false)
        } else {
            // COM_QUERY 路径
            self.inner.stream.send_packet(Query(sql)).await?;
            (Arc::default(), MySqlValueFormat::Text, true)
        };

        // 循环接收 ResultSet 或 OK packet
        loop {
            let mut packet = self.inner.stream.recv_packet().await?;

            if packet[0] == 0x00 || packet[0] == 0xff {
                // OK 或 ERR 包
                let ok = packet.ok()?;
                self.inner.status_flags = ok.status;
                r#yield!(Either::Left(MySqlQueryResult {
                    rows_affected: ok.affected_rows,
                    last_insert_id: ok.last_insert_id,
                }));
                // 检查 multi-result
                if ok.status.contains(Status::SERVER_MORE_RESULTS_EXISTS) { continue; }
                break;
            }

            // ResultSet: ColumnCount + ColumnDef + Rows + EOF
            // ... 略
        }
    })
}

两条分叉

  • COM_QUERY(无参数)—— 简单路径。
  • COM_STMT_PREPARE + EXECUTE(有参数)—— 有 cache。非 persistent 额外发 STMT_CLOSE。

17.7.1 与 sqlx-postgres::run 的对比

关键差异

维度sqlx-postgres runsqlx-mysql run
发送路径Parse + Bind + Execute + Close + SyncCOM_STMT_PREPARE + EXECUTE + CLOSE
消息数5 条(cache hit 省 Parse/Describe)2 条(EXECUTE + 可选 CLOSE)
同步机制Sync 消息 + ReadyForQuery序号 + 每响应包带 status_flags
缓存淘汰Close::Statement 放在 next query 前STMT_CLOSE 可立即发(如果非 persistent)
多结果集不支持在一条 Execute 内SERVER_MORE_RESULTS_EXISTS 标志 + 循环

一个值得注意的点:MySQL 支持 "multi-statement"(CLIENT_MULTI_STATEMENTS capability)——一个 COM_QUERY 包多条 SQL、server 顺序执行、返回多个结果集。sqlx-mysql 的 run 循环通过 SERVER_MORE_RESULTS_EXISTS 标志检测并处理。Postgres 不支持一条 prepared statement 内多语句。

17.8 MySQL 特殊:status_flags 跟踪事务

MySqlConnectionInner::status_flags: Statusconnection/mod.rs:40)——跟踪 server 最新响应里的 status flags。几个关键位:

rust
// protocol/response/status.rs
pub struct Status: u16 {
    const SERVER_STATUS_IN_TRANS = 0x0001;
    const SERVER_STATUS_AUTOCOMMIT = 0x0002;
    const SERVER_MORE_RESULTS_EXISTS = 0x0008;
    const SERVER_STATUS_CURSOR_EXISTS = 0x0040;
    const SERVER_STATUS_LAST_ROW_SENT = 0x0080;
    // ...
}

每个 OK / EOF 响应包都带 status——sqlx 每次响应后更新 status_flags。这条持续状态同步让 sqlx 知道:

  • 事务中吗?—— status_flags.contains(SERVER_STATUS_IN_TRANS)
  • autocommit 吗?—— status_flags.contains(SERVER_STATUS_AUTOCOMMIT)
  • 还有下一个结果集?—— SERVER_MORE_RESULTS_EXISTS

Postgres 的等价机制是 ReadyForQuery 消息的 transaction_status 字段——但 Postgres 是查询结束时一次给,MySQL 是每个响应都带——信息更密集。

in_transaction 方法mod.rs:52-55):

rust
pub(crate) fn in_transaction(&self) -> bool {
    self.inner.status_flags.intersects(Status::SERVER_STATUS_IN_TRANS)
}

直接查 status——O(1) 没网络开销。Transaction 的 commit/rollback 要实时知道事务状态时用这个。

17.9 LENENC 变长长度编码

第 6 章 §6.5.1 讲过 LENENC——这里总结一下 MySQL 为什么需要它:

  • MySQL 包:3 字节长度(max 16MB per 包)。
  • 包内每个长度字段:如果都固定 4 字节,小值浪费;用 LENENC 变长。

LENENC 编码(first byte 决定格式):

  • 0-250:单字节。
  • 251:NULL 标记(COM_QUERY text 里用,STMT_EXECUTE 不用)。
  • 252 + u16 小端:2 字节长度(251-65535)。
  • 253 + u24 小端:3 字节长度(65536-16M)。
  • 254 + u64 小端:8 字节长度(> 16M,极少用)。

每次用 LENENC 读长度时先读 1 字节再按 case 分支——稍复杂但空间紧凑。

Postgres 的对应-1 用于 NULL、正数用于长度——但长度字段永远 4 字节固定。MySQL 的 LENENC 在短值场景(大部分)节省 1-3 字节每值——累加下来在高并发能省带宽。

17.10 字符集协商

MySQL 连接建立时要选字符集——server 支持 240+ 种(utf8mb4 / utf8 / latin1 / gbk / big5 / ...)。每种字符集有对应的 collation(排序规则)——每个 collation 有数字 ID。

sqlx-mysql/src/collation.rs 枚举所有 collation:

rust
// 简化
pub enum Collation {
    Utf8mb40900AiCi = 255,          // MySQL 8 默认 utf8mb4
    Utf8mb4BinaryCi = 46,            // utf8mb4 binary
    Latin1Swedish = 8,               // MySQL 5.7 默认
    // ... 200+ 条
}

Client 在 HandshakeResponse41 里选一个 character_set 编号——server 按这个编号对后续所有字符串交互。选错会导致乱码——sqlx 默认选 utf8mb4_0900_ai_ci(现代 MySQL 最安全的 UTF-8 支持)。

这条细节在 Postgres 里不存在——Postgres 也有 server encoding、但 wire protocol 不做 per-connection 字符集协商(用 client_encoding 参数在 Startup 里传)。MySQL 的 collation 体系比 Postgres 复杂——是历史包袱。

17.11 MariaDB 兼容性

MariaDB 是 MySQL 的 fork(2009 年分叉)——协议层基本兼容但略有差异:

兼容的部分(大多数):

  • Handshake V10、HandshakeResponse41。
  • COM_QUERY、COM_STMT_* 家族。
  • 基本数据类型和 field types。

差异的部分

  • server_version 字符串——MySQL 是 "8.0.32"、MariaDB 是 "10.11.2-MariaDB"。sqlx 检测字符串决定走哪条方言路径。
  • Capability flags——MariaDB 有自己的扩展 capabilities(高 32 位)。
  • 某些 SQL 方言——MariaDB 支持 RETURNING(MySQL 不支持)、一些函数名差异。

sqlx-mysql 的 options/mod.rs 通过 URL scheme mariadb:// vs mysql:// 标记、后续代码路径大部分相同——少量 if is_mariadb 分叉处理差异。

用户代码视角:写的 SQL 如果只用标准 SQL 子集——两家 DB 都跑。用了 MariaDB 特有的 RETURNING——只能跑 MariaDB。sqlx 不做 SQL 翻译——用户自己保证 SQL 的方言对齐。

17.12 MySQL 特性实战

MySQL 独有的几个实战场景:

17.12.1 last_insert_id

rust
let result = sqlx::query("INSERT INTO users (name) VALUES (?)")
    .bind("Alice").execute(&pool).await?;
let new_id = result.last_insert_id();  // AUTO_INCREMENT 的新 ID

MySQL 的 AUTO_INCREMENT + last_insert_id() 是业务常见模式——MySqlQueryResult 直接暴露这个值、省一次 SELECT。

Postgres 的对应INSERT ... RETURNING id + fetch_one——一条 SQL 拿回 ID。两种模式各有千秋——MySQL 的 last_insert_id 简洁但只能拿一列(自增 ID),Postgres 的 RETURNING 灵活能拿整行。

17.12.2 ON DUPLICATE KEY UPDATE

rust
sqlx::query("INSERT INTO counters (key, count) VALUES (?, 1)
             ON DUPLICATE KEY UPDATE count = count + 1")
    .bind("view_count").execute(&pool).await?;

MySQL 的 UPSERT 语法——Postgres 用 INSERT ... ON CONFLICT ... DO UPDATE。两者语法不同但语义类似。

sql
SELECT * FROM articles WHERE MATCH(title, body) AGAINST ('rust sqlx' IN NATURAL LANGUAGE MODE)

MySQL 的原生全文索引——比 Postgres 的 tsvector 简单、功能也少。sqlx 不做特殊封装、普通 query 就能用。

17.12.4 事务隔离级别

MySQL 默认 REPEATABLE READ(比 Postgres 的 READ COMMITTED 更严)——业务迁移从 Postgres 到 MySQL 要注意这条差异可能带来的并发行为变化。

conn.begin_with("SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN") 可以定制——但 MySQL 的 SET TRANSACTIONBEGIN 必须分开两条 SQL(不能一起写)——sqlx 的 begin_with 直接支持。

17.13 sqlx-mysql 和 sqlx-postgres 的全面对比

一张完整对比表:

维度sqlx-postgressqlx-mysql
代码量~19841 行~9195 行
主要扩展LISTEN/NOTIFY, COPY, advisory lock无(MySQL 协议不含等价物)
类型系统丰富(400+ 内建)简单(数十种)
认证机制SCRAM-SHA-256(主流)caching_sha2_password / mysql_native
协议顺序client 先发 Startupserver 先发 Handshake
消息类型字节每消息 1 字节类型无(命令码在载荷第一字节)
协议分割Sync 消息包序号
长度编码固定 4 字节LENENC 变长
NULL 编码长度字段 -1独立 null_bitmap
结果 binary/text同时支持(per column)同时支持(per query 类型)
批量 INSERTUNNEST 或 VALUES (...)VALUES (...) 或 LOAD DATA
AUTO_INCREMENT 返回RETURNING 语句last_insert_id()
事务默认隔离READ COMMITTEDREPEATABLE READ
PgListener / NotifyListener否(需要轮询 polling)
COPY 批量是(极快)否(LOAD DATA 走 SQL 层)
字符集一个(UTF-8)240+ collations

读完这张表就读完了本章的精华——sqlx-mysql 和 sqlx-postgres 的每一条差异都能定位到协议级原因

17.7.2 MySQL 握手序列图

把 MySQL handshake + auth 用 mermaid 表达:

两条路径

  1. Fast auth(server 已缓存密码 hash)——握手 3 消息完成(接近 Postgres)。
  2. Full auth(缓存 miss 首次登录)——多 2-3 消息(公钥交换 + RSA 加密)——首次登录后 server 缓存、后续 fast。

MySQL 8 默认 caching_sha2 让 fast auth 成为常态——生产 pool reused 连接不会重复 full auth、开销小。但测试环境重启 server 会清缓存——前几次 handshake 都走 full auth——调试时可能比生产慢。

17.7.3 COM_STMT 执行序列图

一次 query约 2 次 RTT(cache miss)或 1 次 RTT(cache hit)。和 Postgres 基本等同——协议差异大、性能相近。

17.14 本章小结

本章在 Postgres 驱动的参照系上讲清楚了 sqlx-mysql:

  1. 协议定位(§17.1)—— MySQL 比 Postgres 更老、更简单、无 COPY/LISTEN 等扩展。代码量一半。
  2. 协议包结构(§17.2)—— 3 字节长度 + 1 字节序号,无消息类型字节。
  3. Handshake V10(§17.3)—— server 先发;Capabilities 位图协商;与 Postgres 的 Startup-first 相反哲学。
  4. caching_sha2_password(§17.4)—— MySQL 8 默认;缓存未命中时 full auth 流程需要 SSL 或 RSA 公钥加密。
  5. COM_QUERY 文本协议(§17.5)—— 无参数查询的简单路径;NULL 是 0xfb 单字节。
  6. COM_STMT 二进制(§17.6)—— PREPARE / EXECUTE / CLOSE 三件套;null_bitmap 独立。
  7. run 方法(§17.7)—— 和 Postgres 的 run 结构类似但消息数少、multi-statement 支持。
  8. status_flags(§17.8)—— 每响应带状态、实时跟踪 in_transaction / autocommit / more_results。
  9. LENENC 变长编码(§17.9)—— 短值省字节的紧凑格式。
  10. 字符集协商(§17.10)—— 240+ collations、client 在 HandshakeResponse 里选一个。
  11. MariaDB 兼容(§17.11)—— 协议基本兼容、少数方言差异、同一驱动处理。
  12. MySQL 特有实战(§17.12)—— last_insert_id / ON DUPLICATE KEY / full-text / 默认 REPEATABLE READ。
  13. 全面对比表(§17.13)—— 14 个维度的 Postgres vs MySQL 差异。

下一章我们进入 SQLite 驱动——和前两家完全不同的世界(C FFI + worker 线程)。

17.15 sqlx-mysql 独特的协议细节

几条 sqlx-mysql 独有的协议细节值得单独提一下——它们和 Postgres 没对应:

1. Packet 分片(packet splitting)

MySQL 单物理包最大 16MB(3 字节长度上限)。超过时自动分片——一条逻辑消息拆成多个 physical packet、每个带序号递增。

sqlx-mysql 的 io/buf_stream.rs 处理这层——send_packet 自动 chunk、recv_packet 自动 reassemble。用户代码完全透明——写 10MB 的字符串就写、不用关心分片。

Postgres 没有这个问题——单消息 u32 长度 = 4GB 上限、远超实际使用。

2. EOF packet 还是 OK packet

MySQL 5.7 之前,结果集末尾用 EOF 包(0xfe + warnings + status)。5.7+ 可以协商 CLIENT_DEPRECATE_EOF、用 OK 包(0x00)代替。sqlx 默认声明这 capability——所以大部分时候看到的是 OK 包。

这条小细节让 sqlx 代码里的 recv 逻辑要同时处理两种包——按 deprecate_eof 能力分支。代码稍复杂但向后兼容老 MySQL server。

3. LOCAL_INFILE 未处理

MySQL 的 LOAD DATA LOCAL INFILE '/path/...' INTO TABLE ... 让 client 上传文件到 server。协议上 server 回 LOCAL_INFILE 请求、client 流式发文件字节。

sqlx-mysql 未实现 LOCAL_INFILE——客户端代码不能用这个语法。用户需要在 server 侧预先有文件LOAD DATA INFILE 不带 LOCAL) 或者用多条 INSERT 替代

这是 sqlx-mysql 的一个 known limitation——issue tracker 里有长期讨论、未来可能加入。

4. Multi-statement 支持

MySQL 支持一条 COM_QUERY 发多条 SQL(分号分隔)——CLIENT_MULTI_STATEMENTS 位启用。sqlx-mysql 默认不启用——避免注入风险(想象 "SELECT * FROM users WHERE name = '{user}'" 里 user 是 '; DROP TABLE users; -- 的场景)。

用户要真想用、可以通过 MySqlConnectOptions::no_engine_subsitution(true) 启用——但不推荐、因为 query! 宏不支持多语句。

17.16 sqlx-mysql 和 MariaDB 10+ 的特殊支持

MariaDB 相对 MySQL 有几个独有特性——sqlx 对其中一些做了适配:

1. RETURNING(MariaDB 10.5+)

sql
INSERT INTO users (name) VALUES ('Alice') RETURNING id, name

MariaDB 模仿 Postgres 的 RETURNING。sqlx 不需要特殊代码——SQL 字面量就能用。但只 MariaDB 支持——MySQL 同语法会报语法错。

2. Vector Type(MariaDB 11+)

MariaDB 最新版有向量数据类型(用于 ML embedding 之类)。sqlx-mysql 目前不支持——需要 feature gate 加入。

3. JSON 函数方言差异

MySQL 和 MariaDB 的 JSON 函数名不完全一样(比如 JSON_EXTRACT vs JSON_VALUE)——用户 SQL 要按实际 DB 方言写。

这些差异提醒一个实用观察"MySQL 或 MariaDB"不是可互换的——虽然协议兼容,SQL 方言分叉明显。团队选 MariaDB 的应当心理预期会和 MySQL 路线分开,不指望 MySQL 新特性(如 JSON_TABLE)在 MariaDB 有。

17.17 MySQL 协议安全性演进

MySQL 协议的安全演进可以简要梳理:

  • 1995-2004(MySQL 3.x/4.x): mysql_old_password——8-byte scramble。极不安全、2004 年被 break。
  • 2005-2018(MySQL 5.x): mysql_native_password——20-byte SHA1 scramble。比 old 好但 SHA1 已不推荐。
  • 2018 至今(MySQL 8+): caching_sha2_password——SHA2 + 多轮盐 + 可选公钥交换。目前状态最佳。

sqlx-mysql 三种都支持——能连老 MySQL 4 到新 MySQL 8.4。但 默认用 caching_sha2_password——和 MySQL 8 的默认对齐。MySqlConnectOptions::auth_plugin() 可以覆盖、连老 server 时必要。

对用户的实际含义:从 MySQL 5.7 升级到 8.0 时——账号密码要重新 hash(用 ALTER USER ... IDENTIFIED WITH caching_sha2_password BY '...')——老账号沿用 mysql_native_password 也能用但不推荐。这是 sqlx 之外的 ops 话题、但影响 sqlx 连接的可用性。

17.18 MySQL 驱动的可读性观察

读完 sqlx-mysql 源码、和 sqlx-postgres 对比,几条可读性观察:

1. sqlx-mysql 代码风格和 Postgres 高度一致——文件组织、函数命名、错误处理模式都对齐。作者刻意让"熟悉一家就能读另一家"。

2. 注释相对稀疏——比 Postgres 驱动注释少。原因可能是 MySQL 协议文档更系统(官方手册)、代码更容易读。

3. dbg_hex! / tracing 调用——sqlx-mysql 的调试宏用得多、方便开发。

4. 测试覆盖良好——tests/mysql/ 目录有全面的集成测试——MariaDB 和 MySQL 都测。

5. 没有 unsafe——和 sqlx-postgres 一样、全 safe Rust 实现。

代码品质和 Postgres 等级——社区贡献者来回切换两家驱动时能保持一致的工作体验。这是 sqlx 作为 生态型项目 的工程纪律。

17.19 本章的实战技巧集

最后汇总几条用 sqlx-mysql 的实战技巧:

1. URL scheme 明确——mysql://...mariadb://...——后者会启用 MariaDB 兼容路径(少数细节如 connection_attributes)。

2. 字符集设 utf8mb4——?charset=utf8mb4 在 URL 里、或 MySqlConnectOptions::charset("utf8mb4")。避免 utf8(MySQL 的"utf8"其实是 utf8mb3、不支持 4 字节 UTF-8 字符如 emoji)。

3. 时区设明确——?time_zone=%2B00:00 URL param 或 SET TIME_ZONE——MySQL server 的默认时区乱七八糟、显式设 UTC 避免坑。

4. 用 caching_sha2 新用户——CREATE USER 'u'@'%' IDENTIFIED WITH caching_sha2_password BY 'pass'。老 native_password 还能用但不推荐。

5. 配置 max_allowed_packet——MySQL server 默认 16MB。超过的 query(大 BLOB / 大文本)会失败。sqlx 自动处理分片、但 server 侧限制要调大。

6. 小心 LOAD DATA LOCAL INFILE——sqlx 不支持、用 sqlx-cli 或手写 COM_QUERY 包装外部工具。

7. 事务隔离级别显式——MySQL 默认 REPEATABLE READ,Postgres 默认 READ COMMITTED——迁移时用 begin_with("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN") 对齐语义。

这七条是 sqlx-mysql 生产用户最需要记住的实战要点——配合第 16 章学到的通用 sqlx 知识、你能写出生产级 MySQL Rust 代码。

17.20 为什么 MySQL 协议实现代码量只有 Postgres 的一半

第 17.1 提到 MySQL 驱动代码量是 Postgres 的一半——深挖一下为什么:

1. 类型系统简单 - 省 ~3000 行

Postgres 有 400+ 内建类型(INT4/INT8/TEXT/BYTEA/JSON/JSONB/UUID/INET/CIDR/MACADDR/POINT/LINE/LSEG/BOX/PATH/POLYGON/CIRCLE/TSQUERY/TSVECTOR/XML/JSON... 等等)。sqlx-postgres/src/types/ 共 50 个文件、约 8900 行处理这些类型。

MySQL 的内建类型少得多(Integer / Float / Decimal / String / Binary / Date/Time 系列 + 少量 JSON)。sqlx-mysql/src/types/ 约 2600 行——仅 Postgres 的 30%。差 ~6200 行。

2. 无协议扩展 - 省 ~1400 行

Postgres 的 copy.rs + listener.rs + advisory_lock.rs + message/copy.rs 合计约 1430 行。MySQL 无对应、直接省掉。

3. 元数据查询简单 - 省 ~700 行

Postgres 的 nullability 推断要查 pg_attribute 系统表(第 11 章 §11.7)——connection/describe.rs 独占 681 行。MySQL 没有同等级 nullability 推断——describe 代码简短。

4. 消息数少 - 省 ~800 行

Postgres 26 个消息文件 / message 目录总共约 2300 行。MySQL 协议消息少、protocol/ 目录约 1550 行。差 ~750 行。

5. 连接/执行状态机稍简

Postgres connection/ 目录(含 describe)约 2400 行,MySQL connection/ 约 1300 行——差 ~1100 行

五方面合计 ~6200 + 1400 + 700 + 750 + 1100 ≈ 10150 行——对应实际的 19841 − 9195 ≈ 10650 行 gap(500 行落在代码注释 / 测试 / 细碎差异上)。

这条分析给你的启示DB 驱动代码量主要由 DB 本身的能力复杂度决定、不是客户端的设计复杂度。Postgres 代码多是因为 Postgres 功能多、sqlx 要一一支持。

17.21 MySQL 驱动演进历史

sqlx-mysql 的几个关键演进:

  • 0.1(2019):首版 MySQL 支持、只支持 mysql_native_password。
  • 0.3:caching_sha2_password 加入——能连 MySQL 8 默认账号。
  • 0.5:MariaDB 正式测试通过、加 mariadb:// URL scheme。
  • 0.7:GAT 后代码重构、和 sqlx-postgres 对齐结构。
  • 0.8(本书版本):细节稳定、type 支持继续加(GEOMETRY / UUID 等)。
  • 0.9-alpha:预告 vector type 支持。

演进速度比 sqlx-postgres 慢一点——反映 MySQL 用户在 sqlx 生态数量少于 Postgres 用户。这是事实观察——Rust + MySQL 组合不如 Rust + Postgres 普遍,因为后者在 Rust 社区认同度更高(Postgres 更"现代"、JSON 支持强、类型丰富)。

MySQL 用户绝对数量依然可观——企业仍然有大量 MySQL 部署、sqlx-mysql 维护质量足以支持生产。新贡献者要加新类型或新特性都有清晰路径。

17.22 本章的三条概念 take-away

浓缩本章:

1. MySQL 和 Postgres 协议结构相似但语义不同——握手顺序、消息封装、认证机制、类型系统都有差异——"sqlx 屏蔽了这些差异、用户写同一套 Rust 代码跑两家"是核心价值。

2. COM_STMT 和 Extended Query 语义等价、细节不同——PREPARE + EXECUTE + CLOSE 是 MySQL 的 prepared 三件套、对应 Postgres 的 Parse + Bind + Execute + Close + Sync。协议级别 MySQL 消息少一些、性能相近。

3. status_flags 是 MySQL 的"实时状态广播"——每响应都带、sqlx 实时跟踪事务状态。这和 Postgres 的 "查询结束一次给" 不同——MySQL 信息更密集但要更多 bookkeeping。

掌握这三条后、你遇到 sqlx-mysql 的任何问题都有基础线索追踪——协议层级的理解让你从"盲调 config"升级到"有根据调参"。

下一章 SQLite 是另一个世界——C FFI + worker 线程 + 没有网络——完全不同的工程题。读完三家驱动、你对 sqlx 的"跨 DB 抽象"能力有全面理解。

17.23 MySQL 协议值得敬重之处

虽然本章经常拿 MySQL 和 Postgres 对比、有时 Postgres 看起来"更好"——但 MySQL 协议也有 值得敬重的设计

1. LENENC 变长编码——短值紧凑、长值可扩展——Postgres 的固定 4 字节长度前缀浪费。在高并发传参场景 LENENC 能省带宽。

2. 包序号——TCP 时代就考虑了消息顺序问题、给协议加了防护层。即使现代 TCP 有序、这条防护在极端情况(如内核 bug)下仍然有用。

3. status_flags 的实时信息——每响应带事务状态,客户端更新成本低。Postgres 要等 ReadyForQuery 才能更新、短时间内的状态变化(自动 rollback)发现晚一点。

4. COM_QUERY 的纯文本协议——虽然比二进制慢、但调试极其友好。tcpdump 抓包直接看 UTF-8 文本——ops 人员不用特殊工具就能看懂。

5. AUTO_INCREMENT + last_insert_id 简洁——比 Postgres 的 RETURNING 语法更直观(虽然 RETURNING 更通用)。

这些 MySQL 协议的优点不改变 "现代用 Postgres 更好" 的一般建议——但让你尊重 MySQL 的设计 heritage——1990 年代就想到很多持续有效的设计——不简单。

17.24 测试 sqlx-mysql 的建议

如果你要用 sqlx-mysql、推荐测试策略:

1. Docker 起 MySQL 8——不要用老版本测——新版本的 caching_sha2 / UTF-8 才是生产真实配置。

bash
docker run -d --name mysql-test -e MYSQL_ROOT_PASSWORD=test -p 3306:3306 mysql:8.4

2. 同时测 MariaDB 最新版(如果业务要支持):

bash
docker run -d --name mariadb-test -e MARIADB_ROOT_PASSWORD=test -p 3307:3306 mariadb:11

3. 用 sqlx 的集成测试 feature——sqlx::test 注解管理 per-test 数据库:

rust
#[sqlx::test]
async fn test_create_user(pool: MySqlPool) {
    let r = sqlx::query("INSERT INTO users (name) VALUES (?)")
        .bind("Alice").execute(&pool).await.unwrap();
    assert_eq!(r.last_insert_id(), 1);
}

sqlx 的 test attribute 为每个测试创建独立 schema、测试完清理——避免测试之间干扰。

4. Schema migration 用 sqlx-cli——cargo sqlx migrate 管理 SQL 迁移文件、集成测试启动时自动跑。

5. 性能测试用 pgbench-mysql / sysbench——跑实际负载、对比 sqlx 和其他驱动(比如 mysql_async)。

这五条测试建议让你的 sqlx-mysql 代码在CI 里对 MySQL 和 MariaDB 都验证——避免生产才发现方言差异。

17.25 从 sqlx-mysql 学到的工程技巧

本章虽然表面讲 MySQL、但里面有几条Rust 工程技巧值得单独总结——放在任何 Rust 项目里都有用:

1. Bitflags 用来表达协议状态——Status: u16 带多个 flag。sqlx 用 bitflags! 宏(sqlx-mysql/src/protocol/response/status.rs)表达——比多个 bool 字段紧凑、比 u16 原始值直观

2. LENENC 编码用 helper 函数统一——sqlx-mysql/src/io/ 目录有通用的 BufMutExt::put_lenencBufExt::get_lenenc——所有需要 LENENC 的地方调 helper、不重复实现。

3. 协议包的 Newtype pattern——每个消息类型是独立 struct(Query, StatementExecute 等)、不共用 enum variant。类型安全、独立 encode 逻辑、易扩展

4. StatementCache<(u32, MySqlStatementMetadata)>——cache value 是 tuple 而不是 struct——简单场景用 tuple 避免引入一层 struct。

5. waiting: VecDeque<Waiting>——stream 内部队列跟踪"正在等什么响应"。每条 send_packet 后 push、recv_packet 后 pop。这是把异步响应模式显式化的好做法。

这五条技巧具体、可迁移——你在自己的 Rust 项目里处理 "有状态 + 异步协议 + 多消息类型" 场景时都用得上。

17.26 MySQL 驱动的一个观察

最后一点观察——sqlx-mysql 的维护者相对较少。相比 sqlx-postgres 有多位活跃贡献者、sqlx-mysql 主要靠 1-2 个 maintainer。这反映:

  • Rust 社区 MySQL 用户少——许多 Rust 后端用 Postgres。
  • 新类型贡献动力低——没有 pgvector 这种"新鲜功能"驱动贡献。
  • 企业用户多但贡献少——企业 MySQL 用户多、但很多团队内部 fork sqlx 改、不提 PR。

这对用户的含义——sqlx-mysql 的 bug 修复速度可能慢于 Postgres——生产里遇到问题自己准备 patch 提 PR 是常见解法。好在协议基本稳定、bug 不会爆发性出现。

这也说明开源项目的生态健康度用户活跃度相关——sqlx-postgres 社区更活跃是正反馈(更多人用 → 更多贡献 → 更好质量 → 更多人用)。sqlx-mysql 处于稳态——不会死、但增长慢。

实际影响对大多数用户极小——sqlx-mysql 的核心功能已经稳定多年、处理常规业务 CRUD 完全够用。只有你踩上极边缘特性时才会遇到"sqlx-mysql 没做过这个"的困境——那时考虑 fork 修或等社区。

17.27 本章内容的实用价值

读完本章你应该能:

  1. 解释 MySQL wire protocol 的基本结构(长度 + 序号 + 载荷)。
  2. 说出 Handshake V10 和 Postgres Startup 的哲学差异(server-first vs client-first)。
  3. 区分 caching_sha2_password 和 mysql_native_password 的机制。
  4. 知道 COM_QUERY 和 COM_STMT 的使用场景。
  5. 读 sqlx-mysql 的 run 方法并理解每个分支。
  6. 说出 LENENC 编码的优势和使用场景。
  7. 理解 MySQL 的 status_flags 和 Postgres 的 ReadyForQuery 的信息差异。
  8. 知道 MySQL 和 MariaDB 的兼容性 + 差异点。
  9. 用 sqlx-mysql 写出生产级 Rust 代码(配合第 12-15 章的 Pool / Transaction 知识)。
  10. 在 MySQL 和 Postgres 间做技术选型(本书讨论过两家的所有维度)。

这些能力让你成为 Rust 后端 MySQL 专家——不只是"会写代码"、而是"懂协议"+"懂生态"+"懂生产"的全面技能。

下一章 SQLite 驱动会很有趣——协议完全不同(无网络、C FFI)、实现完全不同(OS 线程 + channel)——但在 sqlx 统一的 Executor trait 下对用户同样是 sqlx::query(...).execute(&pool).await?——抽象的力量兑现。

17.28 三家驱动的系统全景

到本章为止、我们讲了 Postgres(第 16 章)和 MySQL(本章)两家驱动——还剩 SQLite 和 Any。一张表预告四家的整体风格:

维度sqlx-postgressqlx-mysqlsqlx-sqlitesqlx-core::Any
通信方式TCP/TLSTCP/TLSC FFI上层 trait object
代码量19841919510061~1500(sqlx-core/any/)
典型用途生产 web生产 web本地/测试/嵌入跨 DB 工具
是否走网络看底层驱动
有 worker 线程?否(代理)
支持 LISTEN
类型复杂度最低(交集)

看这张表就能猜到 SQLite 驱动的独特性——worker 线程C FFI 两项让它和前两家完全不同。代码量接近 MySQL 但走完全不同的路径。

读完这四家驱动你对 "三家主流 DB 在 Rust 的实现" 有完整视角——在一家里学到的协议 / 类型 / 握手知识、足以评估选哪家 DB 的技术层面差异。本书到这里开始兑现**"读完能独立做技术选型"**的承诺。

17.29 MySQL 协议对 Rust 工程的启示

从 sqlx-mysql 的9195 行代码里能提炼出几条Rust 工程的通用教训

1. 二进制协议适合 Rust——强类型 + 无 GC + 零成本抽象——Rust 实现协议栈效率高、bug 少。MySQL 协议在 Rust 里跑得和 C 接近。

2. 巨 struct vs 小 struct 的选择——MySQL 的 Handshake V10 有 10+ 字段——sqlx-mysql 用一个 struct 表达、而不是拆成子 struct。"协议包一对一对应 Rust struct"最直观。

3. bitflags 是协议位图的完美工具——比 enum + match 简洁、比原始整数可读。

4. 异步状态机用 waiting: VecDeque<Waiting> 显式化——告诉你 "接下来期望什么样的响应" ——比隐式状态管理清晰。

5. 协议测试用 "发预期字节 + 收预期字节" mocking——sqlx-mysql 的 unit test 里有一些这种模式、对调试协议 bug 有用。

这五条 Rust 协议实现的教训——你在 "自己实现任何二进制协议" 时都能用。不只是 SQL driver——HTTP/2、gRPC、Kafka、Redis——底层都有类似需求。

17.30 Text vs Binary:run() 里的双协议分派

MySQL wire 协议有两套查询方式——sqlx-mysql 的 run() 在同一函数里分派(sqlx-mysql/src/connection/executor.rs:120-157):

rust
let (mut column_names, format, mut needs_metadata) = if let Some(arguments) = arguments {
    if persistent && self.inner.cache_statement.is_enabled() {
        let (id, metadata) = self.get_or_prepare_statement(sql).await?;
        self.inner.stream.send_packet(StatementExecute {
            statement: id,
            arguments: &arguments,
        }).await?;
        (metadata.column_names, MySqlValueFormat::Binary, false)
    } else {
        let (id, metadata) = self.prepare_statement(sql).await?;
        self.inner.stream.send_packet(StatementExecute { ... }).await?;
        self.inner.stream.send_packet(StmtClose { statement: id }).await?;
        (metadata.column_names, MySqlValueFormat::Binary, false)
    }
} else {
    self.inner.stream.send_packet(Query(sql)).await?;
    (Arc::default(), MySqlValueFormat::Text, true)
};

两大分支

  • 有 arguments——走 COM_STMT_EXECUTE(binary protocol)—— 先 PREPARE、再 EXECUTE、值以 binary 编码、效率高。
  • 无 arguments——走 COM_QUERY(text protocol)—— SQL 作为纯文本发送、结果 text 返回、适合 one-shot DDL/SELECT。

三个子分支

  1. 有 arguments + persistent + cache enabled—— get_or_prepare_statement(查缓存或 PREPARE 一次永驻)—— binary 格式。
  2. 有 arguments + 非 persistent 或 cache disabled—— prepare_statement + 立刻 StmtClose—— binary 格式。
  3. 无 arguments——Query(sql) —— text 格式。

needs_metadata = true 仅在 text 路径—— COM_QUERY 不事先 PREPARE、服务器返回 ResultSet 时才带 column metadata;binary 路径 PREPARE 阶段已拿 metadata、result row 不重发——省带宽。

行解码也分派:230-233):

rust
let row = match format {
    MySqlValueFormat::Binary => packet.decode_with::<BinaryRow, _>(&columns)?.0,
    MySqlValueFormat::Text => packet.decode_with::<TextRow, _>(&columns)?.0,
};

BinaryRow 和 TextRow 是两个 struct——整数 42 在 text 里是字符串 "42"、binary 里是 4 字节小端——解码完全不同。

这套双协议分派是 MySQL 独特的复杂度—— Postgres 只有一套 extended query—— sqlx-mysql 的 run() 因此多出一倍分支——读源码时特别要分清自己在看哪条路径

17.31 StmtClose:非 persistent 路径的资源回收

非 persistent 路径里有一条容易忽略但关键的语句:

rust
self.inner.stream.send_packet(StmtClose { statement: id }).await?;

含义——每次 query 都 PREPARE 一次、立即 StmtClose 释放 DB 端 statement handle—— 执行完就释放——DB 端不留残骸。

顺序不能颠倒—— StmtClose 必须等 StatementExecute 的响应接收完才能发——否则服务器端还在用这个 statement、被 Close 会报错。sqlx 的做法:同步发 Execute、同步收响应、然后发 Close——串行顺序保护正确性。

persistent 路径不发 StmtClose—— statement 留在客户端的 cache_statement 里、下次复用—— LRU 淘汰时才发 Close(见第 22 章 PREPARE cache)——资源生命周期和 cache 绑定。

query!() 宏默认 persistent = true—— 99% 用户走缓存路径;手动 .persistent(false) 才走立即释放路径。

下一章 SQLite 驱动——换轨到完全不同的路径:worker 线程 + C FFI。

17.32 四种认证插件的 enum 承载

MySQL 历史上出过四种密码哈希方案——都需要客户端识别、算对响应。sqlx-mysql 的 AuthPlugin enum(sqlx-mysql/src/protocol/auth.rs:8-13)把它们枚出来:

rust
pub enum AuthPlugin {
    MySqlNativePassword,
    CachingSha2Password,
    Sha256Password,
    MySqlClearPassword,
}

四种含义

  • mysql_native_password—— MySQL 5.x 默认、SHA-1 哈希——长期存在、简单但已不推荐。
  • caching_sha2_password—— MySQL 8.0 默认—— SHA-256 + 服务器端缓存—— 更强、兼容性是 MySQL 8.x 的默认坑。
  • sha256_password—— 中间过渡品—— SHA-256 但无缓存——需 SSL/TLS 或 RSA 公钥交换——少见。
  • mysql_clear_password—— 明文发送——只在专门的 PAM 等上下文用—— 不加密通道绝不用。

FromStr 实现:26-39)—— 从服务器 Handshake 里解析出字符串插件名、映射到 enum——未知插件返回 err_protocol!("unknown authentication plugin: {}", s)——保守失败(MySQL 生态偶尔加新插件、sqlx 宁可报错也不瞎猜)。

生产坑——MySQL 8.0 默认 caching_sha2_password、而老客户端假设 mysql_native_password—— 典型症状 "用 sqlx 连 MySQL 8.0 报 authentication method unknown"——解决:

sql
ALTER USER 'yourapp'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

或者升级 sqlx 到支持 caching_sha2 的版本(sqlx 0.4+ 已支持)。

这个 enum 的存在 反映 sqlx-mysql 对 MySQL 生态碎片化的完整覆盖——不是只支持主流版本、而是把历史上合法存在过的插件都支持——体现生产库的兼容诚意。

17.33 LENENC 整数:MySQL 的变长编码

MySQL 协议几乎所有整数字段都用 length-encoded integer(LENENC)——一种 1/3/4/9 字节变长编码:

第一字节后续字节范围
0x00-0xFA0直接就是值(0-250)
0xFBNULL 标识(仅 row 里有意义)
0xFC2 bytes251-65535
0xFD3 bytes65536-16M
0xFE8 bytes> 16M

设计目的——绝大多数字段值 < 250(列数、参数数、字符串长度)—— 1 字节够—— 节省 90% 字段。极大值有备选路径。

和 Postgres 对比—— Postgres 多数整数是固定 4 字节——简单但浪费——MySQL 选择用 8 倍的复杂度换 2-4 倍的平均带宽节省——协议设计哲学的差异。

sqlx-mysql 把 LENENC 读取封装成方法(packet.get_uint_lenenc())—— 外层代码只关心逻辑整数、不关心编码细节——抽象做对——调用点写起来和读 u32 一样简单。

17.34 和《Hyper 与 Tower:工业级 HTTP 栈》第11章的呼应

MySQL 的 "packet → body Decoder → row 事件流" 管线、对应《Hyper 与 Tower:工业级 HTTP 栈》第 11 章 §11.3 body Decoder:三种长度语义§11.5 Http1Transaction:Server/Client 的对称抽象——两者同一个模式

  • hyper Http1Transaction ↔ sqlx-mysql MySqlStream——都是"以状态机驱动的 wire 协议 transactor"。
  • hyper §11.3 讲的 Decoder 三种长度语义(Content-Length / chunked / EOF)↔ MySQL 的 "OK/ERR/ResultSet/LocalInfile 四种 meta-packet 分派"(sqlx-mysql/src/connection/executor.rs:160-161)—— 都是**"第一字节就是 tag、决定后续 parse 形态"**。
  • hyper 的 httparse SIMD parser(§11.2)↔ sqlx-mysql 的手写 Buf::get_* 读取—— 一个面向文本协议、一个面向二进制协议——但**"解析失败 = bubble 到上层 Error"**的机制相同。

关键差异—— HTTP/1 是文本协议、MySQL 是二进制协议;HTTP/1 每个 request/response 一对一、MySQL 一个连接可以流水多个结果集Status::SERVER_MORE_RESULTS_EXISTS)。从第 11 章的 HTTP transactor 迁移过来理解 MySQL 的 run()——协议复杂度多一层、但心智模型不变

如果你读完 hyper 第 11 章再看本章——"状态机 + 字节协议 + 异步流"的套路已经嵌入思维、MySQL 的 run() 源码读起来像例行公事。这是深度书系的互相强化——一个领域打通、相邻领域加速。

基于 VitePress 构建