Skip to content

Commit 688f967

Browse files
committed
ISSUE-680 # Add database SQL executor
1 parent f52682f commit 688f967

File tree

9 files changed

+347
-0
lines changed

9 files changed

+347
-0
lines changed

core/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@
175175
<artifactId>micro-simulator</artifactId>
176176
<scope>test</scope>
177177
</dependency>
178+
<dependency>
179+
<groupId>commons-dbutils</groupId>
180+
<artifactId>commons-dbutils</artifactId>
181+
</dependency>
178182
<dependency>
179183
<groupId>com.h2database</groupId>
180184
<artifactId>h2</artifactId>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.google.inject.Inject;
5+
import com.google.inject.name.Named;
6+
7+
import org.apache.commons.dbutils.DbUtils;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import java.sql.Connection;
11+
import java.sql.DriverManager;
12+
import java.sql.SQLException;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
/**
18+
* Interaction with a database using SQL to read/write
19+
* Requires the appropriated connection data in the target environment
20+
* properties, see src/test/resources/db_test.properties
21+
*/
22+
public class DbSqlExecutor {
23+
private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class);
24+
public static final String SQL_RESULTS_KEY = "rows";
25+
26+
@Inject
27+
@Named("db.driver.url") private String url;
28+
29+
@Inject(optional = true)
30+
@Named("db.driver.user") private String user;
31+
32+
@Inject(optional = true)
33+
@Named("db.driver.password") private String password;
34+
35+
/**
36+
* The EXECUTE operation returns the records retrieved by the SQL specified in the request
37+
* under the key "rows" (select) or an empty object (insert, update)
38+
*/
39+
public Map<String, Object> EXECUTE(DbSqlRequest request) {
40+
return execute(request);
41+
}
42+
43+
public Map<String, Object> execute(DbSqlRequest request) {
44+
Connection conn = createAndGetConnection();
45+
try {
46+
LOGGER.info("Execute SQL, request -> {} ", request);
47+
DbSqlRunner runner = new DbSqlRunner(conn);
48+
List<Map<String, Object>> results = runner.execute(request.getSql(), request.getSqlParams());
49+
Map<String, Object> response = new HashMap<>();
50+
if (results == null) { // will return empty node, use "verify":{}
51+
response.put(SQL_RESULTS_KEY, new ObjectMapper().createObjectNode());
52+
} else {
53+
response.put(SQL_RESULTS_KEY, results);
54+
}
55+
return response;
56+
} catch (SQLException e) {
57+
LOGGER.error("Failed to execute SQL", e);
58+
throw new RuntimeException(e);
59+
} finally {
60+
closeConnection(conn);
61+
}
62+
}
63+
64+
/**
65+
* Returns a new JDBC connection using DriverManager.
66+
* Override this method in case you get the connections using another approach
67+
* (e.g. DataSource)
68+
*/
69+
protected Connection createAndGetConnection() {
70+
LOGGER.info("Create and get connection, url: {}, user: {}", url, user);
71+
try {
72+
return DriverManager.getConnection(url, user, password);
73+
} catch (SQLException e) {
74+
LOGGER.error("Failed to create connection", e);
75+
throw new RuntimeException(e);
76+
}
77+
}
78+
79+
protected void closeConnection(Connection conn) {
80+
DbUtils.closeQuietly(conn);
81+
}
82+
83+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import java.util.Arrays;
4+
5+
import com.fasterxml.jackson.annotation.JsonCreator;
6+
import com.fasterxml.jackson.annotation.JsonInclude;
7+
import com.fasterxml.jackson.annotation.JsonProperty;
8+
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
10+
public class DbSqlRequest {
11+
private final String sql;
12+
private final Object[] sqlParams;
13+
14+
@JsonCreator
15+
public DbSqlRequest(@JsonProperty("sqlStatement") String sql,
16+
@JsonProperty("sqlParams") Object[] sqlParams) {
17+
this.sql = sql;
18+
this.sqlParams = sqlParams;
19+
}
20+
21+
public String getSql() {
22+
return sql;
23+
}
24+
25+
public Object[] getSqlParams() {
26+
return sqlParams;
27+
}
28+
29+
@Override
30+
public String toString() {
31+
return "Request{"
32+
+ "sql=" + sql
33+
+ ", sqlParams=" + (sqlParams == null ? "[]" : Arrays.asList(sqlParams).toString())
34+
+ '}';
35+
}
36+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import org.apache.commons.dbutils.QueryRunner;
4+
import org.apache.commons.dbutils.handlers.MapListHandler;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import java.sql.Connection;
9+
import java.sql.SQLException;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
class DbSqlRunner {
14+
private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlRunner.class);
15+
private Connection conn;
16+
17+
public DbSqlRunner(Connection conn) {
18+
this.conn = conn;
19+
}
20+
21+
/**
22+
* Execute an sql with parameters (optional) and returns a list of maps
23+
* with the ResultSet content (select) or null (insert, update)
24+
*/
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+
// - Empty (insert and update)
29+
// - With one or more ResultSets (select).
30+
// - Note that some drivers never return more than one ResultSet (e.g. H2)
31+
QueryRunner runner = new QueryRunner();
32+
List<List<Map<String, Object>>> result = runner.execute(conn, sql, new MapListHandler(), params);
33+
if (result.isEmpty()) {
34+
return null;
35+
} else {
36+
if (result.size() > 1)
37+
LOGGER.warn("The SQL query returned more than one ResultSet, keeping only the first one");
38+
return result.get(0);
39+
}
40+
}
41+
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.jsmart.zerocode.core.db;
2+
import org.jsmart.zerocode.core.domain.Scenario;
3+
import org.jsmart.zerocode.core.domain.TargetEnv;
4+
import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
8+
@TargetEnv("db_test.properties")
9+
@RunWith(ZeroCodeUnitRunner.class)
10+
public class DbSqlExecutorScenarioTest {
11+
12+
@Test
13+
@Scenario("integration_test_files/db/db_sql_execute.json")
14+
public void testDbSqlExecute() throws Exception {
15+
}
16+
17+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import static org.hamcrest.CoreMatchers.equalTo;
4+
import static org.hamcrest.CoreMatchers.nullValue;
5+
import static org.hamcrest.MatcherAssert.assertThat;
6+
7+
import java.io.FileNotFoundException;
8+
import java.io.IOException;
9+
import java.sql.Connection;
10+
import java.sql.DriverManager;
11+
import java.sql.SQLException;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Properties;
15+
16+
import org.apache.commons.dbutils.DbUtils;
17+
import org.apache.commons.dbutils.QueryRunner;
18+
import org.jsmart.zerocode.core.utils.PropertiesProviderUtils;
19+
import org.junit.After;
20+
import org.junit.Before;
21+
import org.junit.BeforeClass;
22+
import org.junit.Test;
23+
24+
public class DbSqlRunnerTest {
25+
26+
private static final String DB_PROPERTIES_RESOURCE = "db_test.properties";
27+
private Connection conn;
28+
29+
@BeforeClass
30+
public static void classSetUp() throws FileNotFoundException, SQLException, IOException {
31+
Connection createConn = connect();
32+
new QueryRunner().update(createConn, "DROP TABLE IF EXISTS SQLTABLE; "
33+
+ "CREATE TABLE SQLTABLE (ID INTEGER, NAME VARCHAR(20)); ");
34+
DbUtils.closeQuietly(createConn);
35+
}
36+
37+
@Before
38+
public void setUp() throws ClassNotFoundException, SQLException, FileNotFoundException, IOException {
39+
conn = connect();
40+
new QueryRunner().update(conn, "DELETE FROM SQLTABLE; "
41+
+ "INSERT INTO SQLTABLE VALUES (1, 'string 1'); "
42+
+ "INSERT INTO SQLTABLE VALUES (2, 'string 2');");
43+
}
44+
45+
@After
46+
public void tearDown() throws Exception {
47+
DbUtils.closeQuietly(conn);
48+
}
49+
50+
private static Connection connect() throws SQLException, FileNotFoundException, IOException {
51+
Properties prop = PropertiesProviderUtils.getProperties(DB_PROPERTIES_RESOURCE);
52+
return DriverManager.getConnection(
53+
prop.getProperty("db.driver.url"), prop.getProperty("db.driver.user"), prop.getProperty("db.driver.password") );
54+
}
55+
56+
private List<Map<String, Object>> execute(String sql, Object[] params) throws SQLException {
57+
DbSqlRunner runner = new DbSqlRunner(conn);
58+
return runner.execute(sql, params);
59+
}
60+
61+
@Test
62+
public void sqlSelectQueryShouldReturnListOfMap() throws ClassNotFoundException, SQLException {
63+
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID DESC", null);
64+
assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}, {ID=1, NAME=string 1}]"));
65+
}
66+
67+
@Test
68+
public void sqlSelectWithoutResultsShouldReturnEmptyList() throws ClassNotFoundException, SQLException {
69+
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID<0", null);
70+
assertThat(rows.toString(), equalTo("[]"));
71+
}
72+
73+
@Test
74+
public void multipleSqlSelectShouldReturnTheFirstResultSet() throws ClassNotFoundException, SQLException {
75+
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID=2; SELECT ID, NAME FROM SQLTABLE where ID=1;", null);
76+
assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}]"));
77+
}
78+
79+
@Test
80+
public void sqlInsertShouldReturnNull() throws ClassNotFoundException, SQLException {
81+
Object nullRows = execute("INSERT INTO SQLTABLE VALUES (3, 'string 3')", null);
82+
assertThat(nullRows, nullValue());
83+
// check rows are inserted
84+
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID", new Object[] {});
85+
assertThat(rows.toString(), equalTo("[{ID=1, NAME=string 1}, {ID=2, NAME=string 2}, {ID=3, NAME=string 3}]"));
86+
}
87+
88+
@Test
89+
public void executeWithParametersShouldAllowNulls() throws SQLException {
90+
execute("INSERT INTO SQLTABLE VALUES (?, ?)", new Object[] { 4, null });
91+
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID = ?", new Object[] { 4 });
92+
assertThat(rows.toString(), equalTo("[{ID=4, NAME=null}]"));
93+
}
94+
95+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Connection info used by the DbSqlExecutor
2+
3+
# JDBC connection string to the test database (H2)
4+
db.driver.url=jdbc:h2:./target/test_db_sql_executor
5+
# If connection requires authentication, specify user and password:
6+
# db.driver.user=
7+
# db.driver.password=
8+
9+
# To run the tests with postgres:
10+
# - run container: docker run --name zerocode-postgres -p:5432:5432 -e POSTGRES_PASSWORD=mypassword -d postgres
11+
# - add the driver dependency to the pom.xml: https://central.sonatype.com/artifact/org.postgresql/postgresql
12+
# - and uncomment these properties
13+
# db.driver.url=jdbc:postgresql://localhost:5432/postgres
14+
# db.driver.user=postgres
15+
# db.driver.password=mypassword
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"scenarioName": "DbSqlExecutor: Read and write data using SQL",
3+
"steps": [
4+
{
5+
"name": "Test database setup",
6+
"url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
7+
"operation": "EXECUTE",
8+
"request": {
9+
"sql": "DROP TABLE IF EXISTS PEOPLE; CREATE TABLE PEOPLE (ID INTEGER, NAME VARCHAR(20), START DATE, ACTIVE BOOLEAN);"
10+
},
11+
"verify": { }
12+
},
13+
{
14+
"name": "Insert rows using SQL",
15+
"url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
16+
"operation": "EXECUTE",
17+
"request": {
18+
"sql": "INSERT INTO PEOPLE VALUES (1, 'Jeff Bejo', '2024-09-01', true); INSERT INTO PEOPLE VALUES (2, 'John Bajo', '2024-09-02', false);"
19+
},
20+
"verify": { }
21+
},
22+
{
23+
"name": "Insert with parameters and nulls",
24+
"url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
25+
"operation": "execute", //<-- Uppercase for consistency, but also allows lowercase
26+
"request": {
27+
"sql": "INSERT INTO PEOPLE (ID, NAME, START, ACTIVE) VALUES (?, ?, ?, ?);",
28+
"sqlParams": [3, null, null, true]
29+
},
30+
"verify": { }
31+
},
32+
{
33+
"name": "Retrieve rows using SQL",
34+
"url": "org.jsmart.zerocode.core.db.DbSqlExecutor",
35+
"operation": "EXECUTE",
36+
"request": {
37+
"sql": "SELECT ID, NAME, to_char(START,'yyyy-MM-dd') AS START, ACTIVE FROM PEOPLE WHERE ACTIVE=?",
38+
"sqlParams": [true]
39+
},
40+
"verify": {
41+
"rows.SIZE": 2,
42+
"rows": [
43+
{ "ID": 1, "NAME": "Jeff Bejo", "START": "2024-09-01", "ACTIVE": true },
44+
{ "ID": 3, "NAME": null, "START": null, "ACTIVE": true }
45+
]
46+
}
47+
}
48+
]
49+
}

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<commons-collections4.version>4.4</commons-collections4.version>
8585
<commons-lang3.version>3.14.0</commons-lang3.version>
8686
<commons-text.version>1.11.0</commons-text.version>
87+
<commons-dbutils.version>1.8.1</commons-dbutils.version>
8788
<micro-simulator.version>1.1.10</micro-simulator.version>
8889
<httpclient.version>4.5.13</httpclient.version>
8990
<httpmime.version>4.5.12</httpmime.version>
@@ -287,6 +288,11 @@
287288
<version>${micro-simulator.version}</version>
288289
<scope>test</scope>
289290
</dependency>
291+
<dependency>
292+
<groupId>commons-dbutils</groupId>
293+
<artifactId>commons-dbutils</artifactId>
294+
<version>${commons-dbutils.version}</version>
295+
</dependency>
290296
<dependency>
291297
<groupId>com.h2database</groupId>
292298
<artifactId>h2</artifactId>

0 commit comments

Comments
 (0)