diff --git a/pom.xml b/pom.xml index 143392a6..74afd12b 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220719.154219-3 + 0.2.0-20220725.132738-10 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 5ea27a0a..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 @@ -28,12 +28,16 @@ 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.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.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); + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean supportsQueryFilterInput() + { + return (true); + } + + /******************************************************************************* ** *******************************************************************************/ public DeleteOutput execute(DeleteInput deleteInput) throws QException { - try + 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)) { - DeleteOutput rs = new DeleteOutput(); - QTableMetaData table = deleteInput.getTable(); - - String tableName = getTableName(table); - String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField())); - String sql = "DELETE FROM " - + tableName - + " WHERE " - + primaryKeyName - + " IN (" - + deleteInput.getPrimaryKeys().stream().map(x -> "?").collect(Collectors.joining(",")) - + ")"; - List params = deleteInput.getPrimaryKeys(); - - // todo sql customization - can edit sql and/or param list - - try(Connection connection = getConnection(deleteInput)) + /////////////////////////////////////////////////////////////////////////////////////////////// + // 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 : deleteInput.getPrimaryKeys()) + try { - QRecord qRecord = new QRecord().withTableName(deleteInput.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 30e2c659..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 @@ -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 params = new ArrayList<>(); + List outputRecords = new ArrayList<>(); + rs.setRecords(outputRecords); try(Connection connection = getConnection(insertInput)) { for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE)) { - int recordIndex = 0; + String tableName = getTableName(table); + StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); + List params = new ArrayList<>(); + int recordIndex = 0; + for(QRecord record : page) { if(recordIndex++ > 0) @@ -116,11 +119,9 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte // todo sql customization - can edit sql and/or param list // todo - non-serial-id style tables // todo - other generated values, e.g., createDate... maybe need to re-select? - List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); - List outputRecords = new ArrayList<>(); - rs.setRecords(outputRecords); - int index = 0; - for(QRecord record : insertInput.getRecords()) + List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); + int index = 0; + for(QRecord record : page) { Integer id = idList.get(index++); QRecord outputRecord = new QRecord(record); diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java index a7f6fc17..d7ef8739 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java @@ -58,6 +58,8 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte { private static final Logger LOG = LogManager.getLogger(RDBMSUpdateAction.class); + private int statusCounter = 0; + /******************************************************************************* @@ -119,7 +121,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte ///////////////////////////////////////////////////////////////////////////////////////////// for(List fieldsBeingUpdated : recordsByFieldBeingUpdated.keySet()) { - updateRecordsWithMatchingListOfFields(connection, table, recordsByFieldBeingUpdated.get(fieldsBeingUpdated), fieldsBeingUpdated); + updateRecordsWithMatchingListOfFields(updateInput, connection, table, recordsByFieldBeingUpdated.get(fieldsBeingUpdated), fieldsBeingUpdated); } return rs; @@ -136,15 +138,25 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte /******************************************************************************* ** *******************************************************************************/ - private void updateRecordsWithMatchingListOfFields(Connection connection, QTableMetaData table, List recordList, List fieldsBeingUpdated) throws SQLException + private void updateRecordsWithMatchingListOfFields(UpdateInput updateInput, Connection connection, QTableMetaData table, List recordList, List fieldsBeingUpdated) throws SQLException { //////////////////////////////////////////////////////////////////////////////// // check for an optimization - if all of the records have the same values for // // all fields being updated, just do 1 update, with an IN list on the ids. // //////////////////////////////////////////////////////////////////////////////// - if(areAllValuesBeingUpdatedTheSame(recordList, fieldsBeingUpdated)) + boolean allAreTheSame; + if(updateInput.getAreAllValuesBeingUpdatedTheSame() != null) { - updateRecordsWithMatchingValuesAndFields(connection, table, recordList, fieldsBeingUpdated); + allAreTheSame = updateInput.getAreAllValuesBeingUpdatedTheSame(); + } + else + { + allAreTheSame = areAllValuesBeingUpdatedTheSame(recordList, fieldsBeingUpdated); + } + + if(allAreTheSame) + { + updateRecordsWithMatchingValuesAndFields(updateInput, connection, table, recordList, fieldsBeingUpdated); return; } @@ -174,10 +186,12 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte // let query manager do the batch updates - note that it will internally page // //////////////////////////////////////////////////////////////////////////////// QueryManager.executeBatchUpdate(connection, sql, values); + incrementStatus(updateInput, recordList.size()); } + /******************************************************************************* ** *******************************************************************************/ @@ -198,7 +212,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte /******************************************************************************* ** *******************************************************************************/ - private void updateRecordsWithMatchingValuesAndFields(Connection connection, QTableMetaData table, List recordList, List fieldsBeingUpdated) throws SQLException + private void updateRecordsWithMatchingValuesAndFields(UpdateInput updateInput, Connection connection, QTableMetaData table, List recordList, List fieldsBeingUpdated) throws SQLException { for(List page : CollectionUtils.getPages(recordList, QueryManager.PAGE_SIZE)) { @@ -230,6 +244,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte // let query manager do the update // ///////////////////////////////////// QueryManager.executeUpdate(connection, sql, params); + incrementStatus(updateInput, page.size()); } } @@ -261,4 +276,14 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte return (true); } + /******************************************************************************* + ** + *******************************************************************************/ + private void incrementStatus(UpdateInput updateInput, int amount) + { + statusCounter += amount; + updateInput.getAsyncJobCallback().updateStatus(statusCounter, updateInput.getRecords().size()); + } + + } diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index 4496dd1d..c694cbad 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -55,7 +55,9 @@ import org.apache.commons.lang.NotImplementedException; *******************************************************************************/ public class QueryManager { - public static final int PAGE_SIZE = 2000; + public static final int DEFAULT_PAGE_SIZE = 2000; + public static int PAGE_SIZE = DEFAULT_PAGE_SIZE; + private static final int MS_PER_SEC = 1000; private static final int NINETEEN_HUNDRED = 1900; @@ -93,8 +95,8 @@ public class QueryManager try { statement = prepareStatementAndBindParams(connection, sql, params); - statement.execute(); incrementStatistic(STAT_QUERIES_RAN); + statement.execute(); resultSet = statement.getResultSet(); procesor.processResultSet(resultSet); @@ -354,8 +356,8 @@ public class QueryManager public static PreparedStatement executeUpdate(Connection connection, String sql, Object... params) throws SQLException { PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); - statement.executeUpdate(); incrementStatistic(STAT_QUERIES_RAN); + statement.executeUpdate(); return (statement); } @@ -367,8 +369,8 @@ public class QueryManager public static PreparedStatement executeUpdate(Connection connection, String sql, List params) throws SQLException { PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); - statement.executeUpdate(); incrementStatistic(STAT_QUERIES_RAN); + statement.executeUpdate(); return (statement); } @@ -413,8 +415,8 @@ public class QueryManager { try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) { - statement.executeUpdate(); incrementStatistic(STAT_QUERIES_RAN); + statement.executeUpdate(); return (statement.getUpdateCount()); } } @@ -473,9 +475,9 @@ public class QueryManager try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { bindParams(params.toArray(), statement); + incrementStatistic(STAT_QUERIES_RAN); statement.executeUpdate(); ResultSet generatedKeys = statement.getGeneratedKeys(); - incrementStatistic(STAT_QUERIES_RAN); while(generatedKeys.next()) { rs.add(getInteger(generatedKeys, 1)); @@ -565,8 +567,8 @@ public class QueryManager bindParams(updatePS, params); updatePS.addBatch(); } - updatePS.executeBatch(); incrementStatistic(STAT_BATCHES_RAN); + updatePS.executeBatch(); } } @@ -1617,4 +1619,25 @@ public class QueryManager return statistics; } + + + /******************************************************************************* + ** Note - this changes a static field that impacts all usages. Really, it's meant + ** to only be called in unit tests (at least as of the time of this writing). + *******************************************************************************/ + public static void setPageSize(int pageSize) + { + PAGE_SIZE = pageSize; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void resetPageSize() + { + PAGE_SIZE = DEFAULT_PAGE_SIZE; + } + } diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 7b6a1e45..5354578a 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -36,6 +36,10 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDe public class TestUtils { + public static final String DEFAULT_BACKEND_NAME = "default"; + + + /******************************************************************************* ** *******************************************************************************/ @@ -54,13 +58,12 @@ public class TestUtils *******************************************************************************/ public static RDBMSBackendMetaData defineBackend() { - RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData() + return (new RDBMSBackendMetaData() + .withName(DEFAULT_BACKEND_NAME) .withVendor("h2") .withHostName("mem") .withDatabaseName("test_database") - .withUsername("sa"); - rdbmsBackendMetaData.setName("default"); - return (rdbmsBackendMetaData); + .withUsername("sa")); } diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java index 3c03a72a..7382de50 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java @@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; import static junit.framework.Assert.assertNotNull; @@ -38,17 +39,39 @@ import static junit.framework.Assert.assertNotNull; public class RDBMSActionTest { + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + private void afterEachRDBMSActionTest() + { + QueryManager.resetPageSize(); + QueryManager.resetStatistics(); + QueryManager.setCollectStatistics(false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void primeTestDatabase() throws Exception + { + primeTestDatabase("prime-test-database.sql"); + } + + /******************************************************************************* ** *******************************************************************************/ @SuppressWarnings("unchecked") - protected void primeTestDatabase() throws Exception + protected void primeTestDatabase(String sqlFileName) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); try(Connection connection = connectionManager.getConnection(TestUtils.defineBackend())) { - InputStream primeTestDatabaseSqlStream = RDBMSActionTest.class.getResourceAsStream("/prime-test-database.sql"); + InputStream primeTestDatabaseSqlStream = RDBMSActionTest.class.getResourceAsStream("/" + sqlFileName); assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteActionTest.java index c8bd95f5..0bd18c51 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteActionTest.java @@ -23,9 +23,16 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.util.List; +import java.util.Map; 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.metadata.QInstance; +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.module.rdbms.TestUtils; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -56,11 +63,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest @Test public void testDeleteAll() throws Exception { - DeleteInput deleteInput = initDeleteRequest(); + DeleteInput deleteInput = initStandardPersonDeleteRequest(); deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5)); DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput); - assertEquals(5, deleteResult.getRecords().size(), "Unfiltered delete should return all rows"); - // todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); + assertEquals(5, deleteResult.getDeletedRecordCount(), "Unfiltered delete should return all rows"); + assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors"); runTestSql("SELECT id FROM person", (rs -> assertFalse(rs.next()))); } @@ -72,11 +79,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest @Test public void testDeleteOne() throws Exception { - DeleteInput deleteInput = initDeleteRequest(); + DeleteInput deleteInput = initStandardPersonDeleteRequest(); deleteInput.setPrimaryKeys(List.of(1)); DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput); - assertEquals(1, deleteResult.getRecords().size(), "Should delete one row"); - // todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); + assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row"); + assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors"); runTestSql("SELECT id FROM person WHERE id = 1", (rs -> assertFalse(rs.next()))); } @@ -88,11 +95,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest @Test public void testDeleteSome() throws Exception { - DeleteInput deleteInput = initDeleteRequest(); + DeleteInput deleteInput = initStandardPersonDeleteRequest(); deleteInput.setPrimaryKeys(List.of(1, 3, 5)); DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput); - assertEquals(3, deleteResult.getRecords().size(), "Should delete one row"); - // todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); + assertEquals(3, deleteResult.getDeletedRecordCount(), "Should delete one row"); + assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors"); runTestSql("SELECT id FROM person", (rs -> { int rowsFound = 0; while(rs.next()) @@ -110,7 +117,22 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest /******************************************************************************* ** *******************************************************************************/ - private DeleteInput initDeleteRequest() + @Test + void testDeleteSomeIdsThatExistAndSomeThatDoNot() throws Exception + { + DeleteInput deleteInput = initStandardPersonDeleteRequest(); + deleteInput.setPrimaryKeys(List.of(1, -1)); + DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput); + assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row"); + assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors (the one not found is just noop)"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private DeleteInput initStandardPersonDeleteRequest() { DeleteInput deleteInput = new DeleteInput(); deleteInput.setInstance(TestUtils.defineInstance()); @@ -118,4 +140,87 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest return deleteInput; } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testDeleteWhereForeignKeyBlocksSome() throws Exception + { + ////////////////////////////////////////////////////////////////// + // load the parent-child tables, with foreign keys and instance // + ////////////////////////////////////////////////////////////////// + super.primeTestDatabase("prime-test-database-parent-child-tables.sql"); + DeleteInput deleteInput = initChildTableInstanceAndDeleteRequest(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // try to delete all of the child records - 2 should fail, because they are referenced by parent_table.child_id // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5)); + + QueryManager.setCollectStatistics(true); + QueryManager.resetStatistics(); + + DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput); + + //////////////////////////////////////////////////////////////////////////////// + // assert that 6 queries ran - the initial delete (which failed), then 6 more // + //////////////////////////////////////////////////////////////////////////////// + QueryManager.setCollectStatistics(false); + Map queryStats = QueryManager.getStatistics(); + assertEquals(6, queryStats.get(QueryManager.STAT_QUERIES_RAN), "Number of queries ran"); + + assertEquals(2, deleteResult.getRecordsWithErrors().size(), "Should get back the 2 records with errors"); + assertTrue(deleteResult.getRecordsWithErrors().stream().noneMatch(r -> r.getErrors().isEmpty()), "All we got back should have errors"); + assertEquals(3, deleteResult.getDeletedRecordCount(), "Should get back that 3 were deleted"); + + runTestSql("SELECT id FROM child_table", (rs -> { + int rowsFound = 0; + while(rs.next()) + { + rowsFound++; + /////////////////////////////////////////// + // child_table rows 1 & 3 should survive // + /////////////////////////////////////////// + assertTrue(rs.getInt(1) == 1 || rs.getInt(1) == 3); + } + assertEquals(2, rowsFound); + })); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private DeleteInput initChildTableInstanceAndDeleteRequest() + { + QInstance qInstance = TestUtils.defineInstance(); + + String childTableName = "childTable"; + qInstance.addTable(new QTableMetaData() + .withName(childTableName) + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withBackendDetails(new RDBMSTableBackendDetails() + .withTableName("child_table"))); + + qInstance.addTable(new QTableMetaData() + .withName("parentTable") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("childId", QFieldType.INTEGER).withBackendName("child_id")) + .withBackendDetails(new RDBMSTableBackendDetails() + .withTableName("parent_table"))); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setInstance(qInstance); + deleteInput.setTableName(childTableName); + return deleteInput; + } } \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java index f55fc550..96f182d1 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java @@ -29,6 +29,7 @@ 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.module.rdbms.TestUtils; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -97,17 +98,7 @@ public class RDBMSInsertActionTest extends RDBMSActionTest assertEquals(1, insertOutput.getRecords().size(), "Should return 1 row"); assertNotNull(insertOutput.getRecords().get(0).getValue("id"), "Should have an id in the row"); // todo - add errors to QRecord? assertTrue(insertResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); - runTestSql("SELECT * FROM person WHERE last_name = 'Kirk'", (rs -> { - int rowsFound = 0; - while(rs.next()) - { - rowsFound++; - assertEquals(6, rs.getInt("id")); - assertEquals("James", rs.getString("first_name")); - assertNotNull(rs.getString("create_date")); - } - assertEquals(1, rowsFound); - })); + assertAnInsertedPersonRecord("James", "Kirk", 6); } @@ -118,6 +109,8 @@ public class RDBMSInsertActionTest extends RDBMSActionTest @Test public void testInsertMany() throws Exception { + QueryManager.setPageSize(2); + InsertInput insertInput = initInsertRequest(); QRecord record1 = new QRecord().withTableName("person") .withValue("firstName", "Jean-Luc") @@ -129,29 +122,35 @@ public class RDBMSInsertActionTest extends RDBMSActionTest .withValue("lastName", "Riker") .withValue("email", "notthomas@starfleet.net") .withValue("birthDate", "2320-05-20"); - insertInput.setRecords(List.of(record1, record2)); + QRecord record3 = new QRecord().withTableName("person") + .withValue("firstName", "Beverly") + .withValue("lastName", "Crusher") + .withValue("email", "doctor@starfleet.net") + .withValue("birthDate", "2320-06-26"); + insertInput.setRecords(List.of(record1, record2, record3)); InsertOutput insertOutput = new RDBMSInsertAction().execute(insertInput); - assertEquals(2, insertOutput.getRecords().size(), "Should return 1 row"); + assertEquals(3, insertOutput.getRecords().size(), "Should return right # of rows"); assertEquals(6, insertOutput.getRecords().get(0).getValue("id"), "Should have next id in the row"); assertEquals(7, insertOutput.getRecords().get(1).getValue("id"), "Should have next id in the row"); - // todo - add errors to QRecord? assertTrue(insertResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); - runTestSql("SELECT * FROM person WHERE last_name = 'Picard'", (rs -> { + assertEquals(8, insertOutput.getRecords().get(2).getValue("id"), "Should have next id in the row"); + assertAnInsertedPersonRecord("Jean-Luc", "Picard", 6); + assertAnInsertedPersonRecord("William", "Riker", 7); + assertAnInsertedPersonRecord("Beverly", "Crusher", 8); + } + + + + private void assertAnInsertedPersonRecord(String firstName, String lastName, Integer id) throws Exception + { + runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs -> { int rowsFound = 0; while(rs.next()) { rowsFound++; - assertEquals(6, rs.getInt("id")); - assertEquals("Jean-Luc", rs.getString("first_name")); - } - assertEquals(1, rowsFound); - })); - runTestSql("SELECT * FROM person WHERE last_name = 'Riker'", (rs -> { - int rowsFound = 0; - while(rs.next()) - { - rowsFound++; - assertEquals(7, rs.getInt("id")); - assertEquals("William", rs.getString("first_name")); + assertEquals(id, rs.getInt("id")); + assertEquals(firstName, rs.getString("first_name")); + assertNotNull(rs.getString("create_date")); + assertNotNull(rs.getString("modify_date")); } assertEquals(1, rowsFound); })); diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java index b71bb58d..76ded1c6 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java @@ -30,13 +30,15 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -59,17 +61,6 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest } - /******************************************************************************* - ** - *******************************************************************************/ - @AfterEach - public void afterEach() throws Exception - { - QueryManager.resetStatistics(); - QueryManager.setCollectStatistics(false); - } - - /******************************************************************************* ** @@ -311,6 +302,45 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testModifyDateGetsUpdated() throws Exception + { + String originalModifyDate = selectModifyDate(1); + + UpdateInput updateInput = initUpdateRequest(); + List records = new ArrayList<>(); + records.add(new QRecord().withTableName("person") + .withValue("id", 1) + .withValue("firstName", "Johnny Updated")); + updateInput.setRecords(records); + new RDBMSUpdateAction().execute(updateInput); + + String updatedModifyDate = selectModifyDate(1); + + assertTrue(StringUtils.hasContent(originalModifyDate)); + assertTrue(StringUtils.hasContent(updatedModifyDate)); + assertNotEquals(originalModifyDate, updatedModifyDate); + } + + + + private String selectModifyDate(Integer id) throws Exception + { + StringBuilder modifyDate = new StringBuilder(); + runTestSql("SELECT modify_date FROM person WHERE id = " + id, (rs -> { + if(rs.next()) + { + modifyDate.append(rs.getString("modify_date")); + } + })); + return (modifyDate.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/test/resources/prime-test-database-parent-child-tables.sql b/src/test/resources/prime-test-database-parent-child-tables.sql new file mode 100644 index 00000000..7acb63a0 --- /dev/null +++ b/src/test/resources/prime-test-database-parent-child-tables.sql @@ -0,0 +1,48 @@ +-- +-- QQQ - Low-code Application Framework for Engineers. +-- Copyright (C) 2021-2022. Kingsrook, LLC +-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States +-- contact@kingsrook.com +-- https://github.com/Kingsrook/ +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +-- + +DROP TABLE IF EXISTS child_table; +CREATE TABLE child_table +( + id INT AUTO_INCREMENT primary key, + name VARCHAR(80) NOT NULL +); + +INSERT INTO child_table (id, name) VALUES (1, 'Timmy'); +INSERT INTO child_table (id, name) VALUES (2, 'Jimmy'); +INSERT INTO child_table (id, name) VALUES (3, 'Johnny'); +INSERT INTO child_table (id, name) VALUES (4, 'Gracie'); +INSERT INTO child_table (id, name) VALUES (5, 'Suzie'); + +DROP TABLE IF EXISTS parent_table; +CREATE TABLE parent_table +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + child_id INT, + foreign key (child_id) references child_table(id) +); + +INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1); +INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1); +INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null); +INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null); +INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3);