保留原始视图定义与编码,按依赖排序导出,并严格识别和重排官方最终视图块;增加运行上下文和权限检查。补充视图、清单、恢复失败保护测试,并修正旧事件、触发器和例程测试的权限前提。 隔离MySQL 5.7和8.0完整专项分别通过176和202个叶子用例,无失败或跳过;后端常规全量、静态检查、模块校验及Linux测试包交叉编译通过。测试产物和凭据未提交。本提交不代表生产部署验收完成。
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
|
|
)
|
|
|
|
func TestMySQLRealViewDependencies(t *testing.T) {
|
|
server := mysqltest.New(t)
|
|
source := server.Database(t)
|
|
dsn, err := mysql.ParseDSN(source.DSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grantMySQLViewAccess(t, server, dsn.User, dsn.DBName)
|
|
external := server.Database(t)
|
|
other, err := mysql.ParseDSN(external.DSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grantMySQLViewAccess(t, server, dsn.User, other.DBName)
|
|
if _, err := server.Admin().ExecContext(t.Context(), "CREATE VIEW "+quoteMySQLIdentifier(other.DBName)+".z_external AS SELECT 41 AS id"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db, err := database.Open(t.Context(), source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
conn, err := db.DB.Conn(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
statements := []string{
|
|
"CREATE TABLE source_values (id INT PRIMARY KEY, label VARCHAR(30), stamp DATE) ENGINE=InnoDB",
|
|
"INSERT INTO source_values VALUES (1, ' xx ', '2026-01-02')",
|
|
"CREATE VIEW z_data AS SELECT * FROM source_values",
|
|
"CREATE VIEW a_expression AS SELECT id, TRIM(BOTH ' ' FROM label) AS label, EXTRACT(YEAR FROM stamp) AS year_value FROM z_data",
|
|
"CREATE VIEW a_scalar AS SELECT (SELECT MAX(id) FROM z_data) AS id",
|
|
"CREATE VIEW a_join AS SELECT x.id FROM (z_data x JOIN z_data y ON x.id=y.id), z_data z WHERE z.id=x.id",
|
|
"CREATE VIEW a_derived AS SELECT id FROM (SELECT id FROM z_data UNION ALL SELECT id FROM z_data) x",
|
|
"CREATE VIEW a_external AS SELECT id FROM " + quoteMySQLIdentifier(other.DBName) + ".z_external",
|
|
"CREATE VIEW z_external AS SELECT id FROM a_external",
|
|
"SET SESSION sql_mode = 'ANSI_QUOTES,NO_BACKSLASH_ESCAPES'",
|
|
"CREATE VIEW a_mode AS SELECT 'from z_data' AS `from`, 'join z_external' AS `join`, id FROM z_data",
|
|
"SET SESSION sql_mode = ''",
|
|
}
|
|
if server.IsMySQL8() {
|
|
statements = append(statements,
|
|
"CREATE VIEW a_cte AS WITH z_data AS (SELECT id FROM z_data) SELECT id FROM z_data",
|
|
"CREATE VIEW a_recursive AS WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<3) SELECT n FROM seq",
|
|
"CREATE VIEW a_cte_scope AS SELECT id FROM (WITH z_data AS (SELECT id FROM z_data) SELECT id FROM z_data) x")
|
|
}
|
|
// 超过 InnoDB 基础表列数上限,证明导出不再依赖占位表。
|
|
var columns []string
|
|
for i := 0; i < 1020; i++ {
|
|
columns = append(columns, fmt.Sprintf("%d AS c%d", i, i))
|
|
}
|
|
statements = append(statements, "CREATE VIEW wide_view AS SELECT "+strings.Join(columns, ","))
|
|
for _, statement := range statements {
|
|
if _, err := conn.ExecContext(t.Context(), statement); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
original := readMySQLViewDefinitions(t, db.DB)
|
|
output := filepath.Join(server.Root(), "complex-views.sql")
|
|
if err := backupMySQLDatabase(t.Context(), source, output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(output)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(data), "CREATE TABLE `wide_view`") || strings.Contains(string(data), "DROP TABLE `wide_view`") {
|
|
t.Fatal("视图导出仍在创建或删除占位基础表")
|
|
}
|
|
for _, direction := range []string{"scert", "official"} {
|
|
t.Run(direction, func(t *testing.T) {
|
|
target := server.Database(t)
|
|
parsed, err := mysql.ParseDSN(target.DSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grantMySQLViewAccess(t, server, dsn.User, parsed.DBName)
|
|
parsed.User = dsn.User
|
|
target.DSN = parsed.FormatDSN()
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
if direction == "official" {
|
|
server.Import(t, target, output)
|
|
} else if err := restoreMySQL(t.Context(), target, output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
verify, err := database.Open(t.Context(), target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer verify.Close()
|
|
if actual := readMySQLViewDefinitions(t, verify.DB); actual != original {
|
|
t.Fatal("复杂视图恢复后的原始定义或列元数据发生变化")
|
|
}
|
|
for query, expected := range map[string]string{
|
|
"SELECT CONCAT(id, '|', label, '|', year_value) FROM a_expression": "1|xx|2026",
|
|
"SELECT id FROM a_scalar": "1",
|
|
"SELECT id FROM a_join": "1",
|
|
"SELECT COUNT(*) FROM a_derived": "2",
|
|
"SELECT id FROM z_external": "41",
|
|
"SELECT c1019 FROM wide_view": "1019",
|
|
} {
|
|
checkMySQLInteropValue(t, target, query, expected)
|
|
}
|
|
if server.IsMySQL8() {
|
|
checkMySQLInteropValue(t, target, "SELECT id FROM a_cte", "1")
|
|
checkMySQLInteropValue(t, target, "SELECT id FROM a_cte_scope", "1")
|
|
checkMySQLInteropValue(t, target, "SELECT SUM(n) FROM a_recursive", "6")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMySQLRealInvalidViewPreservesBackup(t *testing.T) {
|
|
server := mysqltest.New(t)
|
|
cfg := server.Database(t)
|
|
dsn, err := mysql.ParseDSN(cfg.DSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grantMySQLViewAccess(t, server, dsn.User, dsn.DBName)
|
|
db, err := database.Open(t.Context(), cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
for _, statement := range []string{
|
|
"CREATE TABLE source_values(id INT) ENGINE=InnoDB",
|
|
"CREATE VIEW broken_view AS SELECT id FROM source_values",
|
|
"DROP TABLE source_values",
|
|
} {
|
|
if _, err := db.DB.ExecContext(t.Context(), statement); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
output := filepath.Join(server.Root(), "invalid-view.sql")
|
|
original := []byte("保留原备份")
|
|
writeMySQLBackupFixture(t, output, original)
|
|
if err := backupMySQLDatabase(t.Context(), cfg, output); err == nil {
|
|
t.Fatal("失效视图不能导出为成功备份")
|
|
}
|
|
assertMySQLBackupFile(t, output, original)
|
|
assertMySQLBackupAbsent(t, output+".tmp")
|
|
}
|