保留存储过程与函数导出、权限检查、编码处理、共享存储对象逻辑及正式测试,同步两份任务进度文档。 已知阻塞:已有MySQL 5.7实库记录中,两种恢复入口均存在Latin-1例程注释乱码;已有8.0编码与权限专项通过,但不能代替最终双版本全量复测。本次仅重新运行例程单元测试及相关包静态检查,均通过。 这是未完成整改的保存检查点,不是发布版本,不表示P0-05完成或生产验收通过。未纳入运行数据、构建产物、原始实库日志或凭据。
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
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 例程声明缺少完整参数列表")
|
||
}
|