119 lines
4.5 KiB
Go
119 lines
4.5 KiB
Go
package cli
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
)
|
||
|
||
func dumpMySQLTriggers(ctx context.Context, tx *sql.Tx, output io.Writer) error {
|
||
// 名称排序不能代表执行顺序;同一表、时机和事件按服务器的 ACTION_ORDER 导出。
|
||
rows, err := tx.QueryContext(ctx, `SELECT TRIGGER_NAME FROM information_schema.TRIGGERS
|
||
WHERE TRIGGER_SCHEMA = DATABASE()
|
||
ORDER BY EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORDER`)
|
||
if err != nil {
|
||
return fmt.Errorf("读取 MySQL 触发器列表失败:%w", err)
|
||
}
|
||
var names []string
|
||
for rows.Next() {
|
||
var name string
|
||
if err := rows.Scan(&name); err != nil {
|
||
_ = rows.Close()
|
||
return fmt.Errorf("读取 MySQL 触发器名称失败:%w", err)
|
||
}
|
||
names = append(names, name)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
_ = rows.Close()
|
||
return fmt.Errorf("遍历 MySQL 触发器失败:%w", err)
|
||
}
|
||
if err := rows.Close(); err != nil {
|
||
return err
|
||
}
|
||
if len(names) == 0 {
|
||
return nil
|
||
}
|
||
var databaseCollation string
|
||
if err := tx.QueryRowContext(ctx, "SELECT @@collation_database").Scan(&databaseCollation); err != nil {
|
||
return fmt.Errorf("读取 MySQL 数据库排序规则失败:%w", err)
|
||
}
|
||
if _, err := fmt.Fprintln(output, `SET @scert_trigger_sql_mode = @@SESSION.sql_mode;
|
||
SET @scert_trigger_charset = @@SESSION.character_set_client;
|
||
SET @scert_trigger_collation = @@SESSION.collation_connection;`); err != nil {
|
||
return err
|
||
}
|
||
for _, name := range names {
|
||
definition, err := readMySQLTrigger(ctx, tx, name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 文件中的创建语句使用原客户端字符集,恢复时才能同时保留正文和创建元数据。
|
||
var encoded, roundTrip []byte
|
||
charset := quoteMySQLIdentifier(definition["character_set_client"])
|
||
query := "SELECT CAST(CONVERT(? USING " + charset + ") AS BINARY), CAST(CONVERT(CONVERT(? USING " + charset + ") USING utf8mb4) AS BINARY)"
|
||
original := definition["SQL Original Statement"]
|
||
if err := tx.QueryRowContext(ctx, query, original, original).Scan(&encoded, &roundTrip); err != nil {
|
||
return fmt.Errorf("转换 MySQL 触发器字符集失败(%s):%w", name, err)
|
||
}
|
||
if string(roundTrip) != original {
|
||
return fmt.Errorf("MySQL 触发器无法无损转换到创建字符集(%s)", name)
|
||
}
|
||
delimiter := ";;"
|
||
for strings.Contains(string(encoded), delimiter) {
|
||
delimiter += ";"
|
||
}
|
||
_, 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%s%s\nDELIMITER ;\n",
|
||
quoteMySQLIdentifier(definition["Database Collation"]), mysqlStringLiteral(definition["sql_mode"]),
|
||
mysqlStringLiteral(definition["character_set_client"]), mysqlStringLiteral(definition["collation_connection"]), delimiter, encoded, delimiter)
|
||
if err != nil {
|
||
return fmt.Errorf("写入 MySQL 触发器失败(%s):%w", name, err)
|
||
}
|
||
}
|
||
_, err = fmt.Fprintf(output, "ALTER DATABASE COLLATE %s;\nSET SESSION sql_mode = @scert_trigger_sql_mode;\nSET SESSION character_set_client = @scert_trigger_charset;\nSET SESSION collation_connection = @scert_trigger_collation;\n\n", quoteMySQLIdentifier(databaseCollation))
|
||
return err
|
||
}
|
||
|
||
func readMySQLTrigger(ctx context.Context, tx *sql.Tx, name string) (map[string]string, error) {
|
||
rows, err := tx.QueryContext(ctx, "SHOW CREATE TRIGGER "+quoteMySQLIdentifier(name))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取 MySQL 触发器定义失败(%s):%w", 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)", 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", "SQL Original Statement", "character_set_client", "collation_connection", "Database Collation"} {
|
||
value, exists := definition[column]
|
||
if !exists || (value == "" && column != "sql_mode") {
|
||
return nil, fmt.Errorf("MySQL 触发器定义字段不完整(%s):%s", name, column)
|
||
}
|
||
}
|
||
if rows.Next() {
|
||
return nil, fmt.Errorf("MySQL 触发器定义返回了多行(%s)", name)
|
||
}
|
||
return definition, rows.Err()
|
||
}
|