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
4 changed files with 32 additions and 36 deletions

View File

@@ -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

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) {
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+"%"))
}

View File

@@ -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)

View File

@@ -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)
}
})
}