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;
+ }
+}