diff --git a/.circleci/config.yml b/.circleci/config.yml
index fe4c371c..658f75d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,6 +24,8 @@ commands:
name: Run Maven
command: |
mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
+ - store_artifacts:
+ path: target/site/jacoco
- run:
name: Save test results
command: |
diff --git a/.gitignore b/.gitignore
index edbffa9b..1223e629 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
target/
*.iml
+.env
#############################################
## Original contents from github template: ##
diff --git a/pom.xml b/pom.xml
index 640fe1c0..9aea844e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
com.kingsrook.qqq
qqq-backend-module-rdbms
- 0.1.0
+ 0.2.0
scm:git:git@github.com:Kingsrook/qqq-backend-module-rdbms.git
@@ -44,6 +44,8 @@
17
true
true
+ true
+ 0.80
@@ -51,7 +53,7 @@
com.kingsrook.qqq
qqq-backend-core
- 0.1.0
+ 0.2.0
@@ -116,6 +118,9 @@
org.apache.maven.plugins
maven-surefire-plugin
3.0.0-M5
+
+ @{jaCoCoArgLine}
+
org.apache.maven.plugins
@@ -165,7 +170,84 @@
1
-
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.8
+
+
+ pre-unit-test
+
+ prepare-agent
+
+
+ jaCoCoArgLine
+
+
+
+ unit-test-check
+
+ check
+
+
+
+ ${coverage.haltOnFailure}
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ ${coverage.instructionCoveredRatioMinimum}
+
+
+
+
+
+
+
+ post-unit-test
+ verify
+
+ report
+
+
+
+
+
+ exec-maven-plugin
+ org.codehaus.mojo
+ 3.0.0
+
+
+ test-coverage-summary
+ verify
+
+ exec
+
+
+ sh
+
+ -c
+
+ /tmp/$$.headers
+xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
+echo
+echo "Jacoco coverage summary report:"
+echo " See also target/site/jacoco/index.html"
+echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
+echo "------------------------------------------------------------"
+paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
+rm /tmp/$$.headers /tmp/$$.values
+ ]]>
+
+
+
+
+
+
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
index 0820c159..194b81e0 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
@@ -22,14 +22,14 @@
package com.kingsrook.qqq.backend.module.rdbms;
+import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableBackendDetails;
-import com.kingsrook.qqq.backend.core.modules.interfaces.CountInterface;
-import com.kingsrook.qqq.backend.core.modules.interfaces.DeleteInterface;
-import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface;
-import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface;
-import com.kingsrook.qqq.backend.core.modules.interfaces.QueryInterface;
-import com.kingsrook.qqq.backend.core.modules.interfaces.UpdateInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSCountAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction;
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
index 85166328..9a9f1c1a 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
@@ -29,12 +29,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
-import com.kingsrook.qqq.backend.core.model.actions.AbstractQTableRequest;
-import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
@@ -85,7 +85,7 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
** Get a database connection, per the backend in the request.
*******************************************************************************/
- protected Connection getConnection(AbstractQTableRequest qTableRequest) throws SQLException
+ protected Connection getConnection(AbstractTableActionInput qTableRequest) throws SQLException
{
ConnectionManager connectionManager = new ConnectionManager();
return connectionManager.getConnection((RDBMSBackendMetaData) qTableRequest.getBackend());
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
index d0970a73..885dc6f6 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
@@ -25,15 +25,14 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountResult;
-import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.interfaces.CountInterface;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import org.apache.logging.log4j.LogManager;
@@ -52,16 +51,16 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
/*******************************************************************************
**
*******************************************************************************/
- public CountResult execute(CountRequest countRequest) throws QException
+ public CountOutput execute(CountInput countInput) throws QException
{
try
{
- QTableMetaData table = countRequest.getTable();
+ QTableMetaData table = countInput.getTable();
String tableName = getTableName(table);
String sql = "SELECT count(*) as record_count FROM " + tableName;
- QQueryFilter filter = countRequest.getFilter();
+ QQueryFilter filter = countInput.getFilter();
List params = new ArrayList<>();
if(filter != null && CollectionUtils.nullSafeHasContents(filter.getCriteria()))
{
@@ -70,13 +69,12 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
// todo sql customization - can edit sql and/or param list
- CountResult rs = new CountResult();
+ CountOutput rs = new CountOutput();
- try(Connection connection = getConnection(countRequest))
+ try(Connection connection = getConnection(countInput))
{
QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) ->
{
- ResultSetMetaData metaData = resultSet.getMetaData();
if(resultSet.next())
{
rs.setCount(resultSet.getInt("record_count"));
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
index ee75105d..feb81cd1 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
@@ -27,13 +27,17 @@ import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
@@ -41,45 +45,63 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
*******************************************************************************/
public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInterface
{
+ private static final Logger LOG = LogManager.getLogger(RDBMSDeleteAction.class);
+
/*******************************************************************************
**
*******************************************************************************/
- public DeleteResult execute(DeleteRequest deleteRequest) throws QException
+ @Override
+ public boolean supportsQueryFilterInput()
{
- try
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public DeleteOutput execute(DeleteInput deleteInput) throws QException
+ {
+ DeleteOutput deleteOutput = new DeleteOutput();
+ deleteOutput.setRecordsWithErrors(new ArrayList<>());
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // Our strategy is: //
+ // - if there's a query filter, try to do a delete WHERE that filter. //
+ // - - if that has an error, or if there wasn't a query filter, then continue: //
+ // - if there's only 1 pkey to delete, just run a delete where $pkey=? query //
+ // - else if there's a list, try to delete it, but upon error: //
+ // - - do a single-delete for each entry in the list. //
+ /////////////////////////////////////////////////////////////////////////////////
+ try(Connection connection = getConnection(deleteInput))
{
- DeleteResult rs = new DeleteResult();
- QTableMetaData table = deleteRequest.getTable();
-
- String tableName = getTableName(table);
- String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
- String sql = "DELETE FROM "
- + tableName
- + " WHERE "
- + primaryKeyName
- + " IN ("
- + deleteRequest.getPrimaryKeys().stream().map(x -> "?").collect(Collectors.joining(","))
- + ")";
- List params = deleteRequest.getPrimaryKeys();
-
- // todo sql customization - can edit sql and/or param list
-
- try(Connection connection = getConnection(deleteRequest))
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // if there's a query filter, try to do a single-delete with that filter in the WHERE clause //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ if(deleteInput.getQueryFilter() != null)
{
- QueryManager.executeUpdateForRowCount(connection, sql, params);
- List outputRecords = new ArrayList<>();
- rs.setRecords(outputRecords);
- for(Serializable primaryKey : deleteRequest.getPrimaryKeys())
+ try
{
- QRecord qRecord = new QRecord().withTableName(deleteRequest.getTableName()).withValue("id", primaryKey);
- // todo uh, identify any errors?
- QRecord outputRecord = new QRecord(qRecord);
- outputRecords.add(outputRecord);
+ deleteInput.getAsyncJobCallback().updateStatus("Running bulk delete via query filter.");
+ deleteQueryFilter(connection, deleteInput, deleteOutput);
+ return (deleteOutput);
+ }
+ catch(Exception e)
+ {
+ deleteInput.getAsyncJobCallback().updateStatus("Error running bulk delete via filter. Fetching keys for individual deletes.");
+ LOG.info("Exception trying to delete by filter query. Moving on to deleting by id now.");
+ deleteInput.setPrimaryKeys(DeleteAction.getPrimaryKeysFromQueryFilter(deleteInput));
}
}
- return rs;
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // at this point, there either wasn't a query filter, or there was an error executing it (in which case, the query should //
+ // have been converted to a list of primary keys in the deleteInput). so, proceed now by deleting a list of pkeys. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ deleteList(connection, deleteInput, deleteOutput);
+ return (deleteOutput);
}
catch(Exception e)
{
@@ -87,4 +109,142 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void deleteList(Connection connection, DeleteInput deleteInput, DeleteOutput deleteOutput)
+ {
+ List primaryKeys = deleteInput.getPrimaryKeys();
+ if(primaryKeys.size() == 1)
+ {
+ doDeleteOne(connection, deleteInput.getTable(), primaryKeys.get(0), deleteOutput);
+ }
+ else
+ {
+ // todo - page this? or binary-tree it?
+ try
+ {
+ deleteInput.getAsyncJobCallback().updateStatus("Running bulk delete via key list.");
+ doDeleteList(connection, deleteInput.getTable(), primaryKeys, deleteOutput);
+ }
+ catch(Exception e)
+ {
+ deleteInput.getAsyncJobCallback().updateStatus("Error running bulk delete via key list. Performing individual deletes.");
+ LOG.info("Caught an error doing list-delete - going to single-deletes now", e);
+ int current = 1;
+ for(Serializable primaryKey : primaryKeys)
+ {
+ deleteInput.getAsyncJobCallback().updateStatus(current++, primaryKeys.size());
+ doDeleteOne(connection, deleteInput.getTable(), primaryKey, deleteOutput);
+ }
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void doDeleteOne(Connection connection, QTableMetaData table, Serializable primaryKey, DeleteOutput deleteOutput)
+ {
+ String tableName = getTableName(table);
+ String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
+
+ // todo sql customization - can edit sql and/or param list?
+ String sql = "DELETE FROM "
+ + tableName
+ + " WHERE "
+ + primaryKeyName + " = ?";
+
+ try
+ {
+ int rowCount = QueryManager.executeUpdateForRowCount(connection, sql, primaryKey);
+ deleteOutput.addToDeletedRecordCount(rowCount);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // it seems like maybe we shouldn't do the below - ids that aren't found will hit this condition, //
+ // but we (1) don't care and (2) can't detect this case when doing an in-list delete, so, let's //
+ // make the results match, and just avoid adding to the deleted count, not marking it as an error. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if(rowCount == 1)
+ // {
+ // deleteOutput.addToDeletedRecordCount(1);
+ // }
+ // else
+ // {
+ // LOG.debug("rowCount 0 trying to delete [" + tableName + "][" + primaryKey + "]");
+ // deleteOutput.addRecordWithError(new QRecord(table, primaryKey).withError("Record was not deleted (but no error was given from the database)"));
+ // }
+ }
+ catch(Exception e)
+ {
+ LOG.debug("Exception trying to delete [" + tableName + "][" + primaryKey + "]", e);
+ deleteOutput.addRecordWithError(new QRecord(table, primaryKey).withError("Record was not deleted: " + e.getMessage()));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void doDeleteList(Connection connection, QTableMetaData table, List primaryKeys, DeleteOutput deleteOutput) throws QException
+ {
+ try
+ {
+ String tableName = getTableName(table);
+ String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
+ String sql = "DELETE FROM "
+ + tableName
+ + " WHERE "
+ + primaryKeyName
+ + " IN ("
+ + primaryKeys.stream().map(x -> "?").collect(Collectors.joining(","))
+ + ")";
+
+ // todo sql customization - can edit sql and/or param list
+
+ Integer rowCount = QueryManager.executeUpdateForRowCount(connection, sql, primaryKeys);
+ deleteOutput.addToDeletedRecordCount(rowCount);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing delete: " + e.getMessage(), e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void deleteQueryFilter(Connection connection, DeleteInput deleteInput, DeleteOutput deleteOutput) throws QException
+ {
+ QQueryFilter filter = deleteInput.getQueryFilter();
+ List params = new ArrayList<>();
+ QTableMetaData table = deleteInput.getTable();
+
+ String tableName = getTableName(table);
+ String whereClause = makeWhereClause(table, filter.getCriteria(), params);
+
+ // todo sql customization - can edit sql and/or param list?
+ String sql = "DELETE FROM "
+ + tableName
+ + " WHERE "
+ + whereClause;
+
+ try
+ {
+ int rowCount = QueryManager.executeUpdateForRowCount(connection, sql, params);
+
+ deleteOutput.setDeletedRecordCount(rowCount);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing delete with filter: " + e.getMessage(), e);
+ }
+ }
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
index 0176f96c..3c32d746 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
@@ -28,13 +28,13 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import org.apache.logging.log4j.LogManager;
@@ -53,21 +53,21 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
/*******************************************************************************
**
*******************************************************************************/
- public InsertResult execute(InsertRequest insertRequest) throws QException
+ public InsertOutput execute(InsertInput insertInput) throws QException
{
- InsertResult rs = new InsertResult();
+ InsertOutput rs = new InsertOutput();
- if(CollectionUtils.nullSafeIsEmpty(insertRequest.getRecords()))
+ if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
LOG.info("Insert request called with 0 records. Returning with no-op");
rs.setRecords(new ArrayList<>());
return (rs);
}
- QTableMetaData table = insertRequest.getTable();
+ QTableMetaData table = insertInput.getTable();
Instant now = Instant.now();
- for(QRecord record : insertRequest.getRecords())
+ for(QRecord record : insertInput.getRecords())
{
///////////////////////////////////////////
// todo .. better (not hard-coded names) //
@@ -89,15 +89,18 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
.map(x -> "?")
.collect(Collectors.joining(", "));
- String tableName = getTableName(table);
- StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES");
- List