保留存储过程与函数导出、权限检查、编码处理、共享存储对象逻辑及正式测试,同步两份任务进度文档。 已知阻塞:已有MySQL 5.7实库记录中,两种恢复入口均存在Latin-1例程注释乱码;已有8.0编码与权限专项通过,但不能代替最终双版本全量复测。本次仅重新运行例程单元测试及相关包静态检查,均通过。 这是未完成整改的保存检查点,不是发布版本,不表示P0-05完成或生产验收通过。未纳入运行数据、构建产物、原始实库日志或凭据。
95 lines
3.3 KiB
Go
95 lines
3.3 KiB
Go
package cli
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
)
|
||
|
||
func readMySQLStoredDefinition(ctx context.Context, tx *sql.Tx, kind, name, statementColumn string) (map[string]string, error) {
|
||
rows, err := tx.QueryContext(ctx, "SHOW CREATE "+kind+" "+quoteMySQLIdentifier(name))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取 MySQL 存储对象定义失败(%s %s):%w", kind, name, err)
|
||
}
|
||
defer rows.Close()
|
||
columns, err := rows.Columns()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
values := make([]sql.NullString, len(columns))
|
||
pointers := make([]any, len(columns))
|
||
for i := range values {
|
||
pointers[i] = &values[i]
|
||
}
|
||
if !rows.Next() {
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return nil, fmt.Errorf("MySQL 存储对象定义不可见(%s %s)", kind, name)
|
||
}
|
||
if err := rows.Scan(pointers...); err != nil {
|
||
return nil, err
|
||
}
|
||
definition := make(map[string]string, len(columns))
|
||
for i, column := range columns {
|
||
if values[i].Valid {
|
||
definition[column] = values[i].String
|
||
}
|
||
}
|
||
for _, column := range []string{"sql_mode", statementColumn, "character_set_client", "collation_connection", "Database Collation"} {
|
||
value, exists := definition[column]
|
||
if !exists || (value == "" && column != "sql_mode") {
|
||
return nil, fmt.Errorf("MySQL 存储对象定义字段不完整(%s %s):%s", kind, name, column)
|
||
}
|
||
}
|
||
if rows.Next() {
|
||
return nil, fmt.Errorf("MySQL 存储对象定义返回了多行(%s %s)", kind, name)
|
||
}
|
||
return definition, rows.Err()
|
||
}
|
||
|
||
func writeMySQLStoredDefinition(ctx context.Context, tx *sql.Tx, output io.Writer, definition map[string]string, column, drop string) error {
|
||
charset := quoteMySQLIdentifier(definition["character_set_client"])
|
||
encode := func(original string) (string, error) {
|
||
var encoded, roundTrip []byte
|
||
query := "SELECT CAST(CONVERT(? USING " + charset + ") AS BINARY), CAST(CONVERT(CONVERT(? USING " + charset + ") USING utf8mb4) AS BINARY)"
|
||
if err := tx.QueryRowContext(ctx, query, original, original).Scan(&encoded, &roundTrip); err != nil {
|
||
return "", fmt.Errorf("转换 MySQL 存储对象字符集失败:%w", err)
|
||
}
|
||
if string(roundTrip) != original {
|
||
return "", fmt.Errorf("MySQL 存储对象无法无损转换到创建字符集")
|
||
}
|
||
return string(encoded), nil
|
||
}
|
||
// 删除和创建语句均使用原客户端编码,非 ASCII 标识符不能混用 UTF-8 字节。
|
||
encoded, err := encode(definition[column])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
encodedDrop := ""
|
||
if drop != "" {
|
||
encodedDrop, err = encode(drop)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
delimiter := ";;"
|
||
for strings.Contains(encoded, delimiter) || strings.Contains(encodedDrop, delimiter) {
|
||
delimiter += ";"
|
||
}
|
||
if _, err := fmt.Fprintf(output, "ALTER DATABASE COLLATE %s;\nSET SESSION sql_mode = %s;\nSET SESSION character_set_client = %s;\nSET SESSION collation_connection = %s;\nDELIMITER %s\n",
|
||
quoteMySQLIdentifier(definition["Database Collation"]), mysqlStringLiteral(definition["sql_mode"]),
|
||
mysqlStringLiteral(definition["character_set_client"]), mysqlStringLiteral(definition["collation_connection"]), delimiter); err != nil {
|
||
return err
|
||
}
|
||
if encodedDrop != "" {
|
||
if _, err := fmt.Fprintf(output, "%s%s\n", encodedDrop, delimiter); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
_, err = fmt.Fprintf(output, "%s%s\nDELIMITER ;\n", encoded, delimiter)
|
||
return err
|
||
}
|