分离5.7与8.0声明头和注释编码,增加无损往返校验及中文错误上下文,补充五种异常元数据拒绝测试。 最终Windows MySQL 5.7.44与8.0.45完整专项分别通过110、121个叶子用例,零失败零跳过。后端常规回归、静态检查、模块校验及Linux测试包交叉编译通过;32个常规条件测试跳过,不作为通过证据。 同步七份任务、支持矩阵及运维文档;P0-05保持进行中,全部44项范围不变。本批不代表Linux实库、其余数据库、前端或生产验收通过。
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
package cli
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"strings"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
// 两个版本的目录注释均需单独还原,8.0 还需还原声明头;
|
||
// 非 UTF-8 例程必须分段处理,不能整体反解原客户端编码的正文。
|
||
func normalizeMySQLRoutineMetadata(ctx context.Context, tx *sql.Tx, routine mysqlRoutine, definition map[string]string, column string) error {
|
||
charset := definition["character_set_client"]
|
||
if charset == "utf8" || charset == "utf8mb3" || charset == "utf8mb4" {
|
||
return nil
|
||
}
|
||
original := definition[column]
|
||
end, err := mysqlRoutineHeaderEnd(original)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
decode := func(value string) (string, error) {
|
||
var encoded, roundTrip []byte
|
||
identifier := quoteMySQLIdentifier(charset)
|
||
query := "SELECT CAST(CONVERT(? USING " + identifier + ") AS BINARY), CAST(CONVERT(CONVERT(? USING " + identifier + ") USING utf8mb4) AS BINARY)"
|
||
if err := tx.QueryRowContext(ctx, query, value, value).Scan(&encoded, &roundTrip); err != nil {
|
||
return "", fmt.Errorf("MySQL 例程目录元数据无法无损还原为 UTF-8(%s):%w", routine.name, err)
|
||
}
|
||
if string(roundTrip) != value || !utf8.Valid(encoded) {
|
||
return "", fmt.Errorf("MySQL 例程目录元数据无法无损还原为 UTF-8:%s", routine.name)
|
||
}
|
||
return string(encoded), nil
|
||
}
|
||
header := original[:end]
|
||
if routine.utf8MetadataHeader {
|
||
header, err = decode(header)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
var comment string
|
||
var body sql.NullString
|
||
if err := tx.QueryRowContext(ctx, `SELECT ROUTINE_COMMENT, ROUTINE_DEFINITION FROM information_schema.ROUTINES
|
||
WHERE ROUTINE_SCHEMA = DATABASE() AND ROUTINE_NAME = ? AND ROUTINE_TYPE = ?`, routine.name, routine.kind).Scan(&comment, &body); err != nil {
|
||
return fmt.Errorf("读取 MySQL 例程编码元数据失败:%w", err)
|
||
}
|
||
rest := original[end:]
|
||
if comment != "" {
|
||
if !body.Valid || body.String == "" || !strings.HasSuffix(rest, body.String) {
|
||
return fmt.Errorf("MySQL 例程正文与目录元数据不一致,拒绝猜测编码:%s", routine.name)
|
||
}
|
||
metadata := rest[:len(rest)-len(body.String)]
|
||
// 注释位于声明选项的末尾;先隔离正文,避免误改业务 SQL 中的 COMMENT 文本。
|
||
start := strings.LastIndex(metadata, "\n COMMENT ")
|
||
if start < 0 {
|
||
return fmt.Errorf("MySQL 例程注释声明不完整:%s", routine.name)
|
||
}
|
||
decoded, err := decode(metadata[start:])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
rest = metadata[:start] + decoded + body.String
|
||
}
|
||
definition[column] = header + rest
|
||
return nil
|
||
}
|
||
|
||
func mysqlRoutineHeaderEnd(statement string) (int, error) {
|
||
var quote byte
|
||
for i := 0; i < len(statement); i++ {
|
||
current := statement[i]
|
||
if quote != 0 {
|
||
if current == quote {
|
||
if i+1 < len(statement) && statement[i+1] == quote {
|
||
i++
|
||
} else {
|
||
quote = 0
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
if current == '`' || current == '"' || current == '\'' {
|
||
quote = current
|
||
} else if current == '(' {
|
||
return i, nil
|
||
}
|
||
}
|
||
return 0, fmt.Errorf("MySQL 例程声明缺少完整参数列表")
|
||
}
|