支持MySQL官方存储对象备份的分隔符恢复
识别DELIMITER客户端指令,保留复合语句及可执行注释,词法格式错误时拒绝执行部分备份。新增双版本官方导出对照与Scert恢复实库测试,验证触发器顺序、过程、函数、事件调度、权限失败及格式错误保护。仅覆盖本批恢复场景,自有导出对象完整性与生产验收仍未完成。
This commit is contained in:
parent
67070596fc
commit
e6e67197f6
@ -323,7 +323,11 @@ func restoreMySQL(ctx context.Context, cfg config.DatabaseConfig, input string)
|
|||||||
return fmt.Errorf("获取 MySQL 恢复会话失败:%w", err)
|
return fmt.Errorf("获取 MySQL 恢复会话失败:%w", err)
|
||||||
}
|
}
|
||||||
defer connection.Close()
|
defer connection.Close()
|
||||||
for _, statement := range splitMySQLStatements(string(script)) {
|
statements, err := splitMySQLStatements(string(script))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, statement := range statements {
|
||||||
if _, err := connection.ExecContext(ctx, statement); err != nil {
|
if _, err := connection.ExecContext(ctx, statement); err != nil {
|
||||||
return fmt.Errorf("执行 MySQL 恢复语句失败:%w", err)
|
return fmt.Errorf("执行 MySQL 恢复语句失败:%w", err)
|
||||||
}
|
}
|
||||||
@ -331,9 +335,11 @@ func restoreMySQL(ctx context.Context, cfg config.DatabaseConfig, input string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitMySQLStatements(script string) []string {
|
func splitMySQLStatements(script string) ([]string, error) {
|
||||||
|
script = strings.TrimPrefix(script, "\uFEFF")
|
||||||
var statements []string
|
var statements []string
|
||||||
var statement strings.Builder
|
var statement strings.Builder
|
||||||
|
delimiter := ";"
|
||||||
var quote byte
|
var quote byte
|
||||||
lineComment := false
|
lineComment := false
|
||||||
blockComment := false
|
blockComment := false
|
||||||
@ -392,6 +398,32 @@ func splitMySQLStatements(script string) []string {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (index == 0 || script[index-1] == '\n') && strings.TrimSpace(statement.String()) == "" {
|
||||||
|
end := strings.IndexByte(script[index:], '\n')
|
||||||
|
if end < 0 {
|
||||||
|
end = len(script) - index
|
||||||
|
}
|
||||||
|
line := strings.TrimSpace(script[index : index+end])
|
||||||
|
if len(line) >= len("delimiter") && strings.EqualFold(line[:len("delimiter")], "delimiter") &&
|
||||||
|
(len(line) == len("delimiter") || line[len("delimiter")] == ' ' || line[len("delimiter")] == '\t') {
|
||||||
|
value := strings.TrimSpace(line[len("delimiter"):])
|
||||||
|
if len(value) >= 2 && (value[0] == '\'' || value[0] == '"') && value[len(value)-1] == value[0] {
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
if value == "" || strings.ContainsAny(value, " \t\r\n") || strings.ContainsRune(value, '\\') || strings.ContainsRune(value, '\'') || strings.ContainsRune(value, '"') || strings.ContainsRune(value, '`') {
|
||||||
|
return nil, fmt.Errorf("MySQL 备份中的 DELIMITER 指令无效")
|
||||||
|
}
|
||||||
|
delimiter = value
|
||||||
|
statement.Reset()
|
||||||
|
index += end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(script[index:], delimiter) {
|
||||||
|
flush()
|
||||||
|
index += len(delimiter) - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
if current == '\'' || current == '"' || current == '`' {
|
if current == '\'' || current == '"' || current == '`' {
|
||||||
quote = current
|
quote = current
|
||||||
statement.WriteByte(current)
|
statement.WriteByte(current)
|
||||||
@ -418,12 +450,11 @@ func splitMySQLStatements(script string) []string {
|
|||||||
index++
|
index++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if current == ';' {
|
|
||||||
flush()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
statement.WriteByte(current)
|
statement.WriteByte(current)
|
||||||
}
|
}
|
||||||
|
if quote != 0 || blockComment || executableComment {
|
||||||
|
return nil, fmt.Errorf("MySQL 备份包含未闭合的字符串、标识符或注释")
|
||||||
|
}
|
||||||
flush()
|
flush()
|
||||||
return statements
|
return statements, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,10 @@ func TestMySQLColumnSQLLiteral(t *testing.T) {
|
|||||||
|
|
||||||
func TestSplitMySQLStatementsKeepsQuotedSemicolons(t *testing.T) {
|
func TestSplitMySQLStatementsKeepsQuotedSemicolons(t *testing.T) {
|
||||||
script := "-- 注释\nSET FOREIGN_KEY_CHECKS=0;\nINSERT INTO `items` (`value`) VALUES ('a; b'), ('it''s');\n"
|
script := "-- 注释\nSET FOREIGN_KEY_CHECKS=0;\nINSERT INTO `items` (`value`) VALUES ('a; b'), ('it''s');\n"
|
||||||
statements := splitMySQLStatements(script)
|
statements, err := splitMySQLStatements(script)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if len(statements) != 2 {
|
if len(statements) != 2 {
|
||||||
t.Fatalf("SQL 语句拆分数量不正确:%d %#v", len(statements), statements)
|
t.Fatalf("SQL 语句拆分数量不正确:%d %#v", len(statements), statements)
|
||||||
}
|
}
|
||||||
@ -75,7 +78,10 @@ func TestSplitMySQLStatementsKeepsQuotedSemicolons(t *testing.T) {
|
|||||||
|
|
||||||
func TestSplitMySQLStatementsKeepsExecutableComments(t *testing.T) {
|
func TestSplitMySQLStatementsKeepsExecutableComments(t *testing.T) {
|
||||||
script := "CREATE TABLE `items` (`id` bigint) /*!50100 SET @value = 'a;b' */;\n"
|
script := "CREATE TABLE `items` (`id` bigint) /*!50100 SET @value = 'a;b' */;\n"
|
||||||
statements := splitMySQLStatements(script)
|
statements, err := splitMySQLStatements(script)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if len(statements) != 1 {
|
if len(statements) != 1 {
|
||||||
t.Fatalf("带可执行注释的 SQL 语句拆分数量不正确:%d %#v", len(statements), statements)
|
t.Fatalf("带可执行注释的 SQL 语句拆分数量不正确:%d %#v", len(statements), statements)
|
||||||
}
|
}
|
||||||
|
|||||||
205
backend/internal/cli/mysql_programs_integration_test.go
Normal file
205
backend/internal/cli/mysql_programs_integration_test.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 官方导出文件不做改写;用同一隔离账户跨测试库恢复,保留原有定义者。
|
||||||
|
func TestMySQLRealOfficialStoredPrograms(t *testing.T) {
|
||||||
|
server := mysqltest.New(t)
|
||||||
|
// 仅启用本测试新建实例的事件调度,源事件保持禁用,绝不连接共享服务。
|
||||||
|
if _, err := server.Admin().ExecContext(t.Context(), "SET GLOBAL event_scheduler = ON"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := server.Database(t)
|
||||||
|
parsed, err := mysql.ParseDSN(source.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
grantMySQLProgramAccess(t, server, parsed.User, parsed.DBName)
|
||||||
|
// 独立实例启用二进制日志时,函数创建需要管理权限;不修改全局信任开关。
|
||||||
|
if server.IsMySQL8() {
|
||||||
|
if _, err := server.Admin().ExecContext(t.Context(), "GRANT SUPER ON *.* TO "+mysqlStringLiteral(parsed.User)+"@'127.0.0.1'"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !server.IsMySQL8() {
|
||||||
|
if _, err := server.Admin().ExecContext(t.Context(), "GRANT SELECT ON mysql.proc TO "+mysqlStringLiteral(parsed.User)+"@'127.0.0.1'"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db, err := database.Open(t.Context(), source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
connection, err := db.DB.Conn(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
statements := []string{
|
||||||
|
"SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'",
|
||||||
|
"CREATE TABLE interop (id INT PRIMARY KEY, value INT NOT NULL, label VARCHAR(100)) ENGINE=InnoDB",
|
||||||
|
"INSERT INTO interop VALUES (1, 10, '原始值')",
|
||||||
|
"CREATE TRIGGER first_trigger BEFORE INSERT ON interop FOR EACH ROW BEGIN SET NEW.value = NEW.value + 2; SET NEW.label = CONCAT(NEW.label, ';第一步'); END",
|
||||||
|
"CREATE TRIGGER second_trigger BEFORE INSERT ON interop FOR EACH ROW FOLLOWS first_trigger BEGIN SET NEW.value = NEW.value * 3; SET NEW.label = CONCAT(NEW.label, ';第二步'); END",
|
||||||
|
"CREATE PROCEDURE change_value(IN target_id INT) BEGIN UPDATE interop SET value = value + 5 WHERE id = target_id; UPDATE interop SET label = CONCAT(label, ';过程') WHERE id = target_id; END",
|
||||||
|
"CREATE FUNCTION format_value(input_value INT) RETURNS VARCHAR(100) DETERMINISTIC NO SQL BEGIN DECLARE result VARCHAR(100); SET result = CONCAT('函数;', input_value); RETURN result; END",
|
||||||
|
"CREATE EVENT disabled_event ON SCHEDULE AT '2036-01-01 00:00:00' ON COMPLETION PRESERVE DISABLE DO BEGIN UPDATE interop SET value = value + 100; UPDATE interop SET label = '事件;执行'; END",
|
||||||
|
}
|
||||||
|
for _, statement := range statements {
|
||||||
|
if _, err := connection.ExecContext(t.Context(), statement); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definition := readMySQLProgramDefinitions(t, db.DB)
|
||||||
|
output := filepath.Join(server.Root(), "official-programs.sql")
|
||||||
|
server.DumpStoredPrograms(t, source, output)
|
||||||
|
for _, direction := range []string{"official_control", "official_to_scert"} {
|
||||||
|
t.Run(direction, func(t *testing.T) {
|
||||||
|
target := server.Database(t)
|
||||||
|
targetDSN, err := mysql.ParseDSN(target.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
grantMySQLProgramAccess(t, server, parsed.User, targetDSN.DBName)
|
||||||
|
targetDSN.User = parsed.User
|
||||||
|
target.DSN = targetDSN.FormatDSN()
|
||||||
|
if direction == "official_control" {
|
||||||
|
server.Import(t, target, output)
|
||||||
|
} else if err := restoreMySQL(t.Context(), target, output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT CONCAT(value, '|', label) FROM interop WHERE id = 1", "10|原始值")
|
||||||
|
verify, err := database.Open(t.Context(), target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer verify.Close()
|
||||||
|
if restored := readMySQLProgramDefinitions(t, verify.DB); restored != definition {
|
||||||
|
t.Fatalf("存储对象的定义、执行顺序、权限上下文或事件状态发生变化:\n源:%s\n恢复:%s", definition, restored)
|
||||||
|
}
|
||||||
|
for _, statement := range []string{"INSERT INTO interop VALUES (2, 4, '新行')", "CALL change_value(1)"} {
|
||||||
|
if _, err := verify.DB.ExecContext(t.Context(), statement); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT CONCAT(value, '|', label) FROM interop WHERE id = 2", "18|新行;第一步;第二步")
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT CONCAT(value, '|', label) FROM interop WHERE id = 1", "15|原始值;过程")
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT format_value(7)", "函数;7")
|
||||||
|
if _, err := verify.DB.ExecContext(t.Context(), "ALTER EVENT disabled_event ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND ENABLE"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
var value int
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if err := verify.DB.QueryRowContext(t.Context(), "SELECT value FROM interop WHERE id = 1").Scan(&value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if value == 115 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(25 * time.Millisecond)
|
||||||
|
}
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT CONCAT(value, '|', label) FROM interop WHERE id = 1", "115|事件;执行")
|
||||||
|
checkMySQLInteropValue(t, source, "SELECT CONCAT(value, '|', label) FROM interop WHERE id = 1", "10|原始值")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("insufficient_privileges", func(t *testing.T) {
|
||||||
|
target := server.Database(t)
|
||||||
|
err := restoreMySQL(t.Context(), target, output)
|
||||||
|
var serverError *mysql.MySQLError
|
||||||
|
if !errors.As(err, &serverError) || (serverError.Number != 1142 && serverError.Number != 1227 && serverError.Number != 1419) {
|
||||||
|
t.Fatalf("权限不足时必须返回原始数据库错误,不能忽略或提升权限:%v", err)
|
||||||
|
}
|
||||||
|
parsedTarget, err := mysql.ParseDSN(target.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
if err := server.Admin().QueryRowContext(t.Context(), "SELECT COUNT(*) FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = ?", parsedTarget.DBName).Scan(&count); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatal("触发器恢复失败后仍继续恢复了后面的例程")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("malformed_script_is_not_executed", func(t *testing.T) {
|
||||||
|
target := server.Database(t)
|
||||||
|
invalid := filepath.Join(server.Root(), "malformed-programs.sql")
|
||||||
|
if err := os.WriteFile(invalid, []byte("CREATE TABLE should_not_exist (id INT);\nDELIMITER\nSELECT 1;"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := restoreMySQL(t.Context(), target, invalid); err == nil {
|
||||||
|
t.Fatal("无效分隔符没有阻止恢复")
|
||||||
|
}
|
||||||
|
checkMySQLInteropValue(t, target, "SELECT CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()", "0")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func grantMySQLProgramAccess(t *testing.T, server *mysqltest.Server, user, name string) {
|
||||||
|
t.Helper()
|
||||||
|
statement := "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES, TRIGGER, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, EVENT ON " + quoteMySQLIdentifier(name) + ".* TO " + mysqlStringLiteral(user) + "@'127.0.0.1'"
|
||||||
|
if _, err := server.Admin().ExecContext(t.Context(), statement); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMySQLProgramDefinitions(t *testing.T, db *sql.DB) string {
|
||||||
|
t.Helper()
|
||||||
|
queries := []string{
|
||||||
|
`SELECT JSON_ARRAY(TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_ORDER,
|
||||||
|
ACTION_STATEMENT, ACTION_ORIENTATION, ACTION_TIMING, SQL_MODE, DEFINER,
|
||||||
|
CHARACTER_SET_CLIENT, COLLATION_CONNECTION, DATABASE_COLLATION)
|
||||||
|
FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = DATABASE() ORDER BY TRIGGER_NAME`,
|
||||||
|
`SELECT JSON_ARRAY(ROUTINE_NAME, ROUTINE_TYPE, DATA_TYPE, DTD_IDENTIFIER, ROUTINE_DEFINITION,
|
||||||
|
IS_DETERMINISTIC, SQL_DATA_ACCESS, SECURITY_TYPE, SQL_MODE, DEFINER, ROUTINE_COMMENT,
|
||||||
|
CHARACTER_SET_CLIENT, COLLATION_CONNECTION, DATABASE_COLLATION)
|
||||||
|
FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE() ORDER BY ROUTINE_NAME`,
|
||||||
|
`SELECT JSON_ARRAY(SPECIFIC_NAME, ORDINAL_POSITION, PARAMETER_MODE, PARAMETER_NAME,
|
||||||
|
DATA_TYPE, DTD_IDENTIFIER, CHARACTER_SET_NAME, COLLATION_NAME, ROUTINE_TYPE)
|
||||||
|
FROM information_schema.PARAMETERS WHERE SPECIFIC_SCHEMA = DATABASE()
|
||||||
|
ORDER BY SPECIFIC_NAME, ORDINAL_POSITION`,
|
||||||
|
`SELECT JSON_ARRAY(EVENT_NAME, DEFINER, TIME_ZONE, EVENT_BODY, EVENT_DEFINITION, EVENT_TYPE,
|
||||||
|
EXECUTE_AT, SQL_MODE, STATUS, ON_COMPLETION, EVENT_COMMENT,
|
||||||
|
CHARACTER_SET_CLIENT, COLLATION_CONNECTION, DATABASE_COLLATION)
|
||||||
|
FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE() ORDER BY EVENT_NAME`,
|
||||||
|
}
|
||||||
|
var definitions []string
|
||||||
|
for _, query := range queries {
|
||||||
|
rows, err := db.QueryContext(t.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var definition string
|
||||||
|
if err := rows.Scan(&definition); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
definitions = append(definitions, definition)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(definitions) != 8 {
|
||||||
|
t.Fatalf("存储对象及参数数量不完整:得到 %d,期望 8", len(definitions))
|
||||||
|
}
|
||||||
|
return strings.Join(definitions, "\n")
|
||||||
|
}
|
||||||
67
backend/internal/cli/mysql_statements_test.go
Normal file
67
backend/internal/cli/mysql_statements_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitMySQLStatementsDelimiter(t *testing.T) {
|
||||||
|
for _, item := range []struct {
|
||||||
|
name string
|
||||||
|
script string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "procedure",
|
||||||
|
script: "DELIMITER ;;\nCREATE PROCEDURE p() BEGIN SELECT 1; SELECT '值;;分号'; END;;\nDELIMITER ;\nSELECT 2;",
|
||||||
|
want: []string{"CREATE PROCEDURE p() BEGIN SELECT 1; SELECT '值;;分号'; END", "SELECT 2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted_crlf_bom",
|
||||||
|
script: "\uFEFF-- 注释\r\n dElImItEr '$$'\r\nSELECT '$$', `a$$b`, 'it''s';$$\r\nDELIMITER ;\r\nSELECT 2;",
|
||||||
|
want: []string{"SELECT '$$', `a$$b`, 'it''s';", "SELECT 2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comments",
|
||||||
|
script: "DELIMITER //\n/* // */ SELECT 1 # //\n//\nDELIMITER ;\nSELECT/**/2;",
|
||||||
|
want: []string{"SELECT 1", "SELECT 2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "executable_comments",
|
||||||
|
script: "DELIMITER ;;\n/*!50003 CREATE*/ /*!50003 TRIGGER t BEFORE INSERT ON x FOR EACH ROW BEGIN SET NEW.x = ';;'; END */;;\nDELIMITER ;",
|
||||||
|
want: []string{"/*!50003 CREATE*/ /*!50003 TRIGGER t BEFORE INSERT ON x FOR EACH ROW BEGIN SET NEW.x = ';;'; END */"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keyword_is_not_command",
|
||||||
|
script: "SELECT delimiter FROM x;\nSELECT 'DELIMITER $$';\n-- DELIMITER $$\nSELECT 2;",
|
||||||
|
want: []string{"SELECT delimiter FROM x", "SELECT 'DELIMITER $$'", "SELECT 2"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(item.name, func(t *testing.T) {
|
||||||
|
got, err := splitMySQLStatements(item.script)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, item.want) {
|
||||||
|
t.Fatalf("分隔符解析不正确:得到 %#v,期望 %#v", got, item.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitMySQLStatementsRejectsMalformedScript(t *testing.T) {
|
||||||
|
for _, script := range []string{
|
||||||
|
"SELECT 1;\nDELIMITER\nSELECT 2;",
|
||||||
|
"SELECT 1;\nDELIMITER a b\nSELECT 2;",
|
||||||
|
"SELECT 1;\nDELIMITER " + string(rune(92)) + "\nSELECT 2;",
|
||||||
|
"SELECT 1; SELECT '未闭合",
|
||||||
|
"SELECT 1; SELECT `未闭合",
|
||||||
|
"SELECT 1; /* 未闭合",
|
||||||
|
"SELECT 1; /*!50003 未闭合",
|
||||||
|
} {
|
||||||
|
got, err := splitMySQLStatements(script)
|
||||||
|
if err == nil || got != nil {
|
||||||
|
t.Fatalf("格式错误时不应返回可执行的部分语句:%#v,%v", got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,16 +18,25 @@ import (
|
|||||||
|
|
||||||
// 官方工具仅连接本夹具创建的隔离实例,密码只传入子进程环境,不进入命令行或文件。
|
// 官方工具仅连接本夹具创建的隔离实例,密码只传入子进程环境,不进入命令行或文件。
|
||||||
func (f *Server) Dump(t *testing.T, cfg config.DatabaseConfig, output string) {
|
func (f *Server) Dump(t *testing.T, cfg config.DatabaseConfig, output string) {
|
||||||
|
f.dump(t, cfg, output, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Server) DumpStoredPrograms(t *testing.T, cfg config.DatabaseConfig, output string) {
|
||||||
|
f.dump(t, cfg, output, []string{"--routines", "--events"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Server) dump(t *testing.T, cfg config.DatabaseConfig, output string, extra []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
file, err := os.OpenFile(output, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
file, err := os.OpenFile(output, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
f.runTool(t, "mysqldump", cfg, []string{
|
args := []string{
|
||||||
"--single-transaction", "--no-tablespaces", "--set-gtid-purged=OFF",
|
"--single-transaction", "--no-tablespaces", "--set-gtid-purged=OFF",
|
||||||
"--skip-add-locks", "--hex-blob", "--complete-insert", "--skip-comments", "--skip-extended-insert",
|
"--skip-add-locks", "--hex-blob", "--complete-insert", "--skip-comments", "--skip-extended-insert",
|
||||||
}, nil, file)
|
}
|
||||||
|
f.runTool(t, "mysqldump", cfg, append(args, extra...), nil, file)
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user