Skip to content

Commit 5a6a134

Browse files
committed
ISSUE-680 # Developer review and coverage check
1 parent 5972efb commit 5a6a134

File tree

12 files changed

+110
-48
lines changed

12 files changed

+110
-48
lines changed

core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
import com.univocity.parsers.csv.CsvParser;
1717

18+
/**
19+
* Data loading in the database from a CSV external source
20+
*/
1821
class DbCsvLoader {
1922
private static final Logger LOGGER = LoggerFactory.getLogger(DbCsvLoader.class);
2023
private Connection conn;
@@ -26,10 +29,10 @@ public DbCsvLoader(Connection conn, CsvParser csvParser) {
2629
}
2730

2831
/**
29-
* Loads rows in csv format (csvLines) into a table in the database
30-
* and returns the total number of rows
32+
* Loads rows in CSV format (csvLines) into a table in the database
33+
* and returns the total number of rows.
3134
*/
32-
int loadCsv(String table, List<String> csvLines, boolean withHeaders, String nullString) throws SQLException {
35+
public int loadCsv(String table, List<String> csvLines, boolean withHeaders, String nullString) throws SQLException {
3336
if (csvLines == null || csvLines.isEmpty())
3437
return 0;
3538

@@ -87,7 +90,7 @@ private List<Object[]> buildParameters(String table, String[] headers, List<Stri
8790
LOGGER.info(" row [{}] params: {}", i + 1, Arrays.asList(params).toString());
8891
} catch (Exception e) { // Not only SQLException as converter also does parsing
8992
String message = String.format("Error matching data type of parameters and table columns at CSV row %d", i + 1);
90-
LOGGER.error(message); // do not log the exception because it will be logged by the parent executor (DbCsvLoader)
93+
LOGGER.error(message);
9194
LOGGER.error("Exception message: {}", e.getMessage());
9295
throw new RuntimeException(message, e);
9396
}
@@ -123,7 +126,7 @@ private void insertRow(QueryRunner runner, int rowId, String sql, Object[] param
123126
runner.update(conn, sql, params);
124127
} catch (SQLException e) {
125128
String message = String.format("Error inserting data at CSV row %d", rowId + 1);
126-
LOGGER.error(message); // do not log the exception because it will be logged by the parent executor (DbCsvLoader)
129+
LOGGER.error(message);
127130
LOGGER.error("Exception message: {}", e.getMessage());
128131
throw new RuntimeException(message, e);
129132
}

core/src/main/java/org/jsmart/zerocode/core/db/DbCsvRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public String getNullString() {
5050
}
5151

5252
// Code below is duplicated from org.jsmart.zerocode.core.domain.Parametrized.java and not included in tests.
53-
// TODO Consider some refactoring later (to SmartUtils?) and review error message when file not found
53+
// TODO Consider some refactoring later and review error message when file not found
5454

5555
private List<String> getCsvSourceFrom(JsonNode csvSourceJsonNode) {
5656
try {

core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public class DbSqlExecutor {
2525
public static final String SQL_RESULTS_KEY = "rows";
2626
public static final String CSV_RESULTS_KEY = "size";
2727

28-
@Inject
28+
// Optional to log the explanatory error message if the env variables are no defined
29+
@Inject(optional = true)
2930
@Named("db.driver.url") private String url;
3031

3132
@Inject(optional = true)
@@ -56,16 +57,17 @@ public Map<String, Object> loadcsv(DbCsvRequest request) {
5657
response.put(CSV_RESULTS_KEY, result);
5758
return response;
5859
} catch (Exception e) {
59-
LOGGER.error("Failed to load CSV", e);
60-
throw new RuntimeException(e);
60+
String message = "Failed to load CSV";
61+
LOGGER.error(message, e);
62+
throw new RuntimeException(message, e);
6163
} finally {
6264
closeConnection(conn);
6365
}
6466
}
6567

6668
/**
6769
* The EXECUTE operation returns the records retrieved by the SQL specified in the request
68-
* under the key "rows" (select) or an empty object (insert, update)
70+
* under the key "rows" (select), or an empty object (insert, update)
6971
*/
7072
public Map<String, Object> EXECUTE(DbSqlRequest request) {
7173
return execute(request);
@@ -85,8 +87,9 @@ public Map<String, Object> execute(DbSqlRequest request) {
8587
}
8688
return response;
8789
} catch (SQLException e) {
88-
LOGGER.error("Failed to execute SQL", e);
89-
throw new RuntimeException(e);
90+
String message = "Failed to execute SQL";
91+
LOGGER.error(message, e);
92+
throw new RuntimeException(message, e);
9093
} finally {
9194
closeConnection(conn);
9295
}
@@ -102,8 +105,10 @@ protected Connection createAndGetConnection() {
102105
try {
103106
return DriverManager.getConnection(url, user, password);
104107
} catch (SQLException e) {
105-
LOGGER.error("Failed to create connection", e);
106-
throw new RuntimeException(e);
108+
String message = "Failed to create connection, Please check the target environment properties "
109+
+ "to connect the database (db.driver.url, db.driver.user and db.driver.password)";
110+
LOGGER.error(message, e);
111+
throw new RuntimeException(message, e);
107112
}
108113
}
109114

core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRequest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ public class DbSqlRequest {
1212
private final Object[] sqlParams;
1313

1414
@JsonCreator
15-
public DbSqlRequest(@JsonProperty("sqlStatement") String sql,
15+
public DbSqlRequest(
16+
@JsonProperty("sql") String sql,
1617
@JsonProperty("sqlParams") Object[] sqlParams) {
1718
this.sql = sql;
1819
this.sqlParams = sqlParams;

core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRunner.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import java.util.List;
1111
import java.util.Map;
1212

13+
/**
14+
* Execution of SQL statements against a database
15+
*/
1316
class DbSqlRunner {
1417
private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlRunner.class);
1518
private Connection conn;
@@ -19,14 +22,14 @@ public DbSqlRunner(Connection conn) {
1922
}
2023

2124
/**
22-
* Execute an sql with parameters (optional) and returns a list of maps
25+
* Executes a SQL statement with parameters (optional) and returns a list of maps
2326
* with the ResultSet content (select) or null (insert, update)
2427
*/
25-
List<Map<String, Object>> execute(String sql, Object[] params) throws SQLException {
26-
// As there is only one execute operation instead of separate update and query,
27-
// the DbUtils execute method returns a list containing each ResultSet (each is a list of maps):
28+
public List<Map<String, Object>> execute(String sql, Object[] params) throws SQLException {
29+
// There is only one execute operation instead of separate update and query.
30+
// The DbUtils execute method returns a list containing each ResultSet (each is a list of maps):
2831
// - Empty (insert and update)
29-
// - With one or more ResultSets (select).
32+
// - With one or more ResultSets (select): use the first one
3033
// - Note that some drivers never return more than one ResultSet (e.g. H2)
3134
QueryRunner runner = new QueryRunner();
3235
List<List<Map<String, Object>>> result = runner.execute(conn, sql, new MapListHandler(), params);

core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
import java.util.LinkedHashMap;
1111
import java.util.Map;
1212

13+
import org.apache.commons.lang3.ArrayUtils;
1314
import org.slf4j.Logger;
1415
import org.slf4j.LoggerFactory;
1516

1617
/**
1718
* Conversion of string values to be inserted in the database
18-
* into objects compatible with the java.sql type of the target columns.
19+
* into objects compatible with the java sql type of the target columns.
1920
*/
2021
public class DbValueConverter {
2122
private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class);
@@ -58,9 +59,9 @@ private void initializeMetadata() throws SQLException {
5859
}
5960

6061
private String convertToStoredCase(String identifier) throws SQLException {
61-
if (databaseMetaData.storesLowerCaseIdentifiers())
62+
if (databaseMetaData.storesLowerCaseIdentifiers()) // e.g. Postgres
6263
identifier = identifier.toLowerCase();
63-
else if (databaseMetaData.storesUpperCaseIdentifiers())
64+
else if (databaseMetaData.storesUpperCaseIdentifiers()) // e.g. H2
6465
identifier = identifier.toUpperCase();
6566
return identifier;
6667
}
@@ -71,12 +72,12 @@ private void logInitializeError() {
7172
}
7273

7374
/**
74-
* Given an array of column names and their corresponding values (as strings)
75+
* Given an array of column names and other array with their corresponding values (as strings),
7576
* transforms each value to the compatible data type that allow to be inserted in the database.
76-
* If the column names are missing, uses all columns in the current table as fallback.
77+
* If the column names are missing, uses all columns in the current table as the column names.
7778
*/
7879
Object[] convertColumnValues(String[] columns, String[] values) {
79-
if (columns == null || columns.length == 0) // if no specified, use all columns in the table
80+
if (ArrayUtils.isEmpty(columns)) // if no specified, use all columns in the table
8081
columns = columnTypes.keySet().toArray(new String[0]);
8182

8283
Object[] converted = new Object[values.length];
@@ -98,8 +99,8 @@ private Object convertColumnValue(String column, String value) {
9899
}
99100

100101
/**
101-
* Converts the string representation of a data type value into the appropriate simple sql data type.
102-
* If a data type is not handled by this method, returns the input value as fallback.
102+
* Converts the string representation of a data type value into the appropriate simple SQL data type.
103+
* If a data type is not handled by this method (or is string), returns the input string value as fallback.
103104
*
104105
* See table B-1 in JDBC 4.2 Specification
105106
*/
@@ -135,21 +136,26 @@ private Object convertColumnValueFromJavaSqlType(int sqlType, String value) thro
135136
case java.sql.Types.TIMESTAMP: return new java.sql.Timestamp(parseDate(value, getTimestampFormats()).getTime());
136137
default:
137138
return value;
139+
// Not including: binary and advanced datatypes (e.g. blob, array...)
138140
}
139141
}
140142

141-
// Currently, supported date time formats are a few common ISO-8601 formats
142-
// (other common format strings in org.apache.commons.lang3.time.DateFormatUtils)
143+
// Supported date time formats are the common ISO-8601 formats
144+
// defined in org.apache.commons.lang3.time.DateFormatUtils,
145+
// as well as their variants that specify milliseconds.
143146
// This may be made user configurable later, via properties and/or embedded in the payload
144147

145148
private String[] getDateFormats() {
146-
return new String[] {"yyyy-MM-dd"};
149+
return new String[] { "yyyy-MM-dd" };
147150
}
151+
148152
private String[] getTimeFormats() {
149-
return new String[] {"HH:mm:ssZ", "HH:mm:ss.SSSZ"};
153+
return new String[] { "HH:mm:ssZ", "HH:mm:ss.SSSZ", "HH:mm:ss", "HH:mm:ss.SSS" };
150154
}
155+
151156
private String[] getTimestampFormats() {
152-
return new String[] {"yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ"};
157+
return new String[] { "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
158+
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS" };
153159
}
154160

155161
}

core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
@RunWith(ZeroCodeUnitRunner.class)
1010
public class DbSqlExecutorScenarioTest {
1111

12+
// NOTE: Below tests will fail when run in postgres because this database stores identifiers in lowercase.
13+
// to make tests pass, change the keys in the response.rows to lowercase
14+
1215
@Test
1316
@Scenario("integration_test_files/db/db_csv_load_with_headers.json")
1417
public void testDbCsvLoadWithHeaders() throws Exception {
@@ -24,4 +27,17 @@ public void testDbCsvLoadWithoutHeaders() throws Exception {
2427
public void testDbSqlExecute() throws Exception {
2528
}
2629

30+
// Manual test: error handling.
31+
// To facilitate the location of the source of possible errors (e.g. a wrong SQL statement),
32+
// exceptions that occur in the DbSqlExecutor should show:
33+
// - A log entry with the error message
34+
// - The stacktrace of the exception to facilitate locating the source
35+
// - The usual chain of errors and stacktraces produced by zerocode when an step fails
36+
//
37+
// Recommended situations for manual test:
38+
// - Target environment variables are no defined
39+
// - A syntactically wrong SQL statement in a step
40+
// - A header that does not correspond with any column when loading data from CSV
41+
// - A value with the wrong data type (e.g. string in a numeric column) when loading data from CSV
42+
2743
}

core/src/test/java/org/jsmart/zerocode/core/db/DbTestBase.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected void configureTest() {
3939
@Named("db.driver.password") protected String password;
4040

4141
protected Connection conn; // managed connection for each test
42-
protected boolean isPostgres = false; // set by each connection, to allow portable assertions (postgres is lowercase)
42+
protected boolean isPostgres = false; // set by each connection, to allow portable assertions
4343

4444
@Before
4545
public void setUp() throws SQLException {
@@ -61,6 +61,8 @@ protected List<Map<String, Object>> execute(String sql, Object[] params) throws
6161
return runner.execute(sql, params);
6262
}
6363

64+
// Table and columns in all tests are uppercase because H2 stores uppercase by default.
65+
// But postgres stores lowercase, so some expected strings need case conversion
6466
protected String convertDbCase(String value) {
6567
return isPostgres ? value.toLowerCase() : value;
6668
}

core/src/test/java/org/jsmart/zerocode/core/db/DbValueConverterTest.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ public void convertDateAndTimeValues() throws SQLException {
5757
}
5858

5959
@Test
60-
public void convertWithLowerAndUppercase() throws SQLException {
61-
// Test date types to ensure that is the converter who makes the conversion, not the driver
60+
public void convertWithMixedCaseColumnName() throws SQLException {
61+
// Uses a date type to ensure that is the converter who tries making the conversion, not the driver
62+
// (neither H2 nor Postgres drivers do conversion of dates)
6263
List<Map<String, Object>> rows = doTestConversion("", "ITable", "VDATE DATE",
6364
new String[] { "VDate" },
6465
new String[] { "2024-09-04" },
@@ -74,6 +75,16 @@ public void whenNoColumnsSpecified_ThenAllTableColumns_AreIncluded() throws SQLE
7475
"[{VINT=101, VCHAR=astring}]");
7576
}
7677

78+
@Test
79+
public void whenNoColumnsSpecified_AndColumnCountMismatch_ThenConversionFails() throws SQLException {
80+
assertThrows(SQLException.class, () -> {
81+
doTestConversion("", "FMTABLE", "VINT BIGINT",
82+
null,
83+
new String[] { "101", "999" },
84+
"[{VINT=101}]");
85+
});
86+
}
87+
7788
@Test
7889
public void whenColumnNotFound_ThenConversionFails() throws SQLException {
7990
assertThrows(SQLException.class, () -> {
@@ -84,8 +95,20 @@ public void whenColumnNotFound_ThenConversionFails() throws SQLException {
8495
});
8596
}
8697

87-
// Failures due to problems with metadata:
88-
// - These tests will pass because the H2 driver converts numeric values
98+
@Test
99+
public void whenValueHasWrongFormat_ThenConversionFails() throws SQLException {
100+
assertThrows(SQLException.class, () -> {
101+
doTestConversion("", "FVTABLE", "VTS TIMESTAMP",
102+
new String[] { "VTS" },
103+
new String[] { "notadate" },
104+
"[{VTS=notadate}]");
105+
});
106+
}
107+
108+
// Failures due to problems with metadata.
109+
// Simulates failures getting metadata so that the conversion is left to
110+
// be done by the driver.
111+
// - Below tests will pass because the H2 driver converts numeric values
89112
// - but fail if driver is changed to Postgres (does not convert numeric), skipped
90113

91114
@Test

core/src/test/resources/db_test.properties

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ db.driver.url=jdbc:h2:./target/test_db_sql_executor
77
# db.driver.password=
88

99
# To run the tests with postgres:
10-
# - run container: docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres
10+
# - Spin up the DB container (this was tested with the below versions #680, latest version is the recommended)
11+
# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:17.0
12+
# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:12.0
13+
# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:9.0
1114
# - add the driver dependency to the pom.xml: https://central.sonatype.com/artifact/org.postgresql/postgresql
12-
# - and uncomment these properties
15+
# - and uncomment the properties below:
1316
# db.driver.url=jdbc:postgresql://localhost:5432/postgres
1417
# db.driver.user=postgres
1518
# db.driver.password=mypassword

0 commit comments

Comments
 (0)