From 56f22edbe862a186990a07e0129269f823dfc3b3 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Wed, 24 Mar 2021 08:22:49 +0530 Subject: [PATCH] Return hint message on identical columns (#3656) - Return hint message if identical column names are found in SQL query for postgres, MySQL, mssql, redshift plugin. - Add a PluginUtils class to hold general utility functions for plugins. --- .../external/helpers/PluginUtils.java | 50 +++++++++++ .../models/ActionExecutionResult.java | 8 ++ .../com/external/plugins/MssqlPlugin.java | 19 ++++ .../com/external/plugins/MssqlPluginTest.java | 44 ++++++++++ .../com/external/plugins/MySqlPlugin.java | 22 +++++ .../com/external/plugins/MySqlPluginTest.java | 41 +++++++++ .../com/external/plugins/PostgresPlugin.java | 20 +++++ .../external/plugins/PostgresPluginTest.java | 47 ++++++++++ .../com/external/plugins/RedshiftPlugin.java | 20 +++++ .../external/plugins/RedshiftPluginTest.java | 88 ++++++++++++++++++- 10 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java new file mode 100644 index 0000000000..9b58874e1f --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java @@ -0,0 +1,50 @@ +package com.appsmith.external.helpers; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class PluginUtils { + + public static List getColumnsListForJdbcPlugin(ResultSetMetaData metaData) throws SQLException { + List columnsList = IntStream + .range(1, metaData.getColumnCount()+1) // JDBC column indexes start from 1 + .mapToObj(i -> { + try { + return metaData.getColumnName(i); + } catch (SQLException exception) { + /* + * - Need suggestions on alternative ways of handling this exception. + */ + throw new RuntimeException(exception); + } + }) + .collect(Collectors.toList()); + + return columnsList; + } + + public static List getIdenticalColumns(List columnNames) { + /* + * - Get frequency of each column name + */ + Map columnFrequencies = columnNames + .stream() + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + /* + * - Filter only the inputs which have frequency greater than 1 + */ + List identicalColumns = columnFrequencies.entrySet().stream() + .filter(entry -> entry.getValue() > 1) + .map(entry -> entry.getKey()) + .collect(Collectors.toList()); + + return identicalColumns; + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java index ec96e38dfe..743b766f7c 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java @@ -6,6 +6,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import java.util.Set; + @Getter @Setter @ToString @@ -17,6 +19,12 @@ public class ActionExecutionResult { Object body; Boolean isExecutionSuccess = false; + /* + * - To return useful hints to the user. + * - E.g. if sql query result has identical columns + */ + Set messages; + ActionExecutionRequest request; } diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java index 7de48062ad..425bc4488b 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -54,6 +54,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; +import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.models.Connection.Mode.READ_ONLY; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -167,6 +169,7 @@ public class MssqlPlugin extends BasePlugin { } List> rowsList = new ArrayList<>(50); + final List columnsList = new ArrayList<>(); Statement statement = null; PreparedStatement preparedQuery = null; @@ -208,6 +211,7 @@ public class MssqlPlugin extends BasePlugin { } else { ResultSetMetaData metaData = resultSet.getMetaData(); int colCount = metaData.getColumnCount(); + columnsList.addAll(getColumnsListForJdbcPlugin(metaData)); while (resultSet.next()) { // Use `LinkedHashMap` here so that the column ordering is preserved in the response. @@ -287,6 +291,7 @@ public class MssqlPlugin extends BasePlugin { ActionExecutionResult result = new ActionExecutionResult(); result.setBody(objectMapper.valueToTree(rowsList)); + result.setMessages(populateHintMessages(columnsList)); result.setIsExecutionSuccess(true); System.out.println(Thread.currentThread().getName() + ": In the MssqlPlugin, got action execution result"); return Mono.just(result); @@ -316,6 +321,20 @@ public class MssqlPlugin extends BasePlugin { .subscribeOn(scheduler); } + private Set populateHintMessages(List columnNames) { + + Set messages = new HashSet<>(); + + List identicalColumns = getIdenticalColumns(columnNames); + if(!CollectionUtils.isEmpty(identicalColumns)) { + messages.add("Your MsSQL query result may not have all the columns because duplicate column names " + + "were found for the column(s): " + String.join(", ", identicalColumns) + ". You may use the " + + "SQL keyword 'as' to rename the duplicate column name(s) and resolve this issue."); + } + + return messages; + } + @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java index 6fcd2f7f14..f442d91cb5 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java @@ -27,11 +27,17 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -505,4 +511,42 @@ public class MssqlPluginTest { .verifyComplete(); } + @Test + public void testDuplicateColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id, username as id, password, email as password FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your MsSQL query result may not have all the columns because duplicate " + + "column names were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message.contains(expectedMessage)) + ); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "password") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0].split(",")) + .forEach(columnName -> foundColumnNames.add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index 1de133d6af..4132bbb7b6 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -57,6 +57,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static io.r2dbc.spi.ConnectionFactoryOptions.SSL; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -212,6 +213,7 @@ public class MySqlPlugin extends BasePlugin { boolean isSelectOrShowQuery = getIsSelectOrShowQuery(query); final List> rowsList = new ArrayList<>(50); + final List columnsList = new ArrayList<>(); Flux resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE)) .flatMapMany(isValid -> { @@ -233,6 +235,11 @@ public class MySqlPlugin extends BasePlugin { .flatMap(result -> result.map((row, meta) -> { rowsList.add(getRow(row, meta)); + + if(columnsList.isEmpty()) { + columnsList.addAll(meta.getColumnNames()); + } + return result; } ) @@ -259,6 +266,7 @@ public class MySqlPlugin extends BasePlugin { .map(res -> { ActionExecutionResult result = new ActionExecutionResult(); result.setBody(objectMapper.valueToTree(rowsList)); + result.setMessages(populateHintMessages(columnsList)); result.setIsExecutionSuccess(true); System.out.println(Thread.currentThread().getName() + " In the MySqlPlugin, got action " + "execution result"); @@ -333,6 +341,20 @@ public class MySqlPlugin extends BasePlugin { } + private Set populateHintMessages(List columnNames) { + + Set messages = new HashSet<>(); + + List identicalColumns = getIdenticalColumns(columnNames); + if(!CollectionUtils.isEmpty(identicalColumns)) { + messages.add("Your MySQL query result may not have all the columns because duplicate column names " + + "were found for the column(s): " + String.join(", ", identicalColumns) + ". You may use the " + + "SQL keyword 'as' to rename the duplicate column name(s) and resolve this issue."); + } + + return messages; + } + /** * 1. Parse the actual row objects returned by r2dbc driver for mysql statements. * 2. Return the row as a map {column_name -> column_value}. diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index 1a30423ff4..53adc8357a 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -28,10 +28,14 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -696,4 +700,41 @@ public class MySqlPluginTest { .verifyComplete(); } + public void testDuplicateColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id, username as id, password, email as password FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your MySQL query result may not have all the columns because duplicate column names " + + "were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message.contains(expectedMessage)) + ); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "password") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0].split(",")) + .forEach(columnName -> foundColumnNames.add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index f6f9265bc0..9bb942acae 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -59,6 +59,8 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; +import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -212,6 +214,7 @@ public class PostgresPlugin extends BasePlugin { } List> rowsList = new ArrayList<>(50); + final List columnsList = new ArrayList<>(); Statement statement = null; ResultSet resultSet = null; @@ -272,8 +275,10 @@ public class PostgresPlugin extends BasePlugin { ResultSetMetaData metaData = resultSet.getMetaData(); int colCount = metaData.getColumnCount(); + columnsList.addAll(getColumnsListForJdbcPlugin(metaData)); while (resultSet.next()) { + // Use `LinkedHashMap` here so that the column ordering is preserved in the response. Map row = new LinkedHashMap<>(colCount); @@ -374,6 +379,7 @@ public class PostgresPlugin extends BasePlugin { ActionExecutionResult result = new ActionExecutionResult(); result.setBody(objectMapper.valueToTree(rowsList)); + result.setMessages(populateHintMessages(columnsList)); result.setIsExecutionSuccess(true); System.out.println(Thread.currentThread().getName() + ": In the PostgresPlugin, got action execution result"); return Mono.just(result); @@ -405,6 +411,20 @@ public class PostgresPlugin extends BasePlugin { } + private Set populateHintMessages(List columnNames) { + + Set messages = new HashSet<>(); + + List identicalColumns = getIdenticalColumns(columnNames); + if(!CollectionUtils.isEmpty(identicalColumns)) { + messages.add("Your PostgreSQL query result may not have all the columns because duplicate column " + + "names were found for the column(s): " + String.join(", ", identicalColumns) + ". You may use" + + " the SQL keyword 'as' to rename the duplicate column name(s) and resolve this issue."); + } + + return messages; + } + @Override public Mono execute(HikariDataSource connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { // Unused function diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index 2e9b5e2c77..df335fb5e5 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -28,14 +28,19 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -855,4 +860,46 @@ public class PostgresPluginTest { assertTrue(error.getMessage().contains("The server does not support SSL")); }); } + + public void testDuplicateColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id, username as id, password, email as password FROM users WHERE id = 1"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "false")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your PostgreSQL query result may not have all the columns because " + + "duplicate column names were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message.contains(expectedMessage)) + ); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "password") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0].split(",")) + .forEach(columnName -> foundColumnNames.add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java index 356c95146a..2eeb0e12c7 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java @@ -45,6 +45,8 @@ import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; +import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; +import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.models.Connection.Mode.READ_ONLY; @@ -238,6 +240,7 @@ public class RedshiftPlugin extends BasePlugin { } List> rowsList = new ArrayList<>(50); + final List columnsList = new ArrayList<>(); Statement statement = null; ResultSet resultSet = null; @@ -247,6 +250,8 @@ public class RedshiftPlugin extends BasePlugin { if (isResultSet) { resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + columnsList.addAll(getColumnsListForJdbcPlugin(metaData)); while (resultSet.next()) { Map row = getRow(resultSet); @@ -281,6 +286,7 @@ public class RedshiftPlugin extends BasePlugin { ActionExecutionResult result = new ActionExecutionResult(); result.setBody(objectMapper.valueToTree(rowsList)); + result.setMessages(populateHintMessages(columnsList)); result.setIsExecutionSuccess(true); System.out.println( Thread.currentThread().getName() + ": " + @@ -320,6 +326,20 @@ public class RedshiftPlugin extends BasePlugin { .subscribeOn(scheduler); } + private Set populateHintMessages(List columnNames) { + + Set messages = new HashSet<>(); + + List identicalColumns = getIdenticalColumns(columnNames); + if(!CollectionUtils.isEmpty(identicalColumns)) { + messages.add("Your Redshift query result may not have all the columns because duplicate column names " + + "were found for the column(s): " + String.join(", ", identicalColumns) + ". You may use the " + + "SQL keyword 'as' to rename the duplicate column name(s) and resolve this issue."); + } + + return messages; + } + @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { try { diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java index a197089659..74c9ee08b7 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java @@ -1,5 +1,6 @@ package com.external.plugins; +import com.appsmith.external.helpers.PluginUtils; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DBAuth; @@ -30,12 +31,17 @@ import java.sql.Statement; import java.sql.Time; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doNothing; @@ -222,7 +228,7 @@ public class RedshiftPluginTest { */ ResultSetMetaData mockResultSetMetaData = mock(ResultSetMetaData.class); when(mockResultSet.getMetaData()).thenReturn(mockResultSetMetaData); - when(mockResultSetMetaData.getColumnCount()).thenReturn(10); + when(mockResultSetMetaData.getColumnCount()).thenReturn(0).thenReturn(10); when(mockResultSetMetaData.getColumnTypeName(Mockito.anyInt())).thenReturn("int4", "varchar", "varchar", "varchar", "date", "date", "time", "timetz", "timestamp", "timestamptz"); when(mockResultSetMetaData.getColumnName(Mockito.anyInt())).thenReturn("id", "username", "password", "email", @@ -450,4 +456,84 @@ public class RedshiftPluginTest { }) .verifyComplete(); } + + @Test + public void testDuplicateColumnNames() throws SQLException { + /* Mock java.sql.Connection: + * a. isClosed() + * b. isValid() + */ + Connection mockConnection = mock(Connection.class); + when(mockConnection.isClosed()).thenReturn(false); + when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true); + + /* Mock java.sql.Statement: + * a. execute(...) + * b. close() + */ + Statement mockStatement = mock(Statement.class); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.execute(Mockito.any())).thenReturn(true); + doNothing().when(mockStatement).close(); + + /* Mock java.sql.ResultSet: + * a. getObject(...) + * d. next() + * e. close() + */ + ResultSet mockResultSet = mock(ResultSet.class); + when(mockStatement.getResultSet()).thenReturn(mockResultSet); + when(mockResultSet.getObject(Mockito.anyInt())).thenReturn("", 1, "", 1, "", "jill", "", "jill"); + when(mockResultSet.next()).thenReturn(true).thenReturn(false); + doNothing().when(mockResultSet).close(); + + /* Mock java.sql.ResultSetMetaData: + * a. getColumnCount() + * b. getColumnTypeName(...) + * c. getColumnName(...) + */ + ResultSetMetaData mockResultSetMetaData = mock(ResultSetMetaData.class); + when(mockResultSet.getMetaData()).thenReturn(mockResultSetMetaData); + when(mockResultSetMetaData.getColumnCount()).thenReturn(4); + when(mockResultSetMetaData.getColumnTypeName(Mockito.anyInt())).thenReturn("int4", "int4", "varchar", + "varchar"); + when(mockResultSetMetaData.getColumnName(Mockito.anyInt())).thenReturn("id", "id", "username", "username"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id, id, username, username FROM users WHERE id = 1"); + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = Mono.just(mockConnection); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your Redshift query result may not have all the columns because " + + "duplicate column names were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message.contains(expectedMessage)) + ); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "username") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0].split(",")) + .forEach(columnName -> foundColumnNames.add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } }