Scert/backend/internal/cli/mysql_stored_definition.go
xuans 777c7169b6 完善MySQL视图备份恢复及完整清单权限校验
保留原始视图定义与编码,按依赖排序导出,并严格识别和重排官方最终视图块;增加运行上下文和权限检查。补充视图、清单、恢复失败保护测试,并修正旧事件、触发器和例程测试的权限前提。

隔离MySQL 5.7和8.0完整专项分别通过176和202个叶子用例,无失败或跳过;后端常规全量、静态检查、模块校验及Linux测试包交叉编译通过。测试产物和凭据未提交。本提交不代表生产部署验收完成。
2026-09-28 00:35:26 +08:00

99 lines
3.4 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"
"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
}
}
required := []string{"sql_mode", statementColumn, "character_set_client", "collation_connection", "Database Collation"}
if kind == "VIEW" {
required = []string{statementColumn, "character_set_client", "collation_connection"}
}
for _, column := range required {
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
}