Skip to content

Commit

Permalink
Further progress on #46 - start of work to wire RollupRelationshipFie…
Browse files Browse the repository at this point in the history
…ldFinder to larger Rollup framework
  • Loading branch information
jamessimone committed Mar 16, 2021
1 parent c184d86 commit 5f018e0
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 23 deletions.
3 changes: 1 addition & 2 deletions extra-tests/classes/RollupIntegrationTests.cls
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@isTest
private class RollupIntegrationTests {
// "Integration," in the sense that these include custom fields that shouldn't be installed
// we still don't need to actually update the records to prove the point
// "Integration," in the sense that these include custom fields / objects that shouldn't be installed
@TestSetup
static void setup() {
Rollup.defaultControl = new RollupControl__mdt(ShouldAbortRun__c = true);
Expand Down
58 changes: 37 additions & 21 deletions rollup/main/default/classes/RollupRelationshipFieldFinder.cls
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/**
* Big caveat here - this class can only be used with lookup relationships. The larger Rollup framework
* accepts and even welcomes text-based keys being used, but here we accept that text-based keys
* are likely niche, anyway, and that people looking to make use of a grandchild -> grandparent (or greater!)
* rollup are likely operating using lookups anyway
*/
public without sharing class RollupRelationshipFieldFinder {
private final List<String> relationshipParts;
private final Traversal traverseObject;
private final Traversal traversal;
private final SObjectType ultimateParent;
private final String optionalWhereClause;
private final RollupControl__mdt rollupControl;
Expand All @@ -9,11 +15,17 @@ public without sharing class RollupRelationshipFieldFinder {
private Boolean isFirstRun = true;

public RollupRelationshipFieldFinder(RollupControl__mdt rollupControl, String relationshipPathName, SObjectType ultimateParent, Map<Id, SObject> oldRecords) {
this.traverseObject = new Traversal(this);
this.traversal = new Traversal(this);
this.relationshipParts = relationshipPathName.split('\\.');
this.rollupControl = rollupControl;
this.ultimateParent = ultimateParent;
this.oldRecords = oldRecords;

if(this.relationshipParts.size() == 1) {
String fieldName = this.relationshipParts[0];
this.relationshipParts.set(0, ultimateParent.getDescribe().getName());
this.relationshipParts.add(fieldName);
}
}

public RollupRelationshipFieldFinder(
Expand All @@ -28,7 +40,7 @@ public without sharing class RollupRelationshipFieldFinder {
}

public class Traversal {
public Boolean isFinished = true;
public Boolean isFinished = false;
private final Map<Id, SObject> lookupIdToFinalRecords = new Map<Id, SObject>();
private Map<Id, Id> lookupIdMap = new Map<Id, Id>();
private final Map<Id, List<Id>> hierarchy = new Map<Id, List<Id>>();
Expand All @@ -41,6 +53,7 @@ public without sharing class RollupRelationshipFieldFinder {
public SObject retrieveParent(Id descendantId) {
return lookupIdToFinalRecords.get(descendantId);
}

public Boolean isUltimatelyReparented(SObject record, String relationshipFieldName) {
Id currentLookupId = (Id) record.get(relationshipFieldName);
Id oldLookupId = (Id) (this.finder.oldRecords.containsKey(record.Id)
Expand All @@ -63,28 +76,29 @@ public without sharing class RollupRelationshipFieldFinder {
}
}

// TODO handle heap size
public Traversal getParents(List<SObject> records) {
if (records.isEmpty() || this.relationshipParts.isEmpty()) {
this.traverseObject.isFinished = true;
return this.traverseObject;
} else if (this.rollupControl.MaxQueryRows__c < Limits.getQueries() || Limits.getLimitQueryRows() / 4 < Limits.getQueryRows()) {
return this.traverseObject;
this.traversal.isFinished = true;
return this.traversal;
} else if (
this.rollupControl.MaxQueryRows__c < Limits.getQueries() ||
Limits.getLimitQueryRows() / 4 < Limits.getQueryRows() ||
Limits.getLimitHeapSize() / 2 < Limits.getHeapSize()
) {
return this.traversal;
}
this.traverseObject.isFinished = false;

// even before the recursion begins, the List won't be strongly typed
SObjectType baseSObjectType = records[0].getSObjectType();
if (baseSObjectType == this.ultimateParent) {
this.prepFinishedObject(records);
return this.traverseObject;
return this.traversal;
} else {
return this.recurseThroughObjectChain(records, baseSObjectType);
}
}

private SObjectField getField(Map<String, SObjectField> fieldMap, String relationshipPart) {
// this sucks, but we need to find the field with the matching relationship name
for (String key : fieldMap.keySet()) {
SObjectField field = fieldMap.get(key);
if (field.getDescribe().getRelationshipName() == relationshipPart) {
Expand All @@ -98,22 +112,23 @@ public without sharing class RollupRelationshipFieldFinder {
}

private Id getDescendantId(Id lookupId) {
Boolean hasMatch = this.traverseObject.lookupIdMap.containsKey(lookupId);
Boolean hasMatch = this.traversal.lookupIdMap.containsKey(lookupId);
if (hasMatch) {
return this.getDescendantId(this.traverseObject.lookupIdMap.get(lookupId));
return this.getDescendantId(this.traversal.lookupIdMap.get(lookupId));
}
return lookupId;
}

private void prepFinishedObject(List<SObject> records) {
this.traverseObject.isFinished = true;
this.traversal.isFinished = true;
for (SObject record : records) {
Id descendantId = this.getDescendantId(record.Id);
if (descendantId != record.Id) {
this.traverseObject.lookupIdToFinalRecords.put(descendantId, record);
this.traversal.lookupIdToFinalRecords.put(descendantId, record);
}
}
this.traverseObject.lookupIdMap = null; // try to spare the heap
this.traversal.isFinished = true;
this.traversal.lookupIdMap = null; // try to spare the heap
}

private Traversal recurseThroughObjectChain(List<SObject> records, SObjectType baseSObjectType) {
Expand All @@ -131,18 +146,18 @@ public without sharing class RollupRelationshipFieldFinder {
if (String.isNotBlank(lookupId)) {
lookupIds.add(lookupId);

this.traverseObject.lookupIdMap.put(lookupId, record.Id);
this.traversal.lookupIdMap.put(lookupId, record.Id);
if (this.isFirstRun) {
// we need to keep track of potentially reparented lookups to aid with the note below
if (this.oldRecords.containsKey(record.Id)) {
Id oldLookupId = (Id) this.oldRecords.get(record.Id).get(field);
if (String.isNotBlank(oldLookupId) && oldLookupId != lookupId) {
lookupIds.add(oldLookupId);
this.traverseObject.hierarchy.put(oldLookupId, new List<Id>{ oldLookupId });
this.traversal.hierarchy.put(oldLookupId, new List<Id>{ oldLookupId });
}
}
this.traverseObject.hierarchy.put(lookupId, new List<Id>{ lookupId });
} else if (this.traverseObject.hierarchy.containsKey(record.Id)) {
this.traversal.hierarchy.put(lookupId, new List<Id>{ lookupId });
} else if (this.traversal.hierarchy.containsKey(record.Id)) {
// track the hierarchy of objects to help in determining whether or not something
// has ultimately been reparented
// for example:
Expand All @@ -151,7 +166,7 @@ public without sharing class RollupRelationshipFieldFinder {
// this would "traditionally" be a reparenting situation, but if we are skipping
// the intermediate objects for a rollup and the end result is the same, we need
// to avoid reporting false positives like this one
this.traverseObject.hierarchy.get(record.Id).add(lookupId);
this.traversal.hierarchy.get(record.Id).add(lookupId);
}
}
}
Expand All @@ -160,6 +175,7 @@ public without sharing class RollupRelationshipFieldFinder {
Schema.DescribeSObjectResult nextSObjectDescribe = firstId.getSObjectType().getDescribe();
SObjectField nextFieldToken = this.getField(nextSObjectDescribe.fields.getMap(), nextFieldToLookup);
// NB - we only support one route through polymorphic fields such as Task.WhoId and Task.WhatId for this sort of thing
// TODO need to check to see if we need to add in additional fields in the SELECT statement
String query = 'SELECT Id, ' + nextFieldToken.getDescribe().getName() + ' FROM ' + nextSObjectDescribe.getName() + ' WHERE Id = :lookupIds';
if (this.isFirstRun && String.isNotBlank(this.optionalWhereClause)) {
query += '\nAND ' + this.optionalWhereClause;
Expand Down
35 changes: 35 additions & 0 deletions rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ private class RollupRelationshipFieldFinderTests {
RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List<Opportunity>{ opp });

System.assertEquals(parent, traversal.retrieveParent(opp.Id));

finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, new Map<Id, SObject>());
traversal = finder.getParents(new List<Opportunity>{ opp });

System.assertEquals(parent, traversal.retrieveParent(opp.Id));
}

@isTest
Expand Down Expand Up @@ -89,6 +94,17 @@ private class RollupRelationshipFieldFinderTests {
Account intermediateTwo = new Account(Name = 'Intermediate 2');
insert new List<Account>{ intermediateOne, intermediateTwo };

List<Account> updatedAccounts = [SELECT Id, OwnerId FROM Account];
if(updatedAccounts.size() == 2) {
// don't run the rest of the test if the org has some kind of ownership assignment going on that would invalidate
// the results
Account one = updatedAccounts[0];
Account two = updatedAccounts[1];
if(one.OwnerId != two.OwnerId) {
return;
}
}

Opportunity opp = new Opportunity(AccountId = intermediateTwo.Id, Name = 'Child reparented', StageName = 'Prospecting', CloseDate = System.today());
List<Opportunity> opps = new List<Opportunity>{ opp };
insert opps;
Expand Down Expand Up @@ -129,4 +145,23 @@ private class RollupRelationshipFieldFinderTests {
finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps);
System.assertEquals(true, traversal.isUltimatelyReparented(opp, 'AccountId'), 'Should correctly report reparenting if new lookup is null');
}

@isTest
static void shouldReportReparentingCorrectlyForImmediateParent() {
Account parentOne = new Account(Name = 'Parent1');
Account parentTwo = new Account(Name = 'Parent2');
insert new List<Account>{ parentOne, parentTwo };

Opportunity oppOne = new Opportunity(AccountId = parentOne.Id, Name = 'Child1', StageName = 'Prospecting', CloseDate = System.today());
Opportunity oppTwo = new Opportunity(AccountId = parentOne.Id, Name = 'Child1', StageName = 'Prospecting', CloseDate = System.today());
List<Opportunity> opps = new List<Opportunity>{ oppOne, oppTwo };
insert opps;

Map<Id, SObject> oldOpps = new Map<Id, SObject>{ oppOne.Id => oppOne, oppTwo.Id => new Opportunity(AccountId = parentTwo.Id) };
RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, oldOpps);
RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps);

System.assertEquals(true, traversal.isUltimatelyReparented(oppTwo, 'AccountId'));
System.assertEquals(false, traversal.isUltimatelyReparented(oppOne, 'AccountId'));
}
}
33 changes: 33 additions & 0 deletions rollup/main/default/classes/RollupTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2908,6 +2908,39 @@ private class RollupTests {
System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'Queueable' LIMIT 1]?.Status);
}

/** Grandparent rollups */
@isTest
static void shouldAllowGrandparentRollups() {
Account acc = [SELECT Id FROM Account];
List<Opportunity> opps = new List<Opportunity> {
new Opportunity(AccountId = acc.Id, StageName = 'Prospecting', Name = 'One', CloseDate = System.today()),
new Opportunity(AccountId = acc.Id, StageName = 'Prospecting', Name = 'Two', CloseDate = System.today())
};
insert opps;

DMLMock mock = new DMLMock();
Rollup.DML = mock;
Rollup.shouldRun = true;
Rollup.records = opps;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'Opportunity',
RollupFieldOnCalcItem__c = 'Name',
LookupFieldOnCalcItem__c = 'AccountId',
LookupObject__c = 'User',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'AboutMe',
RollupOperation__c = 'CONCAT'
// TODO - time to add in new field for relationship name path
)
};
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

System.assertEquals(1, mock.Records.size(), 'Grandparent record should have been found!');
User updatedUser = (User) mock.Records[0];
System.assertEquals(opps[0].Name + ', ' + opps[1].Name, updatedUser.AboutMe, 'Grandparent rollup should have worked!');
}

//** Helpers */

private static DMLMock loadAccountIdMock(List<SObject> records) {
Expand Down

0 comments on commit 5f018e0

Please sign in to comment.