Scert/backend/internal/cli/mysql_events.go

114 lines
4.8 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"
"unicode/utf8"
)
func listMySQLEvents(ctx context.Context, tx *sql.Tx) ([]string, error) {
if err := requireMySQLEventVisibility(ctx, tx); err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE() ORDER BY EVENT_NAME")
if err != nil {
return nil, fmt.Errorf("读取 MySQL 事件列表失败:%w", err)
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
return names, rows.Err()
}
func dumpMySQLEvents(ctx context.Context, tx *sql.Tx, output io.Writer, names []string) error {
if len(names) == 0 {
return nil
}
var databaseCollation string
if err := tx.QueryRowContext(ctx, "SELECT @@collation_database").Scan(&databaseCollation); err != nil {
return err
}
if _, err := fmt.Fprintln(output, `SET @scert_event_sql_mode = @@SESSION.sql_mode;
SET @scert_event_charset = @@SESSION.character_set_client;
SET @scert_event_collation = @@SESSION.collation_connection;
SET @scert_event_time_zone = @@SESSION.time_zone;`); err != nil {
return err
}
for _, name := range names {
var expired sql.NullBool
var status, completion string
query := `SELECT CASE
WHEN EXECUTE_AT IS NOT NULL THEN EXECUTE_AT <= CONVERT_TZ(UTC_TIMESTAMP(), '+00:00', TIME_ZONE)
WHEN ENDS IS NOT NULL THEN ENDS <= CONVERT_TZ(UTC_TIMESTAMP(), '+00:00', TIME_ZONE)
ELSE FALSE END, STATUS, ON_COMPLETION
FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE() AND EVENT_NAME = ?`
if err := tx.QueryRowContext(ctx, query, name).Scan(&expired, &status, &completion); err != nil {
return fmt.Errorf("读取 MySQL 事件到期状态失败:%w", err)
}
if !expired.Valid || (expired.Bool && (status == "ENABLED" || completion == "NOT PRESERVE")) {
return fmt.Errorf("MySQL 事件已到期或时区不可验证,重新创建将改变状态或删除对象,拒绝生成不完整备份:%s", name)
}
definition, err := readMySQLStoredDefinition(ctx, tx, "EVENT", name, "Create Event")
if err != nil {
return err
}
zone, exists := definition["time_zone"]
if !exists || zone == "" {
return fmt.Errorf("MySQL 事件时区字段不完整:%s", name)
}
if err := normalizeMySQLEventMetadata(ctx, tx, name, definition); err != nil {
return err
}
if _, err := fmt.Fprintf(output, "SET SESSION time_zone = %s;\n", mysqlStringLiteral(zone)); err != nil {
return err
}
drop := "DROP EVENT IF EXISTS " + quoteMySQLIdentifier(name)
if err := writeMySQLStoredDefinition(ctx, tx, output, definition, "Create Event", drop); err != nil {
return fmt.Errorf("写入 MySQL 事件失败(%s):%w", name, err)
}
}
_, err := fmt.Fprintf(output, "ALTER DATABASE COLLATE %s;\nSET SESSION sql_mode = @scert_event_sql_mode;\nSET SESSION character_set_client = @scert_event_charset;\nSET SESSION collation_connection = @scert_event_collation;\nSET SESSION time_zone = @scert_event_time_zone;\n\n", quoteMySQLIdentifier(databaseCollation))
return err
}
// 两个版本的事件声明头均来自 UTF-8 目录,正文仍使用原客户端编码。
// 先与目录正文比对边界,再单独反解声明头;失败不写回部分转换结果。
func normalizeMySQLEventMetadata(ctx context.Context, tx *sql.Tx, name string, definition map[string]string) error {
charset := definition["character_set_client"]
if charset == "utf8" || charset == "utf8mb3" || charset == "utf8mb4" {
return nil
}
var body string
if err := tx.QueryRowContext(ctx, "SELECT EVENT_DEFINITION FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE() AND EVENT_NAME = ?", name).Scan(&body); err != nil {
return fmt.Errorf("读取 MySQL 事件编码元数据失败:%w", err)
}
original := definition["Create Event"]
if body == "" || !strings.HasSuffix(original, body) {
return fmt.Errorf("MySQL 事件正文与目录元数据不一致,拒绝猜测编码:%s", name)
}
header := original[:len(original)-len(body)]
if !strings.HasSuffix(header, " DO ") {
return fmt.Errorf("MySQL 事件声明头不完整:%s", name)
}
identifier := quoteMySQLIdentifier(charset)
var encoded, roundTrip []byte
query := "SELECT CAST(CONVERT(? USING " + identifier + ") AS BINARY), CAST(CONVERT(CONVERT(? USING " + identifier + ") USING utf8mb4) AS BINARY)"
if err := tx.QueryRowContext(ctx, query, header, header).Scan(&encoded, &roundTrip); err != nil {
return fmt.Errorf("MySQL 事件目录元数据无法无损还原为 UTF-8(%s):%w", name, err)
}
if string(roundTrip) != header || !utf8.Valid(encoded) {
return fmt.Errorf("MySQL 事件目录元数据无法无损还原为 UTF-8:%s", name)
}
definition["Create Event"] = string(encoded) + body
return nil
}