diff --git a/README.md b/README.md
index e6d62213..dcd02484 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,12 @@ Create fast, scalable custom rollups driven by Custom Metadata in your Salesforc
### Package deployment options
-
+
-
+
diff --git a/extra-tests/classes/InvocableDrivenTests.cls b/extra-tests/classes/InvocableDrivenTests.cls
index dd0f12d5..0828050c 100644
--- a/extra-tests/classes/InvocableDrivenTests.cls
+++ b/extra-tests/classes/InvocableDrivenTests.cls
@@ -49,7 +49,7 @@ private class InvocableDrivenTests {
System.assertEquals(today.addDays(-2), reparentAccount.DateField__c);
System.assertEquals(3, reparentAccount.NumberOfEmployees, 'Second account should properly reflect reparented record for number of employees');
System.assertEquals(one.Description + ', ' + three.Description, reparentAccount.Description, 'Second account should have only reparented case description');
- System.assertEquals(one.Subject, reparentAccount.Name, 'Second account name field should reflect last subject');
System.assertEquals(2, reparentAccount.AnnualRevenue, 'Second account sum field should include updated amount');
+ System.assertEquals(one.Subject, reparentAccount.Name, 'Second account name field should reflect last subject');
}
}
diff --git a/package.json b/package.json
index 84f6bca8..8a9499bd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "apex-rollup",
- "version": "1.2.37.0",
+ "version": "1.2.38.0",
"description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"repository": {
"type": "git",
diff --git a/plugins/CustomObjectRollupLogger/README.md b/plugins/CustomObjectRollupLogger/README.md
index cef5f29f..19e01867 100644
--- a/plugins/CustomObjectRollupLogger/README.md
+++ b/plugins/CustomObjectRollupLogger/README.md
@@ -1,11 +1,11 @@
# Custom Object Rollup Logger
-
+
-
+
diff --git a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls
index eec0762a..00ba0ab5 100644
--- a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls
+++ b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls
@@ -1,13 +1,22 @@
public class RollupCustomObjectLogger extends RollupLogger {
private final List rollupLogEvents = new List();
+ private final Database.DMLOptions truncatedAllowedOptions;
+
+ public RollupCustomObjectLogger() {
+ super();
+ this.truncatedAllowedOptions = new Database.DMLOptions();
+ this.truncatedAllowedOptions.AllowFieldTruncation = true;
+ }
public override void log(String logString, LoggingLevel logLevel) {
- this.rollupLogEvents.add(new RollupLogEvent__e(
+ RollupLogEvent__e logEvent = new RollupLogEvent__e(
LoggingLevel__c = logLevel.name(),
LoggedBy__c = UserInfo.getUserId(),
Message__c = logString,
TransactionId__c = Request.getCurrent().getRequestId()
- ));
+ );
+ logEvent.setOptions(this.truncatedAllowedOptions);
+ this.rollupLogEvents.add(logEvent);
}
public override void log(String logString, Object logObject, LoggingLevel logLevel) {
diff --git a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls
index 4f79be3f..97f07d65 100644
--- a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls
+++ b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls
@@ -11,12 +11,13 @@ private class RollupCustomObjectLoggerTests {
Test.stopTest();
List rollupLogs = [
- SELECT Id, NumberOfLogEntries__c, TransactionId__c, (SELECT Message__c, LoggingLevel__c FROM RollupLogEntry__r)
+ SELECT Id, NumberOfLogEntries__c, TransactionId__c, ErrorWouldHaveBeenThrown__c, (SELECT Message__c, LoggingLevel__c FROM RollupLogEntry__r)
FROM RollupLog__c
];
System.assertEquals(1, rollupLogs.size(), 'Parent-level rollup log should have been created');
RollupLog__c firstEntry = rollupLogs[0];
System.assertNotEquals(null, firstEntry.TransactionId__c, 'Transaction Id should have been assigned');
+ System.assertEquals(true, firstEntry.ErrorWouldHaveBeenThrown__c, 'ERROR level log message was created, this field should be flagged');
// Rollup Log Entries
System.assertEquals(2, firstEntry.RollupLogEntry__r.size());
diff --git a/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls b/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls
index 31307e00..d07db4e5 100644
--- a/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls
+++ b/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls
@@ -3,7 +3,11 @@ public class RollupLogEventHandler {
Map transactionIdToLogs = new Map();
for (RollupLogEvent__e logEvent : logEvents) {
- RollupLog__c rollupLog = new RollupLog__c(TransactionId__c = logEvent.TransactionId__c, LoggedBy__c = Id.valueOf(logEvent.LoggedBy__c));
+ RollupLog__c rollupLog = new RollupLog__c(
+ ErrorWouldHaveBeenThrown__c = logEvent.LoggingLevel__c == LoggingLevel.ERROR.name(),
+ LoggedBy__c = Id.valueOf(logEvent.LoggedBy__c),
+ TransactionId__c = logEvent.TransactionId__c
+ );
transactionIdToLogs.put(logEvent.TransactionId__c, rollupLog);
}
diff --git a/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml b/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml
index 5fe5f6c8..fb5a6647 100644
--- a/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml
+++ b/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml
@@ -21,6 +21,10 @@
Readonly
NumberOfLogEntries__c
+
+ Edit
+ ErrorWouldHaveBeenThrown__c
+
@@ -44,6 +48,10 @@
Readonly
LastModifiedById
+
+ Edit
+ TransactionId__c
+
diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml
index 6ea57fd6..b76e02b9 100644
--- a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml
+++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml
@@ -161,7 +161,7 @@
AutoNumber
Rollup Logs
-
+
ReadWrite
Public
diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml
new file mode 100644
index 00000000..986f89e0
--- /dev/null
+++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ ErrorWouldHaveBeenThrown__c
+ false
+ If Rollup logs an otherwise fatal exception (with LoggingLevel.ERROR), this field is checked off
+ false
+ If Rollup logs an otherwise fatal exception (with LoggingLevel.ERROR), this field is checked off
+
+ false
+ false
+ Checkbox
+
diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml
index 3333dde2..a009b98a 100644
--- a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml
+++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml
@@ -1,4 +1,4 @@
-
+
TransactionId__c
false
@@ -10,4 +10,4 @@
false
Text
true
-
\ No newline at end of file
+
diff --git a/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml b/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml
index a22250eb..12829ac7 100644
--- a/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml
+++ b/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml
@@ -1,6 +1,11 @@
Grants permission to Rollup Log information
+
+ false
+ RollupLog__c.ErrorWouldHaveBeenThrown__c
+ true
+
false
RollupLogEntry__c.Message__c
diff --git a/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml b/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml
index 670484ff..9f5b75f8 100644
--- a/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml
+++ b/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml
@@ -1,5 +1,10 @@
+
+ false
+ RollupLog__c.ErrorWouldHaveBeenThrown__c
+ true
+
true
RollupLog__c.LoggedBy__c
diff --git a/rollup/core/classes/Rollup.cls b/rollup/core/classes/Rollup.cls
index a84a05b7..f0339e9e 100644
--- a/rollup/core/classes/Rollup.cls
+++ b/rollup/core/classes/Rollup.cls
@@ -18,31 +18,29 @@ global without sharing virtual class Rollup {
@testVisible
private static RollupControl__mdt specificControl;
@testVisible
- private static Integer maxQueryRowOverride;
- @testVisible
- private static final List CACHED_ROLLUPS = new List();
+ private static final List CACHED_ROLLUPS = new List();
+ private static final Set REPARENTED_KEYS = new Set();
private static Map> CACHED_APEX_OPERATIONS = new Map>();
private static Boolean isCDC = false;
private static Boolean isDeferralAllowed = true;
private static final String CONTROL_ORG_DEFAULTS = 'Org_Defaults';
- private static final Set ALWAYS_FULL_RECALC_OPS = new Set{
- Op.FIRST.name(),
- Op.LAST.name(),
- Op.AVERAGE.name(),
- Op.CONCAT_DISTINCT.name(),
- Op.COUNT_DISTINCT.name()
- };
+ protected final Set matchingCalcItemIds;
protected final RollupControl__mdt rollupControl;
protected final InvocationPoint invokePoint;
protected final Boolean isBatched;
protected final List rollups = new List();
// non-final instance variables
+ protected List calcItems;
+ protected Map oldCalcItems;
+ protected Rollup__mdt metadata;
protected Boolean isFullRecalc = false;
protected Boolean isNoOp;
protected Boolean isCDCUpdate = false;
+ protected RollupAsyncProcessor fullRecalcProcessor;
+ protected Integer queryCount;
/**
* receiving an interface/subclass from a property get/set (from the book "The Art Of Unit Testing") is an old technique;
@@ -122,7 +120,10 @@ global without sharing virtual class Rollup {
}
private class FilterResults {
- public List matchingItems { get; set; }
+ public FilterResults() {
+ this.matchingItemIds = new Set();
+ }
+ public Set matchingItemIds { get; private set; }
public Evaluator eval { get; set; }
}
@@ -164,9 +165,18 @@ global without sharing virtual class Rollup {
protected Rollup(InvocationPoint invokePoint) {
this.invokePoint = invokePoint;
this.rollupControl = getSingleControlOrDefault(RollupControl__mdt.DeveloperName, CONTROL_ORG_DEFAULTS, defaultControl);
+ this.isBatched = true;
+ // a batch only becomes valid if other Rollups are added to it
+ this.isNoOp = true;
+ }
+
+ protected Rollup(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) {
+ this(invokePoint);
+ this.calcItems = calcItems;
+ this.oldCalcItems = oldCalcItems;
}
- protected List getCachedRollups() {
+ protected List getCachedRollups() {
return CACHED_ROLLUPS;
}
@@ -182,7 +192,7 @@ global without sharing virtual class Rollup {
isDeferralAllowed = value;
}
- protected RollupAsyncProcessor getAsyncRollup(
+ protected Rollup getAsyncRollup(
List rollupOperations,
SObjectType sObjectType,
List calcItems,
@@ -201,6 +211,10 @@ global without sharing virtual class Rollup {
return 'Not implemented';
}
+ protected virtual String getHashedContents() {
+ return this.toString();
+ }
+
/**
* global facing RollupAsyncProcessor calculation section
* - Trigger operations
@@ -228,6 +242,7 @@ global without sharing virtual class Rollup {
public Set queryFields = new Set();
public List metadata = new List();
public String whereClause = '';
+ public Integer recordCount = RollupQueryBuilder.SENTINEL_COUNT_VALUE;
}
@AuraEnabled
@@ -257,12 +272,13 @@ global without sharing virtual class Rollup {
SObjectType childType = getSObjectFromName(matchingMeta.CalcItem__c).getSObjectType();
Set whereFields = getQueryFieldsFromMetadata(matchingMeta, RollupEvaluator.getWhereEval(matchingMeta.CalcItemWhereClause__c, childType));
+ RollupMetadata metaWrapper;
if (childToMetaWrapper.containsKey(childType)) {
- RollupMetadata metaWrapper = childToMetaWrapper.get(childType);
+ metaWrapper = childToMetaWrapper.get(childType);
metaWrapper.queryFields.addAll(whereFields);
metaWrapper.metadata.add(matchingMeta);
} else {
- RollupMetadata metaWrapper = new RollupMetadata();
+ metaWrapper = new RollupMetadata();
metaWrapper.queryFields = wherefields;
metaWrapper.metadata.add(matchingMeta);
metaWrapper.whereClause = potentialWhereClause;
@@ -275,16 +291,19 @@ global without sharing virtual class Rollup {
if (currentCount == RollupQueryBuilder.SENTINEL_COUNT_VALUE) {
amountOfCalcItems = currentCount;
} else {
+ metaWrapper.recordCount = currentCount;
amountOfCalcItems += currentCount;
}
}
}
- List processors = new List();
+ List processors = new List();
for (SObjectType childType : childToMetaWrapper.keySet()) {
RollupMetadata metaWrapper = childToMetaWrapper.get(childType);
String queryString = RollupQueryBuilder.Current.getQuery(childType, new List(metaWrapper.queryFields), 'Id', '!=', metaWrapper.whereClause);
- processors.add(buildFullRecalcRollup(metaWrapper.metadata, amountOfCalcItems, queryString, objIds, recordIds, childType, null, localInvokePoint));
+ Rollup fullRecalcRoll = buildFullRecalcRollup(metaWrapper.metadata, amountOfCalcItems, queryString, objIds, recordIds, childType, null, localInvokePoint);
+ fullRecalcRoll.queryCount = metaWrapper.recordCount;
+ processors.add(fullRecalcRoll);
}
return batch(processors);
@@ -416,7 +435,7 @@ global without sharing virtual class Rollup {
)
global static List performRollup(List flowInputs) {
List flowOutputs = new List();
- List localRollups = new List();
+ List localRollups = new List();
InvocationPoint fromInvocable = InvocationPoint.FROM_INVOCABLE;
for (FlowInput flowInput : flowInputs) {
@@ -466,22 +485,16 @@ global without sharing virtual class Rollup {
processCustomMetadata(localRollups, metas, flowInput.recordsToRollup, oldFlowRecords, new Set(), rollupContext, fromInvocable);
if (metas.isEmpty() == false) {
- RollupAsyncProcessor batchedRollup = getRollup(
- new List{ rollupMeta },
- sObjectType,
- flowInput.recordsToRollup,
- oldFlowRecords,
- null,
- fromInvocable
- );
+ Rollup batchedRollup = getRollup(new List{ rollupMeta }, sObjectType, flowInput.recordsToRollup, oldFlowRecords, null, fromInvocable);
+ batchedRollup.metadata = rollupMeta;
String logMessage = 'adding invocable rollup to list';
if (flowInput.deferProcessing) {
logMessage = 'deferring processing for rollup';
- CACHED_ROLLUPS.addAll(batchedRollup.rollups);
+ CACHED_ROLLUPS.add(batchedRollup);
} else {
- localRollups.addAll(batchedRollup.rollups);
+ localRollups.add(batchedRollup);
}
- RollupLogger.Instance.log(logMessage, batchedRollup.rollups, LoggingLevel.DEBUG);
+ RollupLogger.Instance.log(logMessage, batchedRollup, LoggingLevel.DEBUG);
}
}
@@ -497,7 +510,7 @@ global without sharing virtual class Rollup {
}
}
RollupLogger.Instance.save();
- RollupAsyncProcessor.flatten(CACHED_ROLLUPS);
+ flatten(CACHED_ROLLUPS);
return flowOutputs;
}
@@ -1102,34 +1115,6 @@ global without sharing virtual class Rollup {
);
}
- private static Rollup operateFromApex(
- SObjectField operationFieldOnCalcItem,
- SObjectField lookupFieldOnCalcItem,
- SObjectField lookupFieldOnOperationObject,
- SObjectField operationFieldOnOperationObject,
- SObjectType lookupSObjectType,
- Op rollupOperation,
- Object defaultRecalculationValue,
- String orderByFirstLast,
- Evaluator eval
- ) {
- Rollup__mdt meta = new Rollup__mdt(
- RollupFieldOnCalcItem__c = operationFieldOnCalcItem.getDescribe().getName(),
- LookupObject__c = String.valueOf(lookupSObjectType),
- LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName(),
- LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.getDescribe().getName(),
- RollupFieldOnLookupObject__c = operationFieldOnOperationObject.getDescribe().getName(),
- RollupOperation__c = rollupOperation.name(),
- OrderByFirstLast__c = orderByFirstLast
- );
- if (defaultRecalculationValue instanceof Decimal) {
- meta.FullRecalculationDefaultNumberValue__c = (Decimal) defaultRecalculationValue;
- } else if (defaultRecalculationValue instanceof String) {
- meta.FullRecalculationDefaultStringValue__c = (String) defaultRecalculationValue;
- }
- return runFromApex(new List{ meta }, eval, getTriggerRecords(), getOldTriggerRecordsMap());
- }
-
global static void runFromCDCTrigger() {
isCDC = true;
// CDC always uses Trigger.new
@@ -1219,9 +1204,9 @@ global without sharing virtual class Rollup {
}
global static Rollup runFromApex(List rollupMetadata, Evaluator eval, List calcItems, Map oldCalcItems) {
- Rollup batchRollup = new RollupAsyncProcessor(InvocationPoint.FROM_APEX);
+ Rollup rollupConductor = new RollupAsyncProcessor(InvocationPoint.FROM_APEX, calcItems, oldCalcItems);
if (shouldRunFromTrigger() == false) {
- return batchRollup;
+ return rollupConductor;
}
String rollupContext;
@@ -1240,7 +1225,7 @@ global without sharing virtual class Rollup {
rollupContext = '';
}
when AFTER_DELETE {
- reparentAndGetMergedRecordIds(calcItems, rollupMetadata, mergedParentIds, batchRollup.rollupControl);
+ reparentAndGetMergedRecordIds(calcItems, rollupMetadata, mergedParentIds, rollupConductor.rollupControl);
}
when else {
shouldReturn = true;
@@ -1248,7 +1233,7 @@ global without sharing virtual class Rollup {
}
if (shouldReturn) {
- return batchRollup;
+ return rollupConductor;
}
List localRollups = new List();
@@ -1258,18 +1243,18 @@ global without sharing virtual class Rollup {
populateCachedApexOperations(calcItemSObjectType, apexContext);
if (rollupMetadata.isEmpty() == false) {
- localRollups.addAll(getRollup(rollupMetadata, calcItemSObjectType, calcItems, oldCalcItems, eval, InvocationPoint.FROM_APEX).rollups);
+ localRollups.add(getRollup(rollupMetadata, calcItemSObjectType, calcItems, oldCalcItems, eval, InvocationPoint.FROM_APEX));
}
- flattenBatches(batchRollup, localRollups);
- return batchRollup;
+ flattenBatches(rollupConductor, localRollups);
+ return rollupConductor;
}
/** end global-facing section, begin public/private static helpers */
public static void processStoredFlowRollups() {
RollupLogger.Instance.log('processing deferred flow rollups', LoggingLevel.DEBUG);
- List rollupsToProcess = new List(CACHED_ROLLUPS);
+ List rollupsToProcess = new List(CACHED_ROLLUPS);
CACHED_ROLLUPS.clear();
batch(rollupsToProcess, InvocationPoint.FROM_INVOCABLE);
}
@@ -1353,6 +1338,103 @@ global without sharing virtual class Rollup {
return operationsWithUnderscores.contains(fullOpName) == false && fullOpName.contains('_') ? fullOpName.substringAfter('_') : fullOpName;
}
+ private static Rollup operateFromApex(
+ SObjectField operationFieldOnCalcItem,
+ SObjectField lookupFieldOnCalcItem,
+ SObjectField lookupFieldOnOperationObject,
+ SObjectField operationFieldOnOperationObject,
+ SObjectType lookupSObjectType,
+ Op rollupOperation,
+ Object defaultRecalculationValue,
+ String orderByFirstLast,
+ Evaluator eval
+ ) {
+ Rollup__mdt meta = new Rollup__mdt(
+ RollupFieldOnCalcItem__c = operationFieldOnCalcItem.getDescribe().getName(),
+ LookupObject__c = String.valueOf(lookupSObjectType),
+ LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName(),
+ LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.getDescribe().getName(),
+ RollupFieldOnLookupObject__c = operationFieldOnOperationObject.getDescribe().getName(),
+ RollupOperation__c = rollupOperation.name(),
+ OrderByFirstLast__c = orderByFirstLast
+ );
+ if (defaultRecalculationValue instanceof Decimal) {
+ meta.FullRecalculationDefaultNumberValue__c = (Decimal) defaultRecalculationValue;
+ } else if (defaultRecalculationValue instanceof String) {
+ meta.FullRecalculationDefaultStringValue__c = (String) defaultRecalculationValue;
+ }
+ return runFromApex(new List{ meta }, eval, getTriggerRecords(), getOldTriggerRecordsMap());
+ }
+
+ private static void flatten(List stackedRollups) {
+ Map rollupOperationToRollup = new Map();
+ Integer counter = 0;
+ Map> operationToProcessedRecords = new Map>();
+ for (Rollup stackedRollup : stackedRollups) {
+ // If the hashed contents aren't the same, we can't collapse
+ // the two rollup operations, and instead have to juggle the updated values for the parent in memory
+ String rollupKey = stackedRollup.getHashedContents();
+ Boolean shouldAddSameRollupOperation = false;
+
+ if (rollupOperationToRollup.containsKey(rollupKey)) {
+ Rollup matchingRollup = rollupOperationToRollup.get(rollupKey);
+ for (Integer index = stackedRollup.calcItems.size() - 1; index >= 0; index--) {
+ SObject calcItem = stackedRollup.calcItems[index];
+ if (matchingRollup.calcItems.contains(calcItem) == false && operationToProcessedRecords.containsKey(rollupKey) == false) {
+ doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index);
+ } else if (
+ matchingRollup.calcItems.contains(calcItem) == false &&
+ operationToProcessedRecords.containsKey(rollupKey) &&
+ operationToProcessedRecords.get(rollupKey).contains(calcItem.Id) == false
+ ) {
+ doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index);
+ }
+ }
+
+ if (stackedRollup.calcItems.isEmpty() == false) {
+ shouldAddSameRollupOperation = true;
+ }
+ } else {
+ rollupOperationToRollup.put(rollupKey, stackedRollup);
+ }
+ if (shouldAddSameRollupOperation) {
+ counter++;
+ rollupOperationToRollup.put(rollupKey + counter, stackedRollup);
+ }
+ }
+ stackedRollups.clear();
+ stackedRollups.addAll(rollupOperationToRollup.values());
+ }
+
+ private static void doBookkeepingOnCachedItems(
+ Rollup matchingRollup,
+ Rollup stackedRollup,
+ Map> operationToProcessedRecords,
+ SObject calcItem,
+ String rollupKey,
+ Integer index
+ ) {
+ List processedRecords = operationToProcessedRecords.containsKey(rollupKey)
+ ? operationToProcessedRecords.get(rollupKey)
+ : new List{ calcItem.Id };
+ operationToProcessedRecords.put(rollupKey, processedRecords);
+ Map idToCalcItem = new Map(matchingRollup.calcItems);
+ idToCalcItem.put(calcItem.Id, calcItem);
+ matchingRollup.calcItems.clear();
+ matchingRollup.calcItems.addAll(idToCalcItem.values());
+ stackedRollup.calcItems.remove(index);
+
+ if (stackedRollup.oldCalcItems.isEmpty() == false && stackedRollup.oldCalcItems.containsKey(calcItem.Id) == false) {
+ matchingRollup.oldCalcItems.put(calcItem.Id, stackedRollup.oldCalcItems.get(calcItem.Id));
+ }
+ for (Rollup innerRollup : matchingRollup.rollups) {
+ if (innerRollup.matchingCalcItemIds?.isEmpty() == false) {
+ innerRollup.matchingCalcItemIds.addAll(idToCalcItem.keySet());
+ innerRollup.matchingCalcItemIds.addAll(matchingRollup.oldCalcItems.keySet());
+ }
+ }
+ }
+
private static void processCustomMetadata(
List rollups,
List metas,
@@ -1456,12 +1538,15 @@ global without sharing virtual class Rollup {
) {
List typedCalcItems = calcItems.clone();
typedCalcItems.clear();
+ String potentialDeleteOpName = 'DELETE_' + getBaseOperationName(meta.RollupOperation__c);
for (SObject calcItem : calcItems) {
if (meta.IsRollupStartedFromParent__c == false && String.isBlank(meta.GrandparentRelationshipFieldPath__c) && oldCalcItems.containsKey(calcItem.Id)) {
SObject oldCalcItem = oldCalcItems.get(calcItem.Id);
- if (calcItem.get(meta.LookupFieldOnCalcItem__c) != oldCalcItem.get(meta.LookupFieldOnCalcItem__c)) {
+ String key = potentialDeleteOpName + oldCalcItem.Id + JSON.serialize(meta);
+ if (calcItem.get(meta.LookupFieldOnCalcItem__c) != oldCalcItem.get(meta.LookupFieldOnCalcItem__c) && REPARENTED_KEYS.contains(key) == false) {
typedCalcItems.add(oldCalcItem);
+ REPARENTED_KEYS.add(key);
}
}
}
@@ -1469,10 +1554,18 @@ global without sharing virtual class Rollup {
// we only need to perform the delete if we've arrived here
// via a route where there were oldCalcItems AND one or more of their lookup keys changed
if (typedCalcItems.isEmpty() == false) {
- String potentialDeleteOpName = 'DELETE_' + getBaseOperationName(meta.RollupOperation__c);
Rollup__mdt clonedMeta = meta.clone();
clonedMeta.RollupOperation__c = potentialDeleteOpName;
- rollups.add(getRollup(new List{ clonedMeta }, typedCalcItems.getSObjectType(), typedCalcItems, new Map(), null, invokePoint));
+ Rollup deleteProcessor = getRollup(
+ new List{ clonedMeta },
+ typedCalcItems.getSObjectType(),
+ typedCalcItems,
+ new Map(),
+ null,
+ invokePoint
+ );
+ RollupLogger.Instance.log('adding delete operation for:', deleteProcessor, LoggingLevel.DEBUG);
+ rollups.add(deleteProcessor);
}
}
@@ -1490,7 +1583,7 @@ global without sharing virtual class Rollup {
return '\nORDER BY ' + String.join(orderByFields, ',');
}
- private static RollupAsyncProcessor getFullRecalcRollup(Rollup__mdt meta, QueryWrapper queryWrapper, InvocationPoint invokePoint) {
+ private static Rollup getFullRecalcRollup(Rollup__mdt meta, QueryWrapper queryWrapper, InvocationPoint invokePoint) {
// just how many items are we talking, here? If it's less than the query limit, we can proceed
// otherwise, kick off a batch to fetch the calc items and then chain into the regular code path
SObjectType childType = getSObjectFromName(meta.CalcItem__c).getSObjectType();
@@ -1511,10 +1604,12 @@ global without sharing virtual class Rollup {
queryFields.addAll(RollupEvaluator.getWhereEval(meta.CalcItemWhereClause__c, childType).getQueryFields());
String queryString = RollupQueryBuilder.Current.getQuery(childType, new List(queryFields), 'Id', '!=', queryWrapper.getQuery());
- return buildFullRecalcRollup(new List{ meta }, amountOfCalcItems, queryString, objIds, recordIds, childType, whereEval, invokePoint);
+ Rollup fullRecalc = buildFullRecalcRollup(new List{ meta }, amountOfCalcItems, queryString, objIds, recordIds, childType, whereEval, invokePoint);
+ fullRecalc.queryCount = amountOfCalcItems;
+ return fullRecalc;
}
- private static RollupAsyncProcessor buildFullRecalcRollup(
+ private static Rollup buildFullRecalcRollup(
List matchingMeta,
Integer amountOfCalcItems,
String queryString,
@@ -1527,13 +1622,13 @@ global without sharing virtual class Rollup {
for (Rollup__mdt meta : matchingMeta) {
meta.IsFullRecordSet__c = true;
}
- RollupAsyncProcessor instance = new RollupAsyncProcessor(invokePoint);
+ Rollup instance = new Rollup(invokePoint);
Boolean shouldQueue =
amountOfCalcItems != RollupQueryBuilder.SENTINEL_COUNT_VALUE &&
amountOfCalcItems < instance.rollupControl.MaxLookupRowsBeforeBatching__c;
if (shouldQueue) {
List calculationItems = Database.query(queryString);
- RollupAsyncProcessor thisRollup = getRollup(matchingMeta, calcItemType, calculationItems, new Map(calculationItems), eval, invokePoint);
+ Rollup thisRollup = getRollup(matchingMeta, calcItemType, calculationItems, new Map(calculationItems), eval, invokePoint);
thisRollup.isFullRecalc = true;
return thisRollup;
} else {
@@ -1543,21 +1638,30 @@ global without sharing virtual class Rollup {
}
private static String batch(List rollups, InvocationPoint invokePoint) {
- Rollup batchRollup = new RollupAsyncProcessor(invokePoint);
- flattenBatches(batchRollup, rollups);
- return batchRollup.runCalc();
+ if (rollups.isEmpty()) {
+ return new Rollup(invokePoint).runCalc();
+ }
+ Rollup batchedRollup = new RollupAsyncProcessor(invokePoint, rollups[0].calcItems, rollups[0].oldCalcItems);
+ flattenBatches(batchedRollup, rollups);
+ return batchedRollup.runCalc();
}
private static void flattenBatches(Rollup outerRollup, List rollups) {
for (Rollup rollup : rollups) {
if (rollup.rollups.isEmpty() == false) {
for (Rollup innerRoll : rollup.rollups) {
+ if (rollup.calcItems?.isEmpty() == false) {
+ innerRoll.calcItems = rollup.calcItems;
+ }
+ if (rollup.oldCalcItems?.isEmpty() == false) {
+ innerRoll.oldCalcItems = rollup.oldCalcItems;
+ }
innerRoll.isFullRecalc = rollup.isFullRecalc;
}
// recurse through lists until there aren't any more nested rollups
flattenBatches(outerRollup, rollup.rollups);
} else {
- loadRollups(rollup, outerRollup);
+ loadRollups((RollupAsyncProcessor) rollup, outerRollup);
}
}
}
@@ -1584,9 +1688,7 @@ global without sharing virtual class Rollup {
if (String.isNotBlank(errorMessage)) {
Exception ex = new IllegalArgumentException(errorMessage);
- RollupLogger.Instance.log('an error occurred while validating flow inputs', ex, LoggingLevel.ERROR);
- RollupLogger.Instance.save();
- throw ex;
+ logAndThrowFlowException(ex, 'an error occurred while validating flow inputs');
}
}
@@ -1609,12 +1711,16 @@ global without sharing virtual class Rollup {
(flowInput.isRollupStartedFromParent ? firstRecord : lookupItem).get(flowInput.lookupFieldOnOpObject);
}
} catch (Exception ex) {
- RollupLogger.Instance.log('an error occurred while validating flow-specific rules', ex, LoggingLevel.ERROR);
- RollupLogger.Instance.save();
- throw ex;
+ logAndThrowFlowException(ex, 'an error occurred while validating flow-specific rules');
}
}
+ private static void logAndThrowFlowException(Exception ex, String logString) {
+ RollupLogger.Instance.log(logString, ex, LoggingLevel.ERROR);
+ RollupLogger.Instance.save();
+ throw ex;
+ }
+
private static QueryWrapper getIntermediateGrandparentQueryWrapper(String grandparentFieldPath, List calcItems, Map oldCalcItems) {
if (isCDC) {
return new QueryWrapper();
@@ -1825,7 +1931,7 @@ global without sharing virtual class Rollup {
return null;
}
- private static RollupAsyncProcessor getRollup(
+ private static Rollup getRollup(
List rollupOperations,
SObjectType sObjectType,
List calcItems,
@@ -1833,8 +1939,9 @@ global without sharing virtual class Rollup {
Evaluator eval,
InvocationPoint rollupInvokePoint
) {
+ Rollup rollupConductor = new RollupAsyncProcessor(rollupInvokePoint, calcItems, oldCalcItems);
if (rollupOperations.isEmpty() || calcItems.isEmpty()) {
- return new RollupAsyncProcessor(rollupInvokePoint);
+ return rollupConductor;
}
if (sObjectType == null) {
sObjectType = calcItems[0].getSObjectType();
@@ -1843,7 +1950,6 @@ global without sharing virtual class Rollup {
* We have rollup operations to perform. That's great!
* Let's get ready to rollup!
*/
- RollupAsyncProcessor batchRollup = new RollupAsyncProcessor(rollupInvokePoint);
DescribeSObjectResult describeForSObject = sObjectType.getDescribe();
Map fieldNameToField = describeForSObject.fields.getMap();
@@ -1865,7 +1971,7 @@ global without sharing virtual class Rollup {
rollupMetadata.LookupFieldOnLookupObject__c = lookupFieldOnOpObject.getDescribe().getName();
rollupMetadata.RollupFieldOnLookupObject__c = rollupFieldOnOpObject.getDescribe().getName();
- if (rollupMetadata.IsFullRecordSet__c != true && ALWAYS_FULL_RECALC_OPS.contains(getBaseOperationName(rollupMetadata.RollupOperation__c))) {
+ if (rollupMetadata.IsFullRecordSet__c != true && isFullRecalcOp(getBaseOperationName(rollupMetadata.RollupOperation__c))) {
rollupMetadata.IsFullRecordSet__c = true;
}
@@ -1889,16 +1995,15 @@ global without sharing virtual class Rollup {
lookupSObjectType,
sObjectType, // calc item SObjectType
rollupOp,
- filterResults.matchingItems,
- oldCalcItems,
- batchRollup,
+ filterResults.matchingItemIds,
+ rollupConductor,
filterResults.eval,
localControl,
rollupInvokePoint,
rollupMetadata
);
}
- return batchRollup;
+ return rollupConductor;
}
private static SObjectField getSObjectFieldByName(DescribeSObjectResult objectDescribe, String desiredField) {
@@ -1913,6 +2018,14 @@ global without sharing virtual class Rollup {
return null;
}
+ private static Boolean isFullRecalcOp(String baseOperationName) {
+ return baseOperationName == Op.FIRST.name() ||
+ baseOperationName == Op.LAST.name() ||
+ baseOperationName == Op.AVERAGE.name() ||
+ baseOperationName == Op.CONCAT_DISTINCT.name() ||
+ baseOperationName == Op.COUNT_DISTINCT.name();
+ }
+
private static String getRollupControlKey(
InvocationPoint invokePoint,
SObjectField rollupFieldOnCalcItem,
@@ -2052,16 +2165,15 @@ global without sharing virtual class Rollup {
SObjectType lookupSObjectType,
SObjectType calcItemSObjectType,
Op rollupOp,
- List calcItems,
- Map oldCalcItems,
- Rollup batchRollup,
+ Set matchingCalcItemIds,
+ Rollup rollupConductor,
Evaluator eval,
RollupControl__mdt rollupControl,
InvocationPoint invokePoint,
Rollup__mdt rollupMetadata
) {
- Rollup rollup = RollupAsyncProcessor.getProcessor(
- calcItems,
+ RollupAsyncProcessor processor = RollupAsyncProcessor.getProcessor(
+ matchingCalcItemIds,
rollupFieldOnCalcItem,
lookupFieldOnCalcItem,
lookupFieldOnOpObject,
@@ -2069,23 +2181,19 @@ global without sharing virtual class Rollup {
lookupSObjectType,
calcItemSObjectType,
rollupOp,
- oldCalcItems,
eval,
invokePoint,
rollupControl,
rollupMetadata
);
- return loadRollups(rollup, batchRollup);
+ return loadRollups(processor, rollupConductor);
}
- private static Rollup loadRollups(Rollup rollup, Rollup batchRollup) {
- RollupAsyncProcessor processor = (RollupAsyncProcessor) rollup;
- if (batchRollup != null && rollup != null && rollup.isNoOp == false) {
- batchRollup.rollups.add(processor);
- } else if (rollup != null && rollup.isNoOp == false) {
- rollup.rollups.add(processor);
+ private static Rollup loadRollups(RollupAsyncProcessor processor, Rollup rollupConductor) {
+ if (rollupConductor != null && processor != null && processor.isNoOp == false) {
+ rollupConductor.rollups.add(processor);
}
- return batchRollup != null ? batchRollup : rollup;
+ return rollupConductor;
}
private static RollupControl__mdt getSingleControlOrDefault(SObjectField whereField, Object whereValue, RollupControl__mdt testOverrideData) {
@@ -2138,14 +2246,7 @@ global without sharing virtual class Rollup {
private static FilterResults filter(List calcItems, Map oldCalcItems, Evaluator eval, Rollup__mdt metadata, SObjectType calcItemType) {
FilterResults results = new FilterResults();
- List matchingItems = calcItems == null ? new List() : calcItems.clone();
- results.matchingItems = matchingItems;
- if (matchingItems.isEmpty()) {
- return results;
- }
- matchingItems.clear(); // retains the strong-typing on the list for downstream calls to List.getSbjectType()
calcItems = replaceCalcItemsForPolymorphicWhereClause(calcItems, metadata);
-
results.eval = RollupEvaluator.getEvaluator(eval, metadata, oldCalcItems, calcItemType);
// while we iterate through calcItems, the only possible mutation to that array is through "replaceCalcItemsForPolymorphicWhereClause"
@@ -2156,13 +2257,12 @@ global without sharing virtual class Rollup {
// if the where clause would exclude something, but we're in an update
// and the old value wouldn't have been excluded, pass it on through
// to be handled further downstream
- SObject potentialOldItem = oldCalcItems?.isEmpty() == false && oldCalcItems.containsKey(item.Id) ? oldCalcItems.get(item.Id) : item;
- if (results.eval.matches(item) || results.eval.matches(potentialOldItem)) {
- matchingItems.add(item);
- // metadata shouldn't be null, but it's good to check; unfortunately, if(null) throws so we
- // have to do this EXTRA explicit check
+ if (results.eval.matches(item)) {
+ results.matchingItemIds.add(item.Id);
+ } else if (oldCalcItems != null && oldCalcItems.containsKey(item.Id) && results.eval.matches(oldCalcItems.get(item.Id))) {
+ results.matchingItemIds.add(item.Id);
} else if (metadata?.IsFullRecordSet__c == true) {
- matchingItems.add(item);
+ results.matchingItemIds.add(item.Id);
}
}
return results;
diff --git a/rollup/core/classes/RollupAsyncProcessor.cls b/rollup/core/classes/RollupAsyncProcessor.cls
index e57f97a9..8a9cb83a 100644
--- a/rollup/core/classes/RollupAsyncProcessor.cls
+++ b/rollup/core/classes/RollupAsyncProcessor.cls
@@ -10,16 +10,12 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
private final List deferredRollups = new List();
private final List syncRollups = new List();
- protected final List calcItems;
- protected final Map oldCalcItems;
- protected final Rollup__mdt metadata;
protected final SObjectType calcItemType;
private RollupRelationshipFieldFinder.Traversal traversal;
private Map> lookupObjectToUniqueFieldNames;
private Map> calcObjectToUniqueFieldNames;
private List lookupItems;
- private RollupAsyncProcessor fullRecalcProcessor;
private Map> cachedFullRecalcs;
private static final RollupSettings__c SETTINGS = RollupSettings__c.getInstance();
@@ -39,48 +35,17 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
}
}
- public static void flatten(List stackedRollups) {
- Map rollupOperationToRollup = new Map();
- Integer counter = 0;
- Map> operationToProcessedRecords = new Map>();
- for (RollupAsyncProcessor stackedRollup : stackedRollups) {
- // If the hashed contents are the same, we can't collapse
- // the two rollup operations, and instead have to juggle the updated values for the parent in memory
- String rollupKey = stackedRollup.getHashedContents();
- Boolean shouldAddSameRollupOperation = false;
-
- if (rollupOperationToRollup.containsKey(rollupKey)) {
- RollupAsyncProcessor matchingRollup = rollupOperationToRollup.get(rollupKey);
- for (Integer index = stackedRollup.calcItems.size() - 1; index >= 0; index--) {
- SObject calcItem = stackedRollup.calcItems[index];
- if (matchingRollup.calcItems.contains(calcItem) == false && operationToProcessedRecords.containsKey(rollupKey) == false) {
- doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index);
- } else if (
- matchingRollup.calcItems.contains(calcItem) == false &&
- operationToProcessedRecords.containsKey(rollupKey) &&
- operationToProcessedRecords.get(rollupKey).contains(calcItem.Id) == false
- ) {
- doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index);
- }
- }
-
- if (stackedRollup.calcItems.isEmpty() == false) {
- shouldAddSameRollupOperation = true;
- }
- } else {
- rollupOperationToRollup.put(rollupKey, stackedRollup);
- }
- if (shouldAddSameRollupOperation) {
- counter++;
- rollupOperationToRollup.put(rollupKey + counter, stackedRollup);
- }
+ private class CalcItemData {
+ public CalcItemData(List items, Map oldItems) {
+ this.items = items.clone();
+ this.oldItems = oldItems.clone();
}
- stackedRollups.clear();
- stackedRollups.addAll(rollupOperationToRollup.values());
+ public final List items;
+ public final Map oldItems;
}
public static RollupAsyncProcessor getProcessor(
- List calcItems,
+ Set matchingCalcItemIds,
SObjectField opFieldOnCalcItem,
SObjectField lookupFieldOnCalcItem,
SObjectField lookupFieldOnLookupObject,
@@ -88,14 +53,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
SObjectType lookupObj,
SObjectType calcItem,
Op operation,
- Map oldCalcItems,
Evaluator eval,
InvocationPoint rollupInvokePoint,
RollupControl__mdt rollupControl,
Rollup__mdt metadata
) {
return new QueueableProcessor(
- calcItems,
+ matchingCalcItemIds,
opFieldOnCalcItem,
lookupFieldOnCalcItem,
lookupFieldOnLookupObject,
@@ -103,7 +67,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
lookupObj,
calcItem,
operation,
- oldCalcItems,
eval,
rollupInvokePoint,
rollupControl,
@@ -113,41 +76,23 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
public RollupAsyncProcessor(InvocationPoint invokePoint) {
super(invokePoint);
- this.isBatched = true;
- // a batch only becomes valid if other Rollups are added to it
- this.isNoOp = true;
}
- public RollupAsyncProcessor(RollupAsyncProcessor innerRollup, Op op, List calcItems) {
- this(
- calcItems,
- innerRollup.opFieldOnCalcItem,
- innerRollup.lookupFieldOnCalcItem,
- innerRollup.lookupFieldOnLookupObject,
- innerRollup.opFieldOnLookupObject,
- innerRollup.lookupObj,
- innerRollup.calcItemType,
- op,
- innerRollup.oldCalcItems,
- null, // eval gets assigned below
- innerRollup.invokePoint,
- innerRollup.rollupControl,
- innerRollup.metadata
- );
+ public RollupAsyncProcessor(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) {
+ super(invokePoint, calcItems, oldCalcItems);
+ }
+
+ public RollupAsyncProcessor(RollupAsyncProcessor innerRollup) {
+ super(innerRollup.invokePoint, innerRollup.calcItems, innerRollup.oldCalcItems);
this.rollups.addAll(innerRollup.rollups);
this.isNoOp = this.rollups.isEmpty() && innerRollup.metadata?.IsFullRecordSet__c == false;
this.isFullRecalc = innerRollup.isFullRecalc;
this.isCDCUpdate = innerRollup.isCDCUpdate;
- this.eval = innerRollup.eval;
- }
-
- public RollupAsyncProcessor(RollupAsyncProcessor innerRollup) {
- this(innerRollup, innerRollup.op, innerRollup.calcItems);
}
public RollupAsyncProcessor(
- List calcItems,
+ Set matchingCalcItemIds,
SObjectField opFieldOnCalcItem,
SObjectField lookupFieldOnCalcItem,
SObjectField lookupFieldOnLookupObject,
@@ -155,14 +100,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
SObjectType lookupObj,
SObjectType calcItemType,
Op op,
- Map oldCalcItems,
Evaluator eval,
InvocationPoint invokePoint,
RollupControl__mdt rollupControl,
Rollup__mdt rollupMetadata
) {
super();
- this.calcItems = calcItems;
+ this.matchingCalcItemIds = matchingCalcItemIds;
this.opFieldOnCalcItem = opFieldOnCalcItem;
this.lookupFieldOnCalcItem = lookupFieldOnCalcItem;
this.lookupFieldOnLookupObject = lookupFieldOnLookupObject;
@@ -170,7 +114,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
this.lookupObj = lookupObj;
this.calcItemType = calcItemType;
this.op = op;
- this.oldCalcItems = oldCalcItems;
this.invokePoint = invokePoint;
this.rollupControl = rollupControl;
this.metadata = rollupMetadata;
@@ -180,7 +123,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
this.eval = eval;
}
- this.isNoOp = this.calcItems?.isEmpty() == true && this.metadata?.IsFullRecordSet__c == false;
+ this.isNoOp = this.matchingCalcItemIds?.isEmpty() == true && this.metadata?.IsFullRecordSet__c == false;
}
public Integer compareTo(Object otherRollup) {
@@ -211,14 +154,17 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
public override String toString() {
Map props = new Map{
+ 'Type' => this.getTypeName(),
'Invocation Point' => this.invokePoint.name(),
- 'Calc Items' => JSON.serializePretty(this.calcItems),
- 'Old Calc Items' => JSON.serializePretty(this.oldCalcItems),
- 'Rollup Metadata' => JSON.serializePretty(this.metadata),
- 'Rollup Control' => JSON.serializePretty(this.rollupControl),
'Is Full Recalc' => String.valueOf(this.isFullRecalc),
'Is No Op' => String.valueOf(this.isNoOp)
};
+
+ this.addToMap(props, 'Calc Items', this.calcItems);
+ this.addToMap(props, 'Old Calc Items', this.oldCalcItems);
+ this.addToMap(props, 'Rollup Metadata', this.metadata);
+ this.addToMap(props, 'Rollup Control', this.rollupControl);
+
String baseString = '';
for (String key : props.keySet()) {
baseString += key + ': ' + props.get(key) + '\n';
@@ -300,6 +246,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
return rollupProcessId;
}
+ protected virtual String getTypeName() {
+ return 'RollupAsyncProcessor';
+ }
+
protected RollupAsyncProcessor getAsyncRollup() {
// swap off on which async process is running to achieve infinite scaling
isRunningAsync = true;
@@ -323,7 +273,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
roll = this;
} else {
// the end of the line
- this.throwWithRollupData(this.rollups);
+ this.logFatalRollups(this.rollups);
}
return roll;
@@ -331,7 +281,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
private class QueueableProcessor extends RollupAsyncProcessor implements System.Queueable {
private QueueableProcessor(
- List calcItems,
+ Set matchingCalcItemIds,
SObjectField opFieldOnCalcItem,
SObjectField lookupFieldOnCalcItem,
SObjectField lookupFieldOnLookupObject,
@@ -339,14 +289,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
SObjectType lookupObj,
SObjectType calcItem,
Op operation,
- Map oldCalcItems,
Evaluator eval,
InvocationPoint rollupInvokePoint,
RollupControl__mdt rollupControl,
Rollup__mdt metadata
) {
super(
- calcItems,
+ matchingCalcItemIds,
opFieldOnCalcItem,
lookupFieldOnCalcItem,
lookupFieldOnLookupObject,
@@ -354,7 +303,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
lookupObj,
calcItem,
operation,
- oldCalcItems,
eval,
rollupInvokePoint,
rollupControl,
@@ -370,6 +318,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
super(roll);
}
+ protected override String getTypeName() {
+ return 'QueueableProcessor';
+ }
+
protected override String beginAsyncRollup() {
RollupLogger.Instance.log('about to queue', LoggingLevel.DEBUG);
RollupLogger.Instance.save();
@@ -426,7 +378,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
protected void processDelegatedFullRecalcRollup(List rollupInfo, List calcItems, Map oldCalcItems) {
isRunningAsync = true; // the first rollup can immediately start rolling up, instead of dispatching to a queueable / another batchable
- RollupAsyncProcessor roll = this.getAsyncRollup(rollupInfo, this.calcItemType, calcItems, oldCalcItems, null, this.invokePoint);
+ Rollup roll = this.getAsyncRollup(rollupInfo, this.calcItemType, calcItems, oldCalcItems, null, this.invokePoint);
roll.isFullRecalc = true;
roll.fullRecalcProcessor = this;
roll.runCalc();
@@ -491,9 +443,12 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
Map updatedLookupRecords = new Map();
Map grandparentRollups = new Map();
for (RollupAsyncProcessor roll : rollups) {
+ CalcItemData data = new CalcItemData(this.calcItems, this.oldCalcItems);
+ this.setupCalcItemData(roll);
RollupLogger.Instance.log('starting rollup for: ', roll, LoggingLevel.DEBUG);
// for each iteration, ensure we're not operating beyond the bounds of our query limits
if (hasExceededCurrentRollupLimits(roll.rollupControl) || roll instanceof RollupFullBatchRecalculator) {
+ RollupLogger.Instance.log('Deferring current rollup, past limits', LoggingLevel.DEBUG);
this.deferredRollups.add(roll);
continue;
}
@@ -519,6 +474,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
for (SObject updatedRecord : updatedParentRecords) {
updatedLookupRecords.put(updatedRecord.Id, updatedRecord);
}
+ this.resetCalcItemData(data);
}
this.splitUpdates(updatedLookupRecords);
@@ -528,15 +484,21 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
this.processDeferredRollups();
}
- private String getHashedContents() {
+ protected override String getHashedContents() {
// the only thing that necessarily makes a rollup unique is the sum total of the metadata behind it
// as well as the calc items driving that calculation.
- // you could have multiple rollups with different calc item where clauses all rolling up to the same field
+ // you could have multiple rollups with different Calc Item Where Clauses all rolling up to the same field
// even worse - in situations where multiple DML operations are enqueued in the same transaction
// the same calc items by Id might differ slightly by field value.
return String.valueOf(this.metadata);
}
+ private void addToMap(Map props, String key, Object ref) {
+ if (ref != null) {
+ props.put(key, JSON.serializePretty(ref));
+ }
+ }
+
private void handleMultipleDMLRollupsEnqueuedInTheSameTransaction(List rolls) {
// if items are inserted, updated, deleted (etc ...)
// all in the same transaction, they can be introduced out of order
@@ -612,15 +574,35 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
this.getAsyncRollup().beginAsyncRollup();
} else if (this.deferredRollups.isEmpty() == false) {
- this.throwWithRollupData(this.deferredRollups);
+ this.logFatalRollups(this.deferredRollups);
}
}
}
- private void throwWithRollupData(List rolls) {
+ private void logFatalRollups(List rolls) {
String exceptionString = 'rollup failed to re-queue for: ';
RollupLogger.Instance.log(exceptionString, rolls, LoggingLevel.ERROR);
- throw new AsyncException(exceptionString + rolls);
+ RollupLogger.Instance.save();
+ }
+
+ private void setupCalcItemData(Rollup roll) {
+ // if a rollup has calc items set, we take their calcItem dependencies as the source of truth
+ if (roll.calcItems?.isEmpty() == false) {
+ this.calcItems = roll.calcItems;
+ this.oldCalcItems = roll.oldCalcItems != null ? roll.oldCalcItems : new Map();
+ } else if (this.calcItems?.isEmpty() == false) {
+ roll.calcItems = this.calcItems;
+ roll.oldCalcItems = this.oldCalcItems;
+ }
+ }
+
+ private void resetCalcItemData(CalcItemData data) {
+ if (data.items?.isEmpty() == false) {
+ this.calcItems = data.items;
+ }
+ if (data.oldItems?.isEmpty() == false) {
+ this.oldCalcItems = data.oldItems;
+ }
}
private void getFieldNamesForRollups(List rollups) {
@@ -660,15 +642,18 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
rollup.metadata,
uniqueQueryFieldNames,
rollup.lookupObj,
- rollup.oldCalcItems
+ this.oldCalcItems
)
- .getParents(rollup.calcItems);
+ .getParents(this.calcItems);
} else if (rollup.traversal?.getIsFinished() == false) {
rollup.traversal.recommence();
}
return rollup.traversal.getIsFinished() ? rollup.traversal.getParentLookupToRecords() : lookupFieldToCalcItems;
}
- for (SObject calcItem : rollup.calcItems) {
+ for (SObject calcItem : this.calcItems) {
+ if (rollup.matchingCalcItemIds.contains(calcItem.Id) == false) {
+ continue;
+ }
String key = (String) calcItem.get(rollup.lookupFieldOnCalcItem);
if (lookupFieldToCalcItems.containsKey(key) == false) {
lookupFieldToCalcItems.put(key, new Rollup.CalcItemBag(new List{ calcItem }));
@@ -678,7 +663,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
// if the lookup key differs from what it was on the old calc item,
// include that value as well so that we can fix reparented records' rollup values
- SObject potentialOldCalcItem = rollup.oldCalcItems?.get(calcItem.Id);
+ SObject potentialOldCalcItem = this.oldCalcItems?.get(calcItem.Id);
if (potentialOldCalcItem != null) {
String oldKey = (String) potentialOldCalcItem.get(rollup.lookupFieldOnCalcItem);
@@ -771,6 +756,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
SObjectType targetType;
Map> queryCountsToLookupIds = new Map>();
+ Integer totalCountOfRecords = 0;
for (RollupAsyncProcessor roll : this.rollups) {
if (targetType == null) {
targetType = roll.lookupObj;
@@ -778,22 +764,28 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
hasMoreThanOneTarget = true;
}
+ if (roll.queryCount != null) {
+ totalCountOfRecords += roll.queryCount;
+ continue;
+ }
+
+ if (hasMoreThanOneTarget) {
+ break;
+ }
+
if (String.isNotBlank(roll.metadata?.GrandparentRelationshipFieldPath__c)) {
// getting the count for grandparent (or greater) relationships will be handled further
// downstream; for our purposes, it isn't useful to try to get all of the records while
// we're still in a sync context
continue;
- } else if (roll.calcItems?.isEmpty() != false) {
+ } else if (roll.calcItems?.isEmpty() == true) {
continue;
}
- if (hasMoreThanOneTarget) {
- break;
- }
-
Set uniqueIds = new Set();
- for (SObject calcItem : roll.calcItems) {
+ List items = roll.calcItems?.isEmpty() == false ? roll.calcItems : this.calcItems;
+ for (SObject calcItem : items) {
String lookupKey = (String) calcItem.get(roll.lookupFieldOnCalcItem);
if (String.isNotBlank(lookupKey)) {
uniqueIds.add(lookupKey);
@@ -813,7 +805,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
}
}
- Integer totalCountOfRecords = 0;
if (hasMoreThanOneTarget == false) {
for (String countQuery : queryCountsToLookupIds.keySet()) {
Set objIds = queryCountsToLookupIds.get(countQuery);
@@ -836,7 +827,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
) {
Map recordsToUpdate = new Map();
Map> oldLookupItems = new Map>();
- Set unprocessedCalcItems = new Set();
+ Set unprocessedCalcItems = new Set();
RollupSObjectUpdater updater = new RollupSObjectUpdater(rollup.opFieldOnLookupObject);
if (this.fullRecalcProcessor != null) {
rollup.fullRecalcProcessor = this.fullRecalcProcessor;
@@ -860,7 +851,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
localCalcItems.addAll(bag.additional);
if (hasExceededCurrentRollupLimits(this.rollupControl)) {
- unprocessedCalcItems.addAll(localCalcItems);
+ unprocessedCalcItems = new Map(localCalcItems).keySet();
lookupItems.remove(index);
continue;
}
@@ -885,19 +876,21 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
return recordsToUpdate.values();
}
- private void deferCalculationsWhenApproachingLimits(RollupAsyncProcessor roll, Set unprocessedCalcItems) {
+ private void deferCalculationsWhenApproachingLimits(RollupAsyncProcessor roll, Set unprocessedCalcItems) {
// remove the calc items that were successfully processed -
// they're the ones that aren't in the unprocessed Set
- for (Integer index = roll.calcItems.size() - 1; index >= 0; index--) {
- SObject calcItem = roll.calcItems[index];
- if (unprocessedCalcItems.contains(calcItem) == false) {
- roll.calcItems.remove(index);
+ List mutableItems = this.calcItems.clone();
+ for (Integer index = mutableItems.size() - 1; index >= 0; index--) {
+ SObject calcItem = mutableItems[index];
+ if (unprocessedCalcItems.contains(calcItem.Id) == false) {
+ mutableItems.remove(index);
}
}
// if all of the calc items have been processed, we're golden - no need to proceed
// otherwise, the newly trimmed-down Rollup will get picked up downstream for
// reprocessing!
- if (roll.calcItems.isEmpty() == false) {
+ if (mutableItems.isEmpty() == false) {
+ roll.calcItems = mutableItems;
this.deferredRollups.add(roll);
}
}
@@ -908,8 +901,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
if (
roll.metadata?.IsFullRecordSet__c == true &&
(roll.eval.matches(calcItem) == false &&
- (roll.oldCalcItems.containsKey(calcItem.Id) == false ||
- roll.eval.matches(roll.oldCalcItems.get(calcItem.Id)) == false))
+ (this.oldCalcItems.containsKey(calcItem.Id) == false ||
+ roll.eval.matches(this.oldCalcItems.get(calcItem.Id)) == false))
) {
// technically it should only be possible for a calc item that doesn't match
// to still exist if it is a Full Record Set operation; this gives people the chance
@@ -918,7 +911,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
continue;
}
// Check for reparented records
- SObject oldCalcItem = roll.oldCalcItems.get(calcItem.Id);
+ SObject oldCalcItem = this.oldCalcItems?.get(calcItem.Id);
if (oldCalcItem == null) {
continue;
@@ -959,13 +952,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
}
private SObject reassignOldCalcItemIfValueChanged(String lookupId, SObject oldCalcItem, RollupAsyncProcessor rollup) {
- if (String.isBlank(lookupId)) {
+ if (String.isBlank(lookupId) || this.oldCalcItems == null) {
return oldCalcItem;
}
// truly terrible, but before we pass the old item through the reparenting code path, we need to validate that it's only
// the lookup field that has changed; otherwise, if the opFieldOnCalcItem has changed too, substitute the item whose value
// previously corresponded to the parent record
- for (SObject otherOldCalcItem : rollup.oldCalcItems.values()) {
+ for (SObject otherOldCalcItem : this.oldCalcItems.values()) {
if (otherOldCalcItem.get(rollup.lookupFieldOnCalcItem) == lookupId) {
if (otherOldCalcItem.get(rollup.opFieldOnCalcItem) != oldCalcItem.get(rollup.opFieldOnCalcItem)) {
return otherOldCalcItem;
@@ -988,7 +981,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
);
rollupCalc.setEvaluator(roll.eval);
rollupCalc.setCDCUpdate(this.isCDCUpdate);
- rollupCalc.performRollup(calcItems, roll.oldCalcItems);
+ rollupCalc.performRollup(calcItems, this.oldCalcItems);
return rollupCalc.getReturnValue();
}
@@ -1011,7 +1004,23 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
String currentOp = getBaseOperationName(roll.op.name());
String deleteOpName = 'DELETE_' + currentOp;
Op deleteOp = opNameToOp.get(deleteOpName);
- RollupAsyncProcessor oldLookupsRollup = new RollupAsyncProcessor(roll, deleteOp, reparentedCalcItems);
+
+ // by default this returns a "batched" (set) of rollups; we
+ // just want the first (and only) inner rollup to perform the pseudo-delete
+ RollupAsyncProcessor oldLookupsRollup = getProcessor(
+ new Set(),
+ roll.opFieldOnCalcItem,
+ roll.lookupFieldOnCalcItem,
+ roll.lookupFieldOnLookupObject,
+ roll.opFieldOnLookupObject,
+ roll.lookupObj,
+ roll.calcItemType,
+ deleteOp,
+ null,
+ this.invokePoint,
+ this.rollupControl,
+ roll.metadata
+ );
RollupLogger.Instance.log('reparenting operation: ', oldLookupsRollup, LoggingLevel.DEBUG);
RollupLogger.Instance.log('Reparented item prior to reparenting rollup: ', lookupRecord, LoggingLevel.DEBUG);
@@ -1027,27 +1036,4 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
}
}
}
-
- private static void doBookkeepingOnCachedItems(
- RollupAsyncProcessor matchingRollup,
- RollupAsyncProcessor stackedRollup,
- Map> operationToProcessedRecords,
- SObject calcItem,
- String rollupKey,
- Integer index
- ) {
- List processedRecords = operationToProcessedRecords.containsKey(rollupKey)
- ? operationToProcessedRecords.get(rollupKey)
- : new List{ calcItem.Id };
- operationToProcessedRecords.put(rollupKey, processedRecords);
- Map idToCalcItem = new Map(matchingRollup.calcItems);
- idToCalcItem.put(calcItem.Id, calcItem);
- matchingRollup.calcItems.clear();
- matchingRollup.calcItems.addAll(idToCalcItem.values());
- stackedRollup.calcItems.remove(index);
-
- if (stackedRollup.oldCalcItems.isEmpty() == false && stackedRollup.oldCalcItems.containsKey(calcItem.Id) == false) {
- matchingRollup.oldCalcItems.put(calcItem.Id, stackedRollup.oldCalcItems.get(calcItem.Id));
- }
- }
}
diff --git a/rollup/core/classes/RollupFullBatchRecalculator.cls b/rollup/core/classes/RollupFullBatchRecalculator.cls
index b7b28c4e..e8c5dc8e 100644
--- a/rollup/core/classes/RollupFullBatchRecalculator.cls
+++ b/rollup/core/classes/RollupFullBatchRecalculator.cls
@@ -28,6 +28,7 @@ public class RollupFullBatchRecalculator extends RollupAsyncProcessor implements
}
public override void execute(Database.BatchableContext bc, List calcItems) {
+ RollupLogger.Instance.log('starting full batch recalc run', this, LoggingLevel.DEBUG);
/**
* this batch class is a glorified "for loop" for the calc items, dispatching
* them to the overall Rollup framework while breaking us out of the query limits
@@ -44,6 +45,10 @@ public class RollupFullBatchRecalculator extends RollupAsyncProcessor implements
RollupLogger.Instance.save();
}
+ protected override String getTypeName() {
+ return 'RollupFullBatchRecalculator';
+ }
+
protected override void retrieveAdditionalCalcItems(Map lookupToCalcItems, RollupAsyncProcessor rollup) {
Map local = new Map();
for (String lookupKey : lookupToCalcItems.keySet()) {
diff --git a/rollup/core/classes/RollupLogger.cls b/rollup/core/classes/RollupLogger.cls
index 51239b93..0a767995 100644
--- a/rollup/core/classes/RollupLogger.cls
+++ b/rollup/core/classes/RollupLogger.cls
@@ -86,6 +86,7 @@ public virtual class RollupLogger extends Rollup implements ILogger {
try {
loggerInstance = (ILogger) Type.forName(SELF.rollupControl.RollupLoggerName__c).newInstance();
} catch (Exception ex) {
+ SELF.log('cast to Rollup.ILogger failed with message: ' + ex.getMessage() + ', falling back to default logger', SELF, LoggingLevel.WARN);
loggerInstance = SELF;
}
} else {
diff --git a/rollup/core/classes/RollupQueryBuilder.cls b/rollup/core/classes/RollupQueryBuilder.cls
index e5d5c5bb..bcc78f88 100644
--- a/rollup/core/classes/RollupQueryBuilder.cls
+++ b/rollup/core/classes/RollupQueryBuilder.cls
@@ -113,7 +113,7 @@ public without sharing class RollupQueryBuilder {
optionalWhereClause = optionalWhereClause.replace(whereClause, '').trim();
}
} catch (Exception ex) {
- RollupLogger.Instance.log('exception occurred while building query: ', ex, LoggingLevel.ERROR);
+ RollupLogger.Instance.log('exception occurred while building query: ', ex, LoggingLevel.WARN);
}
return optionalWhereClause;
}
diff --git a/rollup/tests/RollupStandardIntegrationTests.cls b/rollup/tests/RollupStandardIntegrationTests.cls
index 9055637d..0d080bc8 100644
--- a/rollup/tests/RollupStandardIntegrationTests.cls
+++ b/rollup/tests/RollupStandardIntegrationTests.cls
@@ -2,9 +2,10 @@
private class RollupStandardIntegrationTests {
@TestSetup
static void setup() {
+ upsert new RollupSettings__c(IsEnabled__c = true);
+ // gets nulled out at the end of the setup context
Rollup.defaultControl = new RollupControl__mdt(ShouldAbortRun__c = true);
insert new Account(Name = 'RollupStandardIntegrationTests');
- upsert new RollupSettings__c(IsEnabled__c = true);
}
@isTest
@@ -99,7 +100,8 @@ private class RollupStandardIntegrationTests {
@isTest
static void shouldNotFailForTruncatedTextFields() {
Account acc = [SELECT Id FROM Account];
- Contact con = new Contact(AccountId = acc.Id, Description = '0'.repeat(256), LastName = 'Truncate', Email = 'rollup@gmail.com');
+ Integer maxAccountNameLength = Account.Name.getDescribe().getLength();
+ Contact con = new Contact(AccountId = acc.Id, Description = '0'.repeat(maxAccountNameLength + 1), LastName = 'Truncate', Email = 'rollup@gmail.com');
insert con;
Rollup__mdt meta = new Rollup__mdt(
@@ -117,7 +119,7 @@ private class RollupStandardIntegrationTests {
Test.stopTest();
acc = [SELECT Name FROM Account];
- System.assertEquals(255, acc.Name.length(), acc.Name);
+ System.assertEquals(maxAccountNameLength, acc.Name.length(), acc.Name);
}
@isTest
@@ -652,13 +654,9 @@ private class RollupStandardIntegrationTests {
update cpas;
Test.startTest();
- List flowOutputs = Rollup.performRollup(flowInputs);
+ Rollup.performRollup(flowInputs);
Test.stopTest();
- System.assertEquals(1, flowOutputs.size(), 'Flow outputs were not provided');
- System.assertEquals('SUCCESS', flowOutputs[0].message);
- System.assertEquals(true, flowOutputs[0].isSuccess);
-
acc = [SELECT Id, AnnualRevenue FROM Account WHERE Id = :acc.Id];
System.assertEquals(1500, acc.AnnualRevenue, 'SUM REFRESH from flow should fully recalc');
reparentedAccount = [SELECT Id, AnnualRevenue FROM Account WHERE Id = :reparentedAccount.Id];
diff --git a/rollup/tests/RollupTests.cls b/rollup/tests/RollupTests.cls
index 5f76df3e..49d54814 100644
--- a/rollup/tests/RollupTests.cls
+++ b/rollup/tests/RollupTests.cls
@@ -3386,13 +3386,11 @@ private class RollupTests {
/** Re-queueing */
@isTest
static void shouldRequeueRollupsWhenQueryLimitsExceeded() {
- DMLMock mock = loadAccountIdMock(new List{ new ContactPointAddress(Id = RollupTestUtils.createId(ContactPointAddress.SObjectType), PreferenceRank = 1) });
- Rollup.apexContext = TriggerOperation.AFTER_INSERT;
- Rollup.defaultControl = new RollupControl__mdt(
- MaxRollupRetries__c = 1,
- IsRollupLoggingEnabled__c = true,
- MaxNumberOfQueries__c = 1
+ DMLMock mock = loadAccountIdMock(
+ new List{ new ContactPointAddress(Id = RollupTestUtils.createId(ContactPointAddress.SObjectType), PreferenceRank = 1) }
);
+ Rollup.apexContext = TriggerOperation.AFTER_INSERT;
+ Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 1, IsRollupLoggingEnabled__c = true, MaxNumberOfQueries__c = 1);
Rollup.specificControl = new RollupControl__mdt(
ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.SYNCHRONOUS,
MaxLookupRowsBeforeBatching__c = 1,
@@ -3410,29 +3408,6 @@ private class RollupTests {
System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'Queueable' LIMIT 1]?.Status, [SELECT Status, JobType FROM AsyncApexJob]);
}
- @isTest
- static void shouldThrowIfDeferralNotPossible() {
- DMLMock mock = loadAccountIdMock(new List{ new ContactPointAddress(PreferenceRank = 1) });
- Rollup.apexContext = TriggerOperation.AFTER_INSERT;
- Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 0, IsRollupLoggingEnabled__c = true);
- Rollup.specificControl = new RollupControl__mdt(
- ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.SYNCHRONOUS,
- MaxLookupRowsBeforeBatching__c = -1,
- BatchChunkSize__c = 0
- );
-
- try {
- Test.startTest();
- Rollup.countFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc();
- Test.stopTest();
- // a throw should occur within startTest()/stopTest() - the assertion below is uncatchable
- System.assert(false, 'shouldThrowIfDeferralNotPossible should not make it here');
- } catch (Exception ex) {
- System.assertEquals(true, ex.getMessage().contains('rollup failed to re-queue for'), ex.getMessage());
- System.assertNotEquals(true, ex.getMessage().contains('shouldThrowIfDeferralNotPossible should not make it here'));
- }
- }
-
/** Grandparent rollups */
@isTest
static void shouldAllowGrandparentRollups() {
@@ -3521,6 +3496,11 @@ private class RollupTests {
Rollup.DML = mock;
Rollup.shouldRun = true;
Rollup.records = [SELECT Id, AboutMe FROM User WHERE Id = :acc.OwnerId];
+ Rollup.defaultControl = new RollupControl__mdt(
+ ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.QUEUEABLE,
+ BatchChunkSize__c = 100,
+ IsRollupLoggingEnabled__c = true
+ );
Rollup.rollupMetadata = new List{
new Rollup__mdt(
CalcItem__c = 'ContactPointAddress',
diff --git a/scripts/build-and-promote-package.ps1 b/scripts/build-and-promote-package.ps1
index c34967e0..4ae8e807 100644
--- a/scripts/build-and-promote-package.ps1
+++ b/scripts/build-and-promote-package.ps1
@@ -59,7 +59,7 @@ if(Test-Path ".\PACKAGING_SFDX_URL.txt") {
}
$sfdxProjectJson = Get-SFDX-Project-JSON
-$currentPackageVersion = $sfdxProjectJson.packageDirectories.versionNumber
+$currentPackageVersion = $sfdxProjectJson.packageDirectories[0].versionNumber
Write-Output "Current package version number: $currentPackageVersion"
@@ -98,8 +98,7 @@ if($currentBranch -eq "main") {
# main is a push-protected branch; only create new package versions as part of PRs against main
Write-Output "Creating new package version"
- $packageVersionNotes = $sfdxProjectJson.packageDirectories.versionDescription
- sfdx force:package:version:create -d $sfdxProjectJson.packageDirectories.path -x -w 30 -e $packageVersionNotes -c --releasenotesurl $sfdxProjectJson.packageDirectories.releaseNotesUrl
+ sfdx force:package:version:create -d $sfdxProjectJson.packageDirectories[0].path -x -w 30
git add ./sfdx-project.json
# Now that sfdx-project.json has been updated, grab the latest package version
@@ -109,7 +108,10 @@ if($currentBranch -eq "main") {
if($currentPackageVersionId -ne $priorPackageVersionId) {
$readmePath = "./README.md"
- ((Get-Content -path $readmePath -Raw) -replace $priorPackageVersionId, $currentPackageVersionId) | Set-Content -Path $readmePath -NoNewline
+ $loginReplacement = "https://login.salesforce.com/packaging/installPackage.apexp?p0=" + $currentPackageVersionId
+ $testReplacement = "https://test.salesforce.com/packaging/installPackage.apexp?p0=" + $currentPackageVersionId
+ ((Get-Content -path $readmePath -Raw) -replace "https:\/\/login.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}", $loginReplacement) | Set-Content -Path $readmePath -NoNewline
+ ((Get-Content -path $readmePath -Raw) -replace "https:\/\/test.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}", $testReplacement) | Set-Content -Path $readmePath -NoNewline
git add $readmePath
}
diff --git a/scripts/deploy-sfdx-project.json b/scripts/deploy-sfdx-project.json
index 4af37e70..165e69de 100644
--- a/scripts/deploy-sfdx-project.json
+++ b/scripts/deploy-sfdx-project.json
@@ -8,5 +8,5 @@
],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
- "sourceApiVersion": "51.0"
+ "sourceApiVersion": "52.0"
}
diff --git a/sfdx-project.json b/sfdx-project.json
index dcb76ba9..fb666050 100644
--- a/sfdx-project.json
+++ b/sfdx-project.json
@@ -4,20 +4,23 @@
"default": true,
"package": "apex-rollup",
"path": "rollup",
- "versionNumber": "1.2.37.0",
- "versionDescription": "Fixing deferred rollup logic and adding more logging surrounding deferrals",
- "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest"
+ "versionNumber": "1.2.38.0",
+ "versionDescription": "Including extra code coverage during package version creation, REFRESH updates",
+ "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest",
+ "unpackagedMetadata": {
+ "path": "extra-tests"
+ }
},
{
"package": "Apex Rollup - Custom Logger",
"path": "plugins/CustomObjectRollupLogger",
"dependencies": [
{
- "package": "apex-rollup@1.2.37-0"
+ "package": "apex-rollup@1.2.38-0"
}
],
- "versionNumber": "0.0.2.0",
- "versionDescription": "Introducing basic logging plugin",
+ "versionNumber": "0.0.3.0",
+ "versionDescription": "Prevent string overflow on log messages, added ErrorWouldHaveBeenThrown__c field on RollupLog__c",
"default": false
},
{
@@ -25,7 +28,7 @@
"path": "plugins/NebulaLogger",
"dependencies": [
{
- "package": "apex-rollup@1.2.37-0"
+ "package": "apex-rollup@1.2.38-0"
},
{
"package": "Nebula Logger - Unlocked Package@4.5.2-0-plugin-framework-enhancements"
@@ -36,7 +39,8 @@
"default": false
},
{
- "path": "extra-tests"
+ "path": "extra-tests",
+ "default": false
}
],
"namespace": "",
@@ -47,6 +51,7 @@
"Apex Rollup - Custom Logger": "0Ho6g000000Gn8ZCAS",
"Apex Rollup - Custom Logger@0.0.1-0": "04t6g000008SgtwAAC",
"Apex Rollup - Custom Logger@0.0.2-0": "04t6g000008SgufAAC",
+ "Apex Rollup - Custom Logger@0.0.3-0": "04t6g000008SgyNAAS",
"Apex Rollup - Nebula Logger": "0Ho6g000000Gn8PCAS",
"Apex Rollup - Nebula Logger@0.0.1-0": "04t6g000008SgolAAC",
"Nebula Logger - Unlocked Package@4.5.2-0-plugin-framework-enhancements": "04t5Y0000027FNaQAM",
@@ -81,6 +86,7 @@
"apex-rollup@1.2.34-0": "04t6g000008SglrAAC",
"apex-rollup@1.2.35-0": "04t6g000008SguGAAS",
"apex-rollup@1.2.36-0": "04t6g000008SguaAAC",
- "apex-rollup@1.2.37-0": "04t6g000008SguzAAC"
+ "apex-rollup@1.2.37-0": "04t6g000008SguzAAC",
+ "apex-rollup@1.2.38-0": "04t6g000008SgySAAS"
}
}
\ No newline at end of file