Scert/backend/internal/cli/mysql_routine_encoding.go
xuans 0dcda4f5ed 修复MySQL例程注释编码并完成双版本隔离回归
分离5.7与8.0声明头和注释编码,增加无损往返校验及中文错误上下文,补充五种异常元数据拒绝测试。

最终Windows MySQL 5.7.44与8.0.45完整专项分别通过110、121个叶子用例,零失败零跳过。后端常规回归、静态检查、模块校验及Linux测试包交叉编译通过;32个常规条件测试跳过,不作为通过证据。

同步七份任务、支持矩阵及运维文档;P0-05保持进行中,全部44项范围不变。本批不代表Linux实库、其余数据库、前端或生产验收通过。
2026-09-27 22:30:47 +08:00

91 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 例程声明缺少完整参数列表")
}