This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 1fc4db92f fix(server): match literal search text in the audit, 
metadata and alert queries (#7086)
1fc4db92f is described below

commit 1fc4db92f9bc88c289ef11a150186f94edbb8d70
Author: Loyal-Young <[email protected]>
AuthorDate: Mon Oct 26 21:21:11 2026 +0800

    fix(server): match literal search text in the audit, metadata and alert 
queries (#7086)
    
    Integration batch of three same-family fixes by Loyal-Young: free-text 
search in the audit repository (#7074), topic/group name search in the metadata 
provider (#7083) and rule search in the alert repository (#7086) all passed the 
raw term into a LIKE condition, so a '%' or '_' in the term acted as a 
wildcard. Each escape is now one shared util (common/util/LikePatterns), which 
also absorbs the fifth private copy in the AI conversation repository and the 
one in QueryHistoryService. Th [...]
---
 .../rocketmq/studio/common/util/LikePatterns.java  | 38 +++++++++++++++++
 .../instance/message/QueryHistoryService.java      | 16 ++------
 .../MybatisPlusAiConversationRepository.java       | 13 +-----
 .../ops/alert/MybatisPlusAlertRepository.java      |  8 ++--
 .../ops/audit/MybatisPlusAuditRepository.java      | 48 +++++++++++-----------
 .../provider/apache/RocketMQMetadataProvider.java  | 13 ++++--
 .../ops/alert/MybatisPlusAlertRepositoryTest.java  | 36 ++++++++++++++++
 .../ops/audit/MybatisPlusAuditRepositoryTest.java  | 19 ++++++++-
 .../apache/RocketMQMetadataProviderTest.java       | 46 +++++++++++++++++++++
 9 files changed, 182 insertions(+), 55 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/LikePatterns.java 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/LikePatterns.java
new file mode 100644
index 000000000..a8daf8def
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/LikePatterns.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+/**
+ * Escapes the wildcards a MyBatis-Plus {@code like} condition would otherwise 
honour, so a search
+ * term containing {@code %}, {@code _} or a backslash matches itself 
literally instead of acting
+ * as a pattern. Four repositories used to carry private copies of this; the 
escape is a property
+ * of the SQL LIKE operator, not of any one table.
+ */
+public final class LikePatterns {
+
+    private LikePatterns() {
+    }
+
+    /**
+     * @param value raw search term, may be null
+     * @return the escaped term, or null when {@code value} is null
+     */
+    public static String escape(String value) {
+        return value == null ? null
+                : value.replace("\\", "\\\\").replace("%", "\\%").replace("_", 
"\\_");
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
index b7c18fa7c..cf4215b5f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.apache.rocketmq.studio.common.util.LikePatterns;
 import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
 import org.apache.rocketmq.studio.persistence.entity.RmqMessageQuery;
 import org.apache.rocketmq.studio.persistence.entity.RmqTraceQuery;
@@ -184,7 +185,7 @@ public class QueryHistoryService {
 
     public PageResult<MessageQueryHistoryVO> listMessageQueries(String 
clusterId, String queryType,
                                                                  String 
search, int page, int pageSize) {
-        String pattern = escapeLike(search);
+        String pattern = LikePatterns.escape(search);
         String queriedBy = AuthenticatedUserContext.currentUsernameOrSystem();
         QueryWrapper<RmqMessageQuery> query = new 
QueryWrapper<RmqMessageQuery>()
                 .eq(StringUtils.hasText(clusterId), "cluster_id", clusterId)
@@ -204,7 +205,7 @@ public class QueryHistoryService {
 
     public PageResult<TraceQueryHistoryVO> listTraceQueries(String clusterId, 
String search,
                                                              int page, int 
pageSize) {
-        String pattern = escapeLike(search);
+        String pattern = LikePatterns.escape(search);
         String queriedBy = AuthenticatedUserContext.currentUsernameOrSystem();
         QueryWrapper<RmqTraceQuery> query = new QueryWrapper<RmqTraceQuery>()
                 .eq(StringUtils.hasText(clusterId), "cluster_id", clusterId)
@@ -346,17 +347,6 @@ public class QueryHistoryService {
                 .queriedAt(query.getGmtCreate()).build();
     }
 
-    /**
-     * Escapes LIKE wildcards so user-supplied search terms match literally 
instead of being
-     * interpreted as {@code %}/{@code _} patterns.
-     */
-    private static String escapeLike(String search) {
-        if (!StringUtils.hasText(search)) {
-            return search;
-        }
-        return search.replace("\\", "\\\\").replace("%", "\\%").replace("_", 
"\\_");
-    }
-
     private static String normalizeOptional(String value) {
         return StringUtils.hasText(value) ? value.trim() : null;
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
index 359849b94..c95d7acd8 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
@@ -20,6 +20,7 @@ import 
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.util.LikePatterns;
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.persistence.entity.RmqAiConversation;
 import org.apache.rocketmq.studio.persistence.mapper.RmqAiConversationMapper;
@@ -67,7 +68,7 @@ public class MybatisPlusAiConversationRepository implements 
AiConversationReposi
         QueryWrapper<RmqAiConversation> query = new 
QueryWrapper<RmqAiConversation>()
                 .eq("owner", owner)
                 .eq(archived != null, "archived", archived)
-                .like(StringUtils.hasText(search), "title", escapeLike(search))
+                .like(StringUtils.hasText(search), "title", 
LikePatterns.escape(search))
                 // gmt_modified, not the project-wide gmt_create: a 
conversation resumed today
                 // must float to the top. idx_ai_conversation_owner is built 
for this ordering.
                 .orderByDesc("gmt_modified", "id");
@@ -123,14 +124,4 @@ public class MybatisPlusAiConversationRepository 
implements AiConversationReposi
         return conversationMapper.deleteByIds(ids);
     }
 
-    /**
-     * Escapes LIKE wildcards so user-supplied search terms match literally. 
Mirrors
-     * QueryHistoryService.escapeLike; a shared util is tracked separately 
upstream.
-     */
-    private static String escapeLike(String search) {
-        if (!StringUtils.hasText(search)) {
-            return search;
-        }
-        return search.replace("\\", "\\\\").replace("%", "\\%").replace("_", 
"\\_");
-    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
index 24098c320..1c6e7e13d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
@@ -21,6 +21,7 @@ import 
com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.util.LikePatterns;
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
 import org.apache.rocketmq.studio.persistence.entity.RmqAlertRule;
@@ -82,19 +83,20 @@ public class MybatisPlusAlertRepository implements 
AlertRepository {
 
     private QueryWrapper<RmqAlertRule> ruleQuery(String search, Boolean 
enabled) {
         return new QueryWrapper<RmqAlertRule>()
-                .like(StringUtils.hasText(search), "name", search)
+                .like(StringUtils.hasText(search), "name", 
LikePatterns.escape(search))
                 .eq(enabled != null, "enabled", enabled)
                 .orderByAsc("name", "id");
     }
 
     @Override
     public PageResult<AlertRuleVO> findRulesPage(AlertRuleQuery query) {
+        String literalSearch = StringUtils.hasText(query.search()) ? 
LikePatterns.escape(query.search().trim()) : null;
         QueryWrapper<RmqAlertRule> conditions = new 
QueryWrapper<RmqAlertRule>()
                 .eq(query.enabled() != null, "enabled", query.enabled())
                 .and(StringUtils.hasText(query.search()), wrapper -> wrapper
-                        .like("name", query.search().trim())
+                        .like("name", literalSearch)
                         .or()
-                        .like("metric", query.search().trim()))
+                        .like("metric", literalSearch))
                 .orderByAsc("name")
                 .orderByAsc("id");
         if (query.domain() == AlertDomain.BUSINESS) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
index 787ea5a37..b9fefd1db 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
@@ -18,6 +18,7 @@ package org.apache.rocketmq.studio.ops.audit;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.rocketmq.studio.common.util.LikePatterns;
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.util.JdbcRowValues;
 import org.apache.rocketmq.studio.persistence.entity.RmqOperationAudit;
@@ -189,29 +190,6 @@ public class MybatisPlusAuditRepository implements 
AuditRepository {
                 .toList();
     }
 
-    /**
-     * Single source of truth for the audit filters, shared by the paged list, 
the summary and the
-     * hotspot queries so a card can never disagree with the rows it describes.
-     */
-    private void applyFilters(QueryWrapper<RmqOperationAudit> query, String 
search,
-                              String operator, String operationType, String 
resourceType,
-                              String target, String clusterId, boolean 
clusterIdMissing,
-                              LocalDateTime startDate, LocalDateTime endDate, 
String result) {
-        query.and(StringUtils.hasText(search), w -> w
-                        .like("operator", search)
-                        .or().like("resource_name", search)
-                        .or().like("detail", search))
-                .eq(StringUtils.hasText(operator), "operator", operator)
-                .eq(StringUtils.hasText(operationType), "operation", 
operationType)
-                .eq(StringUtils.hasText(resourceType), "resource_type", 
resourceType)
-                .eq(StringUtils.hasText(target), "resource_name", target)
-                .eq(!clusterIdMissing && StringUtils.hasText(clusterId), 
"cluster_id", clusterId)
-                .and(clusterIdMissing, scope -> 
scope.isNull("cluster_id").or().eq("cluster_id", ""))
-                .ge(startDate != null, "gmt_create", startDate)
-                .le(endDate != null, "gmt_create", endDate)
-                .eq(StringUtils.hasText(result), "result", result);
-    }
-
     @Override
     public void save(AuditRecordVO record) {
         RmqOperationAudit entity = new RmqOperationAudit();
@@ -265,6 +243,30 @@ public class MybatisPlusAuditRepository implements 
AuditRepository {
         return totalDeleted;
     }
 
+    /**
+     * Single source of truth for the audit filters, shared by the paged list, 
the summary and the
+     * hotspot queries so a card can never disagree with the rows it describes.
+     */
+    private void applyFilters(QueryWrapper<RmqOperationAudit> query, String 
search,
+                              String operator, String operationType, String 
resourceType,
+                              String target, String clusterId, boolean 
clusterIdMissing,
+                              LocalDateTime startDate, LocalDateTime endDate, 
String result) {
+        String searchPattern = StringUtils.hasText(search) ? 
LikePatterns.escape(search) : null;
+        query.and(searchPattern != null, w -> w
+                        .like("operator", searchPattern)
+                        .or().like("resource_name", searchPattern)
+                        .or().like("detail", searchPattern))
+                .eq(StringUtils.hasText(operator), "operator", operator)
+                .eq(StringUtils.hasText(operationType), "operation", 
operationType)
+                .eq(StringUtils.hasText(resourceType), "resource_type", 
resourceType)
+                .eq(StringUtils.hasText(target), "resource_name", target)
+                .eq(!clusterIdMissing && StringUtils.hasText(clusterId), 
"cluster_id", clusterId)
+                .and(clusterIdMissing, scope -> 
scope.isNull("cluster_id").or().eq("cluster_id", ""))
+                .ge(startDate != null, "gmt_create", startDate)
+                .le(endDate != null, "gmt_create", endDate)
+                .eq(StringUtils.hasText(result), "result", result);
+    }
+
     private List<String> findDistinctValues(List<Map<String, Object>> rows, 
String column) {
         return rows.stream()
                 .map(row -> row.get(column))
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
index 456643478..415aca29b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
@@ -30,6 +30,7 @@ import 
org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData;
 import org.apache.rocketmq.remoting.protocol.route.BrokerData;
 import org.apache.rocketmq.remoting.protocol.route.QueueData;
 import org.apache.rocketmq.remoting.protocol.route.TopicRouteData;
+import org.apache.rocketmq.studio.common.util.LikePatterns;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
 import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
@@ -144,6 +145,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
     public List<TopicVO> listTopics(String instanceId, String clusterId, 
String type, String search) {
         String configuredCluster = StringUtils.hasText(instanceId)
                 ? runtimeAdminClientResolver.configuredClusterName(instanceId) 
: null;
+        String literalSearch = LikePatterns.escape(search);
         LambdaQueryWrapper<RmqTopic> query = new LambdaQueryWrapper<RmqTopic>()
                 .and(instanceId != null, scope -> {
                     scope.eq(RmqTopic::getInstanceId, 
normalizeMetadataScope(instanceId));
@@ -155,7 +157,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
                 })
                 .eq(StringUtils.hasText(clusterId), RmqTopic::getClusterId, 
clusterId)
                 .eq(StringUtils.hasText(type), RmqTopic::getTopicType, type)
-                .like(StringUtils.hasText(search), RmqTopic::getName, search)
+                .like(StringUtils.hasText(search), RmqTopic::getName, 
literalSearch)
                 .orderByAsc(RmqTopic::getName);
 
         List<TopicVO> result = new ArrayList<>();
@@ -173,6 +175,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
             String search, int page, int pageSize) {
         String configuredCluster = StringUtils.hasText(instanceId)
                 ? runtimeAdminClientResolver.configuredClusterName(instanceId) 
: null;
+        String literalSearch = LikePatterns.escape(search);
         LambdaQueryWrapper<RmqTopic> query = new LambdaQueryWrapper<RmqTopic>()
                 .and(instanceId != null, scope -> {
                     scope.eq(RmqTopic::getInstanceId, 
normalizeMetadataScope(instanceId));
@@ -184,7 +187,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
                 })
                 .eq(StringUtils.hasText(clusterId), RmqTopic::getClusterId, 
clusterId)
                 .eq(StringUtils.hasText(type), RmqTopic::getTopicType, type)
-                .like(StringUtils.hasText(search), RmqTopic::getName, search)
+                .like(StringUtils.hasText(search), RmqTopic::getName, 
literalSearch)
                 .notIn(RmqTopic::getName, TopicValidator.getSystemTopicSet())
                 .notLikeRight(RmqTopic::getName, "rmq_sys_")
                 .notLikeRight(RmqTopic::getName, "%RETRY%")
@@ -243,6 +246,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
     public List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
clusterId, String search) {
         String configuredCluster = StringUtils.hasText(instanceId)
                 ? runtimeAdminClientResolver.configuredClusterName(instanceId) 
: null;
+        String literalSearch = LikePatterns.escape(search);
         LambdaQueryWrapper<RmqGroup> query = new LambdaQueryWrapper<RmqGroup>()
                 .and(instanceId != null, scope -> {
                     scope.eq(RmqGroup::getInstanceId, 
normalizeMetadataScope(instanceId));
@@ -253,7 +257,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
                     }
                 })
                 .eq(StringUtils.hasText(clusterId), RmqGroup::getClusterId, 
clusterId)
-                .like(StringUtils.hasText(search), RmqGroup::getName, search)
+                .like(StringUtils.hasText(search), RmqGroup::getName, 
literalSearch)
                 .orderByAsc(RmqGroup::getName);
 
         List<ConsumerGroupVO> result = new ArrayList<>();
@@ -275,6 +279,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
             String search, String subscriptionMode, int page, int pageSize) {
         String configuredCluster = StringUtils.hasText(instanceId)
                 ? runtimeAdminClientResolver.configuredClusterName(instanceId) 
: null;
+        String literalSearch = LikePatterns.escape(search);
         // message_model stores the subscription mode ("Push"/"Pop"); blank 
and "ALL" mean no
         // restriction, so the count and the page both describe the filtered 
result set.
         boolean modeRestricted = StringUtils.hasText(subscriptionMode)
@@ -290,7 +295,7 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
                     }
                 })
                 .eq(StringUtils.hasText(clusterId), RmqGroup::getClusterId, 
clusterId)
-                .like(StringUtils.hasText(search), RmqGroup::getName, search)
+                .like(StringUtils.hasText(search), RmqGroup::getName, 
literalSearch)
                 .and(modeRestricted, scope -> {
                     if (popOnly) {
                         scope.eq(RmqGroup::getMessageModel, 
SubscriptionMode.Pop.name());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepositoryTest.java
index 0ce8dc06d..dd389d179 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepositoryTest.java
@@ -189,6 +189,42 @@ class MybatisPlusAlertRepositoryTest {
         verify(ruleMapper, never()).selectList(any());
     }
 
+    @Test
+    void findRulePageShouldMatchLiteralSearchTextTest() {
+        Page<RmqAlertRule> mapperPage = new Page<RmqAlertRule>(1, 20)
+                .setRecords(List.of()).setTotal(0);
+        when(ruleMapper.selectPage(any(IPage.class), 
any(Wrapper.class))).thenReturn(mapperPage);
+
+        assertThat(repository.findRulePage("broker_%\\east", true, 1, 
20).getItems()).isEmpty();
+
+        ArgumentCaptor<Wrapper<RmqAlertRule>> queryCaptor = 
ArgumentCaptor.forClass(Wrapper.class);
+        verify(ruleMapper).selectPage(any(IPage.class), queryCaptor.capture());
+        QueryWrapper<RmqAlertRule> query = (QueryWrapper<RmqAlertRule>) 
queryCaptor.getValue();
+        assertThat(query.getSqlSegment()).contains("name LIKE", "enabled =");
+        assertThat(query.getParamNameValuePairs().values())
+                .contains("%broker\\_\\%\\\\east%", true);
+    }
+
+    @Test
+    void findRulesPageShouldMatchLiteralNameAndMetricTextTest() {
+        Page<RmqAlertRule> mapperPage = new Page<RmqAlertRule>(1, 20)
+                .setRecords(List.of()).setTotal(0);
+        when(ruleMapper.selectPage(any(IPage.class), 
any(Wrapper.class))).thenReturn(mapperPage);
+
+        assertThat(repository.findRulesPage(
+                new AlertRuleQuery(AlertDomain.BUSINESS, " metric_50% ", true, 
1, 20))
+                .getItems()).isEmpty();
+
+        ArgumentCaptor<Wrapper<RmqAlertRule>> queryCaptor = 
ArgumentCaptor.forClass(Wrapper.class);
+        verify(ruleMapper).selectPage(any(IPage.class), queryCaptor.capture());
+        QueryWrapper<RmqAlertRule> query = (QueryWrapper<RmqAlertRule>) 
queryCaptor.getValue();
+        assertThat(query.getSqlSegment()).contains("name LIKE", "metric LIKE", 
"enabled =");
+        assertThat(query.getParamNameValuePairs().values())
+                .contains("%metric\\_50\\%%", true);
+        assertThat(query.getParamNameValuePairs().values().stream()
+                .filter("%metric\\_50\\%%"::equals)).hasSize(2);
+    }
+
     @Test
     void findRuleByIdShouldUsePrimaryKeyLookupWithoutFullListRead() {
         RmqAlertRule entity = new RmqAlertRule();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
index 12187b675..0e8d62599 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
@@ -372,4 +372,21 @@ class MybatisPlusAuditRepositoryTest {
         return result;
     }
 
-}
+    @Test
+    void findPageTreatsSearchWildcardsAsLiteralAuditTextTest() {
+        when(auditMapper.selectPage(any(IPage.class), any(Wrapper.class)))
+                .thenReturn(new Page<RmqOperationAudit>(1, 
20).setRecords(List.of()).setTotal(0));
+
+        repository.findPage("topic_%", null, null, null, null, null, false,
+                null, null, null, 1, 20);
+
+        ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor = 
ArgumentCaptor.forClass(Wrapper.class);
+        verify(auditMapper).selectPage(any(IPage.class), 
queryCaptor.capture());
+        assertThat(((QueryWrapper<RmqOperationAudit>) 
queryCaptor.getValue()).getSqlSegment())
+                .contains("operator LIKE", "resource_name LIKE", "detail 
LIKE");
+        assertThat(((QueryWrapper<RmqOperationAudit>) queryCaptor.getValue())
+                .getParamNameValuePairs().values())
+                .containsExactlyInAnyOrder("%topic\\_\\%%", "%topic\\_\\%%", 
"%topic\\_\\%%");
+    }
+
+}
\ No newline at end of file
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
index 03de6cfa7..6de3d9da9 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
@@ -290,6 +290,52 @@ class RocketMQMetadataProviderTest {
                 .contains("message_model IS NULL", "message_model <>");
     }
 
+    @Test
+    void topicListAndPageShouldSearchLiteralNameCharactersTest() {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
+        when(topicMapper.selectList(any())).thenReturn(List.of());
+        Page<RmqTopic> databasePage = new Page<>(1, 20, 0);
+        databasePage.setRecords(List.of());
+        when(topicMapper.selectPage(any(Page.class), 
any(LambdaQueryWrapper.class))).thenReturn(databasePage);
+        RocketMQMetadataProvider provider = newProvider();
+
+        assertThat(provider.listTopics(null, null, null, 
"orders_%\\west")).isEmpty();
+        assertThat(provider.listTopicsPage(null, null, null, "orders_%\\west", 
1, 20).getItems())
+                .isEmpty();
+
+        ArgumentCaptor<LambdaQueryWrapper<RmqTopic>> listQuery = 
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        ArgumentCaptor<LambdaQueryWrapper<RmqTopic>> pageQuery = 
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(topicMapper).selectList(listQuery.capture());
+        verify(topicMapper).selectPage(any(Page.class), pageQuery.capture());
+        for (LambdaQueryWrapper<RmqTopic> query : 
List.of(listQuery.getValue(), pageQuery.getValue())) {
+            assertThat(query.getSqlSegment()).contains("name LIKE");
+            
assertThat(query.getParamNameValuePairs().values()).contains("%orders\\_\\%\\\\west%");
+        }
+    }
+
+    @Test
+    void groupListAndPageShouldSearchLiteralNameCharactersTest() {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
+        when(groupMapper.selectList(any())).thenReturn(List.of());
+        Page<RmqGroup> databasePage = new Page<>(1, 20, 0);
+        databasePage.setRecords(List.of());
+        when(groupMapper.selectPage(any(Page.class), 
any(LambdaQueryWrapper.class))).thenReturn(databasePage);
+        RocketMQMetadataProvider provider = newProvider();
+
+        assertThat(provider.listConsumerGroups(null, null, 
"orders_%\\west")).isEmpty();
+        assertThat(provider.listConsumerGroupsPage(null, null, 
"orders_%\\west", 1, 20).getItems())
+                .isEmpty();
+
+        ArgumentCaptor<LambdaQueryWrapper<RmqGroup>> listQuery = 
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        ArgumentCaptor<LambdaQueryWrapper<RmqGroup>> pageQuery = 
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(groupMapper).selectList(listQuery.capture());
+        verify(groupMapper).selectPage(any(Page.class), pageQuery.capture());
+        for (LambdaQueryWrapper<RmqGroup> query : 
List.of(listQuery.getValue(), pageQuery.getValue())) {
+            assertThat(query.getSqlSegment()).contains("name LIKE");
+            
assertThat(query.getParamNameValuePairs().values()).contains("%orders\\_\\%\\\\west%");
+        }
+    }
+
     @Test
     void getTopicRoutesShouldUseSelectedInstanceRuntimeClient() {
         List<BrokerRouteVO> routes = 
List.of(BrokerRouteVO.builder().brokerName("broker-a").build());

Reply via email to