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 index 841c6d6b..04177cdb 100644 --- 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 @@ -47,6 +47,7 @@ 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; +import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -79,9 +80,11 @@ public class ReplaceAction extends AbstractQActionFunction, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey); + Map, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey, allowNullKeyValuesToEqual); + for(QRecord record : page) { - Optional> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record); + Optional> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual); if(keyValues.isPresent()) { if(existingKeys.containsKey(keyValues.get())) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java index adab071e..7832344e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UniqueKeyHelper.java @@ -54,7 +54,7 @@ public class UniqueKeyHelper /******************************************************************************* ** *******************************************************************************/ - public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey) throws QException + public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey, boolean allowNullKeyValuesToEqual) throws QException { List ukFieldNames = uniqueKey.getFieldNames(); Map, Serializable> existingRecords = new HashMap<>(); @@ -112,7 +112,7 @@ public class UniqueKeyHelper QueryOutput queryOutput = new QueryAction().execute(queryInput); for(QRecord record : queryOutput.getRecords()) { - Optional> keyValues = getKeyValues(table, uniqueKey, record); + Optional> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual); if(keyValues.isPresent()) { existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField())); @@ -128,7 +128,17 @@ public class UniqueKeyHelper /******************************************************************************* ** *******************************************************************************/ - public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record) + public static Map, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List recordList, UniqueKey uniqueKey) throws QException + { + return (getExistingKeys(transaction, table, recordList, uniqueKey, false)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record, boolean allowNullKeyValuesToEqual) { try { @@ -138,7 +148,19 @@ public class UniqueKeyHelper QFieldMetaData field = table.getField(fieldName); Serializable value = record.getValue(fieldName); Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value); - keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue); + + /////////////////////////////////////////////////////////////////////////////////// + // if null value, look at flag to determine if a null should be used (which will // + // allow keys to match), or a NullUniqueKeyValue, (which will never match) // + /////////////////////////////////////////////////////////////////////////////////// + if(typedValue == null) + { + keyValues.add(allowNullKeyValuesToEqual ? null : new NullUniqueKeyValue()); + } + else + { + keyValues.add(typedValue); + } } return (Optional.of(keyValues)); } @@ -150,6 +172,16 @@ public class UniqueKeyHelper + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record) + { + return (getKeyValues(table, uniqueKey, record, false)); + } + + + /******************************************************************************* ** To make a list of unique key values here behave like they do in an RDBMS ** (which is what we're trying to mimic - which is - 2 null values in a field 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 index 81944a6a..354a3bde 100644 --- 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 @@ -39,7 +39,8 @@ public class ReplaceInput extends AbstractTableActionInput private UniqueKey key; private List records; private QQueryFilter filter; - private boolean performDeletes = true; + private boolean performDeletes = true; + private boolean allowNullKeyValuesToEqual = false; private boolean omitDmlAudit = false; @@ -239,4 +240,35 @@ public class ReplaceInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for allowNullKeyValuesToEqual + *******************************************************************************/ + public boolean getAllowNullKeyValuesToEqual() + { + return (this.allowNullKeyValuesToEqual); + } + + + + /******************************************************************************* + ** Setter for allowNullKeyValuesToEqual + *******************************************************************************/ + public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual) + { + this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual; + } + + + + /******************************************************************************* + ** Fluent setter for allowNullKeyValuesToEqual + *******************************************************************************/ + public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual) + { + this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual; + 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 index 4ce0e8ee..da57c703 100644 --- 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 @@ -43,7 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* - ** Unit test for ReplaceAction + ** Unit test for ReplaceAction *******************************************************************************/ class ReplaceActionTest extends BaseTest { @@ -157,6 +157,134 @@ class ReplaceActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullsNotMatchingAllowingDelete() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + ////////////////////////////// + // replace allowing deletes // + ////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + 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()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullsNotMatchingNotAllowingDelete() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + ///////////////////////////////// + // replace disallowing deletes // + ///////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + replaceInput.setFilter(null); + replaceInput.setPerformDeletes(false); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(1, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size()); + assertNull(replaceOutput.getDeleteOutput()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoKeysWithNullMatching() throws QException + { + String tableName = TestUtils.TABLE_NAME_TWO_KEYS; + + //////////////////////////////// + // start with these 2 records // + //////////////////////////////// + new InsertAction().execute(new InsertInput(tableName).withRecords(List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ))); + + //////////////////////////////////////////////////// + // now do a replace action that just updates them // + //////////////////////////////////////////////////// + List newThings = List.of( + new QRecord().withValue("key1", 1).withValue("key2", 2), + new QRecord().withValue("key1", 3) + ); + + /////////////////////////////////////////////// + // replace treating null key values as equal // + /////////////////////////////////////////////// + ReplaceInput replaceInput = new ReplaceInput(); + replaceInput.setTableName(tableName); + replaceInput.setKey(new UniqueKey("key1", "key2")); + replaceInput.setOmitDmlAudit(true); + replaceInput.setRecords(newThings); + replaceInput.setFilter(null); + replaceInput.setAllowNullKeyValuesToEqual(true); + ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput); + + assertEquals(0, replaceOutput.getInsertOutput().getRecords().size()); + assertEquals(2, replaceOutput.getUpdateOutput().getRecords().size()); + assertEquals(0, replaceOutput.getDeleteOutput().getDeletedRecordCount()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -297,4 +425,4 @@ class ReplaceActionTest extends BaseTest 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 +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 0086c351..000afa15 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -138,6 +138,7 @@ public class TestUtils public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; + public static final String TABLE_NAME_TWO_KEYS = "twoKeys"; public static final String TABLE_NAME_PERSON = "person"; public static final String TABLE_NAME_SHAPE = "shape"; public static final String TABLE_NAME_SHAPE_CACHE = "shapeCache"; @@ -196,6 +197,7 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addTable(defineTableTwoKeys()); qInstance.addTable(definePersonFileTable()); qInstance.addTable(definePersonMemoryTable()); qInstance.addTable(definePersonMemoryCacheTable()); @@ -545,6 +547,24 @@ public class TestUtils + /******************************************************************************* + ** Define the 'two key' table used in standard tests. + *******************************************************************************/ + public static QTableMetaData defineTableTwoKeys() + { + return new QTableMetaData() + .withName(TABLE_NAME_TWO_KEYS) + .withLabel("Two Keys") + .withBackendName(MEMORY_BACKEND_NAME) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withUniqueKey(new UniqueKey("key1", "key2")) + .withField(new QFieldMetaData("key1", QFieldType.INTEGER)) + .withField(new QFieldMetaData("key2", QFieldType.INTEGER)); + } + + + /******************************************************************************* ** Define the 'person' table used in standard tests. *******************************************************************************/ @@ -791,6 +811,26 @@ public class TestUtils + /******************************************************************************* + ** Define a table with unique key where one is nullable + *******************************************************************************/ + public static QTableMetaData defineTwoKeyTable() + { + return (new QTableMetaData() + .withName(TABLE_NAME_BASEPULL) + .withLabel("Basepull Test") + .withPrimaryKeyField("id") + .withBackendName(MEMORY_BACKEND_NAME) + .withFields(TestUtils.defineTablePerson().getFields())) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) + .withField(new QFieldMetaData(BASEPULL_KEY_FIELD_NAME, QFieldType.STRING).withBackendName("process_name").withIsRequired(true)) + .withField(new QFieldMetaData(BASEPULL_LAST_RUN_TIME_FIELD_NAME, QFieldType.DATE_TIME).withBackendName("last_run_time").withIsRequired(true)); + } + + + /******************************************************************************* ** Define a basepullTable *******************************************************************************/