Scert/backend/internal/cli/mysql_routine_encoding.go
xuans c4051c9166 保存MySQL例程备份整改检查点(验证未完成)
保留存储过程与函数导出、权限检查、编码处理、共享存储对象逻辑及正式测试,同步两份任务进度文档。

已知阻塞:已有MySQL 5.7实库记录中,两种恢复入口均存在Latin-1例程注释乱码;已有8.0编码与权限专项通过,但不能代替最终双版本全量复测。本次仅重新运行例程单元测试及相关包静态检查,均通过。

这是未完成整改的保存检查点,不是发布版本,不表示P0-05完成或生产验收通过。未纳入运行数据、构建产物、原始实库日志或凭据。
2026-09-27 22:16:23 +08:00

87 lines
2.7 KiB
Go
Raw 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"
)
// MySQL 8.0 将 UTF-8 目录中的声明头和注释与原客户端编码的正文拼接,
// 再按客户端字符集返回 SHOW CREATE;非 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 []byte
query := "SELECT CAST(CONVERT(? USING " + quoteMySQLIdentifier(charset) + ") AS BINARY)"
if err := tx.QueryRowContext(ctx, query, value).Scan(&encoded); err != nil {
return "", err
}
if !utf8.Valid(encoded) {
return "", fmt.Errorf("MySQL 例程目录元数据无法无损还原为 UTF-8:%s", routine.name)
}
return string(encoded), nil
}
header, err := decode(original[:end])
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 例程声明缺少完整参数列表")
}