diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java new file mode 100644 index 00000000..6f5c441b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceAction.java @@ -0,0 +1,174 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper; +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.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceOutput; +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.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Action to do a "replace" - e.g: Update rows with unique-key values that are + ** already in the table; insert rows whose unique keys weren't already in the + ** table, and delete rows that weren't in the input (all based on a + ** UniqueKey that's part of the input) + ** + ** Note - the filter in the ReplaceInput - its role is to limit what rows are + ** potentially deleted. e.g., if you have a table that's segmented, and you're + ** only replacing a particular segment of it (say, for 1 client), then you pass + ** in a filter that finds rows matching that segment. See Test for example. + *******************************************************************************/ +public class ReplaceAction extends AbstractQActionFunction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ReplaceOutput execute(ReplaceInput input) throws QException + { + ReplaceOutput output = new ReplaceOutput(); + + QBackendTransaction transaction = input.getTransaction(); + boolean weOwnTheTransaction = false; + + try + { + QTableMetaData table = input.getTable(); + UniqueKey uniqueKey = input.getKey(); + String primaryKeyField = table.getPrimaryKeyField(); + if(transaction == null) + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(input.getTableName()); + transaction = new InsertAction().openTransaction(insertInput); + weOwnTheTransaction = true; + } + + List insertList = new ArrayList<>(); + List updateList = new ArrayList<>(); + List primaryKeysToKeep = new ArrayList<>(); + + for(List page : CollectionUtils.getPages(input.getRecords(), 1000)) + { + /////////////////////////////////////////////////////////////////////////////////// + // originally it was thought that we'd need to pass the filter in here // + // but, it's been decided not to. the filter only applies to what we can delete // + /////////////////////////////////////////////////////////////////////////////////// + Map, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey); + for(QRecord record : page) + { + Optional> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record); + if(keyValues.isPresent()) + { + if(existingKeys.containsKey(keyValues.get())) + { + Serializable primaryKey = existingKeys.get(keyValues.get()); + record.setValue(primaryKeyField, primaryKey); + updateList.add(record); + primaryKeysToKeep.add(primaryKey); + } + else + { + insertList.add(record); + } + } + } + } + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(table.getName()); + insertInput.setRecords(insertList); + insertInput.setTransaction(transaction); + insertInput.setOmitDmlAudit(input.getOmitDmlAudit()); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + primaryKeysToKeep.addAll(insertOutput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList()); + output.setInsertOutput(insertOutput); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(table.getName()); + updateInput.setRecords(updateList); + updateInput.setTransaction(transaction); + updateInput.setOmitDmlAudit(input.getOmitDmlAudit()); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + output.setUpdateOutput(updateOutput); + + QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep)); + if(input.getFilter() != null) + { + deleteFilter.addSubFilter(input.getFilter()); + } + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(table.getName()); + deleteInput.setQueryFilter(deleteFilter); + deleteInput.setTransaction(transaction); + deleteInput.setOmitDmlAudit(input.getOmitDmlAudit()); + DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); + output.setDeleteOutput(deleteOutput); + + if(weOwnTheTransaction) + { + transaction.commit(); + } + + return (output); + } + catch(Exception e) + { + if(weOwnTheTransaction) + { + transaction.rollback(); + } + throw (new QException("Error executing replace action", e)); + } + finally + { + if(weOwnTheTransaction) + { + transaction.close(); + } + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java new file mode 100644 index 00000000..fe09cf5e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceInput.java @@ -0,0 +1,210 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +package com.kingsrook.qqq.backend.core.model.actions.tables.replace; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +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.UniqueKey; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ReplaceInput extends AbstractTableActionInput +{ + private QBackendTransaction transaction; + private UniqueKey key; + private List records; + private QQueryFilter filter; + + private boolean omitDmlAudit = false; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ReplaceInput() + { + } + + + + /******************************************************************************* + ** Getter for transaction + *******************************************************************************/ + public QBackendTransaction getTransaction() + { + return (this.transaction); + } + + + + /******************************************************************************* + ** Setter for transaction + *******************************************************************************/ + public void setTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + } + + + + /******************************************************************************* + ** Fluent setter for transaction + *******************************************************************************/ + public ReplaceInput withTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + return (this); + } + + + + /******************************************************************************* + ** Getter for records + *******************************************************************************/ + public List getRecords() + { + return (this.records); + } + + + + /******************************************************************************* + ** Setter for records + *******************************************************************************/ + public void setRecords(List records) + { + this.records = records; + } + + + + /******************************************************************************* + ** Fluent setter for records + *******************************************************************************/ + public ReplaceInput withRecords(List records) + { + this.records = records; + return (this); + } + + + + /******************************************************************************* + ** Getter for filter + *******************************************************************************/ + public QQueryFilter getFilter() + { + return (this.filter); + } + + + + /******************************************************************************* + ** Setter for filter + *******************************************************************************/ + public void setFilter(QQueryFilter filter) + { + this.filter = filter; + } + + + + /******************************************************************************* + ** Fluent setter for filter + *******************************************************************************/ + public ReplaceInput withFilter(QQueryFilter filter) + { + this.filter = filter; + return (this); + } + + + + /******************************************************************************* + ** Getter for key + *******************************************************************************/ + public UniqueKey getKey() + { + return (this.key); + } + + + + /******************************************************************************* + ** Setter for key + *******************************************************************************/ + public void setKey(UniqueKey key) + { + this.key = key; + } + + + + /******************************************************************************* + ** Fluent setter for key + *******************************************************************************/ + public ReplaceInput withKey(UniqueKey key) + { + this.key = key; + return (this); + } + + + + /******************************************************************************* + ** Getter for omitDmlAudit + *******************************************************************************/ + public boolean getOmitDmlAudit() + { + return (this.omitDmlAudit); + } + + + + /******************************************************************************* + ** Setter for omitDmlAudit + *******************************************************************************/ + public void setOmitDmlAudit(boolean omitDmlAudit) + { + this.omitDmlAudit = omitDmlAudit; + } + + + + /******************************************************************************* + ** Fluent setter for omitDmlAudit + *******************************************************************************/ + public ReplaceInput withOmitDmlAudit(boolean omitDmlAudit) + { + this.omitDmlAudit = omitDmlAudit; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceOutput.java new file mode 100644 index 00000000..95c6f649 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/replace/ReplaceOutput.java @@ -0,0 +1,133 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +package com.kingsrook.qqq.backend.core.model.actions.tables.replace; + + +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ReplaceOutput extends AbstractActionOutput +{ + private InsertOutput insertOutput; + private UpdateOutput updateOutput; + private DeleteOutput deleteOutput; + + + + /******************************************************************************* + ** Getter for insertOutput + *******************************************************************************/ + public InsertOutput getInsertOutput() + { + return (this.insertOutput); + } + + + + /******************************************************************************* + ** Setter for insertOutput + *******************************************************************************/ + public void setInsertOutput(InsertOutput insertOutput) + { + this.insertOutput = insertOutput; + } + + + + /******************************************************************************* + ** Fluent setter for insertOutput + *******************************************************************************/ + public ReplaceOutput withInsertOutput(InsertOutput insertOutput) + { + this.insertOutput = insertOutput; + return (this); + } + + + + /******************************************************************************* + ** Getter for updateOutput + *******************************************************************************/ + public UpdateOutput getUpdateOutput() + { + return (this.updateOutput); + } + + + + /******************************************************************************* + ** Setter for updateOutput + *******************************************************************************/ + public void setUpdateOutput(UpdateOutput updateOutput) + { + this.updateOutput = updateOutput; + } + + + + /******************************************************************************* + ** Fluent setter for updateOutput + *******************************************************************************/ + public ReplaceOutput withUpdateOutput(UpdateOutput updateOutput) + { + this.updateOutput = updateOutput; + return (this); + } + + + + /******************************************************************************* + ** Getter for deleteOutput + *******************************************************************************/ + public DeleteOutput getDeleteOutput() + { + return (this.deleteOutput); + } + + + + /******************************************************************************* + ** Setter for deleteOutput + *******************************************************************************/ + public void setDeleteOutput(DeleteOutput deleteOutput) + { + this.deleteOutput = deleteOutput; + } + + + + /******************************************************************************* + ** Fluent setter for deleteOutput + *******************************************************************************/ + public ReplaceOutput withDeleteOutput(DeleteOutput deleteOutput) + { + this.deleteOutput = deleteOutput; + return (this); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java new file mode 100644 index 00000000..4ce0e8ee --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/ReplaceActionTest.java @@ -0,0 +1,300 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for ReplaceAction + *******************************************************************************/ +class ReplaceActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithoutFilter() throws QException + { + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + /////////////////////////////// + // start with these 2 people // + /////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 1), + new QRecord().withValue("firstName", "Mr.").withValue("lastName", "Burns") + ))); + + assertEquals(1, countByFirstName("Homer")); + assertEquals(1, countByFirstName("Mr.")); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // now do a replace - updating one, inserting one, and (since it's not included in the list), deleting the other // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List newPeople = List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 2), + new QRecord().withValue("firstName", "Ned").withValue("lastName", "Flanders") + ); + + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("firstName", "lastName")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newPeople); + replaceInput.setFilter(null); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(1, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(1, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + + ////////////////////////////// + // assert homer was updated // + ////////////////////////////// + assertEquals(1, countByFirstName("Homer")); + assertEquals(2, getNoOfShoes("Homer", "Simpson")); + + /////////////////////////////////// + // assert Mr (burns) was deleted // + /////////////////////////////////// + assertEquals(0, countByFirstName("Mr.")); + + ///////////////////////////// + // assert ned was inserted // + ///////////////////////////// + assertEquals(1, countByFirstName("Ned")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOnlyInsertAndDelete() throws QException + { + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + /////////////////////////////// + // start with these 2 people // + /////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 1), + new QRecord().withValue("firstName", "Marge").withValue("lastName", "Simpson").withValue("noOfShoes", 1) + ))); + + ////////////////////////////////////////// + // now do a replace that fully replaces // + ////////////////////////////////////////// + List newPeople = List.of( + new QRecord().withValue("firstName", "Ned").withValue("lastName", "Flanders"), + new QRecord().withValue("firstName", "Maude").withValue("lastName", "Flanders") + ); + + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("firstName", "lastName")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newPeople); + replaceInput.setFilter(null); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(2, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(0, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(2, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + + /////////////////////////////////////// + // assert homer & marge were deleted // + /////////////////////////////////////// + assertEquals(0, countByFirstName("Homer")); + assertEquals(0, countByFirstName("Marge")); + + ////////////////////////////////////// + // assert ned & maude were inserted // + ////////////////////////////////////// + assertEquals(1, countByFirstName("Ned")); + assertEquals(1, countByFirstName("Maude")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOnlyUpdates() throws QException + { + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + /////////////////////////////// + // start with these 2 people // + /////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 1), + new QRecord().withValue("firstName", "Marge").withValue("lastName", "Simpson").withValue("noOfShoes", 1) + ))); + + ///////////////////////////////////////////// + // now do a replace that just updates them // + ///////////////////////////////////////////// + List newPeople = List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 2), + new QRecord().withValue("firstName", "Marge").withValue("lastName", "Simpson").withValue("noOfShoes", 2) + ); + + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("firstName", "lastName")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newPeople); + replaceInput.setFilter(null); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(0, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(2, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(0, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + + /////////////////////////////////////// + // assert homer & marge were updated // + /////////////////////////////////////// + assertEquals(2, getNoOfShoes("Homer", "Simpson")); + assertEquals(2, getNoOfShoes("Marge", "Simpson")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithFilter() throws QException + { + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + ///////////////////////////////////// + // start w/ 3 simpsons and a burns // + ///////////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 1), + new QRecord().withValue("firstName", "Marge").withValue("lastName", "Simpson").withValue("noOfShoes", 2), + new QRecord().withValue("firstName", "Bart").withValue("lastName", "Simpson").withValue("noOfShoes", 3), + new QRecord().withValue("firstName", "Mr.").withValue("lastName", "Burns") + ))); + + assertEquals(1, countByFirstName("Homer")); + assertEquals(1, countByFirstName("Marge")); + assertEquals(1, countByFirstName("Bart")); + assertEquals(1, countByFirstName("Mr.")); + + ///////////////////////////////////////////////////////////////////////////////// + // now - we'll replace the simpsons only - note the filter in the ReplaceInput // + // so even though Burns isn't in this list, he shouldn't be deleted. // + ///////////////////////////////////////////////////////////////////////////////// + List newPeople = List.of( + new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("noOfShoes", 4), + new QRecord().withValue("firstName", "Marge").withValue("lastName", "Simpson"), + new QRecord().withValue("firstName", "Lisa").withValue("lastName", "Simpson").withValue("noOfShoes", 5) + ); + + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("firstName", "lastName")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newPeople); + replaceInput.setFilter(new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Simpson"))); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(1, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(2, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(1, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + + ////////////////////////////// + // assert homer was updated // + ////////////////////////////// + assertEquals(1, countByFirstName("Homer")); + assertEquals(4, getNoOfShoes("Homer", "Simpson")); + + /////////////////////////////// + // assert Marge was no-op'ed // + /////////////////////////////// + assertEquals(1, countByFirstName("Marge")); + assertEquals(2, getNoOfShoes("Marge", "Simpson")); + + //////////////////////////////////// + // assert Mr (burns) was no-op'ed // + //////////////////////////////////// + assertEquals(1, countByFirstName("Mr.")); + assertNull(getNoOfShoes("Mr.", "Burns")); + + ///////////////////////////// + // assert Bart was deleted // + ///////////////////////////// + assertEquals(0, countByFirstName("Bart")); + + ////////////////////////////// + // assert Lisa was inserted // + ////////////////////////////// + assertEquals(1, countByFirstName("Lisa")); + assertEquals(5, getNoOfShoes("Lisa", "Simpson")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer countByFirstName(String firstName) throws QException + { + return new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, firstName)))).getCount(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer getNoOfShoes(String firstName, String lastName) throws QException + { + return new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withUniqueKey(Map.of("firstName", firstName, "lastName", lastName))).getValueInteger("noOfShoes"); + } + +} \ No newline at end of file