diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelper.java
index ac0acdcb..5762480e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelper.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelper.java
@@ -171,7 +171,7 @@ public class UpdateActionRecordSplitHelper
/*******************************************************************************
- ** Getter for haveAnyWithoutErorrs
+ ** Getter for haveAnyWithoutErrors
**
*******************************************************************************/
public boolean getHaveAnyWithoutErrors()
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactoryTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactoryTest.java
new file mode 100644
index 00000000..cc660cfe
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactoryTest.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.processes;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import com.kingsrook.qqq.backend.core.BaseTest;
+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 org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for QProcessCallbackFactory
+ *******************************************************************************/
+class QProcessCallbackFactoryTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ QProcessCallback qProcessCallback = QProcessCallbackFactory.forFilter(new QQueryFilter(new QFilterCriteria("foo", QCriteriaOperator.EQUALS, "bar")));
+
+ QQueryFilter queryFilter = qProcessCallback.getQueryFilter();
+ assertEquals(1, queryFilter.getCriteria().size());
+ assertEquals("foo", queryFilter.getCriteria().get(0).getFieldName());
+ assertEquals(QCriteriaOperator.EQUALS, queryFilter.getCriteria().get(0).getOperator());
+ assertEquals("bar", queryFilter.getCriteria().get(0).getValues().get(0));
+
+ assertEquals(Collections.emptyMap(), qProcessCallback.getFieldValues(new ArrayList<>()));
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java
new file mode 100644
index 00000000..7d3a55d0
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/UpdateActionRecordSplitHelperTest.java
@@ -0,0 +1,147 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.helpers;
+
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+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.model.statusmessages.SystemErrorStatusMessage;
+import com.kingsrook.qqq.backend.core.utils.ListingHash;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for UpdateActionRecordSplitHelper
+ *******************************************************************************/
+class UpdateActionRecordSplitHelperTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ String tableName = getClass().getSimpleName();
+ QContext.getQInstance().addTable(new QTableMetaData()
+ .withName(tableName)
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("A", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("B", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)));
+
+ UpdateInput updateInput = new UpdateInput(tableName)
+ .withRecord(new QRecord().withValue("id", 1).withValue("A", 1))
+ .withRecord(new QRecord().withValue("id", 2).withValue("A", 2))
+ .withRecord(new QRecord().withValue("id", 3).withValue("B", 3))
+ .withRecord(new QRecord().withValue("id", 4).withValue("B", 3))
+ .withRecord(new QRecord().withValue("id", 5).withValue("B", 3))
+ .withRecord(new QRecord().withValue("id", 6).withValue("A", 4).withValue("B", 5));
+ UpdateActionRecordSplitHelper updateActionRecordSplitHelper = new UpdateActionRecordSplitHelper();
+ updateActionRecordSplitHelper.init(updateInput);
+ ListingHash, QRecord> recordsByFieldBeingUpdated = updateActionRecordSplitHelper.getRecordsByFieldBeingUpdated();
+
+ Function, Set> extractIds = (records) ->
+ records.stream().map(r -> r.getValueInteger("id")).collect(Collectors.toSet());
+
+ ////////////////////////////////////////
+ // validate that modify dates got set //
+ ////////////////////////////////////////
+ updateInput.getRecords().forEach(r ->
+ assertThat(r.getValue("modifyDate")).isInstanceOf(Instant.class));
+
+ //////////////////////////////////////////////////////////////
+ // validate the grouping of records by fields-being-updated //
+ //////////////////////////////////////////////////////////////
+ assertEquals(3, recordsByFieldBeingUpdated.size());
+ assertEquals(Set.of(1, 2), extractIds.apply(recordsByFieldBeingUpdated.get(List.of("A", "modifyDate"))));
+ assertEquals(Set.of(3, 4, 5), extractIds.apply(recordsByFieldBeingUpdated.get(List.of("B", "modifyDate"))));
+ assertEquals(Set.of(6), extractIds.apply(recordsByFieldBeingUpdated.get(List.of("A", "B", "modifyDate"))));
+
+ ///////////////////////////////////////////////////////////////////
+ // validate the output records were built, in the order expected //
+ ///////////////////////////////////////////////////////////////////
+ List outputRecords = updateActionRecordSplitHelper.getOutputRecords();
+ for(int i = 0; i < outputRecords.size(); i++)
+ {
+ assertEquals(i + 1, outputRecords.get(i).getValueInteger("id"));
+ }
+
+ /////////////////////////////////////////////////////
+ // test the areAllValuesBeingUpdatedTheSame method //
+ /////////////////////////////////////////////////////
+ Function, Boolean> runAreAllValuesBeingUpdatedTheSame = (fields) ->
+ UpdateActionRecordSplitHelper.areAllValuesBeingUpdatedTheSame(updateInput, recordsByFieldBeingUpdated.get(fields), fields);
+
+ assertFalse(runAreAllValuesBeingUpdatedTheSame.apply(List.of("A", "modifyDate")));
+ assertTrue(runAreAllValuesBeingUpdatedTheSame.apply(List.of("B", "modifyDate")));
+ assertTrue(runAreAllValuesBeingUpdatedTheSame.apply(List.of("A", "B", "modifyDate")));
+
+ ////////////////////////////////////////////////////////////////////
+ // make sure that the override of the logic for this method works //
+ ////////////////////////////////////////////////////////////////////
+ updateInput.setAreAllValuesBeingUpdatedTheSame(true);
+ assertTrue(runAreAllValuesBeingUpdatedTheSame.apply(List.of("A", "modifyDate")));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordsWithErrors()
+ {
+ String tableName = getClass().getSimpleName() + "WithErrors";
+ QContext.getQInstance().addTable(new QTableMetaData()
+ .withName(tableName)
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("A", QFieldType.INTEGER)));
+
+ {
+ UpdateInput updateInput = new UpdateInput(tableName)
+ .withRecord(new QRecord().withValue("id", 1).withValue("A", 1).withError(new SystemErrorStatusMessage("error")))
+ .withRecord(new QRecord().withValue("id", 2).withValue("A", 2).withError(new SystemErrorStatusMessage("error")))
+ .withRecord(new QRecord().withValue("id", 2).withValue("A", 3).withError(new SystemErrorStatusMessage("error")));
+ UpdateActionRecordSplitHelper updateActionRecordSplitHelper = new UpdateActionRecordSplitHelper();
+ updateActionRecordSplitHelper.init(updateInput);
+ assertFalse(updateActionRecordSplitHelper.getHaveAnyWithoutErrors());
+ }
+
+ }
+
+}
\ No newline at end of file