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

zihaoxiang pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 9ce565ecc8 [Fix-17701][SqlTask] handle duplicate column aliases in SQL 
result by appending column index suffix (#17702)
9ce565ecc8 is described below

commit 9ce565ecc8d3f752ef4f12be247a8af0d42c20bc
Author: njnu-seafish <[email protected]>
AuthorDate: Thu Dec 4 10:35:35 2025 +0800

    [Fix-17701][SqlTask] handle duplicate column aliases in SQL result by 
appending column index suffix (#17702)
---
 .../dolphinscheduler/plugin/task/sql/SqlTask.java  |  16 ++-
 .../plugin/task/sql/SqlTaskTest.java               | 144 +++++++++++++++++++++
 2 files changed, 159 insertions(+), 1 deletion(-)

diff --git 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
index 5b96c1d004..44296bc331 100644
--- 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
+++ 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
@@ -49,9 +49,11 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -246,11 +248,23 @@ public class SqlTask extends AbstractTask {
         if (resultSet != null) {
             ResultSetMetaData md = resultSet.getMetaData();
             int num = md.getColumnCount();
+            String[] columnLabels = new String[num];
+
+            // Check for duplicates in column definitions (across all columns)
+            Set<String> uniqueLabels = new HashSet<>(num);
+            for (int i = 1; i <= num; i++) {
+                String label = md.getColumnLabel(i);
+                columnLabels[i - 1] = label;
+                if (!uniqueLabels.add(label)) {
+                    throw new TaskException("SQL column name conflict: 
duplicate column name '" + label
+                            + "'. Please use aliases to ensure unique column 
names.");
+                }
+            }
 
             while (resultSet.next()) {
                 ObjectNode mapOfColValues = JSONUtils.createObjectNode();
                 for (int i = 1; i <= num; i++) {
-                    mapOfColValues.set(md.getColumnLabel(i), 
JSONUtils.toJsonNode(resultSet.getObject(i)));
+                    mapOfColValues.set(columnLabels[i - 1], 
JSONUtils.toJsonNode(resultSet.getObject(i)));
                 }
                 resultJSONArray.add(mapOfColValues);
             }
diff --git 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
index 233d19757a..3832d653a5 100644
--- 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
+++ 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
@@ -17,8 +17,12 @@
 
 package org.apache.dolphinscheduler.plugin.task.sql;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 import org.apache.dolphinscheduler.common.utils.JSONUtils;
 import org.apache.dolphinscheduler.plugin.task.api.TaskConstants;
+import org.apache.dolphinscheduler.plugin.task.api.TaskException;
 import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
 import org.apache.dolphinscheduler.plugin.task.api.enums.DataType;
 import org.apache.dolphinscheduler.plugin.task.api.enums.Direct;
@@ -30,6 +34,10 @@ import 
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceP
 import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils;
 import org.apache.dolphinscheduler.spi.enums.DbType;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -37,6 +45,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.Lists;
 
 class SqlTaskTest {
@@ -197,4 +207,138 @@ class SqlTaskTest {
         Assertions.assertEquals("1", varPoolParam.getValue());
         Assertions.assertEquals(Direct.OUT, varPoolParam.getDirect());
     }
+
+    @Test
+    void 
testGenerateEmptyRow_WithNonNullResultSet_ReturnsEmptyValuesForAllColumns() 
throws Exception {
+        // Arrange
+        ResultSet mockResultSet = mock(ResultSet.class);
+        ResultSetMetaData mockMetaData = mock(ResultSetMetaData.class);
+
+        when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
+        when(mockMetaData.getColumnCount()).thenReturn(2);
+        when(mockMetaData.getColumnLabel(1)).thenReturn("id");
+        when(mockMetaData.getColumnLabel(2)).thenReturn("name");
+
+        Method method = SqlTask.class.getDeclaredMethod("generateEmptyRow", 
ResultSet.class);
+        method.setAccessible(true);
+
+        // Act
+        ArrayNode result = (ArrayNode) method.invoke(sqlTask, mockResultSet);
+
+        // Assert
+        Assertions.assertNotNull(result);
+        Assertions.assertEquals(1, result.size());
+
+        ObjectNode row = (ObjectNode) result.get(0);
+        Assertions.assertEquals("", row.get("id").asText());
+        Assertions.assertEquals("", row.get("name").asText());
+    }
+
+    @Test
+    void testGenerateEmptyRow_WithNullResultSet_ReturnsErrorObject() throws 
Exception {
+        // Arrange
+        Method method = SqlTask.class.getDeclaredMethod("generateEmptyRow", 
ResultSet.class);
+        method.setAccessible(true);
+
+        // Act
+        ArrayNode result = (ArrayNode) method.invoke(sqlTask, (ResultSet) 
null);
+
+        // Assert
+        Assertions.assertNotNull(result);
+        Assertions.assertEquals(1, result.size());
+
+        ObjectNode row = (ObjectNode) result.get(0);
+        Assertions.assertTrue(row.has("error"));
+        Assertions.assertEquals("resultSet is null", 
row.get("error").asText());
+    }
+
+    @Test
+    void testGenerateEmptyRow_WithDuplicateColumns_DeduplicatesLabels() throws 
Exception {
+        ResultSet mockResultSet = mock(ResultSet.class);
+        ResultSetMetaData mockMetaData = mock(ResultSetMetaData.class);
+
+        when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
+        when(mockMetaData.getColumnCount()).thenReturn(3);
+        when(mockMetaData.getColumnLabel(1)).thenReturn("id");
+        when(mockMetaData.getColumnLabel(2)).thenReturn("id"); // duplicate
+        when(mockMetaData.getColumnLabel(3)).thenReturn("name");
+
+        Method method = SqlTask.class.getDeclaredMethod("generateEmptyRow", 
ResultSet.class);
+        method.setAccessible(true);
+
+        ArrayNode result = (ArrayNode) method.invoke(sqlTask, mockResultSet);
+
+        Assertions.assertNotNull(result);
+        Assertions.assertEquals(1, result.size());
+
+        ObjectNode row = (ObjectNode) result.get(0);
+        Assertions.assertTrue(row.has("id"));
+        Assertions.assertTrue(row.has("name"));
+    }
+
+    @Test
+    void testResultProcess_NullResultSet_ReturnsEmptyResult() throws Exception 
{
+        Method resultProcessMethod = 
SqlTask.class.getDeclaredMethod("resultProcess", ResultSet.class);
+        resultProcessMethod.setAccessible(true);
+
+        // Mock a null ResultSet
+        String result = (String) resultProcessMethod.invoke(sqlTask, 
(ResultSet) null);
+
+        Assertions.assertNotNull(result);
+        Assertions.assertTrue(result.equalsIgnoreCase("[{\"error\":\"resultSet 
is null\"}]"));
+    }
+
+    @Test
+    void testResultProcess_EmptyResultSet_ReturnsEmptyResult() throws 
Exception {
+        // Mock a non-null ResultSet that contains no data rows
+        ResultSet mockResultSet = mock(ResultSet.class);
+        ResultSetMetaData mockMetaData = mock(ResultSetMetaData.class);
+
+        when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
+        when(mockMetaData.getColumnCount()).thenReturn(2);
+        when(mockMetaData.getColumnLabel(1)).thenReturn("id");
+        when(mockMetaData.getColumnLabel(2)).thenReturn("name");
+        when(mockResultSet.next()).thenReturn(false); // no rows available
+
+        Method resultProcessMethod = 
SqlTask.class.getDeclaredMethod("resultProcess", ResultSet.class);
+        resultProcessMethod.setAccessible(true);
+
+        String result = (String) resultProcessMethod.invoke(sqlTask, 
mockResultSet);
+
+        Assertions.assertNotNull(result);
+        // Verify the result contains empty string values for all columns and 
is a valid JSON array
+        Assertions.assertTrue(result.contains("\"id\":\"\""));
+        Assertions.assertTrue(result.contains("\"name\":\"\""));
+        Assertions.assertTrue(result.startsWith("[{"));
+        Assertions.assertTrue(result.endsWith("}]"));
+    }
+
+    @Test
+    void testResultProcess_DuplicateColumnLabels_ThrowsTaskException() throws 
Exception {
+        ResultSet mockRs = mock(ResultSet.class);
+        ResultSetMetaData mockMd = mock(ResultSetMetaData.class);
+
+        when(mockRs.getMetaData()).thenReturn(mockMd);
+        when(mockMd.getColumnCount()).thenReturn(2);
+        when(mockMd.getColumnLabel(1)).thenReturn("id");
+        when(mockMd.getColumnLabel(2)).thenReturn("id"); // duplicate column 
name
+
+        Method method = SqlTask.class.getDeclaredMethod("resultProcess", 
ResultSet.class);
+        method.setAccessible(true);
+
+        // Assert that InvocationTargetException is thrown
+        InvocationTargetException thrown = Assertions.assertThrows(
+                InvocationTargetException.class,
+                () -> method.invoke(sqlTask, mockRs));
+
+        // Check the actual cause
+        Throwable cause = thrown.getCause();
+        Assertions.assertNotNull(cause);
+        Assertions.assertInstanceOf(TaskException.class, cause,
+                "Cause should be TaskException, but was: " + cause.getClass());
+        Assertions.assertTrue(
+                cause.getMessage().contains("duplicate column name"),
+                "TaskException message should mention duplicate column name");
+    }
+
 }

Reply via email to