diff --git a/models/db/common.go b/models/db/common.go index ea628bf2a0..b3c43f8b62 100644 --- a/models/db/common.go +++ b/models/db/common.go @@ -12,30 +12,30 @@ import ( "xorm.io/builder" ) -// BuildCaseInsensitiveLike returns a condition to check if the given value is like the given key case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// BuildCaseInsensitiveLike returns a case-insensitive LIKE condition for the given key and value. +// Cast the search value and the database column value to the same case for case-insensitive matching. +// * SQLite: only cast ASCII chars because it doesn't handle complete Unicode case folding +// * Other databases: use database's string function, assuming that they are able to handle complete Unicode case folding correctly func BuildCaseInsensitiveLike(key, value string) builder.Cond { + // ToLowerASCII is about 7% faster than ToUpperASCII (according to Golang's benchmark) if setting.Database.Type.IsSQLite3() { - return builder.Like{"UPPER(" + key + ")", util.ToUpperASCII(value)} + return builder.Like{"LOWER(" + key + ")", util.ToLowerASCII(value)} } - return builder.Like{"UPPER(" + key + ")", strings.ToUpper(value)} + return builder.Like{"LOWER(" + key + ")", strings.ToLower(value)} } // BuildCaseInsensitiveIn returns a condition to check if the given value is in the given values case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// See BuildCaseInsensitiveLike for more details func BuildCaseInsensitiveIn(key string, values []string) builder.Cond { - uppers := make([]string, 0, len(values)) + incaseValues := make([]string, len(values)) + caseCast := strings.ToLower if setting.Database.Type.IsSQLite3() { - for _, value := range values { - uppers = append(uppers, util.ToUpperASCII(value)) - } - } else { - for _, value := range values { - uppers = append(uppers, strings.ToUpper(value)) - } + caseCast = util.ToLowerASCII } - - return builder.In("UPPER("+key+")", uppers) + for i, value := range values { + incaseValues[i] = caseCast(value) + } + return builder.In("LOWER("+key+")", incaseValues) } // BuilderDialect returns the xorm.Builder dialect of the engine diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 232087d865..08cf964bc8 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -151,7 +151,7 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { users := make([]*user_model.User, 0, 30) var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} - if isShowFullName { + if search != "" && isShowFullName { prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) } diff --git a/modules/util/util.go b/modules/util/util.go index f197d4d6a4..d7702439d6 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -90,12 +90,12 @@ func CryptoRandomBytes(length int64) ([]byte, error) { return buf, err } -// ToUpperASCII returns s with all ASCII letters mapped to their upper case. -func ToUpperASCII(s string) string { +// ToLowerASCII returns s with all ASCII letters mapped to their lower case. +func ToLowerASCII(s string) string { b := []byte(s) for i, c := range b { - if 'a' <= c && c <= 'z' { - b[i] -= 'a' - 'A' + if 'A' <= c && c <= 'Z' { + b[i] += 'a' - 'A' } } return string(b) diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 38876276e3..fd677f5c11 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -178,30 +178,26 @@ type StringTest struct { in, out string } -var upperTests = []StringTest{ +var lowerTests = []StringTest{ {"", ""}, - {"ONLYUPPER", "ONLYUPPER"}, - {"abc", "ABC"}, - {"AbC123", "ABC123"}, - {"azAZ09_", "AZAZ09_"}, - {"longStrinGwitHmixofsmaLLandcAps", "LONGSTRINGWITHMIXOFSMALLANDCAPS"}, - {"long\u0250string\u0250with\u0250nonascii\u2C6Fchars", "LONG\u0250STRING\u0250WITH\u0250NONASCII\u2C6FCHARS"}, - {"\u0250\u0250\u0250\u0250\u0250", "\u0250\u0250\u0250\u0250\u0250"}, - {"a\u0080\U0010FFFF", "A\u0080\U0010FFFF"}, - {"lél", "LéL"}, + {"ABC", "abc"}, + {"AbC123_", "abc123_"}, + {"LONG\u0250string\u0250WITH\u0250non-ascii\u2C6FCHARS\u0080\uFFFF", "long\u0250string\u0250with\u0250non-ascii\u2C6Fchars\u0080\uFFFF"}, + {"lél", "lél"}, + {"LÉL", "lÉl"}, } -func TestToUpperASCII(t *testing.T) { - for _, tc := range upperTests { - assert.Equal(t, ToUpperASCII(tc.in), tc.out) +func TestToLowerASCII(t *testing.T) { + for _, tc := range lowerTests { + assert.Equal(t, ToLowerASCII(tc.in), tc.out) } } -func BenchmarkToUpper(b *testing.B) { - for _, tc := range upperTests { +func BenchmarkToLower(b *testing.B) { + for _, tc := range lowerTests { b.Run(tc.in, func(b *testing.B) { for b.Loop() { - ToUpperASCII(tc.in) + ToLowerASCII(tc.in) } }) }