From 688f9670c69de916e92c7be1dc9328311f2e870b Mon Sep 17 00:00:00 2001
From: Javier <10879637+javiertuya@users.noreply.github.com>
Date: Wed, 2 Oct 2024 17:50:22 +0200
Subject: [PATCH 1/6] ISSUE-680 # Add database SQL executor
---
core/pom.xml | 4 +
.../zerocode/core/db/DbSqlExecutor.java | 83 ++++++++++++++++
.../jsmart/zerocode/core/db/DbSqlRequest.java | 36 +++++++
.../jsmart/zerocode/core/db/DbSqlRunner.java | 42 ++++++++
.../core/db/DbSqlExecutorScenarioTest.java | 17 ++++
.../zerocode/core/db/DbSqlRunnerTest.java | 95 +++++++++++++++++++
core/src/test/resources/db_test.properties | 15 +++
.../db/db_sql_execute.json | 49 ++++++++++
pom.xml | 6 ++
9 files changed, 347 insertions(+)
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRequest.java
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbSqlRunner.java
create mode 100644 core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java
create mode 100644 core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java
create mode 100644 core/src/test/resources/db_test.properties
create mode 100644 core/src/test/resources/integration_test_files/db/db_sql_execute.json
diff --git a/core/pom.xml b/core/pom.xml
index f59e00aa..5878b99a 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -175,6 +175,10 @@
micro-simulator
test
+
+ commons-dbutils
+ commons-dbutils
+
com.h2database
h2
diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java
new file mode 100644
index 00000000..dbdb6a41
--- /dev/null
+++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java
@@ -0,0 +1,83 @@
+package org.jsmart.zerocode.core.db;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interaction with a database using SQL to read/write
+ * Requires the appropriated connection data in the target environment
+ * properties, see src/test/resources/db_test.properties
+ */
+public class DbSqlExecutor {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class);
+ public static final String SQL_RESULTS_KEY = "rows";
+
+ @Inject
+ @Named("db.driver.url") private String url;
+
+ @Inject(optional = true)
+ @Named("db.driver.user") private String user;
+
+ @Inject(optional = true)
+ @Named("db.driver.password") private String password;
+
+ /**
+ * The EXECUTE operation returns the records retrieved by the SQL specified in the request
+ * under the key "rows" (select) or an empty object (insert, update)
+ */
+ public Map EXECUTE(DbSqlRequest request) {
+ return execute(request);
+ }
+
+ public Map execute(DbSqlRequest request) {
+ Connection conn = createAndGetConnection();
+ try {
+ LOGGER.info("Execute SQL, request -> {} ", request);
+ DbSqlRunner runner = new DbSqlRunner(conn);
+ List
+
+ commons-dbutils
+ commons-dbutils
+ ${commons-dbutils.version}
+
com.h2database
h2
From 1d2edb37f90c73efd3617a206e2a5d90455960b2 Mon Sep 17 00:00:00 2001
From: Javier <10879637+javiertuya@users.noreply.github.com>
Date: Fri, 4 Oct 2024 17:59:08 +0200
Subject: [PATCH 2/6] ISSUE-680 # Add value converter and refactor tests
---
core/pom.xml | 8 +
.../zerocode/core/db/DbValueConverter.java | 155 ++++++++++++++++++
.../zerocode/core/db/DbSqlRunnerTest.java | 49 +-----
.../jsmart/zerocode/core/db/DbTestBase.java | 53 ++++++
.../core/db/DbValueConverterTest.java | 150 +++++++++++++++++
.../db/db_sql_execute.json | 2 +-
6 files changed, 375 insertions(+), 42 deletions(-)
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java
create mode 100644 core/src/test/java/org/jsmart/zerocode/core/db/DbTestBase.java
create mode 100644 core/src/test/java/org/jsmart/zerocode/core/db/DbValueConverterTest.java
diff --git a/core/pom.xml b/core/pom.xml
index 5878b99a..8e5593e5 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -184,6 +184,14 @@
h2
test
+
com.aventstack
extentreports
diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java
new file mode 100644
index 00000000..149ae885
--- /dev/null
+++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbValueConverter.java
@@ -0,0 +1,155 @@
+package org.jsmart.zerocode.core.db;
+
+import static org.apache.commons.lang3.time.DateUtils.parseDate;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.ParseException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Conversion of string values to be inserted in the database
+ * into objects compatible with the java.sql type of the target columns.
+ */
+public class DbValueConverter {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class);
+
+ private Connection conn;
+ private String table;
+ private DatabaseMetaData databaseMetaData;
+ public Map columnTypes; // java.sql.Types
+
+ public DbValueConverter(Connection conn, String table) {
+ this.conn = conn;
+ this.table = table;
+ try {
+ initializeMetadata();
+ } catch (Exception e) {
+ logInitializeError();
+ }
+ }
+
+ private void initializeMetadata() throws SQLException {
+ LOGGER.info("Metadata initialization for table: {}", table);
+ columnTypes = new LinkedHashMap<>(); // must keep column order
+ databaseMetaData = conn.getMetaData();
+
+ table = convertToStoredCase(table); // to locate table name in metadata
+ LOGGER.info("Database storesLowerCaseIdentifiers={}, storesUpperCaseIdentifiers={}",
+ databaseMetaData.storesLowerCaseIdentifiers(), databaseMetaData.storesUpperCaseIdentifiers());
+
+ try (ResultSet rs = databaseMetaData.getColumns(null, null, table, "%")) {
+ while (rs.next()) {
+ String storedName = rs.getString("COLUMN_NAME");
+ int typeValue = rs.getInt("DATA_TYPE");
+ // internally, key is lowercase to allow case insensitive lookups
+ columnTypes.put(storedName.toLowerCase(), typeValue);
+ }
+ }
+ LOGGER.info("Mapping from java columns to sql types: {}", columnTypes.toString());
+ if (columnTypes.isEmpty())
+ logInitializeError();
+ }
+
+ private String convertToStoredCase(String identifier) throws SQLException {
+ if (databaseMetaData.storesLowerCaseIdentifiers())
+ identifier = identifier.toLowerCase();
+ else if (databaseMetaData.storesUpperCaseIdentifiers())
+ identifier = identifier.toUpperCase();
+ return identifier;
+ }
+
+ private void logInitializeError() {
+ LOGGER.error("Initialization of metadata for table {} failed. "
+ + "Errors may appear when matching query parameters to their data types", table);
+ }
+
+ /**
+ * Given an array of column names and their corresponding values (as strings)
+ * transforms each value to the compatible data type that allow to be inserted in the database.
+ * If the column names are missing, uses all columns in the current table as fallback.
+ */
+ Object[] convertColumnValues(String[] columns, String[] values) {
+ if (columns == null || columns.length == 0) // if no specified, use all columns in the table
+ columns = columnTypes.keySet().toArray(new String[0]);
+
+ Object[] converted = new Object[values.length];
+ for (int i = 0; i < values.length; i++) {
+ converted[i] = i < columns.length && i < values.length
+ ? convertColumnValue(columns[i], values[i])
+ : values[i];
+ }
+ return converted;
+ }
+
+ private Object convertColumnValue(String column, String value) {
+ try {
+ return convertColumnValueWithThrow(column, value);
+ } catch (ParseException e) {
+ LOGGER.error("Can't convert the data type of value {} at column {}", value, column);
+ return value;
+ }
+ }
+
+ /**
+ * Converts the string representation of a data type value into the appropriate simple sql data type.
+ * If a data type is not handled by this method, returns the input value as fallback.
+ *
+ * See table B-1 in JDBC 4.2 Specification
+ */
+ private Object convertColumnValueWithThrow(String column, String value) throws ParseException {
+ if (value == null)
+ return null;
+ if (!columnTypes.containsKey(column.toLowerCase())) // fallback if no metadata
+ return value;
+
+ int sqlType = columnTypes.get(column.toLowerCase());
+ return convertColumnValueFromJavaSqlType(sqlType, value);
+ }
+
+ private Object convertColumnValueFromJavaSqlType(int sqlType, String value) throws ParseException {
+ switch (sqlType) {
+ case java.sql.Types.NUMERIC:
+ case java.sql.Types.DECIMAL: return java.math.BigDecimal.valueOf(Double.parseDouble(value));
+
+ case java.sql.Types.BIT: //accepts "1" as true (e.g. SqlServer)
+ case java.sql.Types.BOOLEAN: return Boolean.valueOf("1".equals(value) ? "true" : value);
+
+ case java.sql.Types.TINYINT: return Byte.valueOf(value);
+ case java.sql.Types.SMALLINT: return Short.valueOf(value);
+ case java.sql.Types.INTEGER: return Integer.valueOf(value);
+ case java.sql.Types.BIGINT: return Long.valueOf(value);
+
+ case java.sql.Types.REAL: return Float.valueOf(value);
+ case java.sql.Types.FLOAT: return Double.valueOf(value);
+ case java.sql.Types.DOUBLE: return Double.valueOf(value);
+
+ case java.sql.Types.DATE: return new java.sql.Date(parseDate(value, getDateFormats()).getTime());
+ case java.sql.Types.TIME: return new java.sql.Time(parseDate(value, getTimeFormats()).getTime());
+ case java.sql.Types.TIMESTAMP: return new java.sql.Timestamp(parseDate(value, getTimestampFormats()).getTime());
+ default:
+ return value;
+ }
+ }
+
+ // Currently, supported date time formats are a few common ISO-8601 formats
+ // (other common format strings in org.apache.commons.lang3.time.DateFormatUtils)
+ // This may be made user configurable later, via properties and/or embedded in the payload
+
+ private String[] getDateFormats() {
+ return new String[] {"yyyy-MM-dd"};
+ }
+ private String[] getTimeFormats() {
+ return new String[] {"HH:mm:ssZ", "HH:mm:ss.SSSZ"};
+ }
+ private String[] getTimestampFormats() {
+ return new String[] {"yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ"};
+ }
+
+}
diff --git a/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java
index 5d267754..59fc19e6 100644
--- a/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java
+++ b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java
@@ -6,62 +6,29 @@
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.sql.Connection;
-import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
-import java.util.Properties;
-import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
-import org.jsmart.zerocode.core.utils.PropertiesProviderUtils;
-import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Test;
-public class DbSqlRunnerTest {
-
- private static final String DB_PROPERTIES_RESOURCE = "db_test.properties";
- private Connection conn;
-
- @BeforeClass
- public static void classSetUp() throws FileNotFoundException, SQLException, IOException {
- Connection createConn = connect();
- new QueryRunner().update(createConn, "DROP TABLE IF EXISTS SQLTABLE; "
- + "CREATE TABLE SQLTABLE (ID INTEGER, NAME VARCHAR(20)); ");
- DbUtils.closeQuietly(createConn);
- }
+public class DbSqlRunnerTest extends DbTestBase {
@Before
public void setUp() throws ClassNotFoundException, SQLException, FileNotFoundException, IOException {
- conn = connect();
- new QueryRunner().update(conn, "DELETE FROM SQLTABLE; "
+ super.setUp();
+ new QueryRunner().update(conn, "DROP TABLE IF EXISTS SQLTABLE; "
+ + "CREATE TABLE SQLTABLE (ID INTEGER, NAME VARCHAR(20)); "
+ "INSERT INTO SQLTABLE VALUES (1, 'string 1'); "
+ "INSERT INTO SQLTABLE VALUES (2, 'string 2');");
}
- @After
- public void tearDown() throws Exception {
- DbUtils.closeQuietly(conn);
- }
-
- private static Connection connect() throws SQLException, FileNotFoundException, IOException {
- Properties prop = PropertiesProviderUtils.getProperties(DB_PROPERTIES_RESOURCE);
- return DriverManager.getConnection(
- prop.getProperty("db.driver.url"), prop.getProperty("db.driver.user"), prop.getProperty("db.driver.password") );
- }
-
- private List> execute(String sql, Object[] params) throws SQLException {
- DbSqlRunner runner = new DbSqlRunner(conn);
- return runner.execute(sql, params);
- }
-
@Test
public void sqlSelectQueryShouldReturnListOfMap() throws ClassNotFoundException, SQLException {
List> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID DESC", null);
- assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}, {ID=1, NAME=string 1}]"));
+ assertThat(rows.toString(), equalTo(convertDbCase("[{ID=2, NAME=string 2}, {ID=1, NAME=string 1}]")));
}
@Test
@@ -73,7 +40,7 @@ public void sqlSelectWithoutResultsShouldReturnEmptyList() throws ClassNotFoundE
@Test
public void multipleSqlSelectShouldReturnTheFirstResultSet() throws ClassNotFoundException, SQLException {
List> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID=2; SELECT ID, NAME FROM SQLTABLE where ID=1;", null);
- assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}]"));
+ assertThat(rows.toString(), equalTo(convertDbCase("[{ID=2, NAME=string 2}]")));
}
@Test
@@ -82,14 +49,14 @@ public void sqlInsertShouldReturnNull() throws ClassNotFoundException, SQLExcept
assertThat(nullRows, nullValue());
// check rows are inserted
List> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID", new Object[] {});
- assertThat(rows.toString(), equalTo("[{ID=1, NAME=string 1}, {ID=2, NAME=string 2}, {ID=3, NAME=string 3}]"));
+ assertThat(rows.toString(), equalTo(convertDbCase("[{ID=1, NAME=string 1}, {ID=2, NAME=string 2}, {ID=3, NAME=string 3}]")));
}
@Test
public void executeWithParametersShouldAllowNulls() throws SQLException {
execute("INSERT INTO SQLTABLE VALUES (?, ?)", new Object[] { 4, null });
List> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID = ?", new Object[] { 4 });
- assertThat(rows.toString(), equalTo("[{ID=4, NAME=null}]"));
+ assertThat(rows.toString(), equalTo(convertDbCase("[{ID=4, NAME=null}]")));
}
}
diff --git a/core/src/test/java/org/jsmart/zerocode/core/db/DbTestBase.java b/core/src/test/java/org/jsmart/zerocode/core/db/DbTestBase.java
new file mode 100644
index 00000000..6fcb2a70
--- /dev/null
+++ b/core/src/test/java/org/jsmart/zerocode/core/db/DbTestBase.java
@@ -0,0 +1,53 @@
+package org.jsmart.zerocode.core.db;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.jsmart.zerocode.core.utils.PropertiesProviderUtils;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Base class for the unit DB test classes: manages connections,
+ * execution of queries and DBMS specific features
+ */
+public class DbTestBase {
+
+ private static final String DB_PROPERTIES_RESOURCE = "db_test.properties";
+ protected Connection conn; // managed connection for each test
+ protected boolean isPostgres = false; // set by each connection, to allow portable assertions (postgres is lowercase)
+
+ @Before
+ public void setUp() throws ClassNotFoundException, SQLException, FileNotFoundException, IOException {
+ conn = connect();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ DbUtils.closeQuietly(conn);
+ }
+
+ protected Connection connect() throws SQLException {
+ Properties prop = PropertiesProviderUtils.getProperties(DB_PROPERTIES_RESOURCE);
+ isPostgres = prop.getProperty("db.driver.url").startsWith("jdbc:postgresql:");
+ return DriverManager.getConnection(
+ prop.getProperty("db.driver.url"), prop.getProperty("db.driver.user"), prop.getProperty("db.driver.password") );
+ }
+
+ protected List> execute(String sql, Object[] params) throws SQLException {
+ DbSqlRunner runner = new DbSqlRunner(conn);
+ return runner.execute(sql, params);
+ }
+
+ protected String convertDbCase(String value) {
+ return isPostgres ? value.toLowerCase() : value;
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/jsmart/zerocode/core/db/DbValueConverterTest.java b/core/src/test/java/org/jsmart/zerocode/core/db/DbValueConverterTest.java
new file mode 100644
index 00000000..fc9665f8
--- /dev/null
+++ b/core/src/test/java/org/jsmart/zerocode/core/db/DbValueConverterTest.java
@@ -0,0 +1,150 @@
+package org.jsmart.zerocode.core.db;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.sql.SQLException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.junit.Test;
+
+public class DbValueConverterTest extends DbTestBase {
+
+ @Test
+ public void convertBasicDataTypeValues() throws SQLException {
+ doTestConversion("", "Btable", "VINT INTEGER, VCHAR VARCHAR(20), VBOOL BOOLEAN, EXTRA CHAR(1)",
+ new String[] { "Vint", "Vchar", "Vbool" },
+ new String[] { "101", "astring", "true" },
+ "[{VINT=101, VCHAR=astring, VBOOL=true, EXTRA=null}]");
+ }
+
+ @Test
+ public void convertNullAndBitValues() throws SQLException {
+ doTestConversion("", "NTABLE", "VINT INT, VCHAR VARCHAR(20), VBOOL BOOLEAN",
+ new String[] { "VINT", "VCHAR", "VBOOL" },
+ new String[] { null, null, "1" }, // incl. alternate boolean
+ "[{VINT=null, VCHAR=null, VBOOL=true}]");
+ }
+
+ @Test
+ public void convertDecimalAndFloatValues() throws SQLException {
+ doTestConversion("", "FTABLE", "VEXACT NUMERIC(10,0), VDEC DECIMAL(10,2), VFLOAT FLOAT, VREAL REAL",
+ new String[] { "VEXACT", "VDEC", "VFLOAT", "VREAL" },
+ new String[] { "102", "123.45", "234.56", "3.4561E+2" },
+ "[{VEXACT=102, VDEC=123.45, VFLOAT=234.56, VREAL=345.61}]");
+ }
+
+ @Test
+ public void convertDateAndTimeValues() throws SQLException {
+ List> rows = doTestConversion("", "DTABLE", "VTS1 TIMESTAMP, VTS2 TIMESTAMP, VTIME TIME, VDATE DATE",
+ new String[] { "VTS1", "VTS2", "VTIME", "VDATE" },
+ new String[] { "2024-09-04T08:01:02.456+0300", "2024-09-04T08:01:02+0300", "08:01:02+0300", "2024-09-04" },
+ null);
+ // assert individually to allow compare with GMT time (not local)
+ assertThat(gmtTimestamp((Date) rows.get(0).get("VTS1")), equalTo("2024-09-04T05:01:02.456"));
+ assertThat(gmtTimestamp((Date) rows.get(0).get("VTS2")), equalTo("2024-09-04T05:01:02.000"));
+ assertThat(gmtTimestamp((Date) rows.get(0).get("VTIME")), equalTo("1970-01-01T05:01:02.000"));
+ assertThat(rows.get(0).get("VDATE").toString(), "2024-09-04", equalTo(rows.get(0).get("VDATE").toString()));
+ }
+
+ @Test
+ public void convertWithLowerAndUppercase() throws SQLException {
+ // Test date types to ensure that is the converter who makes the conversion, not the driver
+ List> rows = doTestConversion("", "ITable", "VDATE DATE",
+ new String[] { "VDate" },
+ new String[] { "2024-09-04" },
+ "[{VDATE=2024-09-04}]");
+ assertThat(rows.get(0).get("VDATE").toString(), equalTo("2024-09-04"));
+ }
+
+ @Test
+ public void whenNoColumnsSpecified_ThenAllTableColumns_AreIncluded() throws SQLException {
+ doTestConversion("", "OTABLE", "VINT SMALLINT, VCHAR VARCHAR(20)",
+ null,
+ new String[] { "101", "astring" },
+ "[{VINT=101, VCHAR=astring}]");
+ }
+
+ @Test
+ public void whenColumnNotFound_ThenConversionFails() throws SQLException {
+ assertThrows(SQLException.class, () -> {
+ doTestConversion("", "FCTABLE", "VINT INTEGER, VCHAR VARCHAR(20)",
+ new String[] { "VINT", "NOTEXISTS" },
+ new String[] { "101", "astring" },
+ "[{VINT=101, VCHAR=notexists}]");
+ });
+ }
+
+ // Failures due to problems with metadata:
+ // - These tests will pass because the H2 driver converts numeric values
+ // - but fail if driver is changed to Postgres (does not convert numeric), skipped
+
+ @Test
+ public void whenMetadataNotFound_ThenConversions_AreUpToTheDriver_WithColumns() throws SQLException {
+ if (isPostgres)
+ return;
+ doTestConversion("table", "F1TABLE", "VINT INTEGER, VCHAR VARCHAR(20)",
+ new String[] { "VINT", "VCHAR" },
+ new String[] { "101", "astring" },
+ "[{VINT=101, VCHAR=astring}]");
+ }
+
+ @Test
+ public void whenMetadataNotFound_ThenConversions_AreUpToTheDriver_WithoutColumns() throws SQLException {
+ if (isPostgres)
+ return;
+ doTestConversion("table", "F2CTABLE", "VINT INTEGER, VCHAR VARCHAR(20)",
+ null,
+ new String[] { "101", "astring" },
+ "[{VINT=101, VCHAR=astring}]");
+ }
+
+ @Test
+ public void whenMetadataFails_ThenConversions_AreUpToTheDriver() throws SQLException {
+ if (isPostgres)
+ return;
+ doTestConversion("conn", "F3TABLE", "VINT INTEGER, VCHAR VARCHAR(20)",
+ new String[] { "VINT", "VCHAR" },
+ new String[] { "101", "astring" },
+ "[{VINT=101, VCHAR=astring}]");
+ }
+
+ private List> doTestConversion(String failureToSimulate, String table,
+ String ddlTypes, String[] columns, String[] params, String expected) throws SQLException {
+ execute("DROP TABLE IF EXISTS " + table + ";"
+ + " CREATE TABLE " + table + " (" + ddlTypes + ");", null);
+ String sql = "INSERT INTO " + table
+ + (columns != null ? " (" + String.join(",", columns) + ")" : "")
+ + " VALUES (" + placeholders(params.length) + ")";
+
+ DbValueConverter converter = new DbValueConverter(
+ "conn".equals(failureToSimulate) ? null : conn,
+ "table".equals(failureToSimulate) ? "notexists" : table);
+ Object[] converted = converter.convertColumnValues(columns, params);
+ execute(sql, converted);
+
+ List> rows = execute("SELECT * FROM " + table, null);
+ if (expected != null) // null to check without specified columns
+ assertThat(rows.toString(), equalTo(convertDbCase(expected)));
+ return rows;
+ }
+
+ private String placeholders(int columnCount) {
+ String[] placeholders = new String[columnCount];
+ Arrays.fill(placeholders, "?");
+ return String.join(",", placeholders);
+ }
+
+ private String gmtTimestamp(Date dt) {
+ java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return df.format(dt);
+ }
+
+}
diff --git a/core/src/test/resources/integration_test_files/db/db_sql_execute.json b/core/src/test/resources/integration_test_files/db/db_sql_execute.json
index 1ec9ffe2..6e0d0daf 100644
--- a/core/src/test/resources/integration_test_files/db/db_sql_execute.json
+++ b/core/src/test/resources/integration_test_files/db/db_sql_execute.json
@@ -39,7 +39,7 @@
},
"verify": {
"rows.SIZE": 2,
- "rows": [
+ "rows": [ //<-- to make this pass in postgres, set the keys to lowercase
{ "ID": 1, "NAME": "Jeff Bejo", "START": "2024-09-01", "ACTIVE": true },
{ "ID": 3, "NAME": null, "START": null, "ACTIVE": true }
]
From 5972efb3dcac927d747c318531f39504d3f90858 Mon Sep 17 00:00:00 2001
From: Javier <10879637+javiertuya@users.noreply.github.com>
Date: Sat, 5 Oct 2024 18:26:44 +0200
Subject: [PATCH 3/6] ISSUE-680 # Add database CSV loader
---
.../jsmart/zerocode/core/db/DbCsvLoader.java | 132 ++++++++++++++++++
.../jsmart/zerocode/core/db/DbCsvRequest.java | 101 ++++++++++++++
.../zerocode/core/db/DbSqlExecutor.java | 31 ++++
.../zerocode/core/db/DbCsvLoaderTest.java | 120 ++++++++++++++++
.../core/db/DbSqlExecutorScenarioTest.java | 10 ++
.../zerocode/core/db/DbSqlRunnerTest.java | 7 +-
.../jsmart/zerocode/core/db/DbTestBase.java | 37 +++--
.../core/db/DbValueConverterTest.java | 3 +
.../db/db_csv_load_with_headers.json | 43 ++++++
.../db/db_csv_load_without_headers.json | 43 ++++++
.../db/players_with_headers.csv | 4 +
.../db/players_without_headers.csv | 3 +
12 files changed, 520 insertions(+), 14 deletions(-)
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java
create mode 100644 core/src/main/java/org/jsmart/zerocode/core/db/DbCsvRequest.java
create mode 100644 core/src/test/java/org/jsmart/zerocode/core/db/DbCsvLoaderTest.java
create mode 100644 core/src/test/resources/integration_test_files/db/db_csv_load_with_headers.json
create mode 100644 core/src/test/resources/integration_test_files/db/db_csv_load_without_headers.json
create mode 100644 core/src/test/resources/integration_test_files/db/players_with_headers.csv
create mode 100644 core/src/test/resources/integration_test_files/db/players_without_headers.csv
diff --git a/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java
new file mode 100644
index 00000000..72896878
--- /dev/null
+++ b/core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java
@@ -0,0 +1,132 @@
+package org.jsmart.zerocode.core.db;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.univocity.parsers.csv.CsvParser;
+
+class DbCsvLoader {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DbCsvLoader.class);
+ private Connection conn;
+ private CsvParser csvParser;
+
+ public DbCsvLoader(Connection conn, CsvParser csvParser) {
+ this.conn = conn;
+ this.csvParser = csvParser;
+ }
+
+ /**
+ * Loads rows in csv format (csvLines) into a table in the database
+ * and returns the total number of rows
+ */
+ int loadCsv(String table, List csvLines, boolean withHeaders, String nullString) throws SQLException {
+ if (csvLines == null || csvLines.isEmpty())
+ return 0;
+
+ List lines = parseLines(table, csvLines);
+
+ String[] headers = buildHeaders(lines.get(0), withHeaders);
+ List
-
com.aventstack
extentreports
diff --git a/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioPostgresTest.java b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioPostgresTest.java
new file mode 100644
index 00000000..7dd0c31a
--- /dev/null
+++ b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioPostgresTest.java
@@ -0,0 +1,24 @@
+package org.jsmart.zerocode.core.db;
+import org.jsmart.zerocode.core.domain.Scenario;
+import org.jsmart.zerocode.core.domain.TargetEnv;
+import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@TargetEnv("db_test_postgres.properties")
+@RunWith(ZeroCodeUnitRunner.class)
+public class DbSqlExecutorScenarioPostgresTest {
+
+ // Note: Spin up the DB container before running this test: docker/compose/pg_compose.yml
+
+ @Test
+ @Scenario("integration_test_files/db/db_csv_load_with_headers_postgres.json")
+ public void testDbCsvLoadWithHeaders() throws Exception {
+ }
+
+ @Test
+ @Scenario("integration_test_files/db/db_sql_execute_postgres.json")
+ public void testDbSqlExecute() throws Exception {
+ }
+
+}
diff --git a/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java
index f3893f0d..e294777c 100644
--- a/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java
+++ b/core/src/test/java/org/jsmart/zerocode/core/db/DbSqlExecutorScenarioTest.java
@@ -9,9 +9,6 @@
@RunWith(ZeroCodeUnitRunner.class)
public class DbSqlExecutorScenarioTest {
- // NOTE: Below tests will fail when run in postgres because this database stores identifiers in lowercase.
- // to make tests pass, change the keys in the response.rows to lowercase
-
@Test
@Scenario("integration_test_files/db/db_csv_load_with_headers.json")
public void testDbCsvLoadWithHeaders() throws Exception {
diff --git a/core/src/test/resources/db_test.properties b/core/src/test/resources/db_test.properties
index 2df82c79..fe99f714 100644
--- a/core/src/test/resources/db_test.properties
+++ b/core/src/test/resources/db_test.properties
@@ -5,14 +5,3 @@ db.driver.url=jdbc:h2:./target/test_db_sql_executor
# If connection requires authentication, specify user and password:
# db.driver.user=
# db.driver.password=
-
-# To run the tests with postgres:
-# - Spin up the DB container (this was tested with the below versions #680, latest version is the recommended)
-# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:17.0
-# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:12.0
-# docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres:9.0
-# - add the driver dependency to the pom.xml: https://central.sonatype.com/artifact/org.postgresql/postgresql
-# - and uncomment the properties below:
-# db.driver.url=jdbc:postgresql://localhost:5432/postgres
-# db.driver.user=postgres
-# db.driver.password=mypassword
diff --git a/core/src/test/resources/db_test_postgres.properties b/core/src/test/resources/db_test_postgres.properties
new file mode 100644
index 00000000..ed699717
--- /dev/null
+++ b/core/src/test/resources/db_test_postgres.properties
@@ -0,0 +1,7 @@
+# Connection info used by the DbSqlExecutor
+
+# JDBC connection string to a PostgreSQL test database
+# Spin up the DB container before running the postgres tests: docker/compose/pg_compose.yml
+db.driver.url=jdbc:postgresql://localhost:35432/postgres
+db.driver.user=postgres
+db.driver.password=example
diff --git a/core/src/test/resources/integration_test_files/db/db_csv_load_with_headers_postgres.json b/core/src/test/resources/integration_test_files/db/db_csv_load_with_headers_postgres.json
new file mode 100644
index 00000000..544e32c0
--- /dev/null
+++ b/core/src/test/resources/integration_test_files/db/db_csv_load_with_headers_postgres.json
@@ -0,0 +1,43 @@
+{
+ "scenarioName": "DbSqlExecutor: Load a CSV file with headers - PostgreSQL",
+ "steps": [
+ {
+ "name": "Test database setup",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "EXECUTE",
+ "request": {
+ "sql": "DROP TABLE IF EXISTS PLAYERS; CREATE TABLE PLAYERS (ID INTEGER, NAME VARCHAR(20), AGE INTEGER);"
+ },
+ "verify": { }
+ },
+ {
+ "name": "Insert rows from a CSV file with headers",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "LOADCSV",
+ "request": {
+ "tableName": "players",
+ "csvSource": "integration_test_files/db/players_with_headers.csv",
+ "withHeaders" : true
+ },
+ "verify": {
+ "size" : 3
+ }
+ },
+ {
+ "name": "Check the content of inserted rows",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "EXECUTE",
+ "request": {
+ "sql": "SELECT ID, NAME, AGE FROM PLAYERS ORDER BY ID"
+ },
+ "verify": {
+ "rows.SIZE": 3,
+ "rows": [ //<-- same than db_csv_load_with_headers.json, but keys in lowercase (postgres converts to lower)
+ { "id": 1001, "name": "Ronaldo", "age": 23 },
+ { "id": 1002, "name": "Devaldo", "age": null },
+ { "id": 1003, "name": "Trevaldo", "age": 35 }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/core/src/test/resources/integration_test_files/db/db_sql_execute_postgres.json b/core/src/test/resources/integration_test_files/db/db_sql_execute_postgres.json
new file mode 100644
index 00000000..11d283ef
--- /dev/null
+++ b/core/src/test/resources/integration_test_files/db/db_sql_execute_postgres.json
@@ -0,0 +1,49 @@
+{
+ "scenarioName": "DbSqlExecutor: Read and write data using SQL - PostgreSQL",
+ "steps": [
+ {
+ "name": "Test database setup",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "EXECUTE",
+ "request": {
+ "sql": "DROP TABLE IF EXISTS PEOPLE; CREATE TABLE PEOPLE (ID INTEGER, NAME VARCHAR(20), START DATE, ACTIVE BOOLEAN);"
+ },
+ "verify": { }
+ },
+ {
+ "name": "Insert rows using SQL",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "EXECUTE",
+ "request": {
+ "sql": "INSERT INTO PEOPLE VALUES (1, 'Jeff Bejo', '2024-09-01', true); INSERT INTO PEOPLE VALUES (2, 'John Bajo', '2024-09-02', false);"
+ },
+ "verify": { }
+ },
+ {
+ "name": "Insert with parameters and nulls",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "execute", //<-- Uppercase for consistency, but also allows lowercase
+ "request": {
+ "sql": "INSERT INTO PEOPLE (ID, NAME, START, ACTIVE) VALUES (?, ?, ?, ?);",
+ "sqlParams": [3, null, null, true]
+ },
+ "verify": { }
+ },
+ {
+ "name": "Retrieve rows using SQL",
+ "url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
+ "operation": "EXECUTE",
+ "request": {
+ "sql": "SELECT ID, NAME, to_char(START,'yyyy-MM-dd') AS START, ACTIVE FROM PEOPLE WHERE ACTIVE=?",
+ "sqlParams": [true]
+ },
+ "verify": {
+ "rows.SIZE": 2,
+ "rows": [ //<-- same than db_sql_execute.json, but keys in lowercase (postgres converts to lower)
+ { "id": 1, "name": "Jeff Bejo", "start": "2024-09-01", "active": true },
+ { "id": 3, "name": null, "start": null, "active": true }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b34a4d4a..ad9f4b87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -89,6 +89,7 @@
4.5.13
4.5.12
2.2.220
+ 42.7.4
5.0.9
3.7.0
2.10.1
@@ -299,6 +300,12 @@
${h2.db.version}
test
+
+ org.postgresql
+ postgresql
+ ${postgres.db.version}
+ test
+
com.aventstack
extentreports
From ce8f2aaa751e255c9c893ea35b8d33da0c7bf8de Mon Sep 17 00:00:00 2001
From: Author Japps
Date: Sun, 22 Dec 2024 12:57:32 +0000
Subject: [PATCH 6/6] PG Driver in compile scope. Also removed unused adminer
from compose
---
core/pom.xml | 2 +-
docker/compose/pg_compose.yml | 6 ------
pom.xml | 1 -
3 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/core/pom.xml b/core/pom.xml
index b20e6aad..7fb25c0b 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -187,7 +187,7 @@
org.postgresql
postgresql
- test
+
com.aventstack
diff --git a/docker/compose/pg_compose.yml b/docker/compose/pg_compose.yml
index 100398f6..3ad83a2e 100644
--- a/docker/compose/pg_compose.yml
+++ b/docker/compose/pg_compose.yml
@@ -12,9 +12,3 @@ services:
POSTGRES_PASSWORD: example
ports:
- 35432:5432
-
- adminer:
- image: adminer:4.7.0
- restart: always
- ports:
- - 8080:8080
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ad9f4b87..9d354afa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -304,7 +304,6 @@
org.postgresql
postgresql
${postgres.db.version}
- test
com.aventstack