Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sqle/api/controller/v1/sql_audit_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions sqle/utils/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions sqle/utils/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"math/rand"
"reflect"
"sort"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -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)
}
})
}
Loading