0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-21 23:53:36 +02:00

feat(db): Improve BuildCaseInsensitiveLike with lowercase (#36598)

Improve BuildCaseInsensitiveLike with lowercase, users are more likely
to input lowercase letters, so lowercase letters are used.

---------

Signed-off-by: Tyrone Yeh <siryeh@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Tyrone Yeh 2026-02-14 15:40:59 +08:00 committed by GitHub
parent ce61d6d99d
commit 7a8fe9eb37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 32 additions and 36 deletions

View File

@ -12,30 +12,30 @@ import (
"xorm.io/builder" "xorm.io/builder"
) )
// BuildCaseInsensitiveLike returns a condition to check if the given value is like the given key case-insensitively. // BuildCaseInsensitiveLike returns a case-insensitive LIKE condition for the given key and value.
// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. // 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 { func BuildCaseInsensitiveLike(key, value string) builder.Cond {
// ToLowerASCII is about 7% faster than ToUpperASCII (according to Golang's benchmark)
if setting.Database.Type.IsSQLite3() { 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. // 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 { 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() { if setting.Database.Type.IsSQLite3() {
for _, value := range values { caseCast = util.ToLowerASCII
uppers = append(uppers, util.ToUpperASCII(value))
}
} else {
for _, value := range values {
uppers = append(uppers, strings.ToUpper(value))
}
} }
for i, value := range values {
return builder.In("UPPER("+key+")", uppers) incaseValues[i] = caseCast(value)
}
return builder.In("LOWER("+key+")", incaseValues)
} }
// BuilderDialect returns the xorm.Builder dialect of the engine // BuilderDialect returns the xorm.Builder dialect of the engine

View File

@ -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) { func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
users := make([]*user_model.User, 0, 30) users := make([]*user_model.User, 0, 30)
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
if isShowFullName { if search != "" && isShowFullName {
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
} }

View File

@ -90,12 +90,12 @@ func CryptoRandomBytes(length int64) ([]byte, error) {
return buf, err return buf, err
} }
// ToUpperASCII returns s with all ASCII letters mapped to their upper case. // ToLowerASCII returns s with all ASCII letters mapped to their lower case.
func ToUpperASCII(s string) string { func ToLowerASCII(s string) string {
b := []byte(s) b := []byte(s)
for i, c := range b { for i, c := range b {
if 'a' <= c && c <= 'z' { if 'A' <= c && c <= 'Z' {
b[i] -= 'a' - 'A' b[i] += 'a' - 'A'
} }
} }
return string(b) return string(b)

View File

@ -178,30 +178,26 @@ type StringTest struct {
in, out string in, out string
} }
var upperTests = []StringTest{ var lowerTests = []StringTest{
{"", ""}, {"", ""},
{"ONLYUPPER", "ONLYUPPER"}, {"ABC", "abc"},
{"abc", "ABC"}, {"AbC123_", "abc123_"},
{"AbC123", "ABC123"}, {"LONG\u0250string\u0250WITH\u0250non-ascii\u2C6FCHARS\u0080\uFFFF", "long\u0250string\u0250with\u0250non-ascii\u2C6Fchars\u0080\uFFFF"},
{"azAZ09_", "AZAZ09_"}, {"lél", "lél"},
{"longStrinGwitHmixofsmaLLandcAps", "LONGSTRINGWITHMIXOFSMALLANDCAPS"}, {"LÉL", "lÉl"},
{"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"},
} }
func TestToUpperASCII(t *testing.T) { func TestToLowerASCII(t *testing.T) {
for _, tc := range upperTests { for _, tc := range lowerTests {
assert.Equal(t, ToUpperASCII(tc.in), tc.out) assert.Equal(t, ToLowerASCII(tc.in), tc.out)
} }
} }
func BenchmarkToUpper(b *testing.B) { func BenchmarkToLower(b *testing.B) {
for _, tc := range upperTests { for _, tc := range lowerTests {
b.Run(tc.in, func(b *testing.B) { b.Run(tc.in, func(b *testing.B) {
for b.Loop() { for b.Loop() {
ToUpperASCII(tc.in) ToLowerASCII(tc.in)
} }
}) })
} }