diff --git a/sqle/api/controller/v1/sql_audit_record.go b/sqle/api/controller/v1/sql_audit_record.go index 5b232b521..b80daf446 100644 --- a/sqle/api/controller/v1/sql_audit_record.go +++ b/sqle/api/controller/v1/sql_audit_record.go @@ -421,12 +421,12 @@ func getSqlsFromZip(c echo.Context) (sqlsFromSQLFile []SQLsFromSQLFile, sqlsFrom sqlsFromXML = append(sqlsFromXML, sqlsFromXmls...) } - // 按文件名排序,确保SQL按文件顺序执行 + // 按文件名自然排序,确保SQL按文件顺序执行(数字按数值大小比较,如 "file2.sql" 会排在 "file11.sql" 前面) sort.Slice(sqlsFromSQLFile, func(i, j int) bool { - return sqlsFromSQLFile[i].FilePath < sqlsFromSQLFile[j].FilePath + return utils.CompareNatural(sqlsFromSQLFile[i].FilePath, sqlsFromSQLFile[j].FilePath) }) sort.Slice(sqlsFromXML, func(i, j int) bool { - return sqlsFromXML[i].FilePath < sqlsFromXML[j].FilePath + return utils.CompareNatural(sqlsFromXML[i].FilePath, sqlsFromXML[j].FilePath) }) return sqlsFromSQLFile, sqlsFromXML, true, nil diff --git a/sqle/utils/util.go b/sqle/utils/util.go index 5b9bca73a..a43ea9734 100644 --- a/sqle/utils/util.go +++ b/sqle/utils/util.go @@ -490,6 +490,101 @@ func GenerateSSHKeyPair() (privateKeyStr, publicKeyStr string, err error) { return string(privatePEM), publicKeyStr, nil } +// CompareNatural 实现自然排序比较,数字按数值大小比较,非数字按字典序比较 +// 例如:"file2.sql" 会排在 "file11.sql" 前面 +// 返回值:如果 a < b 返回 true,否则返回 false +func CompareNatural(a, b string) bool { + aRunes := []rune(a) + bRunes := []rune(b) + + aLen := len(aRunes) + bLen := len(bRunes) + + i, j := 0, 0 + + for i < aLen && j < bLen { + // 跳过前导空格 + for i < aLen && unicode.IsSpace(aRunes[i]) { + i++ + } + for j < bLen && unicode.IsSpace(bRunes[j]) { + j++ + } + + if i >= aLen || j >= bLen { + break + } + + // 检查当前位置是否为数字 + aIsDigit := unicode.IsDigit(aRunes[i]) + bIsDigit := unicode.IsDigit(bRunes[j]) + + if aIsDigit && bIsDigit { + // 两者都是数字,提取完整的数字进行比较 + aNumStart := i + bNumStart := j + + // 提取 a 的数字部分 + for i < aLen && unicode.IsDigit(aRunes[i]) { + i++ + } + // 提取 b 的数字部分 + for j < bLen && unicode.IsDigit(bRunes[j]) { + j++ + } + + // 将数字字符串转换为整数进行比较 + aNumStr := string(aRunes[aNumStart:i]) + bNumStr := string(bRunes[bNumStart:j]) + + aNum, err1 := strconv.Atoi(aNumStr) + bNum, err2 := strconv.Atoi(bNumStr) + + // 如果转换失败,按字符串比较 + if err1 != nil || err2 != nil { + if aNumStr < bNumStr { + return true + } + if aNumStr > bNumStr { + return false + } + continue + } + + // 按数值比较 + if aNum < bNum { + return true + } + if aNum > bNum { + return false + } + // 数值相等,但字符串可能不同(如 "02" vs "2"),按字符串比较以保持稳定性 + if aNumStr < bNumStr { + return true + } + if aNumStr > bNumStr { + return false + } + // 数值和字符串都相等,继续比较下一部分 + continue + } + + // 至少有一个不是数字,按字符比较 + if aRunes[i] < bRunes[j] { + return true + } + if aRunes[i] > bRunes[j] { + return false + } + + i++ + j++ + } + + // 一个字符串已经比较完,较短的排在前面 + return aLen < bLen +} + func FindIntersection(slice1, slice2 []string) []string { map1 := make(map[string]bool) map2 := make(map[string]bool) diff --git a/sqle/utils/util_test.go b/sqle/utils/util_test.go index 21cb92dd9..8238b2980 100644 --- a/sqle/utils/util_test.go +++ b/sqle/utils/util_test.go @@ -3,6 +3,7 @@ package utils import ( "math/rand" "reflect" + "sort" "strconv" "testing" "time" @@ -415,3 +416,96 @@ func TestGenerateRandomString(t *testing.T) { // If we've gone through all iterations without finding a duplicate, log a success message. t.Logf("All %d generated strings were unique.", iterations) } + +func TestCompareNatural(t *testing.T) { + testCases := []struct { + name string + a string + b string + expected bool // expected: a < b + }{ + // 基本数字排序:数字按数值大小比较 + {"数字排序:2 < 11", "file2.sql", "file11.sql", true}, + {"数字排序:11 > 2", "file11.sql", "file2.sql", false}, + {"数字排序:相等", "file2.sql", "file2.sql", false}, + + // 多位数比较 + {"多位数:10 < 100", "file10.sql", "file100.sql", true}, + {"多位数:100 > 10", "file100.sql", "file10.sql", false}, + {"多位数:99 < 100", "file99.sql", "file100.sql", true}, + + // 前导零 + {"前导零:02 < 11", "file02.sql", "file11.sql", true}, + {"前导零:02 < 2", "file02.sql", "file2.sql", true}, // 02 作为字符串是 "02",数值是 2 + + // 纯字符串比较 + {"纯字符串:a < b", "a.sql", "b.sql", true}, + {"纯字符串:b > a", "b.sql", "a.sql", false}, + {"纯字符串:相等", "file.sql", "file.sql", false}, + + // 混合:字符串+数字 + {"混合:file1 < file2", "file1.sql", "file2.sql", true}, + {"混合:file2 > file1", "file2.sql", "file1.sql", false}, + {"混合:file < file1", "file.sql", "file1.sql", true}, + {"混合:file1 > file", "file1.sql", "file.sql", false}, + + // 多个数字段 + {"多数字段:1-2 < 1-10", "file1-2.sql", "file1-10.sql", true}, + {"多数字段:1-10 > 1-2", "file1-10.sql", "file1-2.sql", false}, + {"多数字段:2-1 < 10-1", "file2-1.sql", "file10-1.sql", true}, + + // 路径中的排序 + {"路径:dir1 < dir11", "dir1/file.sql", "dir11/file.sql", true}, + {"路径:dir11 > dir2", "dir11/file.sql", "dir2/file.sql", false}, + + // 边界情况 + {"空字符串", "", "a", true}, + {"空字符串相等", "", "", false}, + {"相同字符串", "file.sql", "file.sql", false}, + + // 复杂场景 + {"复杂:test2 < test10", "test2.sql", "test10.sql", true}, + {"复杂:test10 > test2", "test10.sql", "test2.sql", false}, + {"复杂:a2b < a10b", "a2b.sql", "a10b.sql", true}, + {"复杂:a10b > a2b", "a10b.sql", "a2b.sql", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := CompareNatural(tc.a, tc.b) + if result != tc.expected { + t.Errorf("CompareNatural(%q, %q) = %v, want %v", tc.a, tc.b, result, tc.expected) + } + }) + } + + // 测试排序稳定性:验证排序后的顺序 + t.Run("排序稳定性测试", func(t *testing.T) { + files := []string{ + "file11.sql", + "file2.sql", + "file1.sql", + "file10.sql", + "file20.sql", + "file3.sql", + } + + expectedOrder := []string{ + "file1.sql", + "file2.sql", + "file3.sql", + "file10.sql", + "file11.sql", + "file20.sql", + } + + // 使用自然排序进行排序 + sort.Slice(files, func(i, j int) bool { + return CompareNatural(files[i], files[j]) + }) + + if !reflect.DeepEqual(files, expectedOrder) { + t.Errorf("排序结果不正确,got %v, want %v", files, expectedOrder) + } + }) +}