From df44a75d539a8d1c3b4bdbd44d1ce99944f52b35 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 27 Jun 2026 17:03:06 +0800 Subject: [PATCH] fix: adapt ClickHouse log LIKE filters --- model/clickhouse_log_test.go | 28 +++++++++++++++++++++++ model/log.go | 30 +++++++++++++++++++++++-- model/token.go | 43 +++++++++++++++++++++--------------- 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/model/clickhouse_log_test.go b/model/clickhouse_log_test.go index 7d84fea8..d9737e6b 100644 --- a/model/clickhouse_log_test.go +++ b/model/clickhouse_log_test.go @@ -101,6 +101,34 @@ func TestClickHouseLogOrder(t *testing.T) { assert.Equal(t, "logs.created_at desc, logs.request_id desc", clickHouseLogOrder("logs.")) } +func TestBuildLogLikeConditionUsesStandardEscape(t *testing.T) { + originalLogDatabaseType := common.LogDatabaseType() + t.Cleanup(func() { + common.SetLogDatabaseType(originalLogDatabaseType) + }) + common.SetLogDatabaseType(common.DatabaseTypeSQLite) + + condition, pattern, err := buildLogLikeCondition("logs.model_name", "gpt_4%") + + require.NoError(t, err) + assert.Equal(t, "logs.model_name LIKE ? ESCAPE '!'", condition) + assert.Equal(t, "gpt!_4%", pattern) +} + +func TestBuildLogLikeConditionUsesClickHouseEscaping(t *testing.T) { + originalLogDatabaseType := common.LogDatabaseType() + t.Cleanup(func() { + common.SetLogDatabaseType(originalLogDatabaseType) + }) + common.SetLogDatabaseType(common.DatabaseTypeClickHouse) + + condition, pattern, err := buildLogLikeCondition("logs.model_name", `gpt_4\mini%`) + + require.NoError(t, err) + assert.Equal(t, "logs.model_name LIKE ?", condition) + assert.Equal(t, `gpt\_4\\mini%`, pattern) +} + func TestEnsureLogRequestId(t *testing.T) { empty := &Log{} ensureLogRequestId(empty) diff --git a/model/log.go b/model/log.go index 1f54a8b3..0ff348fa 100644 --- a/model/log.go +++ b/model/log.go @@ -22,15 +22,41 @@ func applyExplicitLogTextFilter(tx *gorm.DB, column string, value string) (*gorm return tx, nil } if strings.Contains(value, "%") { - pattern, err := sanitizeLikePattern(value) + condition, pattern, err := buildLogLikeCondition(column, value) if err != nil { return nil, err } - return tx.Where(column+" LIKE ? ESCAPE '!'", pattern), nil + return tx.Where(condition, pattern), nil } return tx.Where(column+" = ?", value), nil } +func buildLogLikeCondition(column string, value string) (string, string, error) { + if common.UsingLogDatabase(common.DatabaseTypeClickHouse) { + pattern, err := sanitizeClickHouseLikePattern(value) + if err != nil { + return "", "", err + } + return column + " LIKE ?", pattern, nil + } + + pattern, err := sanitizeLikePattern(value) + if err != nil { + return "", "", err + } + return column + " LIKE ? ESCAPE '!'", pattern, nil +} + +func sanitizeClickHouseLikePattern(input string) (string, error) { + input = strings.ReplaceAll(input, `\`, `\\`) + input = strings.ReplaceAll(input, `_`, `\_`) + + if err := validateLikePattern(input); err != nil { + return "", err + } + return input, nil +} + type Log struct { Id int `json:"id" gorm:"index:idx_created_at_id,priority:2;index:idx_user_id_id,priority:2"` UserId int `json:"user_id" gorm:"index;index:idx_user_id_id,priority:1"` diff --git a/model/token.go b/model/token.go index ab841f60..cb34b3ce 100644 --- a/model/token.go +++ b/model/token.go @@ -98,30 +98,37 @@ func sanitizeLikePattern(input string) (string, error) { input = strings.ReplaceAll(input, "!", "!!") input = strings.ReplaceAll(input, `_`, `!_`) - // 2. 连续的 % 直接拒绝 - if strings.Contains(input, "%%") { - return "", errors.New("搜索模式中不允许包含连续的 % 通配符") - } - - // 3. 统计 % 数量,不得超过 2 - count := strings.Count(input, "%") - if count > 2 { - return "", errors.New("搜索模式中最多允许包含 2 个 % 通配符") - } - - // 4. 含 % 时,去掉 % 后关键词长度必须 >= 2 - if count > 0 { - stripped := strings.ReplaceAll(input, "%", "") - if len(stripped) < 2 { - return "", errors.New("使用模糊搜索时,关键词长度至少为 2 个字符") - } - return input, nil + if err := validateLikePattern(input); err != nil { + return "", err } // 5. 无 % 时,精确全匹配 return input, nil } +func validateLikePattern(input string) error { + // 1. 连续的 % 直接拒绝 + if strings.Contains(input, "%%") { + return errors.New("搜索模式中不允许包含连续的 % 通配符") + } + + // 2. 统计 % 数量,不得超过 2 + count := strings.Count(input, "%") + if count > 2 { + return errors.New("搜索模式中最多允许包含 2 个 % 通配符") + } + + // 3. 含 % 时,去掉 % 后关键词长度必须 >= 2 + if count > 0 { + stripped := strings.ReplaceAll(input, "%", "") + if len(stripped) < 2 { + return errors.New("使用模糊搜索时,关键词长度至少为 2 个字符") + } + } + + return nil +} + const searchHardLimit = 100 func SearchUserTokens(userId int, keyword string, token string, offset int, limit int) (tokens []*Token, total int64, err error) {