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

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new 8907f0ce7a Add unit tests for JDBC query DAO SQL building (#13800)
8907f0ce7a is described below

commit 8907f0ce7aa1e211fc4df1c31ed962ae37b76a99
Author: Hyunjin-Jeong <[email protected]>
AuthorDate: Mon Apr 6 23:00:11 2026 +0900

    Add unit tests for JDBC query DAO SQL building (#13800)
    
    Add unit tests for SQL building in JDBCAlarmQueryDAO, JDBCLogQueryDAO, 
JDBCTraceQueryDAO, and JDBCTopologyQueryDAO, verifying correct WHERE clause 
construction, JOIN generation, parameter binding, and ORDER BY/LIMIT handling 
across various filter combinations.
---
 .../jdbc/common/dao/JDBCAlarmQueryDAOTest.java     | 162 +++++++++++++++++
 .../jdbc/common/dao/JDBCLogQueryDAOTest.java       | 201 +++++++++++++++++++++
 .../jdbc/common/dao/JDBCTopologyQueryDAOTest.java  | 177 ++++++++++++++++++
 .../jdbc/common/dao/JDBCTraceQueryDAOTest.java     | 159 ++++++++++++++++
 4 files changed, 699 insertions(+)

diff --git 
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAlarmQueryDAOTest.java
 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAlarmQueryDAOTest.java
new file mode 100644
index 0000000000..171e385dbc
--- /dev/null
+++ 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAlarmQueryDAOTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.skywalking.oap.server.storage.plugin.jdbc.common.dao;
+
+import org.apache.skywalking.oap.server.core.alarm.AlarmRecord;
+import org.apache.skywalking.oap.server.core.analysis.manual.searchtag.Tag;
+import org.apache.skywalking.oap.server.core.query.input.Duration;
+import 
org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInstaller;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.SQLAndParameters;
+import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.TableHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class JDBCAlarmQueryDAOTest {
+
+    private static final String TABLE = "alarm_record_20260406";
+    private static final String TAG_TABLE = "alarm_record_tag_20260406";
+
+    @Mock
+    private JDBCClient jdbcClient;
+    @Mock
+    private ModuleManager moduleManager;
+    @Mock
+    private TableHelper tableHelper;
+
+    private JDBCAlarmQueryDAO dao;
+
+    @BeforeEach
+    void setUp() {
+        dao = new JDBCAlarmQueryDAO(jdbcClient, moduleManager, tableHelper);
+    }
+
+    @Test
+    void buildSQL_shouldContainTableColumnConditionOnlyOnce() {
+        final SQLAndParameters result = dao.buildSQL(null, null, 10, 0, null, 
null, TABLE);
+        final String sql = result.sql();
+
+        final long count = countOccurrences(sql, 
JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(count).as("TABLE_COLUMN condition should appear exactly 
once").isEqualTo(1);
+    }
+
+    @Test
+    void buildSQL_withNoConditions_shouldProduceMinimalQuery() {
+        final SQLAndParameters result = dao.buildSQL(null, null, 10, 0, null, 
null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("select * from " + TABLE);
+        assertThat(sql).contains("where " + TABLE + "." + 
JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(sql).contains("order by " + AlarmRecord.START_TIME + " 
desc");
+        assertThat(sql).contains("limit 10");
+        assertThat(sql).doesNotContain("inner join");
+        assertThat(sql).doesNotContain(AlarmRecord.SCOPE);
+        assertThat(sql).doesNotContain("like");
+    }
+
+    @Test
+    void buildSQL_withScopeId_shouldIncludeScopeCondition() {
+        final SQLAndParameters result = dao.buildSQL(1, null, 10, 0, null, 
null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + AlarmRecord.SCOPE + " = ?");
+        assertThat(result.parameters()).contains(1);
+    }
+
+    @Test
+    void buildSQL_withKeyword_shouldIncludeLikeCondition() {
+        final SQLAndParameters result = dao.buildSQL(null, "error", 10, 0, 
null, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + AlarmRecord.ALARM_MESSAGE + " like 
concat('%',?,'%')");
+        assertThat(result.parameters()).contains("error");
+    }
+
+    @Test
+    void buildSQL_withDuration_shouldIncludeTimeBucketConditions() {
+        final Duration duration = new Duration();
+        duration.setStart("2026-04-06 0000");
+        duration.setEnd("2026-04-06 2359");
+        
duration.setStep(org.apache.skywalking.oap.server.core.query.enumeration.Step.MINUTE);
+
+        final SQLAndParameters result = dao.buildSQL(null, null, 10, 0, 
duration, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + TABLE + "." + 
AlarmRecord.TIME_BUCKET + " >= ?");
+        assertThat(sql).contains("and " + TABLE + "." + 
AlarmRecord.TIME_BUCKET + " <= ?");
+    }
+
+    @Test
+    void buildSQL_withSingleTag_shouldUseInnerJoin() {
+        final List<Tag> tags = Collections.singletonList(new Tag("env", 
"prod"));
+
+        final SQLAndParameters result = dao.buildSQL(null, null, 10, 0, null, 
tags, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"0");
+        assertThat(sql).contains(TAG_TABLE + "0." + AlarmRecord.TAGS + " = ?");
+        assertThat(result.parameters()).contains("env=prod");
+    }
+
+    @Test
+    void buildSQL_withMultipleTags_shouldUseMultipleInnerJoins() {
+        final List<Tag> tags = Arrays.asList(new Tag("env", "prod"), new 
Tag("region", "us-east"));
+
+        final SQLAndParameters result = dao.buildSQL(null, null, 10, 0, null, 
tags, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"0");
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"1");
+        assertThat(sql).contains(TAG_TABLE + "0." + AlarmRecord.TAGS + " = ?");
+        assertThat(sql).contains(TAG_TABLE + "1." + AlarmRecord.TAGS + " = ?");
+    }
+
+    @Test
+    void buildSQL_withLimitAndOffset_shouldApplyTotalAsLimit() {
+        final SQLAndParameters result = dao.buildSQL(null, null, 20, 5, null, 
null, TABLE);
+        final String sql = result.sql();
+
+        // JDBC uses offset+limit as the database LIMIT, then skips in 
application
+        assertThat(sql).contains("limit 25");
+    }
+
+    private long countOccurrences(final String text, final String pattern) {
+        int count = 0;
+        int index = 0;
+        while ((index = text.indexOf(pattern, index)) != -1) {
+            count++;
+            index += pattern.length();
+        }
+        return count;
+    }
+}
diff --git 
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCLogQueryDAOTest.java
 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCLogQueryDAOTest.java
new file mode 100644
index 0000000000..f65c3574a7
--- /dev/null
+++ 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCLogQueryDAOTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.skywalking.oap.server.storage.plugin.jdbc.common.dao;
+
+import 
org.apache.skywalking.oap.server.core.analysis.manual.log.AbstractLogRecord;
+import org.apache.skywalking.oap.server.core.analysis.manual.searchtag.Tag;
+import org.apache.skywalking.oap.server.core.query.enumeration.Order;
+import org.apache.skywalking.oap.server.core.query.input.TraceScopeCondition;
+import 
org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInstaller;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.SQLAndParameters;
+import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.TableHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class JDBCLogQueryDAOTest {
+
+    private static final String TABLE = "log_20260406";
+    private static final String TAG_TABLE = "log_tag_20260406";
+
+    @Mock
+    private JDBCClient jdbcClient;
+    @Mock
+    private ModuleManager moduleManager;
+    @Mock
+    private TableHelper tableHelper;
+
+    private JDBCLogQueryDAO dao;
+
+    @BeforeEach
+    void setUp() {
+        dao = new JDBCLogQueryDAO(jdbcClient, moduleManager, tableHelper);
+    }
+
+    @Test
+    void buildSQL_shouldContainTableColumnConditionOnlyOnce() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.DES, 0, 10, null, null, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        final long count = countOccurrences(sql, 
JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(count).as("TABLE_COLUMN condition should appear exactly 
once").isEqualTo(1);
+    }
+
+    @Test
+    void buildSQL_withNoConditions_shouldProduceMinimalQuery() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.DES, 0, 10, null, null, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("select * from " + TABLE);
+        assertThat(sql).contains("where " + JDBCTableInstaller.TABLE_COLUMN + 
" = ?");
+        assertThat(sql).contains("order by " + AbstractLogRecord.TIMESTAMP + " 
desc");
+        assertThat(sql).contains("limit 10");
+        assertThat(sql).doesNotContain("inner join");
+    }
+
+    @Test
+    void buildSQL_withAscOrder_shouldProduceAscQuery() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.ASC, 0, 10, null, null, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("order by " + AbstractLogRecord.TIMESTAMP + " 
asc");
+    }
+
+    @Test
+    void buildSQL_withServiceId_shouldIncludeServiceCondition() {
+        final SQLAndParameters result = dao.buildSQL(
+            "service-1", null, null, null, Order.DES, 0, 10, null, null, null, 
null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + TABLE + "." + 
AbstractLogRecord.SERVICE_ID + " = ?");
+        assertThat(result.parameters()).contains("service-1");
+    }
+
+    @Test
+    void buildSQL_withServiceInstanceId_shouldIncludeInstanceCondition() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, "instance-1", null, null, Order.DES, 0, 10, null, null, 
null, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + 
AbstractLogRecord.SERVICE_INSTANCE_ID + " = ?");
+        assertThat(result.parameters()).contains("instance-1");
+    }
+
+    @Test
+    void buildSQL_withEndpointId_shouldIncludeEndpointCondition() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, "endpoint-1", null, Order.DES, 0, 10, null, null, 
null, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + AbstractLogRecord.ENDPOINT_ID + " = 
?");
+        assertThat(result.parameters()).contains("endpoint-1");
+    }
+
+    @Test
+    void buildSQL_withTraceId_shouldIncludeTraceCondition() {
+        final TraceScopeCondition traceCondition = new TraceScopeCondition();
+        traceCondition.setTraceId("trace-abc");
+
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, traceCondition, Order.DES, 0, 10, null, null, 
null, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + AbstractLogRecord.TRACE_ID + " = ?");
+        assertThat(result.parameters()).contains("trace-abc");
+    }
+
+    @Test
+    void buildSQL_withSegmentIdAndSpanId_shouldIncludeBothConditions() {
+        final TraceScopeCondition traceCondition = new TraceScopeCondition();
+        traceCondition.setSegmentId("segment-abc");
+        traceCondition.setSpanId(1);
+
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, traceCondition, Order.DES, 0, 10, null, null, 
null, null, TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("and " + AbstractLogRecord.TRACE_SEGMENT_ID + 
" = ?");
+        assertThat(sql).contains("and " + AbstractLogRecord.SPAN_ID + " = ?");
+        assertThat(result.parameters()).contains("segment-abc");
+        assertThat(result.parameters()).contains(1);
+    }
+
+    @Test
+    void buildSQL_withSingleTag_shouldUseInnerJoin() {
+        final List<Tag> tags = Collections.singletonList(new Tag("level", 
"ERROR"));
+
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.DES, 0, 10, null, tags, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"0");
+        assertThat(sql).contains(TAG_TABLE + "0." + AbstractLogRecord.TAGS + " 
= ?");
+        assertThat(result.parameters()).contains("level=ERROR");
+    }
+
+    @Test
+    void buildSQL_withMultipleTags_shouldUseMultipleInnerJoins() {
+        final List<Tag> tags = Arrays.asList(new Tag("level", "ERROR"), new 
Tag("service", "order"));
+
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.DES, 0, 10, null, tags, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"0");
+        assertThat(sql).contains("inner join " + TAG_TABLE + " " + TAG_TABLE + 
"1");
+        assertThat(sql).contains(TAG_TABLE + "0." + AbstractLogRecord.TAGS + " 
= ?");
+        assertThat(sql).contains(TAG_TABLE + "1." + AbstractLogRecord.TAGS + " 
= ?");
+    }
+
+    @Test
+    void buildSQL_withLimitAndOffset_shouldApplyTotalAsLimit() {
+        final SQLAndParameters result = dao.buildSQL(
+            null, null, null, null, Order.DES, 5, 20, null, null, null, null, 
TABLE);
+        final String sql = result.sql();
+
+        assertThat(sql).contains("limit 25");
+    }
+
+    private long countOccurrences(final String text, final String pattern) {
+        int count = 0;
+        int index = 0;
+        while ((index = text.indexOf(pattern, index)) != -1) {
+            count++;
+            index += pattern.length();
+        }
+        return count;
+    }
+}
diff --git 
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTopologyQueryDAOTest.java
 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTopologyQueryDAOTest.java
new file mode 100644
index 0000000000..ecc99ad314
--- /dev/null
+++ 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTopologyQueryDAOTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.skywalking.oap.server.storage.plugin.jdbc.common.dao;
+
+import 
org.apache.skywalking.oap.server.core.analysis.manual.relation.instance.ServiceInstanceRelationServerSideMetrics;
+import 
org.apache.skywalking.oap.server.core.analysis.manual.relation.service.ServiceRelationServerSideMetrics;
+import org.apache.skywalking.oap.server.core.query.enumeration.Step;
+import org.apache.skywalking.oap.server.core.query.input.Duration;
+import 
org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInstaller;
+import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.TableHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class JDBCTopologyQueryDAOTest {
+
+    private static final String TABLE = 
"service_relation_server_side_20260406";
+    private static final String INSTANCE_TABLE = 
"service_instance_relation_server_side_20260406";
+
+    @Mock
+    private JDBCClient jdbcClient;
+    @Mock
+    private TableHelper tableHelper;
+
+    private JDBCTopologyQueryDAO dao;
+    private Duration duration;
+
+    @BeforeEach
+    void setUp() {
+        dao = new JDBCTopologyQueryDAO(jdbcClient, tableHelper);
+
+        duration = new Duration();
+        duration.setStart("2026-04-06 0000");
+        duration.setEnd("2026-04-06 2359");
+        duration.setStep(Step.MINUTE);
+    }
+
+    @Test
+    void 
loadServiceRelationsDetectedAtServerSide_withNoServiceIds_shouldNotAddServiceIdFilter()
 throws Exception {
+        when(tableHelper.getTablesForRead(
+            ServiceRelationServerSideMetrics.INDEX_NAME,
+            duration.getStartTimeBucket(),
+            duration.getEndTimeBucket()
+        )).thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.loadServiceRelationsDetectedAtServerSide(duration);
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(sql).doesNotContain("and (");
+        assertThat(sql).contains("group by");
+    }
+
+    @Test
+    void 
loadServiceRelationsDetectedAtServerSide_withSingleServiceId_shouldAddOrCondition()
 throws Exception {
+        when(tableHelper.getTablesForRead(
+            ServiceRelationServerSideMetrics.INDEX_NAME,
+            duration.getStartTimeBucket(),
+            duration.getEndTimeBucket()
+        )).thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.loadServiceRelationsDetectedAtServerSide(duration, 
Collections.singletonList("svc-1"));
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains("and (");
+        
assertThat(sql).contains(ServiceRelationServerSideMetrics.SOURCE_SERVICE_ID + 
"=?");
+        assertThat(sql).contains(" or " + 
ServiceRelationServerSideMetrics.DEST_SERVICE_ID + "=?");
+        // parentheses must be closed
+        assertThat(sql).containsPattern("\\(.*=\\?.*or.*=\\?.*\\)");
+    }
+
+    @Test
+    void 
loadServiceRelationsDetectedAtServerSide_withMultipleServiceIds_shouldChainOrConditions()
 throws Exception {
+        when(tableHelper.getTablesForRead(
+            ServiceRelationServerSideMetrics.INDEX_NAME,
+            duration.getStartTimeBucket(),
+            duration.getEndTimeBucket()
+        )).thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.loadServiceRelationsDetectedAtServerSide(duration, 
Arrays.asList("svc-1", "svc-2"));
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains("and (");
+        // two pairs of source/dest conditions connected with OR
+        assertThat(countOccurrences(sql, 
ServiceRelationServerSideMetrics.SOURCE_SERVICE_ID + "=?")).isEqualTo(2);
+        assertThat(countOccurrences(sql, 
ServiceRelationServerSideMetrics.DEST_SERVICE_ID + "=?")).isEqualTo(2);
+        // parentheses must be closed
+        assertThat(sql).containsPattern("and \\(.*\\)");
+    }
+
+    @Test
+    void 
loadInstanceRelationDetectedAtServerSide_shouldUseBidirectionalCondition() 
throws Exception {
+        when(tableHelper.getTablesForRead(
+            ServiceInstanceRelationServerSideMetrics.INDEX_NAME,
+            duration.getStartTimeBucket(),
+            duration.getEndTimeBucket()
+        )).thenReturn(Collections.singletonList(INSTANCE_TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.loadInstanceRelationDetectedAtServerSide("client-svc", 
"server-svc", duration);
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        // bidirectional: (source=A and dest=B) OR (source=B and dest=A)
+        assertThat(sql).contains("((");
+        assertThat(sql).contains(") or (");
+        assertThat(sql).contains("))");
+        assertThat(countOccurrences(sql, 
ServiceInstanceRelationServerSideMetrics.SOURCE_SERVICE_ID + 
"=?")).isEqualTo(2);
+        assertThat(countOccurrences(sql, 
ServiceInstanceRelationServerSideMetrics.DEST_SERVICE_ID + "=?")).isEqualTo(2);
+    }
+
+    private long countOccurrences(final String text, final String pattern) {
+        int count = 0;
+        int index = 0;
+        while ((index = text.indexOf(pattern, index)) != -1) {
+            count++;
+            index += pattern.length();
+        }
+        return count;
+    }
+}
diff --git 
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTraceQueryDAOTest.java
 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTraceQueryDAOTest.java
new file mode 100644
index 0000000000..01e5131e26
--- /dev/null
+++ 
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCTraceQueryDAOTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.skywalking.oap.server.storage.plugin.jdbc.common.dao;
+
+import 
org.apache.skywalking.oap.server.core.analysis.manual.segment.SegmentRecord;
+import 
org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import 
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInstaller;
+import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.TableHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class JDBCTraceQueryDAOTest {
+
+    private static final String TABLE = "segment_20260406";
+
+    @Mock
+    private JDBCClient jdbcClient;
+    @Mock
+    private ModuleManager moduleManager;
+    @Mock
+    private TableHelper tableHelper;
+
+    private JDBCTraceQueryDAO dao;
+
+    @BeforeEach
+    void setUp() {
+        dao = new JDBCTraceQueryDAO(moduleManager, jdbcClient, tableHelper);
+    }
+
+    @Test
+    void queryByTraceId_shouldContainTableColumnAndTraceIdCondition() throws 
Exception {
+        when(tableHelper.getTablesWithinTTL(SegmentRecord.INDEX_NAME))
+            .thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.queryByTraceId("trace-abc", null);
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(sql).contains(SegmentRecord.TRACE_ID + " = ?");
+        // TABLE_COLUMN should appear exactly once
+        assertThat(countOccurrences(sql, JDBCTableInstaller.TABLE_COLUMN + " = 
?")).isEqualTo(1);
+    }
+
+    @Test
+    void queryBySegmentIdList_shouldUseInClause() throws Exception {
+        when(tableHelper.getTablesWithinTTL(SegmentRecord.INDEX_NAME))
+            .thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.queryBySegmentIdList(Arrays.asList("seg-1", "seg-2", "seg-3"), 
null);
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(sql).contains(SegmentRecord.SEGMENT_ID + " in (?,?,?)");
+        assertThat(sql).doesNotContain(" or ");
+    }
+
+    @Test
+    void queryByTraceIdWithInstanceId_shouldProduceValidSqlWithBothInClauses() 
throws Exception {
+        when(tableHelper.getTablesWithinTTL(SegmentRecord.INDEX_NAME))
+            .thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.queryByTraceIdWithInstanceId(
+            Arrays.asList("trace-1", "trace-2"),
+            Arrays.asList("instance-1", "instance-2"),
+            null
+        );
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(JDBCTableInstaller.TABLE_COLUMN + " = ?");
+        assertThat(sql).contains(SegmentRecord.TRACE_ID + " in (?,?)");
+        assertThat(sql).contains(" and " + SegmentRecord.SERVICE_INSTANCE_ID + 
" in (?,?)");
+        // verify the IN clauses are both properly enclosed with parentheses
+        assertThat(sql).containsPattern("trace_id in \\(\\?,\\?\\) and 
service_instance_id in \\(\\?,\\?\\)");
+    }
+
+    @Test
+    void queryByTraceIdWithInstanceId_withSingleItems_shouldProduceValidSql() 
throws Exception {
+        when(tableHelper.getTablesWithinTTL(SegmentRecord.INDEX_NAME))
+            .thenReturn(Collections.singletonList(TABLE));
+
+        final AtomicReference<String> capturedSql = new AtomicReference<>();
+        doAnswer(invocation -> {
+            capturedSql.set(invocation.getArgument(0));
+            return Collections.emptyList();
+        }).when(jdbcClient).executeQuery(anyString(), any(), 
any(Object[].class));
+
+        dao.queryByTraceIdWithInstanceId(
+            Collections.singletonList("trace-1"),
+            Collections.singletonList("instance-1"),
+            null
+        );
+
+        final String sql = capturedSql.get();
+        assertThat(sql).contains(SegmentRecord.TRACE_ID + " in (?)");
+        assertThat(sql).contains(" and " + SegmentRecord.SERVICE_INSTANCE_ID + 
" in (?)");
+    }
+
+    private long countOccurrences(final String text, final String pattern) {
+        int count = 0;
+        int index = 0;
+        while ((index = text.indexOf(pattern, index)) != -1) {
+            count++;
+            index += pattern.length();
+        }
+        return count;
+    }
+}

Reply via email to