diff --git a/README.md b/README.md index 93e2b511..97e2bf6e 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,13 @@ As well, don't miss [the Wiki](../../wiki), which includes even more info for co ## Deployment & Setup - - Deploy to Salesforce + + Deploy to Salesforce - - Deploy to Salesforce Sandbox + + Deploy to Salesforce Sandbox -

diff --git a/extra-tests/classes/InvocableDrivenTests.cls b/extra-tests/classes/InvocableDrivenTests.cls index 4ac4cdfb..bf5dd279 100644 --- a/extra-tests/classes/InvocableDrivenTests.cls +++ b/extra-tests/classes/InvocableDrivenTests.cls @@ -42,7 +42,7 @@ private class InvocableDrivenTests { @IsTest static void shouldWorkWhenBatchedFromRefresh() { - Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1, IsRollupLoggingEnabled__c = true); + Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1); // Driven by extra-tests/flows/Rollup_Integration_Multiple_Deferred_Case_Rollups.flow-meta.xml Account acc = [SELECT Id FROM Account]; // Description and Subject both are referenced in the Flow diff --git a/extra-tests/classes/RollupCalcItemSorterTests.cls b/extra-tests/classes/RollupCalcItemSorterTests.cls index 44037d26..16daab5f 100644 --- a/extra-tests/classes/RollupCalcItemSorterTests.cls +++ b/extra-tests/classes/RollupCalcItemSorterTests.cls @@ -162,7 +162,7 @@ private class RollupCalcItemSorterTests { new Opportunity(StageName = 'One') }; - itemsToSort.sort(new RollupCalcItemSorter(new List{ Opportunity.Name.getDescribe().getName(), Opportunity.StageName.getDescribe().getName() })); + itemsToSort.sort(new RollupCalcItemSorter(new List{ Opportunity.Name.toString(), Opportunity.StageName.toString() })); System.assertEquals(null, itemsToSort.get(0).StageName); System.assertEquals('One', itemsToSort.get(1).StageName); diff --git a/extra-tests/classes/RollupCalculatorTests.cls b/extra-tests/classes/RollupCalculatorTests.cls index 3a6f526e..943d4b7f 100644 --- a/extra-tests/classes/RollupCalculatorTests.cls +++ b/extra-tests/classes/RollupCalculatorTests.cls @@ -245,9 +245,8 @@ private class RollupCalculatorTests { Rollup__mdt metadata = configureOrderByMetadata(new Rollup__mdt(), 'Name'); RollupCalculator calc = getCalculator(0, Rollup.Op.FIRST, Opportunity.Amount, Account.AnnualRevenue, metadata, lookupKey, Account.Id); RollupState outerState = new RollupState(); - RollupState.SObjectInfo info = new RollupState.SObjectInfo(); + RollupState.SObjectInfo info = (RollupState.SObjectInfo) outerState.getState(lookupKey, metadata, RollupState.SObjectInfo.class); info.setItem(new Opportunity(Name = 'a', Amount = 3)); - outerState.setState(lookupKey, info); calc.setState(outerState); calc.performRollup( @@ -724,17 +723,17 @@ private class RollupCalculatorTests { static void mostRespectsStateValues() { Account acc = [SELECT Id FROM Account]; Integer priorVal = 50; - RollupState.MostInfo info = new RollupState.MostInfo(); - info.setLargestPointCounter(2); RollupState outerState = new RollupState(); - outerState.setState(acc.Id, info); + Rollup__mdt metaKey = new Rollup__mdt(); + RollupState.MostInfo info = (RollupState.MostInfo) outerState.getState(acc.Id, metaKey, RollupState.MostInfo.class); + info.setValues(2, priorVal); RollupCalculator calc = getCalculator( priorVal, Rollup.Op.MOST, ContactPointAddress.PreferenceRank, Account.AnnualRevenue, - new Rollup__mdt(), + metaKey, acc.Id, ContactPointAddress.ParentId ); @@ -886,6 +885,49 @@ private class RollupCalculatorTests { System.assertEquals(null, calc.getReturnValue()); } + @IsTest + static void correctlyAppliesStatefulSums() { + Rollup__mdt metaKey = new Rollup__mdt(); + String accountKey = '0011g00003VDGbF002'; + Integer value = 1; + RollupCalculator calc = getCalculator(null, Rollup.Op.SUM, Opportunity.Amount, Account.AnnualRevenue, metaKey, accountKey, Opportunity.AccountId); + RollupState outerState = new RollupState(); + RollupState.GenericInfo info = (RollupState.GenericInfo) outerState.getState(accountKey, metaKey, RollupState.GenericInfo.class); + info.value = value; + calc.setState(outerState); + + Opportunity one = new Opportunity(Id = '0066g00003VDGbF001', Amount = 5, AccountId = accountKey); + Opportunity two = new Opportunity(Id = '0066g00003VDGbF002', Amount = 5, AccountId = accountKey); + + calc.performRollup(new List{ one, two }, new Map()); + + System.assertEquals(one.Amount + two.Amount + value, calc.getReturnValue()); + System.assertEquals(one.Amount + two.Amount + value, info.value); + } + + @IsTest + static void doesNotResetParentValueWhenOnlyStatefulSumMatches() { + Rollup__mdt metaKey = new Rollup__mdt(); + String accountKey = '0011g00003VDGbF002'; + Integer value = 1; + RollupCalculator calc = getCalculator(null, Rollup.Op.SUM, Opportunity.Amount, Account.AnnualRevenue, metaKey, accountKey, Opportunity.AccountId); + RollupState outerState = new RollupState(); + RollupState.GenericInfo info = (RollupState.GenericInfo) outerState.getState(accountKey, metaKey, RollupState.GenericInfo.class); + info.value = value; + + calc.setState(outerState); + calc.setFullRecalc(true); + calc.setEvaluator(new RollupEvaluator.WhereFieldEvaluator('Amount = 0', Opportunity.SObjectType)); + + Opportunity one = new Opportunity(Id = '0066g00003VDGbF001', Amount = 5, AccountId = accountKey); + Opportunity two = new Opportunity(Id = '0066g00003VDGbF002', Amount = 5, AccountId = accountKey); + + calc.performRollup(new List{ one, two }, new Map()); + + System.assertEquals(value, calc.getReturnValue()); + System.assertEquals(value, info.value); + } + // CONCAT tests @IsTest @@ -2177,8 +2219,10 @@ private class RollupCalculatorTests { @IsTest static void averageFactorsInBatchState() { + Rollup__mdt meta = new Rollup__mdt(); String lookupKey = RollupTestUtils.createId(Account.SObjectType); - RollupState.AverageInfo state = new RollupState.AverageInfo(); + RollupState outerState = new RollupState(); + RollupState.AverageInfo state = (RollupState.AverageInfo) outerState.getState(lookupKey, meta, RollupState.AverageInfo.class); state.denominator = 15; state.numerator = 75; RollupCalculator calc = getCalculator( @@ -2186,14 +2230,12 @@ private class RollupCalculatorTests { Rollup.Op.AVERAGE, Opportunity.Amount, Account.Description, - new Rollup__mdt(), + meta, lookupKey, Account.Id ); - - RollupState outerState = new RollupState(); - outerState.setState(lookupKey, state); calc.setState(outerState); + calc.performRollup( new List{ new Opportunity(Amount = 15), diff --git a/extra-tests/classes/RollupCurrencyInfoTests.cls b/extra-tests/classes/RollupCurrencyInfoTests.cls index d6256a94..4c55a072 100644 --- a/extra-tests/classes/RollupCurrencyInfoTests.cls +++ b/extra-tests/classes/RollupCurrencyInfoTests.cls @@ -206,7 +206,7 @@ private class RollupCurrencyInfoTests { ); List olis = new List{ oliToUpdate }; - RollupCurrencyInfo.overrideDatedMultiCurrency(olis.getSObjectType().getDescribe().getName(), new List{ 'Opportunity', 'CloseDate' }); + RollupCurrencyInfo.overrideDatedMultiCurrency(olis.getSObjectType().toString(), new List{ 'Opportunity', 'CloseDate' }); RollupCurrencyInfo.transform(olis, OpportunityLineItem.TotalPrice, eurPeriodOne.IsoCode, new List()); OpportunityLineItem oli = (OpportunityLineItem) RollupCurrencyInfo.getCalcItem(oliToUpdate, eurPeriodOne.IsoCode); diff --git a/extra-tests/classes/RollupEvaluatorTests.cls b/extra-tests/classes/RollupEvaluatorTests.cls index 18516c0c..98f502f8 100644 --- a/extra-tests/classes/RollupEvaluatorTests.cls +++ b/extra-tests/classes/RollupEvaluatorTests.cls @@ -1129,7 +1129,7 @@ private class RollupEvaluatorTests { @IsTest static void pascalCaseForFieldNamesIsNotRequired() { - String poorlyCasedFieldName = Opportunity.IsClosed.getDescribe().getName().toLowerCase(); + String poorlyCasedFieldName = Opportunity.IsClosed.toString().toLowerCase(); Opportunity isClosedFalse = (Opportunity) JSON.deserialize('{ "IsClosed": false }', Opportunity.class); OpportunityLineItem target = new OpportunityLineItem(Opportunity = isClosedFalse); diff --git a/extra-tests/classes/RollupFinalizerTests.cls b/extra-tests/classes/RollupFinalizerTests.cls index 8de43d52..3ed813d4 100644 --- a/extra-tests/classes/RollupFinalizerTests.cls +++ b/extra-tests/classes/RollupFinalizerTests.cls @@ -1,19 +1,31 @@ @IsTest private class RollupFinalizerTests { - @TestSetup - static void setup() { - upsert new RollupSettings__c(IsEnabled__c = true); - insert new Account(Name = RollupFinalizerTests.class.getName()); + public class ExampleFinalizerContext implements System.FinalizerContext { + public Id getAsyncApexJobId() { + return RollupTestUtils.createId(AsyncApexJob.SObjectType); + } + + public String getRequestId() { + return System.Request.getCurrent().getRequestId(); + } + + public ParentJobResult getResult() { + return ParentJobResult.UNHANDLED_EXCEPTION; + } + + public Exception getException() { + return new DmlException(); + } } @IsTest static void shouldGracefullyLogUnhandledException() { - RollupFinalizer.testResult = ParentJobResult.UNHANDLED_EXCEPTION; + System.FinalizerContext fc = new ExampleFinalizerContext(); Test.startTest(); - new RollupFinalizer().execute(null); + new RollupFinalizer().execute(fc); Test.stopTest(); - System.assertEquals(true, RollupFinalizer.wasCalled); + System.assertEquals(true, RollupFinalizer.wasExceptionLogged); } } diff --git a/extra-tests/classes/RollupFlowFullRecalcTests.cls b/extra-tests/classes/RollupFlowFullRecalcTests.cls index 2e9e80fd..a8498024 100644 --- a/extra-tests/classes/RollupFlowFullRecalcTests.cls +++ b/extra-tests/classes/RollupFlowFullRecalcTests.cls @@ -28,8 +28,8 @@ private class RollupFlowFullRecalcTests { List flowInputs = RollupTestUtils.prepareFlowTest(apps, 'REFRESH', 'SUM'); flowInputs[0].ultimateParentLookup = 'ParentId'; flowInputs[0].rollupToUltimateParent = true; - flowInputs[0].lookupFieldOnCalcItem = Application__c.ParentApplication__c.getDescribe().getName(); - flowInputs[0].rollupFieldOnCalcItem = Application__c.Engagement_Score__c.getDescribe().getName(); + flowInputs[0].lookupFieldOnCalcItem = Application__c.ParentApplication__c.toString(); + flowInputs[0].rollupFieldOnCalcItem = Application__c.Engagement_Score__c.toString(); flowInputs[0].grandparentRelationshipFieldPath = RollupTestUtils.getRelationshipPath( new List{ Application__c.ParentApplication__c, ParentApplication__c.Account__c, Account.AnnualRevenue } ); diff --git a/extra-tests/classes/RollupFullRecalcTests.cls b/extra-tests/classes/RollupFullRecalcTests.cls index e2a5e655..356d2e66 100644 --- a/extra-tests/classes/RollupFullRecalcTests.cls +++ b/extra-tests/classes/RollupFullRecalcTests.cls @@ -10,11 +10,7 @@ private class RollupFullRecalcTests { @IsTest static void correctlyOrdersGrandparentRollupQueryString() { - Rollup.defaultControl = new RollupControl__mdt( - IsRollupLoggingEnabled__c = true, - MaxLookupRowsBeforeBatching__c = 0, - ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable - ); + Rollup.defaultControl = new RollupControl__mdt(IsRollupLoggingEnabled__c = true, MaxLookupRowsBeforeBatching__c = 0); insert new Contact(LastName = 'grandparent rollup query string'); Rollup.performBulkFullRecalc( @@ -35,7 +31,7 @@ private class RollupFullRecalcTests { System.assertEquals(1, Rollup.CACHED_FULL_RECALCS.size()); List queryParts = Rollup.CACHED_FULL_RECALCS.get(0).start(null).getQuery().split('\n'); - System.assertEquals('ORDER BY ReportsTo.Account.Id,ReportsToId', queryParts.get(queryParts.size() - 1)); + System.assertEquals('ORDER BY ReportsTo.Account.Id,ReportsToId,Id', queryParts.get(queryParts.size() - 1)); } @IsTest @@ -128,14 +124,14 @@ private class RollupFullRecalcTests { List metadata = new List{ new Rollup__mdt( - CalcItem__c = Application__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = Application__c.SObjectType.toString(), LookupObject__c = 'Account', RollupToUltimateParent__c = true, - LookupFieldOnCalcItem__c = Application__c.ParentApplication__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = Application__c.ParentApplication__c.toString(), UltimateParentLookup__c = 'ParentId', RollupOperation__c = 'SUM', RollupFieldOnLookupObject__c = 'AnnualRevenue', - RollupFieldOnCalcItem__c = Application__c.Engagement_Score__c.getDescribe().getName(), + RollupFieldOnCalcItem__c = Application__c.Engagement_Score__c.toString(), LookupFieldOnLookupObject__c = 'Id', GrandparentRelationshipFieldPath__c = RollupTestUtils.getRelationshipPath( new List{ Application__c.ParentApplication__c, ParentApplication__c.Account__c, Account.AnnualRevenue } @@ -172,12 +168,12 @@ private class RollupFullRecalcTests { List metadata = new List{ new Rollup__mdt( - CalcItem__c = Application__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = Application__c.SObjectType.toString(), LookupObject__c = 'Individual', - LookupFieldOnCalcItem__c = Application__c.ParentApplication__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = Application__c.ParentApplication__c.toString(), RollupOperation__c = 'SUM', RollupFieldOnLookupObject__c = 'ConsumerCreditScore', - RollupFieldOnCalcItem__c = Application__c.Engagement_Score__c.getDescribe().getName(), + RollupFieldOnCalcItem__c = Application__c.Engagement_Score__c.toString(), LookupFieldOnLookupObject__c = 'Id', GrandparentRelationshipFieldPath__c = String.valueOf(Application__c.ParentApplication__c).replace('__r', '__c') + '.' + @@ -915,8 +911,11 @@ private class RollupFullRecalcTests { insert cpas; Rollup.defaultControl = new RollupControl__mdt( + // prove that it works with multiple chunks + BatchChunkSize__c = 2, MaxLookupRowsBeforeBatching__c = 1, - ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Queueable, // validate that it still batches + // validate that it still batches + ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Queueable, IsRollupLoggingEnabled__c = true ); @@ -937,7 +936,16 @@ private class RollupFullRecalcTests { acc = [SELECT AnnualRevenue FROM Account]; System.assertEquals(6, acc.AnnualRevenue); - System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'BatchApexWorker'].Status); + System.assertEquals( + 'Completed', + [ + SELECT Status + FROM AsyncApexJob + WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class) + LIMIT 1 + ] + ?.Status + ); } @IsTest @@ -959,7 +967,8 @@ private class RollupFullRecalcTests { Rollup.defaultControl = new RollupControl__mdt( MaxLookupRowsBeforeBatching__c = 1, - ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Queueable, // validate that it still batches + // validate that it still batches + ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Queueable, IsRollupLoggingEnabled__c = true ); Rollup.specificControl = new RollupControl__mdt(BatchChunkSize__c = 1); @@ -979,11 +988,14 @@ private class RollupFullRecalcTests { Rollup.performFullRecalculation(meta); Test.stopTest(); - List jobs = [SELECT Status, JobItemsProcessed FROM AsyncApexJob WHERE JobType = 'BatchApexWorker']; + List jobs = [ + SELECT Status + FROM AsyncApexJob + WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class) + ]; System.assertEquals(false, jobs.isEmpty()); for (AsyncApexJob job : jobs) { System.assertEquals('Completed', job.Status); - System.assertEquals(1, job.JobItemsProcessed); } } @@ -1195,12 +1207,12 @@ private class RollupFullRecalcTests { List metas = new List{ new Rollup__mdt( - CalcItem__c = RollupChild__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), - RollupFieldOnCalcItem__c = RollupChild__c.NumberField__c.getDescribe().getName(), - LookupFieldOnCalcItem__c = RollupChild__c.RollupParent__c.getDescribe().getName(), - LookupObject__c = RollupGrandparent__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), - LookupFieldOnLookupObject__c = RollupGrandparent__c.Id.getDescribe().getName(), - RollupFieldOnLookupObject__c = RollupGrandparent__c.AmountFromChildren__c.getDescribe().getName(), + CalcItem__c = RollupChild__c.SObjectType.toString(), + RollupFieldOnCalcItem__c = RollupChild__c.NumberField__c.toString(), + LookupFieldOnCalcItem__c = RollupChild__c.RollupParent__c.toString(), + LookupObject__c = RollupGrandparent__c.SObjectType.toString(), + LookupFieldOnLookupObject__c = RollupGrandparent__c.Id.toString(), + RollupFieldOnLookupObject__c = RollupGrandparent__c.AmountFromChildren__c.toString(), RollupOperation__c = 'SUM', CalcItemWhereClause__c = ' ||| ' + RollupTestUtils.getRelationshipPath( @@ -1223,7 +1235,8 @@ private class RollupFullRecalcTests { @IsTest static void shouldNotBlowUpOnMassiveQuery() { - String endOfWhereClause = '2'.repeat(100001); + Integer maxLength = ContactPointAddress.PreferenceRank.getDescribe().getDigits(); + String endOfWhereClause = '2'.repeat(maxLength); Rollup__mdt meta = new Rollup__mdt( CalcItem__c = 'ContactPointAddress', RollupFieldOnCalcItem__c = 'PreferenceRank', @@ -1232,7 +1245,7 @@ private class RollupFullRecalcTests { RollupFieldOnLookupObject__c = 'AnnualRevenue', LookupObject__c = 'Account', RollupOperation__c = 'SUM', - CalcItemWhereClause__c = 'PreferenceRank = ' + endOfWhereClause + CalcItemWhereClause__c = ('PreferenceRank = ' + endOfWhereClause + ' AND ').repeat(200).removeEnd(' AND ') ); Test.startTest(); @@ -1490,7 +1503,7 @@ private class RollupFullRecalcTests { } @IsTest - static void shouldResetParentValuesWithoutMatchingChildrenFromBulkRouteUnderQueryLimit() { + static void shouldResetParentValuesWithoutMatchingChildrenFromBulkRouteUnderQueryLimitSync() { Account acc = [SELECT Id FROM Account]; acc.AccountNumber = 'someString'; acc.AnnualRevenue = 5; @@ -1522,9 +1535,7 @@ private class RollupFullRecalcTests { ) }; - Test.startTest(); Rollup.performBulkFullRecalc(metas, Rollup.InvocationPoint.FROM_FULL_RECALC_LWC.name()); - Test.stopTest(); List accounts = [SELECT AccountNumber, AnnualRevenue, Name FROM Account]; for (Account updatedAcc : accounts) { @@ -1605,12 +1616,21 @@ private class RollupFullRecalcTests { System.assertEquals(null, updatedAcc.AnnualRevenue, 'AnnualRevenue should have been reset: ' + updatedAcc); System.assertEquals(null, updatedAcc.AccountNumber, 'AccountNumber should have been reset: ' + updatedAcc); } + System.assertEquals( + 'Completed', + [ + SELECT Status + FROM AsyncApexJob + WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupParentResetProcessor.class) + LIMIT 1 + ] + ?.Status + ); } @IsTest static void shouldResetParentValuesWithoutMatchingChildrenFromBulkRouteOverLimitWithBatching() { RollupParentResetProcessor.maxQueryRows = 0; - Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable, IsRollupLoggingEnabled__c = true); Account acc = [SELECT Id FROM Account]; acc.AccountNumber = 'someString'; acc.AnnualRevenue = 5; @@ -1646,12 +1666,9 @@ private class RollupFullRecalcTests { Rollup.performBulkFullRecalc(metas, Rollup.InvocationPoint.FROM_FULL_RECALC_LWC.name()); Test.stopTest(); - List accounts = [SELECT AnnualRevenue, Name FROM Account]; + List accounts = [SELECT AnnualRevenue, Name, AccountNumber FROM Account]; for (Account updatedAcc : accounts) { - // for this set of tests, since we can't add more than one queueable job to the stack in a test context - // and because each full reset is processed separately, we can only validate that the first Rollup__mdt record - // had the parents associated with it reset. It's possible that this will be revisited in the future to consolidate full - // resets into singular jobs + System.assertEquals(null, updatedAcc.AccountNumber, 'AccountNumber should have been reset: ' + updatedAcc); System.assertEquals(null, updatedAcc.AnnualRevenue, 'AnnualRevenue should have been reset: ' + updatedAcc); } } @@ -1925,7 +1942,6 @@ private class RollupFullRecalcTests { @IsTest static void shouldFullRecalcWithInWhereClauses() { RollupParentResetProcessor.maxQueryRows = 0; - Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable); Account acc = [SELECT Id FROM Account]; insert new List{ @@ -2349,15 +2365,10 @@ private class RollupFullRecalcTests { ); Test.stopTest(); - Assert.areEqual( - 1, - [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupAsyncProcessor.class)], - 'Conductor should begin async as queueable' - ); Assert.areEqual( 2, - [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], - 'Only two batch classes should have run' + [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], + 'Exactly two batch full recalcs should have run' ); parentOne = [SELECT AnnualRevenue, Description, NumberOfEmployees FROM Account WHERE Id = :parentOne.Id]; Assert.areEqual(childOne.PreferenceRank, parentOne.AnnualRevenue); @@ -2367,6 +2378,7 @@ private class RollupFullRecalcTests { Assert.areEqual(null, parentTwo.AnnualRevenue); Assert.areEqual('Concat, Two', parentTwo.Description); Assert.areEqual(10, parentTwo.NumberOfEmployees); + Assert.areEqual(0, [SELECT COUNT() FROM RollupState__c], '' + [SELECT COUNT(Id), RelatedJobId__c FROM RollupState__c GROUP BY RelatedJobId__c]); } @IsTest @@ -2434,8 +2446,8 @@ private class RollupFullRecalcTests { Assert.areEqual( 2, - [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], - 'Only two batch classes should have run' + [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], + 'Exactly two batch full recalcs should have run' ); parentOne = [SELECT AnnualRevenue, Description, NumberOfEmployees FROM Account WHERE Id = :parentOne.Id]; Assert.areEqual(childOne.PreferenceRank, parentOne.AnnualRevenue); @@ -2445,6 +2457,7 @@ private class RollupFullRecalcTests { Assert.areEqual(null, parentTwo.AnnualRevenue); Assert.areEqual('Concat, Two', parentTwo.Description); Assert.areEqual(10, parentTwo.NumberOfEmployees); + Assert.areEqual(0, [SELECT COUNT() FROM RollupState__c]); } @IsTest @@ -2452,9 +2465,14 @@ private class RollupFullRecalcTests { // an interesting one because it's again an implementation detail - this time, // of the batch "caboose" process. if the full recalc is BELOW the batch limit, // the queueable will already correctly handle everything since the shared parent field will only be reset once. - // batch processes, on the other hand, would otherwise simply see the existing value on the parent as something + // batch queueables, on the other hand, would otherwise simply see the existing value on the parent as something // that needs to be cleared out - Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 100, MaxLookupRowsBeforeBatching__c = 1, IsRollupLoggingEnabled__c = true); + Rollup.defaultControl = new RollupControl__mdt( + MaxRollupRetries__c = 100, + MaxLookupRowsBeforeBatching__c = 1, + IsRollupLoggingEnabled__c = true, + BatchChunkSize__c = 3 + ); Rollup.onlyUseMockMetadata = true; Account parent = [SELECT Id FROM Account]; @@ -2501,12 +2519,13 @@ private class RollupFullRecalcTests { Assert.areEqual( 2, - [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], - 'Test requires batch apex to have been the full recalc mechanism' + [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)], + 'Test requires RollupFullBatchRecalculator to have been used' ); parent = [SELECT AnnualRevenue, NumberOfEmployees FROM Account WHERE Id = :parent.Id]; Assert.areEqual(40, parent.AnnualRevenue, 'Both children types should have correctly summed to one field in batch recalc'); Assert.areEqual(25, parent.NumberOfEmployees, 'Additional field on parent should have been queried successfully in second rollup'); + Assert.areEqual(0, [SELECT COUNT() FROM RollupState__c]); } @IsTest @@ -2551,7 +2570,7 @@ private class RollupFullRecalcTests { Assert.areEqual( 1, - [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupAsyncProcessor.class)], + [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupDeferredFullRecalcProcessor.class)], JSON.serializePretty([SELECT JobType, ApexClass.Name FROM AsyncApexJob]) ); parent = [SELECT AnnualRevenue FROM Account WHERE Id = :parent.Id]; diff --git a/extra-tests/classes/RollupIntegrationTests.cls b/extra-tests/classes/RollupIntegrationTests.cls index 5ec7919d..922227b0 100644 --- a/extra-tests/classes/RollupIntegrationTests.cls +++ b/extra-tests/classes/RollupIntegrationTests.cls @@ -36,8 +36,8 @@ private class RollupIntegrationTests { new Rollup__mdt( RollupFieldOnCalcItem__c = 'Amount', LookupObject__c = 'Account', - LookupFieldOnCalcItem__c = Opportunity.AccountIdText__c.getDescribe().getName(), - LookupFieldOnLookupObject__c = Account.AccountIdText__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = Opportunity.AccountIdText__c.toString(), + LookupFieldOnLookupObject__c = Account.AccountIdText__c.toString(), RollupFieldOnLookupObject__c = 'AnnualRevenue', RollupOperation__c = 'MAX', CalcItem__c = 'Opportunity' @@ -68,7 +68,7 @@ private class RollupIntegrationTests { Rollup.rollupMetadata = new List{ new Rollup__mdt( - RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.getDescribe().getName(), + RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.toString(), LookupObject__c = 'Account', LookupFieldOnCalcItem__c = 'AccountId', LookupFieldOnLookupObject__c = 'Id', @@ -150,19 +150,15 @@ private class RollupIntegrationTests { Rollup.records = applications; Rollup.FlowInput input = new Rollup.FlowInput(); - input.lookupFieldOnCalcItem = Application__c.ParentApplication__c.getDescribe().getName(); + input.lookupFieldOnCalcItem = Application__c.ParentApplication__c.toString(); input.lookupFieldOnOpObject = 'Id'; input.recordsToRollup = applications; input.rollupContext = 'INSERT'; - input.rollupFieldOnCalcItem = Application__c.Engagement_Score__c.getDescribe().getName(); - input.rollupFieldOnOpObject = ParentApplication__c.Engagement_Rollup__c.getDescribe().getName(); + input.rollupFieldOnCalcItem = Application__c.Engagement_Score__c.toString(); + input.rollupFieldOnOpObject = ParentApplication__c.Engagement_Rollup__c.toString(); input.rollupOperation = 'AVERAGE'; - input.rollupSObjectName = ParentApplication__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); - input.calcItemWhereClause = - Application__c.Something_With_Underscores__c.getDescribe().getName() + - ' != \'' + - applications[0].Something_With_Underscores__c + - '\''; + input.rollupSObjectName = ParentApplication__c.SObjectType.toString(); + input.calcItemWhereClause = Application__c.Something_With_Underscores__c.toString() + ' != \'' + applications[0].Something_With_Underscores__c + '\''; Test.startTest(); Rollup.performRollup(new List{ input }); @@ -188,16 +184,16 @@ private class RollupIntegrationTests { insert apps; Rollup.FlowInput input = new Rollup.FlowInput(); - input.lookupFieldOnCalcItem = Application__c.ParentApplication__c.getDescribe().getName(); + input.lookupFieldOnCalcItem = Application__c.ParentApplication__c.toString(); input.lookupFieldOnOpObject = 'Id'; input.recordsToRollup = new List{ parentApp }; input.rollupContext = 'INSERT'; - input.rollupFieldOnCalcItem = Application__c.Engagement_Score__c.getDescribe().getName(); - input.rollupFieldOnOpObject = ParentApplication__c.Engagement_Rollup__c.getDescribe().getName(); + input.rollupFieldOnCalcItem = Application__c.Engagement_Score__c.toString(); + input.rollupFieldOnOpObject = ParentApplication__c.Engagement_Rollup__c.toString(); input.rollupOperation = 'SUM'; - input.rollupSObjectName = ParentApplication__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); + input.rollupSObjectName = ParentApplication__c.SObjectType.toString(); input.isRollupStartedFromParent = true; - input.calcItemTypeWhenRollupStartedFromParent = Application__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); + input.calcItemTypeWhenRollupStartedFromParent = Application__c.SObjectType.toString(); Test.startTest(); Rollup.performRollup(new List{ input }); @@ -283,9 +279,9 @@ private class RollupIntegrationTests { Rollup.records = appLogs; Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), - RollupFieldOnCalcItem__c = ApplicationLog__c.Object__c.getDescribe().getName(), - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + CalcItem__c = ApplicationLog__c.SObjectType.toString(), + RollupFieldOnCalcItem__c = ApplicationLog__c.Object__c.toString(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'Name', @@ -411,9 +407,9 @@ private class RollupIntegrationTests { Rollup.shouldFlattenAsyncProcesses = true; Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = ApplicationLog__c.SObjectType.toString(), RollupFieldOnCalcItem__c = 'Name', - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'Name', @@ -464,9 +460,9 @@ private class RollupIntegrationTests { Rollup.shouldFlattenAsyncProcesses = true; Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = ApplicationLog__c.SObjectType.toString(), RollupFieldOnCalcItem__c = 'Name', - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'Name', @@ -534,9 +530,9 @@ private class RollupIntegrationTests { Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = ApplicationLog__c.SObjectType.toString(), RollupFieldOnCalcItem__c = 'Name', - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'Name', @@ -584,9 +580,9 @@ private class RollupIntegrationTests { Rollup.shouldRun = true; Rollup.FlowInput input = new Rollup.FlowInput(); input.recordsToRollup = parentApps; - input.calcItemTypeWhenRollupStartedFromParent = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); - input.rollupFieldOnCalcItem = ApplicationLog__c.Name.getDescribe().getName(); - input.lookupFieldOnCalcItem = ApplicationLog__c.Application__c.getDescribe().getName(); + input.calcItemTypeWhenRollupStartedFromParent = ApplicationLog__c.SObjectType.toString(); + input.rollupFieldOnCalcItem = ApplicationLog__c.Name.toString(); + input.lookupFieldOnCalcItem = ApplicationLog__c.Application__c.toString(); input.rollupSObjectName = 'Account'; input.lookupFieldOnOpObject = 'Id'; input.rollupFieldOnOpObject = 'Name'; @@ -638,9 +634,9 @@ private class RollupIntegrationTests { Rollup.records = appLogs; Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.getSObjectType().getDescribe().getName(), - RollupFieldOnCalcItem__c = ApplicationLog__c.Name.getDescribe().getName(), - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + CalcItem__c = ApplicationLog__c.getSObjectType().toString(), + RollupFieldOnCalcItem__c = ApplicationLog__c.Name.toString(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'Name', @@ -683,9 +679,9 @@ private class RollupIntegrationTests { Rollup.shouldFlattenAsyncProcesses = true; Rollup.rollupMetadata = new List{ new Rollup__mdt( - CalcItem__c = ApplicationLog__c.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(), + CalcItem__c = ApplicationLog__c.SObjectType.toString(), RollupFieldOnCalcItem__c = 'Name', - LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.getDescribe().getName(), + LookupFieldOnCalcItem__c = ApplicationLog__c.Application__c.toString(), LookupObject__c = 'Account', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'AnnualRevenue', @@ -925,7 +921,7 @@ private class RollupIntegrationTests { null, new Rollup__mdt( RollupOperation__c = Rollup.Op.UPDATE_CONCAT_DISTINCT.name(), - RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.getDescribe().getName(), + RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.toString(), LookupFieldOnCalcItem__c = 'AccountId' ), new Map(), @@ -940,7 +936,7 @@ private class RollupIntegrationTests { null, new Rollup__mdt( RollupOperation__c = Rollup.Op.UPDATE_CONCAT_DISTINCT.name(), - RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.getDescribe().getName(), + RollupFieldOnCalcItem__c = Opportunity.AmountFormula__c.toString(), LookupFieldOnCalcItem__c = 'AccountId' ), new Map(), @@ -1690,7 +1686,7 @@ private class RollupIntegrationTests { Rollup.records = new List{ cpa }; Rollup.shouldRun = true; Rollup.apexContext = TriggerOperation.AFTER_INSERT; - Rollup.defaultControl = new RollupControl__mdt(MaxParentRowsUpdatedAtOnce__c = 0, ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable); + Rollup.defaultControl = new RollupControl__mdt(MaxParentRowsUpdatedAtOnce__c = 0); Test.startTest(); Rollup.sumFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc(); @@ -1886,8 +1882,7 @@ private class RollupIntegrationTests { static void shouldFilterNonMatchingRollupsOutOfBatch() { Contact con = new Contact(FirstName = 'Something', LastName = 'Required'); insert con; - RollupControl__mdt control = RollupControl__mdt.getInstance(Rollup.CONTROL_ORG_DEFAULTS).clone(); - control.ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable; + Rollup.FilterResults results = new Rollup.FilterResults(); results.matchingItemIds.add(con.Id); RollupAsyncProcessor batchProcessor = new RollupAsyncProcessor( @@ -1900,7 +1895,7 @@ private class RollupIntegrationTests { Contact.SObjectType, Rollup.Op.CONCAT, Rollup.InvocationPoint.FROM_APEX, - control, + RollupControl__mdt.getInstance(Rollup.CONTROL_ORG_DEFAULTS).clone(), new Rollup__mdt( CalcItem__c = 'Contact', RollupFieldOnCalcItem__c = 'FirstName', diff --git a/extra-tests/classes/RollupQueryBuilderTests.cls b/extra-tests/classes/RollupQueryBuilderTests.cls index ea5ca498..4aab8c87 100644 --- a/extra-tests/classes/RollupQueryBuilderTests.cls +++ b/extra-tests/classes/RollupQueryBuilderTests.cls @@ -57,7 +57,7 @@ private class RollupQueryBuilderTests { String queryString = RollupQueryBuilder.Current.getQuery( Event.SObjectType, new List{ 'Subject', 'WhatId' }, - Event.WhatId.getDescribe().getName(), + Event.WhatId.toString(), '!=', '(((What.Type = \'Account\') AND What.Owner.Id = :recordIds))' ); @@ -81,7 +81,7 @@ private class RollupQueryBuilderTests { String queryString = RollupQueryBuilder.Current.getQuery( Event.SObjectType, new List{ 'Subject', 'WhatId' }, - Event.WhatId.getDescribe().getName(), + Event.WhatId.toString(), '!=', '(((What.Type = \'Account\') OR What.Owner.Id = :recordIds))' ); @@ -98,7 +98,7 @@ private class RollupQueryBuilderTests { String queryString = RollupQueryBuilder.Current.getQuery( Opportunity.SObjectType, new List{ 'Amount' }, - Opportunity.AccountId.getDescribe().getName(), + Opportunity.AccountId.toString(), '=', 'Amount > 0 OR CloseDate = YESTERDAY' ); @@ -108,7 +108,7 @@ private class RollupQueryBuilderTests { @IsTest static void correctlyPutsAllRowsAtEnd() { String queryString = - RollupQueryBuilder.Current.getQuery(Event.SObjectType, new List{ 'Subject', 'WhatId' }, Event.WhatId.getDescribe().getName(), '!=') + '\nLIMIT 1'; + RollupQueryBuilder.Current.getQuery(Event.SObjectType, new List{ 'Subject', 'WhatId' }, Event.WhatId.toString(), '!=') + '\nLIMIT 1'; System.assertEquals(true, queryString.contains(RollupQueryBuilder.ALL_ROWS), 'Needs to have all rows in order to be valid'); queryString = RollupQueryBuilder.Current.getAllRowSafeQuery(Event.SObjectType, queryString); diff --git a/extra-tests/classes/RollupStateTests.cls b/extra-tests/classes/RollupStateTests.cls new file mode 100644 index 00000000..8b02d8a6 --- /dev/null +++ b/extra-tests/classes/RollupStateTests.cls @@ -0,0 +1,370 @@ +@IsTest +private class RollupStateTests { + @IsTest + static void commitsAndLoadsStateProperly() { + RollupState state = new RollupState(); + String stubAccountId = RollupTestUtils.createId(Account.SObjectType); + RollupState.GenericInfo info = (RollupState.GenericInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'SUM'), + RollupState.GenericInfo.class + ); + info.value = 5; + RollupState.AverageInfo averageInfo = (RollupState.AverageInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'CONCAT'), + RollupState.AverageInfo.class + ); + averageInfo.increment(10); + RollupState.SObjectInfo sObjectInfo = (RollupState.SObjectInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'FIRST'), + RollupState.SObjectInfo.class + ); + sObjectInfo.setItem(new Account(AnnualRevenue = 1000)); + String secondStubId = RollupTestUtils.createId(Contact.SObjectType); + RollupState.MostInfo mostInfo = (RollupState.MostInfo) state.getState( + secondStubId, + new Rollup__mdt(RollupOperation__c = 'MOST'), + RollupState.MostInfo.class + ); + mostInfo.setValues(5, 'some string'); + // populate a null state value to be sure if the last iteration is "empty" the body is still properly committed + state.getState(secondStubId, new Rollup__mdt(RollupOperation__c = 'LAST'), RollupState.GenericInfo.class); + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + Set relatedRecordKeys = new Set{ '%' + stubAccountId + '%', '%' + secondStubId + '%' }; + + state.commitState(stubJobId); + state.loadState(stubJobId, new Set{ stubAccountId, secondStubId }); + info = (RollupState.GenericInfo) state.getState(stubAccountId, new Rollup__mdt(RollupOperation__c = 'SUM'), RollupState.GenericInfo.class); + info.value = 6; + state.commitState(stubJobId); + + RollupState__c insertedState = [ + SELECT Id, Body0__c + FROM RollupState__c + WHERE RelatedRecordKeys0__c LIKE :relatedRecordKeys + ]; + Assert.isNotNull(insertedState.Body0__c, 'Serialized representation of generic state should be present'); + + state.loadState(stubJobId, new Set{ stubAccountId, secondStubId }); + Set actual = ((RollupState.AverageInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'CONCAT'), + RollupState.AverageInfo.class + )) + .distinctNumerators; + + Assert.areEqual(averageInfo.distinctNumerators.size(), actual.size()); + Assert.areEqual(averageInfo.distinctNumerators.contains(10.00), actual.contains(10.00)); + RollupState.GenericInfo updatedInfo = ((RollupState.GenericInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'SUM'), + RollupState.GenericInfo.class + )); + Assert.areEqual(info.value, updatedInfo.value); + } + + @IsTest + static void handlesMultipleExistingStateValues() { + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + String stubAccountId = RollupTestUtils.createId(Account.SObjectType); + RollupState state = new RollupState(); + RollupState.AverageInfo averageInfo = (RollupState.AverageInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'AVERAGE'), + RollupState.AverageInfo.class + ); + averageInfo.increment(10); + RollupState.GenericInfo info = (RollupState.GenericInfo) state.getState( + stubAccountId, + new Rollup__mdt(RollupOperation__c = 'SUM'), + RollupState.GenericInfo.class + ); + + RollupState__c existingAverageState = new RollupState__c( + RelatedJobId__c = stubJobId, + RelatedRecordKeys0__c = stubAccountId, + Body0__c = JSON.serialize(averageInfo.getUntypedState()) + ); + insert new List{ + existingAverageState, + new RollupState__c(RelatedJobId__c = stubJobId, RelatedRecordKeys0__c = stubAccountId, Body0__c = JSON.serialize(info.getUntypedState())) + }; + + state = new RollupState(); + state.loadState(stubJobId, new Set{ stubAccountId }); + info = (RollupState.GenericInfo) state.getState(stubAccountId, new Rollup__mdt(RollupOperation__c = 'SUM'), RollupState.GenericInfo.class); + info.value = 5; + averageInfo = (RollupState.AverageInfo) state.getState(stubAccountId, new Rollup__mdt(RollupOperation__c = 'AVERAGE'), RollupState.AverageInfo.class); + averageInfo.increment(20); + + Test.startTest(); + state.commitState(stubJobId); + Assert.areEqual(1, Limits.getDmlRows()); + Test.stopTest(); + + Boolean hasCorrectAverageInfo = false; + Boolean hasCorrectGenericInfo = false; + for (RollupState__c createdState : [SELECT Body0__c FROM RollupState__c]) { + hasCorrectAverageInfo = hasCorrectAverageInfo || createdState.Body0__c.contains(JSON.serialize(averageInfo.getUntypedState())); + hasCorrectGenericInfo = hasCorrectGenericInfo || createdState.Body0__c.contains(JSON.serialize(info.getUntypedState())); + } + Assert.isTrue(hasCorrectAverageInfo, 'new average state should have been updated'); + Assert.isTrue(hasCorrectGenericInfo, 'Generic info should not be wiped out'); + } + + @IsTest + static void splitsReallyLongStatesForTheSameRecord() { + // the type names take up a bit more space during namespaced packaging + RollupState.maxBodyLength = RollupTestUtils.IS_NAMESPACED_PACKAGE_ORG ? 20000 : 18000; + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + String stubAccountId = RollupTestUtils.createId(Account.SObjectType); + Rollup__mdt template = new Rollup__mdt(DeveloperName = 'exampleUnique40CharacterLimit'); + RollupState state = new RollupState(); + + List> statesToSample = new List>(); + for (Integer index = 0; index < 400; index++) { + Rollup__mdt clonedMeta = template.clone(); + clonedMeta.DeveloperName += '' + index; + RollupState.GenericInfo info = (RollupState.GenericInfo) state.getState(stubAccountId, clonedMeta, RollupState.GenericInfo.class); + info.value = index; + if (index < 20) { + statesToSample.add(info.getUntypedState()); + } + } + state.commitState(stubJobId); + + List committedStates = [ + SELECT Body0__c, RelatedRecordKeys0__c + FROM RollupState__c + ]; + + Assert.areEqual(3, committedStates.size()); + Boolean body0Filled = false; + Boolean sampleStatesFilled = false; + for (Integer index = 0; index < committedStates.size(); index++) { + RollupState__c committedState = committedStates[index]; + body0Filled = body0Filled || committedState.Body0__c != null; + Assert.areEqual(stubAccountId, committedState.RelatedRecordKeys0__c, 'State was missing key at index: ' + index); + if (sampleStatesFilled) { + Boolean isDuplicate = committedState.Body0__c.contains(JSON.serialize(statesToSample).removeStart('[').removeEnd(']')); + if (isDuplicate) { + throw new IllegalArgumentException('Body0__c should not match for both states'); + } + } else { + sampleStatesFilled = committedState.Body0__c?.contains(JSON.serialize(statesToSample).removeStart('[').removeEnd(']')) == true; + } + } + + Assert.isTrue(body0Filled); + Assert.isTrue(sampleStatesFilled, committedStates[0].Body0__c?.substring(0, 50)); + /** + * ensure that we don't exceed the DataWeave heap: + * System.DataWeaveScriptException: turtles.api.SandboxedLimitException - Heap limit exceeded 6004410 > 6000000 + */ + state.loadState(stubJobId, new Set{ stubAccountId }); + } + + @IsTest + static void splitsRelatedRecordKeysIntoDifferentTextFields() { + Rollup__mdt template = new Rollup__mdt(DeveloperName = 'one rollup'); + RollupState state = new RollupState(); + Set fullKeys = new Set(); + + List fieldKeys0 = new List(); + List fieldKeys1 = new List(); + List fieldKeys2 = new List(); + List fieldKeys3 = new List(); + List fieldKeys4 = new List(); + List fieldKeys5 = new List(); + List fieldKeys6 = new List(); + List fieldKeys7 = new List(); + List fieldKeys8 = new List(); + List fieldKeys9 = new List(); + List fieldKeys10 = new List(); + List secondFieldKeys0 = new List(); + String firstAccountId = RollupTestUtils.createId(Account.SObjectType) + 'aaa'; + // 255 / 18 characters ~= 13 records per field (accounting for commas) + // 12 because with 11 fields, we want to "overflow" to the second RelatedRecordKeys0__c + for (Integer index = 0; index < 13 * 12; index++) { + String stubAccountId = index == 0 ? firstAccountId : RollupTestUtils.createId(Account.SObjectType) + 'aaa'; + RollupState.GenericInfo info = (RollupState.GenericInfo) state.getState(stubAccountId, template, RollupState.GenericInfo.class); + info.value = index; + List keys; + if (index < 13) { + keys = fieldKeys0; + } else if (index < 13 * 2) { + keys = fieldKeys1; + } else if (index < 13 * 3) { + keys = fieldKeys2; + } else if (index < 13 * 4) { + keys = fieldKeys3; + } else if (index < 13 * 5) { + keys = fieldKeys4; + } else if (index < 13 * 6) { + keys = fieldKeys5; + } else if (index < 13 * 7) { + keys = fieldKeys6; + } else if (index < 13 * 8) { + keys = fieldKeys7; + } else if (index < 13 * 9) { + keys = fieldKeys8; + } else if (index < 13 * 10) { + keys = fieldKeys9; + } else if (index < 13 * 11) { + keys = fieldKeys10; + } else if (index < 13 * 12) { + keys = secondFieldKeys0; + } + keys?.add(stubAccountId); + fullKeys.add(stubAccountId); + } + + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + state.commitState(stubJobId); + + List committedStates = [ + SELECT + RelatedRecordKeys0__c, + RelatedRecordKeys1__c, + RelatedRecordKeys2__c, + RelatedRecordKeys3__c, + RelatedRecordKeys4__c, + RelatedRecordKeys5__c, + RelatedRecordKeys6__c, + RelatedRecordKeys7__c, + RelatedRecordKeys8__c, + RelatedRecordKeys9__c, + RelatedRecordKeys10__c + FROM RollupState__c + WHERE RelatedJobId__c = :stubJobId + ]; + // ensure that loadState can be called with the full spread of keys + state.loadState(stubJobId, fullKeys); + + Assert.areEqual(2, committedStates.size()); + RollupState__c firstState = committedStates.get(0); + RollupState__c secondState = committedStates.get(1); + // swap out for the correct "first" state - since they're inserted at the same time + // there's no deterministic way to order them + if (firstState.RelatedRecordKeys0__c.startsWith(firstAccountId) == false) { + secondState = firstState; + firstState = committedStates.get(1); + } + Assert.isNotNull(firstState.RelatedRecordKeys0__c, 'RelatedRecordKeys0__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys1__c, 'RelatedRecordKeys1__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys2__c, 'RelatedRecordKeys2__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys3__c, 'RelatedRecordKeys3__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys4__c, 'RelatedRecordKeys4__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys5__c, 'RelatedRecordKeys5__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys6__c, 'RelatedRecordKeys6__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys7__c, 'RelatedRecordKeys7__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys8__c, 'RelatedRecordKeys8__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys9__c, 'RelatedRecordKeys9__c should have been filled out'); + Assert.isNotNull(firstState.RelatedRecordKeys10__c, 'RelatedRecordKeys10__c should have been filled out'); + Assert.isNotNull(secondState.RelatedRecordKeys0__c, 'Second RelatedRecordKeys0__c should have been filled out'); + + Assert.areEqual(String.join(fieldKeys0, ','), firstState.RelatedRecordKeys0__c, 'fieldKeys0'); + Assert.areEqual(String.join(fieldKeys1, ','), firstState.RelatedRecordKeys1__c, 'fieldKeys1'); + Assert.areEqual(String.join(fieldKeys2, ','), firstState.RelatedRecordKeys2__c, 'fieldKeys2'); + Assert.areEqual(String.join(fieldKeys3, ','), firstState.RelatedRecordKeys3__c, 'fieldKeys3'); + Assert.areEqual(String.join(fieldKeys4, ','), firstState.RelatedRecordKeys4__c, 'fieldKeys4'); + Assert.areEqual(String.join(fieldKeys5, ','), firstState.RelatedRecordKeys5__c, 'fieldKeys5'); + Assert.areEqual(String.join(fieldKeys6, ','), firstState.RelatedRecordKeys6__c, 'fieldKeys6'); + Assert.areEqual(String.join(fieldKeys7, ','), firstState.RelatedRecordKeys7__c, 'fieldKeys7'); + Assert.areEqual(String.join(fieldKeys8, ','), firstState.RelatedRecordKeys8__c, 'fieldKeys8'); + Assert.areEqual(String.join(fieldKeys9, ','), firstState.RelatedRecordKeys9__c, 'fieldKeys9'); + Assert.areEqual(String.join(fieldKeys10, ','), firstState.RelatedRecordKeys10__c, 'fieldKeys10'); + Assert.areEqual(String.join(secondFieldKeys0, ','), secondState.RelatedRecordKeys0__c, 'secondFieldKeys0'); + } + + @IsTest + static void clearsStateProperly() { + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + insert new RollupState__c(RelatedJobId__c = stubJobId); + + Test.startTest(); + new RollupState().cleanup(new Set{ stubJobId }); + Test.stopTest(); + + Assert.areEqual(0, [SELECT COUNT() FROM RollupState__c]); + } + + @IsTest + static void onlyLoadsStateForRelatedRecords() { + Id stubJobId = RollupTestUtils.createId(AsyncApexJob.SObjectType); + String stubAccountId = RollupTestUtils.createId(Account.SObjectType); + insert new List{ + new RollupState__c(RelatedJobId__c = stubJobId), + new RollupState__c(RelatedJobId__c = stubJobId, RelatedRecordKeys0__c = stubAccountId) + }; + + RollupState state = new RollupState(); + state.loadState(stubJobId, new Set{ stubAccountId }); + state.loadState(stubJobId, new Set{ stubAccountId }); + + Assert.areEqual(1, Limits.getQueryRows()); + Assert.areEqual(1, Limits.getQueries()); + } + + @IsTest + static void alwaysTracksRelatedRecordKeys() { + String firstId = RollupTestUtils.createId(Account.SObjectType) + 'aaa'; + RollupState.maxRelatedKeysLength = firstId.length(); + Rollup__mdt template = new Rollup__mdt(DeveloperName = 'unique'); + RollupState state = new RollupState(); + List range = new String[14]; + String serializedLastState; + String thirdToLastKey; + String secondToLastKey; + String lastKey; + for (Integer index = 0; index <= range.size(); index++) { + String stubAccountId = index == 0 || index == 12 ? firstId : RollupTestUtils.createId(Account.SObjectType) + 'bbb'; + RollupState.GenericInfo info = (RollupState.GenericInfo) state.getState(stubAccountId, template, RollupState.GenericInfo.class); + info.value = index; + switch on index { + when 11 { + thirdToLastKey = stubAccountId; + } + when 13 { + secondToLastKey = stubAccountId; + } + when 14 { + lastKey = stubAccountId; + serializedLastState = JSON.serialize(info.getUntypedState()); + } + } + } + + state.commitState(RollupTestUtils.createId(AsyncApexJob.SObjectType)); + + List committedStates = [ + SELECT + Body0__c, + RelatedRecordKeys0__c, + RelatedRecordKeys1__c, + RelatedRecordKeys2__c, + RelatedRecordKeys3__c, + RelatedRecordKeys4__c, + RelatedRecordKeys5__c, + RelatedRecordKeys6__c, + RelatedRecordKeys7__c, + RelatedRecordKeys8__c, + RelatedRecordKeys9__c, + RelatedRecordKeys10__c + FROM RollupState__c + ]; + Assert.areEqual(2, committedStates.size()); + for (RollupState__c committedState : committedStates) { + if (committedState.RelatedRecordKeys3__c == null) { + Assert.isTrue(committedState.Body0__c.contains(serializedLastState)); + Assert.areEqual(thirdToLastKey, committedState.RelatedRecordKeys0__c); + Assert.areEqual(secondToLastKey, committedState.RelatedRecordKeys1__c); + Assert.areEqual(lastKey, committedState.RelatedRecordKeys2__c); + } else { + Assert.areEqual(firstId, committedState.RelatedRecordKeys0__c); + } + } + } +} diff --git a/extra-tests/classes/RollupStateTests.cls-meta.xml b/extra-tests/classes/RollupStateTests.cls-meta.xml new file mode 100644 index 00000000..800ee428 --- /dev/null +++ b/extra-tests/classes/RollupStateTests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/extra-tests/classes/RollupTestUtils.cls b/extra-tests/classes/RollupTestUtils.cls index d5e72649..5de73bf7 100644 --- a/extra-tests/classes/RollupTestUtils.cls +++ b/extra-tests/classes/RollupTestUtils.cls @@ -14,12 +14,12 @@ public class RollupTestUtils { Rollup.FlowInput flowInput = new Rollup.FlowInput(); flowInput.recordsToRollup = records; - flowInput.lookupFieldOnCalcItem = ContactPointAddress.ParentId.getDescribe().getName(); - flowInput.lookupFieldOnOpObject = Account.Id.getDescribe().getName(); + flowInput.lookupFieldOnCalcItem = ContactPointAddress.ParentId.toString(); + flowInput.lookupFieldOnOpObject = Account.Id.toString(); flowInput.rollupContext = rollupContext; - flowInput.rollupFieldOnCalcItem = ContactPointAddress.PreferenceRank.getDescribe().getName(); - flowInput.rollupFieldOnOpObject = Account.AnnualRevenue.getDescribe().getName(); - flowInput.rollupSObjectName = Account.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); + flowInput.rollupFieldOnCalcItem = ContactPointAddress.PreferenceRank.toString(); + flowInput.rollupFieldOnOpObject = Account.AnnualRevenue.toString(); + flowInput.rollupSObjectName = Account.SObjectType.toString(); flowInput.rollupOperation = rollupOperation; flowInput.splitConcatDelimiterOnCalcItem = false; @@ -93,7 +93,7 @@ public class RollupTestUtils { String currencyIscoCodeFieldName = RollupCurrencyInfo.CURRENCY_ISO_CODE_FIELD_NAME; Set fieldNames = new Set(); for (Schema.SObjectField fieldToken : fieldTokens) { - fieldNames.add(fieldToken.getDescribe().getName()); + fieldNames.add(fieldToken.toString()); } if (fieldNames.contains('Name') == false) { diff --git a/extra-tests/classes/RollupTests.cls b/extra-tests/classes/RollupTests.cls index 4769f840..88c8d191 100644 --- a/extra-tests/classes/RollupTests.cls +++ b/extra-tests/classes/RollupTests.cls @@ -2443,7 +2443,7 @@ private class RollupTests { static void shouldRunSuccessfullyAsBatch() { RollupTestUtils.DMLMock mock = RollupTestUtils.loadAccountIdMock(new List{ new ContactPointAddress(PreferenceRank = 1) }); Rollup.apexContext = TriggerOperation.AFTER_INSERT; - Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable, MaxLookupRowsBeforeBatching__c = 1); + Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1); Test.startTest(); Rollup.countFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc(); @@ -2488,7 +2488,7 @@ private class RollupTests { static void shouldRunAsBatchableWhenSpecificRollupIsBatchable() { RollupTestUtils.DMLMock mock = RollupTestUtils.loadAccountIdMock(new List{ new ContactPointAddress(PreferenceRank = 1) }); Rollup.apexContext = TriggerOperation.AFTER_INSERT; - Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1, ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable); + Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1); Test.startTest(); Rollup.countFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc(); @@ -2934,12 +2934,7 @@ private class RollupTests { insert cpas; RollupTestUtils.DMLMock mock = RollupTestUtils.loadMock(cpas); - Rollup.defaultControl = new RollupControl__mdt( - MaxLookupRowsBeforeBatching__c = 0, - MaxNumberOfQueries__c = 2, - MaxRollupRetries__c = 100, - ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable - ); + Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 0, MaxNumberOfQueries__c = 2, MaxRollupRetries__c = 100); // start as synchronous rollup to allow for one deferral Rollup.specificControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous, MaxNumberOfQueries__c = 4); Rollup.rollupMetadata = new List{ @@ -3001,8 +2996,8 @@ private class RollupTests { static void encapsulatesNamespaceInfoProperly() { Rollup.NamespaceInfo namespaceInfo = Rollup.getNamespaceInfo(); - String rollupObjectName = Rollup__mdt.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); - System.assertEquals(rollupObjectName + '.' + Rollup__mdt.RollupOperation__c.getDescribe().getName(), nameSpaceInfo.safeRollupOperationField); + String rollupObjectName = Rollup__mdt.SObjectType.toString(); + System.assertEquals(rollupObjectName + '.' + Rollup__mdt.RollupOperation__c.toString(), nameSpaceInfo.safeRollupOperationField); System.assertEquals(rollupObjectName, nameSpaceInfo.safeObjectName); System.assertEquals(Rollup.class.getName() == 'Rollup' ? '' : Rollup.class.getName().substringBefore('.Rollup') + '__', nameSpaceInfo.namespace); } diff --git a/extra-tests/testSuites/ApexRollupTestSuite.testSuite-meta.xml b/extra-tests/testSuites/ApexRollupTestSuite.testSuite-meta.xml index a6e19298..6cbdc6ab 100644 --- a/extra-tests/testSuites/ApexRollupTestSuite.testSuite-meta.xml +++ b/extra-tests/testSuites/ApexRollupTestSuite.testSuite-meta.xml @@ -25,6 +25,7 @@ RollupRelationshipFieldFinderTests RollupRepositoryTests RollupSObjectUpdaterTests + RollupStateTests RollupTests RollupTestUtils diff --git a/package.json b/package.json index d89ad754..4a7a79d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apex-rollup", - "version": "1.6.37", + "version": "1.7.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/ExtraCodeCoverage/README.md b/plugins/ExtraCodeCoverage/README.md index 12667eb2..b8d57d31 100644 --- a/plugins/ExtraCodeCoverage/README.md +++ b/plugins/ExtraCodeCoverage/README.md @@ -1,11 +1,11 @@ # Extra Code Coverage - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/plugins/RollupCallback/README.md b/plugins/RollupCallback/README.md index d0776202..1aac5a22 100644 --- a/plugins/RollupCallback/README.md +++ b/plugins/RollupCallback/README.md @@ -104,7 +104,7 @@ public class SubflowRollupDispatcher implements RollupSObjectUpdater.IDispatcher List wrappedRecords = new List(); for (SObject record : records) { SObjectDecorator decorator = new SObjectDecorator(); - decorator.SObjectName = record.getSObjectType().getDescribe().getName(); + decorator.SObjectName = record.getSObjectType().toString(); decorator.RecordId = record.Id; decorator.FieldNames = new List(record.getPopulatedFieldsAsMap().keySet()); wrappedRecords.add(decorator); diff --git a/rollup-namespaced/README.md b/rollup-namespaced/README.md index 362450c5..24a4c628 100644 --- a/rollup-namespaced/README.md +++ b/rollup-namespaced/README.md @@ -18,12 +18,12 @@ For more info, see the base `README`. ## Deployment & Setup - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/rollup-namespaced/sfdx-project.json b/rollup-namespaced/sfdx-project.json index 3bd221e8..dbdb0df5 100644 --- a/rollup-namespaced/sfdx-project.json +++ b/rollup-namespaced/sfdx-project.json @@ -4,8 +4,8 @@ "default": true, "package": "apex-rollup-namespaced", "path": "rollup-namespaced/source/rollup", - "versionName": "Fixes mutually exclusive polymorphic where clauses replacing children in RollupCalcItemReplacer", - "versionNumber": "1.1.30.0", + "versionName": "New RollupState implementation to stop running out of memory on large full recalcs", + "versionNumber": "1.2.0.0", "versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.", "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest", "unpackagedMetadata": { @@ -18,21 +18,13 @@ ], "namespace": "please", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "60.0", + "sourceApiVersion": "62.0", "packageAliases": { "apex-rollup-namespaced": "0Ho6g000000PBMjCAO", - "apex-rollup-namespaced@1.1.18": "04t6g000008Ob1iAAC", - "apex-rollup-namespaced@1.1.19": "04t6g000008ObCsAAK", - "apex-rollup-namespaced@1.1.20": "04t6g000008ObCxAAK", - "apex-rollup-namespaced@1.1.21": "04t6g000008ObLCAA0", - "apex-rollup-namespaced@1.1.22": "04t6g000008ObNDAA0", - "apex-rollup-namespaced@1.1.23": "04t6g000008ObNSAA0", - "apex-rollup-namespaced@1.1.24": "04t6g000008ObbqAAC", - "apex-rollup-namespaced@1.1.25": "04t6g000008Obc0AAC", - "apex-rollup-namespaced@1.1.26": "04t6g000008ObeVAAS", "apex-rollup-namespaced@1.1.27": "04t6g000008OfJkAAK", "apex-rollup-namespaced@1.1.28": "04t6g000008OfKnAAK", "apex-rollup-namespaced@1.1.29": "04t6g000008OfMjAAK", - "apex-rollup-namespaced@1.1.30": "04t6g000008OfSJAA0" + "apex-rollup-namespaced@1.1.30": "04t6g000008OfSJAA0", + "apex-rollup-namespaced@1.2.0": "04t6g000008OfU0AAK" } } diff --git a/rollup/app/applications/Rollup.app-meta.xml b/rollup/app/applications/Rollup.app-meta.xml index 66171b3d..d06e01a3 100644 --- a/rollup/app/applications/Rollup.app-meta.xml +++ b/rollup/app/applications/Rollup.app-meta.xml @@ -16,5 +16,6 @@ Standard Recalculate_Rollup + RollupState__c Lightning diff --git a/rollup/app/permissionsets/See_Rollup_App.permissionset-meta.xml b/rollup/app/permissionsets/See_Rollup_App.permissionset-meta.xml index 8f70ad6f..832c9ba1 100644 --- a/rollup/app/permissionsets/See_Rollup_App.permissionset-meta.xml +++ b/rollup/app/permissionsets/See_Rollup_App.permissionset-meta.xml @@ -1,4 +1,4 @@ - + Rollup @@ -8,6 +8,80 @@ Rollup true + + true + RollupState__c.Body0__c + true + + + true + RollupState__c.RelatedJobId__c + true + + + true + RollupState__c.RelatedRecordKeys0__c + true + + + true + RollupState__c.RelatedRecordKeys1__c + true + + + true + RollupState__c.RelatedRecordKeys2__c + true + + + true + RollupState__c.RelatedRecordKeys3__c + true + + + true + RollupState__c.RelatedRecordKeys4__c + true + + + true + RollupState__c.RelatedRecordKeys5__c + true + + + true + RollupState__c.RelatedRecordKeys6__c + true + + + true + RollupState__c.RelatedRecordKeys7__c + true + + + true + RollupState__c.RelatedRecordKeys8__c + true + + + true + RollupState__c.RelatedRecordKeys9__c + true + + + true + RollupState__c.RelatedRecordKeys10__c + true + + + true + true + true + true + true + RollupState__c + true + Allows users to access the Rollup app false @@ -16,4 +90,8 @@ Recalculate_Rollup Visible + + RollupState__c + Visible + diff --git a/rollup/app/tabs/RollupState__c.tab-meta.xml b/rollup/app/tabs/RollupState__c.tab-meta.xml new file mode 100644 index 00000000..56c13653 --- /dev/null +++ b/rollup/app/tabs/RollupState__c.tab-meta.xml @@ -0,0 +1,5 @@ + + + true + Custom78: Map + diff --git a/rollup/core/classes/Rollup.cls b/rollup/core/classes/Rollup.cls index 6efa2a06..f573e532 100644 --- a/rollup/core/classes/Rollup.cls +++ b/rollup/core/classes/Rollup.cls @@ -298,7 +298,6 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje Boolean matches(Object calcItem); } - @SuppressWarnings('PMD.UnusedLocalVariable') global enum InvocationPoint { FROM_APEX, FROM_INVOCABLE, @@ -377,14 +376,14 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje } protected Rollup__mdt addOrderBys(Rollup__mdt localMeta, List orderBys, Schema.SObjectField calcItemRollupField) { - if (localMeta.RollupFieldOnCalcItem__c == calcItemRollupField.getDescribe().getName()) { + if (localMeta.RollupFieldOnCalcItem__c == calcItemRollupField.toString()) { localMeta = appendOrderByMetadata(localMeta, orderBys); } return localMeta; } protected void addLimitToMetadata(Rollup__mdt localMeta, Integer limitAmount, Schema.SObjectField calcItemRollupField) { - if (localMeta.RollupFieldOnCalcItem__c == calcItemRollupField.getDescribe().getName()) { + if (localMeta.RollupFieldOnCalcItem__c == calcItemRollupField.toString()) { localMeta.LimitAmount__c = limitAmount; localMeta.IsFullRecordSet__c = true; } @@ -483,9 +482,9 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje public class NamespaceInfo { @AuraEnabled - public final String safeObjectName = Rollup__mdt.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); + public final String safeObjectName = Rollup__mdt.SObjectType.toString(); @AuraEnabled - public final String safeRollupOperationField = this.safeObjectName + '.' + Rollup__mdt.RollupOperation__c.getDescribe().getName(); + public final String safeRollupOperationField = this.safeObjectName + '.' + Rollup__mdt.RollupOperation__c.toString(); @AuraEnabled public final String namespace = Rollup.class.getName() == 'Rollup' ? '' : Rollup.class.getName().substringBefore('.Rollup') + '__'; } @@ -577,15 +576,25 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje appendQueryCount(wrappedMeta, matchingMeta, matchingMeta.CalcItemWhereClause__c, childType); } - List processors = transformWrappedMetadataToFullRecalcRollups(childToMetaWrapper, localInvokePoint); + List processors = transformWrappedMetadataToFullRecalcRollups(childToMetaWrapper, localInvokePoint); isFullRecalcApp = false; + if (processors.size() > 1 && processors[0].rollupControl.ShouldRunAs__c != RollupMetaPicklists.ShouldRunAs.Synchronous) { + RollupFullRecalcProcessor conductor = processors[0]; + while (processors.size() > 1) { + conductor.rollups.add(processors.remove(1)); + } + return conductor.runCalc(); + } return batch(processors, localInvokePoint); } @AuraEnabled global static String getBatchRollupStatus(String jobId) { - jobId = currentJobId ?? jobId; - return [SELECT Status FROM AsyncApexJob WHERE Id = :jobId LIMIT 1]?.Status; + String jobStatus = [SELECT Status FROM AsyncApexJob WHERE Id = :jobId LIMIT 1]?.Status; + if ([SELECT COUNT() FROM RollupState__c WHERE RelatedJobId__c = :jobId LIMIT 1] > 0) { + jobStatus = 'Processing'; + } + return jobStatus; } /** @@ -1964,12 +1973,12 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje List triggerRecords = getTriggerRecords(); Rollup__mdt meta = appendOrderByMetadata( new Rollup__mdt( - CalcItem__c = triggerRecords.getSObjectType()?.getDescribe().getName(), - LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName(), - LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.getDescribe().getName(), - LookupObject__c = lookupSObjectType.getDescribe().getName(), - RollupFieldOnCalcItem__c = operationFieldOnCalcItem.getDescribe().getName(), - RollupFieldOnLookupObject__c = operationFieldOnOperationObject.getDescribe().getName(), + CalcItem__c = triggerRecords.getSObjectType()?.toString(), + LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.toString(), + LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.toString(), + LookupObject__c = lookupSObjectType.toString(), + RollupFieldOnCalcItem__c = operationFieldOnCalcItem.toString(), + RollupFieldOnLookupObject__c = operationFieldOnOperationObject.toString(), RollupOperation__c = rollupOperation.name() ), orderByMetas @@ -2152,6 +2161,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje orderByFields.add(meta.GrandparentRelationshipFieldPath__c.substringBeforeLast('.') + '.Id'); } orderByFields.add(meta.LookupFieldOnCalcItem__c); + orderByFields.add('Id'); } return '\nORDER BY ' + String.join(orderByFields, ','); } @@ -2441,10 +2451,10 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje String relationshipName = fieldToken.getDescribe().getRelationshipName(); Integer relationshipIndex = meta.GrandparentRelationshipFieldPath__c.indexOf(relationshipName) + relationshipName.length(); String priorFieldPath = meta.GrandparentRelationshipFieldPath__c.substring(0, relationshipIndex) + '.Id'; - if (meta.OneToManyGrandparentFields__c != null && meta.OneToManyGrandparentFields__c.contains(fieldToken.getDescribe().getName())) { + if (meta.OneToManyGrandparentFields__c != null && meta.OneToManyGrandparentFields__c.contains(fieldToken.toString())) { List fieldPathTuples = meta.OneToManyGrandparentFields__c.split(','); for (String fieldPathTuple : fieldPathTuples) { - if (fieldToken.getDescribe().getName().equalsIgnoreCase(fieldPathTuple.substringAfter('.'))) { + if (fieldToken.toString().equalsIgnoreCase(fieldPathTuple.substringAfter('.'))) { priorFieldPath = 'Id'; break; } @@ -2696,11 +2706,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje for (Rollup__mdt rollupMetadata : rollupOperations) { Op rollupOp = OP_NAME_TO_OP.get(rollupMetadata.RollupOperation__c.toUpperCase()); - if ( - rollupMetadata.RollupGrouping__r.Id != null && - rollupMetadata.CalcItem__c != null && - rollupMetadata.CalcItem__c != sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName() - ) { + if (rollupMetadata.RollupGrouping__r.Id != null && rollupMetadata.CalcItem__c != null && rollupMetadata.CalcItem__c != sObjectType.toString()) { Schema.DescribeSObjectResult childDescribe = init.getDescribeFromName(rollupMetadata.CalcItem__c); sObjectType = childDescribe.getSObjectType(); calcItemFields = childDescribe.fields.getMap(); diff --git a/rollup/core/classes/RollupAsyncProcessor.cls b/rollup/core/classes/RollupAsyncProcessor.cls index f601183b..8f92c21b 100644 --- a/rollup/core/classes/RollupAsyncProcessor.cls +++ b/rollup/core/classes/RollupAsyncProcessor.cls @@ -16,8 +16,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme private Boolean wasConvertedToFullRecalculation = false; private Boolean isFromBatchExecute = false; private Map groupedLookupToCalcItems; - private Boolean isRecursivePopulation = false; + protected Id jobId; protected Boolean isTimingOut = false; protected Boolean isProcessed = false; protected Boolean overridesRunCalc = false; @@ -25,6 +25,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme protected RollupFullRecalcProcessor fullRecalcProcessor; protected final SObjectType calcItemType; protected final Set recordIds; + protected RollupFinalizer finalizer; private static Set hashedRollups = new Set(); @TestVisible @@ -73,14 +74,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme set; } - private final Set previouslyResetParents { - get { - this.previouslyResetParents = this.previouslyResetParents ?? new Set(); - return this.previouslyResetParents; - } - set; - } - private final Set uniqueParentFields { get { this.uniqueParentFields = this.uniqueParentFields ?? new Set(); @@ -230,6 +223,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme * `System.AsyncException: Queueable cannot be implemented with other system interfaces` exception */ public Database.QueryLocator start(Database.BatchableContext context) { + this.jobId = this.jobId ?? context?.getJobId(); RollupRepository repo = this.preStart(); this.logger.save(); return repo.getLocator(); @@ -242,7 +236,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.innerBatchExecute(scope); this.isFromBatchExecute = false; - this.executeFinish(new Map{ 'previouslyResetParents size' => '' + this.previouslyResetParents.size() }); + this.executeFinish(); } public virtual void finish(Database.BatchableContext context) { @@ -300,8 +294,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if (this.childToUnexpectedFullRecalc.isEmpty() == false) { equality = '='; } - this.populateObjectFields(this.rollups); - String query = 'SELECT Id FROM Organization WHERE Name ' + equality + '\'' + UserInfo.getOrganizationName() + '\''; + this.populateObjectFields(this.rollups, false); + String query = 'SELECT Id FROM Organization WHERE Name ' + equality + ' \'' + UserInfo.getOrganizationName() + '\''; RollupRepository repo = new RollupRepository(this.runAsMode); if (this.rollups.isEmpty() == false) { RollupAsyncProcessor firstRollup = this.rollups.get(0); @@ -309,7 +303,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme query = RollupQueryBuilder.Current.getQuery( firstRollup.lookupObj, new List(this.lookupObjectToUniqueFieldNames.get(firstRollup.lookupObj)), - firstRollup.lookupFieldOnLookupObject.getDescribe().getName(), + firstRollup.lookupFieldOnLookupObject.toString(), '=' ); } @@ -328,7 +322,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme return props; } - protected String getNoProcessId() { + public String getNoProcessId() { return 'No process Id'; } @@ -361,14 +355,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme // swap off on which async process is running to achieve infinite scaling Boolean canEnqueue = this.getCanEnqueue(); RollupAsyncProcessor roll; - if ( - this.rollups.size() == 1 && - this.rollups[0] instanceof RollupFullRecalcProcessor && - (System.isBatch() == false || this.isBatch() == false && this.rollups[0].isBatch() == false) - ) { - roll = this.rollups[0]; - } else if (this instanceof RollupFullRecalcProcessor && this.isBatch() == false) { + if (this instanceof RollupFullRecalcProcessor) { roll = this; + } else if (this.rollups.size() == 1 && this.rollups[0] instanceof RollupFullRecalcProcessor) { + roll = this.rollups[0]; } else if (System.isBatch() == false && (shouldRunAsBatch || canEnqueue == false)) { // safe to batch because the QueryLocator will only return one type of SObject // we have to re-initialize the rollup because it's the Queueable inner class @@ -410,7 +400,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme super(rollupInvokePoint); } - private QueueableProcessor(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) { + protected QueueableProcessor(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) { super(invokePoint, calcItems, oldCalcItems); } @@ -455,27 +445,36 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } protected virtual override String startAsyncWork() { - if (System.isQueueable()) { - hasAlreadyAsyncEnqueued = true; + if (this.finalizer != null && System.isQueueable() && (System.Limits.getQueueableJobs() == 1 || hasAlreadyAsyncEnqueued)) { + this.finalizer.addCaboose(this); + return this.getNoProcessId(); } + hasAlreadyAsyncEnqueued = hasAlreadyAsyncEnqueued || System.isBatch() || System.isQueueable() || System.isFuture(); return System.enqueueJob(this); } public void execute(System.QueueableContext qc) { - this.logger.log('Starting queueable', this, System.LoggingLevel.INFO); + this.logger.log('Starting ' + this.getTypeName() + ' queueable', this, System.LoggingLevel.INFO); AdditionalContext context = new AdditionalContext(qc); this.setCurrentJobId(context.getJobId()); - System.attachFinalizer(new RollupFinalizer()); + this.jobId = this.jobId ?? context.getJobId(); + this.finalizer = this.finalizer ?? this.getFinalizer(); + System.attachFinalizer(this.finalizer); this.performWork(); if (this.fullRecalcProcessor?.isBatch() != true) { this.finish(context); + } else { + this.executeFinish(); } - this.executeFinish(null); } } + protected virtual RollupFinalizer getFinalizer() { + return new RollupFinalizer(); + } + private class AdditionalContext implements Database.BatchableContext { private final String jobId; @@ -496,23 +495,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - private class RollupEndSnapshot { - public RollupEndSnapshot(Map additionalContext) { - this.additionalContext = additionalContext; - } - public Long currentHeapUsage { - get { - return Limits.getHeapSize(); - } - } - public Long heapLimit { - get { - return Limits.getLimitHeapSize(); - } - } - public Map additionalContext { get; set; } - } - protected virtual String beginAsyncRollup() { this.isTimingOut = false; this.logger.log('about to start for ' + this.getTypeName(), this, System.LoggingLevel.INFO); @@ -551,9 +533,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme List recordsToReset = new List(); for (SObject lookupItem : lookupItemsToAssign) { - if ( - this.parentRollupFieldHasBeenReset('' + roll.lookupFieldOnLookupObject, lookupItem, '' + roll.lookupObj, '' + roll.opFieldOnLookupObject) == false - ) { + if (this.parentRollupFieldHasBeenReset('' + roll.lookupObj, '' + roll.opFieldOnLookupObject) == false) { recordsToReset.add(lookupItem); } } @@ -695,7 +675,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.isProcessed = true; this.handleMultipleDMLRollupsEnqueuedInTheSameTransaction(rollups); this.preProcessRollups(rollups); - this.populateObjectFields(rollups); + this.populateObjectFields(rollups, false); Map updatedLookupRecords = new Map(); Map grandparentRollups = new Map(); @@ -728,25 +708,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme List localLookupItems = this.getLookupItems(calcItemsByLookupField, updatedLookupRecords, roll); this.logger.log('starting rollup for:', roll, System.LoggingLevel.INFO); updatedLookupRecords.putAll(this.getUpdatedLookupItemsByRollup(roll, calcItemsByLookupField, localLookupItems)); - roll.traversal = null; - } - - // full batch recalc housekeeping - allow next batch chunk to reference previously reset parents - // this might look inefficient - and it probably is! - but this particular loop needs to happen - // after ALL of the rollups in the loop above have completed - for (SObject lookupRecord : updatedLookupRecords.values()) { - for (RollupAsyncProcessor roll : rollups) { - if (lookupRecord.getSObjectType() == roll.lookupObj) { - this.storeParentResetField(roll.lookupFieldOnLookupObject, lookupRecord); - } - } } if (this.isTimingOut) { this.getDML().forceSyncUpdate(); } this.cleanup(); - this.getDML().updateRecords(); + this.getDML().setFinalizer(this.finalizer).updateRecords(); this.populateOtherDeferredRollups(); this.processDeferredRollups(); } @@ -782,9 +750,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - protected Boolean parentRollupFieldHasBeenReset(String parentKeyField, SObject parent, String parentTypeName, String parentRollupField) { - return this.previouslyResetParents.contains(String.valueOf(parent.get(parentKeyField))) || - this.uniqueParentFields.contains(parentTypeName + parentRollupField); + protected Boolean parentRollupFieldHasBeenReset(String parentTypeName, String parentRollupField) { + return this.uniqueParentFields.contains(parentTypeName + parentRollupField); } protected void storeUniqueParentFields(Rollup__mdt meta) { @@ -805,10 +772,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme return (Object) meta.FullRecalculationDefaultNumberValue__c ?? (Object) meta.FullRecalculationDefaultStringValue__c; } - private void storeParentResetField(Schema.SObjectField lookupField, SObject parent) { - String resetKey = String.valueOf(parent.get(lookupField)); + private void storeParentResetField(String resetKey, SObject parent) { if (resetKey != null) { - this.previouslyResetParents.add(resetKey); this.fullRecalcProcessor?.trackParentRecord(parent); } } @@ -897,7 +862,11 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme rollupsToProcess.addAll(additionalRollups); } else if (rollup.overridesRunCalc) { rollupsToProcess.remove(index); - rollup.runCalc(); + if (System.isQueueable() == false || hasAlreadyAsyncEnqueued == false || this.finalizer == null) { + rollup.runCalc(); + } else { + this.finalizer.addCaboose(rollup); + } } else if (rollup.getTypeName() == RollupFullBatchRecalculator.class.getName()) { rollupsToProcess.remove(index); RollupFullBatchRecalculator fullRecalc = (RollupFullBatchRecalculator) rollup; @@ -910,7 +879,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } else if ( rollup.metadata?.RollupGrouping__r.Id != null && rollup.calcItems.isEmpty() == false && - rollup.calcItems[0].getSObjectType().getDescribe().getName() != rollup.metadata.CalcItem__c + rollup.calcItems[0].getSObjectType().toString() != rollup.metadata.CalcItem__c ) { List groupedProcessors = groupingToRollups.get(rollup.metadata?.RollupGrouping__r.Id) ?? new List(); groupedProcessors.add(rollup); @@ -1026,6 +995,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme List localLookupItems = new List(); Set lookupItemKeys = new Set(calcItemsByLookupField.keySet()); if (this.fullRecalcProcessor != null) { + this.fullRecalcProcessor.getState()?.loadState(this.jobId, lookupItemKeys); lookupItemKeys.addAll(this.fullRecalcProcessor.getRecordIdentifiers()); } for (String lookupId : calcItemsByLookupField.keySet()) { @@ -1093,7 +1063,14 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.rollups.addAll(this.deferredRollups); this.deferredRollups.clear(); this.logger.log('deferred rollups remaining:', this.rollups, System.LoggingLevel.WARN); - this.getAsyncRollup()?.beginAsyncRollup(); + RollupAsyncProcessor conductor = this.rollups.size() == 1 && this.rollups[0] instanceof RollupFullRecalcProcessor + ? this.rollups[0] + : this.getAsyncRollup(); + if (conductor != null && this.finalizer != null) { + this.finalizer.addCaboose(conductor); + } else if (conductor != null) { + conductor.startAsyncWork(); + } // for synchronous runs with a timeout, we need to ensure this.rollups is re-cleared here // to prevent re-running through the same logic in runCalc() this.rollups.clear(); @@ -1110,7 +1087,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - private void populateObjectFields(List rollups) { + private void populateObjectFields(List rollups, Boolean isRecursivePopulation) { this.lookupObjectToUniqueFieldNames = this.lookupObjectToUniqueFieldNames ?? new Map>(); this.calcObjectToUniqueFieldNames = this.calcObjectToUniqueFieldNames ?? new Map>(); List combinedMeta = new List(); @@ -1122,10 +1099,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.mapFields( this.lookupObjectToUniqueFieldNames, roll.lookupObj, - new Set{ roll.opFieldOnLookupObject.getDescribe().getName(), roll.lookupFieldOnLookupObject.getDescribe().getName() } + new Set{ roll.opFieldOnLookupObject.toString(), roll.lookupFieldOnLookupObject.toString() } ); - Set childFieldsToAdd = new Set{ roll.opFieldOnCalcItem.getDescribe().getName(), roll.lookupFieldOnCalcItem.getDescribe().getName() }; + Set childFieldsToAdd = new Set{ roll.opFieldOnCalcItem.toString(), roll.lookupFieldOnCalcItem.toString() }; if (String.isNotBlank(roll.metadata.CalcItemWhereClause__c)) { childFieldsToAdd.addAll(RollupEvaluator.getWhereEval(roll.metadata.CalcItemWhereClause__c, roll.calcItemType).getQueryFields()); } @@ -1134,23 +1111,21 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } this.mapFields(this.calcObjectToUniqueFieldNames, roll.calcItemType, childFieldsToAdd); if (roll.isGrouped()) { - this.isRecursivePopulation = true; - this.populateObjectFields(roll.rollups); - this.isRecursivePopulation = false; + this.populateObjectFields(roll.rollups, true); } } - if (this.isRecursivePopulation == false && this.calcItems != null && this.calcItemReplacer?.hasProcessedMetadata(combinedMeta, this.calcItems) == false) { - Integer oldHashCode = this.calcItems?.hashCode(); + if (isRecursivePopulation) { + return; + } + + if (this.calcItems != null && this.calcItemReplacer?.hasProcessedMetadata(combinedMeta, this.calcItems) == false) { + Integer oldHashCode = this.calcItems.hashCode(); this.calcItems = this.calcItemReplacer.replace(this.calcItems, combinedMeta); this.replaceCalcItems(oldHashCode, this.calcItems, rollups, CollectionType.ITERABLE); } - if ( - this.isRecursivePopulation == false && - this.oldCalcItems != null && - this.calcItemReplacer?.hasProcessedMetadata(combinedMeta, this.oldCalcItems.values()) == false - ) { - Integer oldHashCode = this.oldCalcItems?.hashCode(); + if (this.oldCalcItems != null && this.calcItemReplacer?.hasProcessedMetadata(combinedMeta, this.oldCalcItems.values()) == false) { + Integer oldHashCode = this.oldCalcItems.hashCode(); this.oldCalcItems = RollupFieldInitializer.Current.createSafeMap(this.calcItemReplacer.replace(this.oldCalcItems.values(), combinedMeta)); this.replaceCalcItems(oldHashCode, this.oldCalcItems, rollups, CollectionType.DICTIONARY); } @@ -1189,7 +1164,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme for (Integer index = potentialRollups.size() - 1; index >= 0; index--) { RollupAsyncProcessor processor = potentialRollups[index]; Map fieldMap = processor.lookupObj?.getDescribe().fields.getMap(); - if (fieldMap?.containsKey(processor.opFieldOnLookupObject.getDescribe().getName()) == false) { + if (fieldMap?.containsKey(processor.opFieldOnLookupObject.toString()) == false) { potentialRollups.remove(index); } } @@ -1297,7 +1272,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } for (SObject calcItem : roll.calcItems) { if (calcItem.getSObjectType() == roll.calcItemType) { - String lookupKey = (String) calcItem.get(roll.lookupFieldOnCalcItem.getDescribe().getName()); + String lookupKey = (String) calcItem.get(roll.lookupFieldOnCalcItem.toString()); if (String.isNotBlank(lookupKey)) { uniqueIds.add(lookupKey); } @@ -1324,8 +1299,9 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if (roll.wasConvertedToFullRecalculation) { for (SObject lookupItem : lookupItems) { if (lookupItem.getSObjectType() == roll.lookupObj) { - recordsToUpdate.put(String.valueOf(lookupItem.get(roll.lookupFieldOnLookupObject)), lookupItem); - this.storeParentResetField(roll.lookupFieldOnLookupObject, lookupItem); + String resetKey = String.valueOf(lookupItem.get(roll.lookupFieldOnLookupObject)); + recordsToUpdate.put(resetKey, lookupItem); + this.storeParentResetField(resetKey, lookupItem); } } return recordsToUpdate.values(); @@ -1337,7 +1313,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if (roll.isFullRecalc && roll.op.name().contains('DELETE')) { rollupOp = this.getOpMap().get(roll.op.name().substringAfter('_')); } - Object fallbackValue = this.getDefaultValue(roll.metadata); + Object defaultVal = this.getDefaultValue(roll.metadata); + Boolean hasNotAlreadyBeenReset = this.parentRollupFieldHasBeenReset('' + roll.lookupObj, '' + roll.opFieldOnLookupObject) == false; for (Integer index = lookupItems.size() - 1; index >= 0; index--) { SObject lookupRecord = lookupItems[index]; if (lookupRecord.getSObjectType() != roll.lookupObj) { @@ -1364,18 +1341,15 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme // this guard clause prevents a full recalculation from taking place unnecessarily if a reparenting operation // has already been setup to adjust the lookup item's rollup field if (localCalcItems.isEmpty() == false || (localCalcItems.isEmpty() && oldLookupItems.isEmpty())) { - Object priorValToUse = roll.isFullRecalc && - this.parentRollupFieldHasBeenReset('' + roll.lookupFieldOnLookupObject, lookupRecord, '' + roll.lookupObj, '' + roll.opFieldOnLookupObject) == false - ? fallbackValue - : priorVal; + Object priorValToUse = roll.isFullRecalc && hasNotAlreadyBeenReset ? defaultVal : priorVal; calc = this.getCalculator(rollupOp, calc, roll, bag, lookupRecord, priorValToUse, key, roll.lookupFieldOnCalcItem); this.conditionallyPerformUpdate(priorVal, calc, lookupRecord, roll, recordsToUpdate, ParentUpdateType.LOOKUP, localCalcItems); } } else { this.logger.log('About to reset parent field for', lookupRecord, System.LoggingLevel.DEBUG); - this.getDML().updateField(roll.opFieldOnLookupObject, lookupRecord, fallbackValue); + this.getDML().updateField(roll.opFieldOnLookupObject, lookupRecord, defaultVal); } - roll.performGroupedRollup(calc, rollupOp, lookupRecord, key, recordsToUpdate); + roll.performGroupedRollup(calc, rollupOp, lookupRecord, key, recordsToUpdate, defaultVal); } this.removeRolledUpValuesFromReparentedRecords(lookupItems, oldLookupItems, recordsToUpdate, roll); @@ -1402,8 +1376,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if (priorLookup != calcItem.get(roll.lookupFieldOnCalcItem) && roll.traversal == null) { this.populateOldLookupItems(priorLookup, oldCalcItem, oldLookupItems); - } else if (roll.traversal?.isUltimatelyReparented(calcItem, roll.lookupFieldOnCalcItem.getDescribe().getName()) == true) { - Id oldLookupId = roll.traversal.getOldLookupId(calcItem, roll.lookupFieldOnCalcItem.getDescribe().getName()); + } else if (roll.traversal?.isUltimatelyReparented(calcItem, roll.lookupFieldOnCalcItem.toString()) == true) { + Id oldLookupId = roll.traversal.getOldLookupId(calcItem, roll.lookupFieldOnCalcItem.toString()); if (oldLookupId != null) { this.populateOldLookupItems(oldLookupId, oldCalcItem, oldLookupItems); } @@ -1530,11 +1504,12 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if (priorVal != newVal) { this.getDML().updateField(roll.opFieldOnLookupObject, lookupRecord, newVal); } + this.storeParentResetField(key, lookupRecord); recordsToUpdate.put(key, lookupRecord); this.logger.log(logKey + ' record after rolling up:', lookupRecord, System.LoggingLevel.DEBUG); } - private void performGroupedRollup(RollupCalculator calc, Op op, SObject lookupRecord, String key, Map recordsToUpdate) { + private void performGroupedRollup(RollupCalculator calc, Op op, SObject lookupRecord, String key, Map recordsToUpdate, Object defaultVal) { if (this.isGrouped() == false) { return; } @@ -1553,7 +1528,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - calc.setDefaultValues(key, this.getDefaultValue(this.metadata)); + calc.setDefaultValues(key, defaultVal); calc.setMultiCurrencyInfo(lookupRecord); this.conditionallyPerformUpdate( @@ -1575,9 +1550,11 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.getCachedApexOperations().clear(); } - private void executeFinish(Map additionalContext) { - RollupEndSnapshot snapshot = new RollupEndSnapshot(additionalContext); - this.logger.log(this.getTypeName() + ' execute end', snapshot, System.LoggingLevel.INFO); + private void executeFinish() { + if (this.jobId != null) { + this.fullRecalcProcessor?.getState()?.commitState(this.jobId); + } + this.logger.log(this.getTypeName() + ' execute end', System.LoggingLevel.INFO); this.logger.save(); } } diff --git a/rollup/core/classes/RollupCalcItemReplacer.cls b/rollup/core/classes/RollupCalcItemReplacer.cls index 71ffa67f..03d5fc75 100644 --- a/rollup/core/classes/RollupCalcItemReplacer.cls +++ b/rollup/core/classes/RollupCalcItemReplacer.cls @@ -27,7 +27,7 @@ public without sharing class RollupCalcItemReplacer { if (fieldToken?.getDescribe().isCalculated() == false && fieldToken?.getDescribe().isUpdateable() == true) { target.put(fieldToken, value); } else { - target = serializeReplace(target, fieldToken.getDescribe().getName(), value); + target = serializeReplace(target, fieldToken.toString(), value); } return target; } @@ -76,7 +76,7 @@ public without sharing class RollupCalcItemReplacer { this.repo = this.repo ?? new RollupRepository(RollupMetaPicklists.getAccessLevel(metadata[0])); SObjectType calcType = calcItems[0].getSObjectType(); for (Rollup__mdt meta : metadata) { - if (meta.CalcItem__c != calcType.getDescribe().getName()) { + if (meta.CalcItem__c != calcType.toString()) { continue; } diff --git a/rollup/core/classes/RollupCalculator.cls b/rollup/core/classes/RollupCalculator.cls index 82e7e8e4..a0e14740 100644 --- a/rollup/core/classes/RollupCalculator.cls +++ b/rollup/core/classes/RollupCalculator.cls @@ -18,7 +18,7 @@ public without sharing abstract class RollupCalculator { protected final Boolean isChangedFieldCalc; protected final SObjectType calcItemSObjectType; - protected List childrenIds = new List(); + protected Set childrenIds = new Set(); protected Set distinctValues = new Set(); protected Rollup.Op op; protected String lookupKeyQuery; @@ -31,6 +31,7 @@ public without sharing abstract class RollupCalculator { protected String parentIsoCode; protected Boolean isFullRecalc = false; protected Boolean isDistinct = false; + protected Boolean hasCustomStateCalculation = false; @TestVisible private static Factory testFactory; @@ -140,10 +141,14 @@ public without sharing abstract class RollupCalculator { lookupRecordKey + '\'' + (String.isBlank(metadata.CalcItemWhereClause__c) ? '' : ' AND (' + metadata.CalcItemWhereClause__c + ')'); - this.childrenIds = new List(); + this.childrenIds = new Set(); } public virtual Object getReturnValue() { + if (this.state != null && this.hasCustomStateCalculation == false) { + RollupState.GenericInfo possibleInfo = (RollupState.GenericInfo) this.state.getState(this.lookupRecordKey, this.metadata, RollupState.GenericInfo.class); + possibleInfo.value = this.returnVal != this.defaultVal ? this.returnVal : possibleInfo.value; + } return this.returnVal; } @@ -226,7 +231,7 @@ public without sharing abstract class RollupCalculator { continue; } else if (this.isCDCUpdate) { // here we don't exclude items because the calc items have already been updated - this.childrenIds = new List(); + this.childrenIds = new Set(); this.shouldTriggerFullRecalc = true; break; } else { @@ -336,9 +341,7 @@ public without sharing abstract class RollupCalculator { } } for (SObject item : items) { - if (item.Id != null) { - this.childrenIds.add(item.Id); - } + this.childrenIds.add(item?.Id); SObject transformedItem = this.getTransformedCalcItem(item); WinnowResult result = new WinnowResult(transformedItem, this.opFieldOnCalcItem); Boolean shouldAddToResults = this.eval?.matches(transformedItem) != false; @@ -376,6 +379,7 @@ public without sharing abstract class RollupCalculator { break; } } + this.childrenIds.remove(null); if (this.metadata.LimitAmount__c != null) { // we can only safely remove the items after sorting while (winnowedItems.size() > this.metadata.LimitAmount__c && winnowedItems.isEmpty() == false) { @@ -386,11 +390,17 @@ public without sharing abstract class RollupCalculator { this.op = Rollup.Op.valueOf(this.metadata.RollupOperation__c.substringAfter('_')); } } + if (this.state != null && this.hasCustomStateCalculation == false) { + RollupState.GenericInfo info = (RollupState.GenericInfo) this.state.getState(this.lookupRecordKey, this.metadata, RollupState.GenericInfo.class); + if (info?.value != null) { + winnowedItems.add(new WinnowResult(info.value)); + } + } return winnowedItems; } - protected virtual Object calculateNewAggregateValue(Rollup.Op op, List objIds) { + protected virtual Object calculateNewAggregateValue(Rollup.Op op, Iterable objIds) { String operationName = Rollup.getBaseOperationName(op.name()); String alias = operationName.toLowerCase() + 'Field'; List aggregate = this.tryQuery(this.calcItemSObjectType, new Set{ operationName + '(' + this.opFieldOnCalcItem + ')' + alias }, objIds); @@ -401,7 +411,7 @@ public without sharing abstract class RollupCalculator { return oldCalcItem != null && calcItem?.get(this.lookupKeyField) != oldCalcItem.get(this.lookupKeyField); } - protected Object performBaseCalculation(Rollup.Op op, List objIds) { + protected Object performBaseCalculation(Rollup.Op op, Iterable objIds) { this.returnVal = RollupFieldInitializer.Current.getDefaultValue(this.opFieldOnLookupObject); if (this.isRecursiveRecalc == false) { @@ -482,7 +492,7 @@ public without sharing abstract class RollupCalculator { public override void setDefaultValues(String lookupRecordKey, Object priorVal) { super.setDefaultValues(lookupRecordKey, priorVal); this.distinctValues = new Set(); - this.isIdCount = this.opFieldOnCalcItem.getDescribe().getName() == 'Id'; + this.isIdCount = this.opFieldOnCalcItem.toString() == 'Id'; Object defaultVal = RollupFieldInitializer.Current.getDefaultValue(opFieldOnLookupObject); if (defaultVal != 0 && this.returnVal != defaultVal && this.isIdCount == false) { this.distinctValues.add(this.returnVal); @@ -515,7 +525,7 @@ public without sharing abstract class RollupCalculator { this.handleShortCircuit(result); } - protected override Object calculateNewAggregateValue(Rollup.Op op, List objIds) { + protected override Object calculateNewAggregateValue(Rollup.Op op, Iterable objIds) { if (this.shouldTriggerFullRecalc == true) { this.distinctValues = new Set(); } @@ -544,7 +554,7 @@ public without sharing abstract class RollupCalculator { } } else { List results = this.repo.get(); - String calcItemOpField = this.opFieldOnCalcItem.getDescribe().getName(); + String calcItemOpField = this.opFieldOnCalcItem.toString(); for (SObject res : results) { // have to use the String representation of the this.opFieldOnCalcItem to avoid: // System.SObjectException: SObject.FieldName does not belong to SObject type AggregateResult @@ -613,9 +623,9 @@ public without sharing abstract class RollupCalculator { this.returnVal = 0.00; } if (this.returnVal == 0) { - this.returnVal = (Decimal) this.defaultVal; + this.returnVal = this.defaultVal; } - return this.returnVal; + return super.getReturnValue(); } protected override void handleShortCircuit(WinnowResult result) { @@ -692,7 +702,7 @@ public without sharing abstract class RollupCalculator { } } - protected virtual override Object calculateNewAggregateValue(Rollup.Op op, List objIds) { + protected virtual override Object calculateNewAggregateValue(Rollup.Op op, Iterable objIds) { Object aggregate; try { aggregate = super.calculateNewAggregateValue(op, objIds); @@ -722,9 +732,11 @@ public without sharing abstract class RollupCalculator { public virtual override Object getReturnValue() { Object superReturnVal = super.getReturnValue(); if (superReturnVal instanceof Decimal) { - superReturnVal = Datetime.newInstance(((Decimal) superReturnVal).longValue()); + this.returnVal = Datetime.newInstance(((Decimal) superReturnVal).longValue()); + } else if (superReturnVal == 0) { + this.returnVal = this.defaultVal; } - return superReturnVal; + return super.getReturnValue(); } public virtual override void setDefaultValues(String lookupRecordKey, Object priorVal) { @@ -756,7 +768,7 @@ public without sharing abstract class RollupCalculator { return RollupFieldInitializer.Current.getApexCompliantDatetime(datetimeWithMs).getTime(); } - protected override Object calculateNewAggregateValue(Rollup.Op op, List excludedItems) { + protected override Object calculateNewAggregateValue(Rollup.Op op, Iterable excludedItems) { Object aggregate = super.calculateNewAggregateValue(op, excludedItems); if (aggregate instanceof Datetime) { aggregate = ((Datetime) aggregate).getTime(); @@ -785,7 +797,10 @@ public without sharing abstract class RollupCalculator { public override Object getReturnValue() { Object superReturnVal = super.getReturnValue(); - return superReturnVal == 0 ? this.defaultVal : ((Datetime) superReturnVal)?.dateGmt(); + if (superReturnVal instanceof Datetime) { + this.returnVal = ((Datetime) superReturnVal).dateGmt(); + } + return super.getReturnValue(); } } @@ -810,11 +825,9 @@ public without sharing abstract class RollupCalculator { public override Object getReturnValue() { Object returnValue = super.getReturnValue(); if (returnValue instanceof Datetime) { - returnValue = ((Datetime) returnValue).timeGmt(); - } else if (returnValue == 0) { - returnValue = this.defaultVal; + this.returnVal = ((Datetime) returnValue).timeGmt(); } - return returnValue; + return super.getReturnValue(); } protected override Decimal getDecimalOrDefault(Object potentiallyUninitialized) { @@ -844,7 +857,8 @@ public without sharing abstract class RollupCalculator { // we shouldn't encourage negative counts. it's totally possible as a rollup is implemented and updates happen before // inserts or deletes, but it doesn't really make sense in the context of tracking Integer potentialReturnVal = ((Decimal) super.getReturnValue())?.intValue(); - return potentialReturnVal < 0 ? 0.00 : potentialReturnVal; + this.returnVal = potentialReturnVal < 0 ? 0.00 : potentialReturnVal; + return super.getReturnValue(); } protected override Decimal getNumericValue(Object value) { @@ -1014,7 +1028,7 @@ public without sharing abstract class RollupCalculator { } } - protected override Object calculateNewAggregateValue(Rollup.Op op, List objIds) { + protected override Object calculateNewAggregateValue(Rollup.Op op, Iterable objIds) { this.stringVal = (String) super.performBaseCalculation(op, objIds); return this.stringVal; } @@ -1084,7 +1098,7 @@ public without sharing abstract class RollupCalculator { .setQuery( RollupQueryBuilder.Current.getQuery( this.calcItemSObjectType, - new List{ this.opFieldOnCalcItem.getDescribe().getName() }, + new List{ this.opFieldOnCalcItem.toString() }, 'Id', '!=', this.lookupKeyQuery @@ -1165,12 +1179,12 @@ public without sharing abstract class RollupCalculator { SObjectField lookupKeyField ) { super(op, opFieldOnCalcItem, opFieldOnLookupObject, metadata, lookupKeyField); + this.hasCustomStateCalculation = true; } public override void performRollup(List calcItems, Map oldCalcItems) { - if (this.state != null) { - this.info = (RollupState.AverageInfo) (this.state.getState(this.lookupRecordKey, RollupState.AverageInfo.class) ?? new RollupState.AverageInfo()); + this.info = (RollupState.AverageInfo) (this.state?.getState(this.lookupRecordKey, this.metadata, RollupState.AverageInfo.class)); + if (this.info != null) { this.distinctValues.addAll(this.info.distinctNumerators); - this.state.setState(this.lookupRecordKey, this.info); } this.rollupResults(this.winnowItems(calcItems, oldCalcItems)); } @@ -1214,17 +1228,13 @@ public without sharing abstract class RollupCalculator { break; } } + this.hasCustomStateCalculation = true; } public override void performRollup(List calcItems, Map oldCalcItems) { - if (this.state != null) { - this.info = (RollupState.SObjectInfo) this.state.getState(this.lookupRecordKey, RollupState.SObjectInfo.class); - if (this.info == null) { - this.info = new RollupState.SObjectInfo(); - this.state.setState(this.lookupRecordKey, this.info); - } else { - calcItems.add(this.info.item); - } + this.info = (RollupState.SObjectInfo) this.state?.getState(this.lookupRecordKey, this.metadata, RollupState.SObjectInfo.class); + if (this.info?.item != null) { + calcItems.add(this.info.item); } this.rollupResults(this.winnowItems(calcItems, oldCalcItems)); } @@ -1247,7 +1257,6 @@ public without sharing abstract class RollupCalculator { } private without sharing class MostRollupCalculator extends RollupCalculator { - private RollupState.MostInfo info; private Map occurrenceToCount = new Map(); private Integer largestCountPointer; public MostRollupCalculator( @@ -1258,21 +1267,14 @@ public without sharing abstract class RollupCalculator { SObjectField lookupKeyField ) { super(op, opFieldOnCalcItem, opFieldOnLookupObject, metadata, lookupKeyField); + this.hasCustomStateCalculation = true; } protected override void rollupResults(List results) { - if (this.state != null) { - this.info = (RollupState.MostInfo) this.state.getState(this.lookupRecordKey, RollupState.MostInfo.class); - if (this.info == null) { - this.info = new RollupState.MostInfo(); - this.state.setState(this.lookupRecordKey, this.info); - } - } + RollupState.MostInfo info = (RollupState.MostInfo) this.state?.getState(this.lookupRecordKey, this.metadata, RollupState.MostInfo.class); this.occurrenceToCount = new Map(); - this.largestCountPointer = this.info?.largestPointCounter ?? -1; - if (this.state == null) { - this.returnVal = this.defaultVal; - } + this.largestCountPointer = info?.largestPointCounter ?? -1; + this.returnVal = info?.value ?? this.defaultVal; for (WinnowResult result : results) { Object value = result.currentValue; @@ -1281,7 +1283,7 @@ public without sharing abstract class RollupCalculator { if (this.largestCountPointer < localCount) { this.largestCountPointer = localCount; this.returnVal = value; - this.info?.setLargestPointCounter(localCount); + info?.setValues(localCount, value); } } } @@ -1480,7 +1482,7 @@ public without sharing abstract class RollupCalculator { String suffix = isLast ? this.trClose : ''; this.columnHeaders.add(prefix + this.thOpen + this.fieldNameToToken.get(fieldName).getDescribe().getLabel() + this.thClose); if (isLast) { - this.columnHeaders.add(this.thOpen + this.targetField.getDescribe().getName() + this.thClose + suffix); + this.columnHeaders.add(this.thOpen + this.targetField.toString() + this.thClose + suffix); } return prefix + this.tdOpen + String.valueOf(groupingValue) + this.tdClose + suffix; } diff --git a/rollup/core/classes/RollupCurrencyInfo.cls b/rollup/core/classes/RollupCurrencyInfo.cls index 285696fb..f5b7e41c 100644 --- a/rollup/core/classes/RollupCurrencyInfo.cls +++ b/rollup/core/classes/RollupCurrencyInfo.cls @@ -141,7 +141,7 @@ public without sharing virtual class RollupCurrencyInfo { return; } for (SObject calcItem : calcItems) { - if (DATED_MULTICURRENCY_SUPPORTED_OBJECTS.containsKey(calcItem.getSObjectType().getDescribe().getName()) == false) { + if (DATED_MULTICURRENCY_SUPPORTED_OBJECTS.containsKey(calcItem.getSObjectType().toString()) == false) { return; } Datetime currencyDate = getCurrencyDate(calcItem); @@ -234,7 +234,7 @@ public without sharing virtual class RollupCurrencyInfo { } private static String getHashKey(SObject calcItem, Schema.SObjectField opFieldOnCalcItem) { - return '' + calcItem.Id + opFieldOnCalcItem.getDescribe().getName() + calcItem.hashCode(); + return '' + calcItem.Id + opFieldOnCalcItem.toString() + calcItem.hashCode(); } private static Map getCurrencyMap() { @@ -264,7 +264,7 @@ public without sharing virtual class RollupCurrencyInfo { } private static Datetime getCurrencyDate(SObject calcItem) { - List itemToDateFieldMapping = DATED_MULTICURRENCY_SUPPORTED_OBJECTS.get(calcItem.getSObjectType().getDescribe().getName()); + List itemToDateFieldMapping = DATED_MULTICURRENCY_SUPPORTED_OBJECTS.get(calcItem.getSObjectType().toString()); switch on itemToDateFieldMapping?.size() { when 1 { return calcItem.isSet(itemToDateFieldMapping[0]) ? (Datetime) calcItem.get(itemToDateFieldMapping[0]) : null; diff --git a/rollup/core/classes/RollupDateLiteral.cls b/rollup/core/classes/RollupDateLiteral.cls index f108654f..54b43dad 100644 --- a/rollup/core/classes/RollupDateLiteral.cls +++ b/rollup/core/classes/RollupDateLiteral.cls @@ -34,7 +34,7 @@ public without sharing abstract class RollupDateLiteral { private static Datetime START_OF_TODAY { get { - return getRelativeDatetime(System.today(), START_TIME); + return Datetime.newInstance(System.today().year(), System.today().month(), System.today().day()); } } @@ -296,7 +296,7 @@ public without sharing abstract class RollupDateLiteral { private class YesterdayLiteral extends RollupDateLiteral { public YesterdayLiteral() { - this.ref = START_OF_TODAY.addDays(-1); + this.ref = getRelativeDatetime(System.today().addDays(-1), START_TIME); this.bound = getRelativeDatetime(this.ref.date(), END_TIME); } } diff --git a/rollup/core/classes/RollupFinalizer.cls b/rollup/core/classes/RollupFinalizer.cls index 047c936c..b06abfb9 100644 --- a/rollup/core/classes/RollupFinalizer.cls +++ b/rollup/core/classes/RollupFinalizer.cls @@ -1,30 +1,68 @@ -public without sharing class RollupFinalizer implements Finalizer { +public without sharing virtual class RollupFinalizer implements Finalizer { @TestVisible - private static Boolean wasCalled = false; - @TestVisible - private static ParentJobResult testResult; + private static Boolean wasExceptionLogged = false; + protected final List cabooses = new List(); + + // Avoids issues with serializing Exceptions in Apex by flattening the finalizer context data structure + private class FinalizerContextLoggable implements RollupLogger.ToStringObject { + public final String exceptionMessage; + public final String exceptionStacktrace; + public final String exceptionType; + public final String jobId; + public final String requestId; + public final String resultName; + + public FinalizerContextLoggable(System.FinalizerContext fc) { + this.jobId = fc.getAsyncApexJobId(); + this.requestId = fc.getRequestId(); + this.resultName = fc.getResult().name(); + this.exceptionMessage = fc.getException()?.getMessage(); + this.exceptionStacktrace = fc.getException()?.getStackTraceString(); + this.exceptionType = fc.getException()?.getTypeName(); + } + + public override String toString() { + return 'FinalizerContext:[ ' + JSON.serializePretty(this, true).removeStart('{').removeEnd('}') + ']'; + } + } - public void execute(FinalizerContext fc) { - ParentJobResult res = this.getParentJobResult(fc); - switch on res { + public virtual void execute(FinalizerContext fc) { + RollupLogger.Instance.log('Beginning finalizer with cabooses: ' + this.cabooses.size(), System.LoggingLevel.INFO); + switch on fc?.getResult() { when UNHANDLED_EXCEPTION { this.logUnhandledException(fc); } + when else { + this.handleSuccess(); + this.recurseThroughConductors(); + } } } - private ParentJobResult getParentJobResult(FinalizerContext fc) { - ParentJobResult res = testResult ?? fc?.getResult(); - testResult = null; - return res; + public void addCaboose(RollupAsyncProcessor caboose) { + this.cabooses.add(caboose); + } + + protected virtual void handleSuccess() { } - private void logUnhandledException(FinalizerContext fc) { - if (wasCalled == false) { + protected void logUnhandledException(FinalizerContext fc) { + if (wasExceptionLogged == false) { + wasExceptionLogged = true; // a finalizer can be re-queued up to five times, but we view this as a one-time "get out of jail free" logger - wasCalled = true; - RollupLogger.Instance.log('finalizer logging failure from:', fc?.getException(), System.LoggingLevel.ERROR); + RollupLogger.Instance.log('Unhandled exception, stopping execution:', new FinalizerContextLoggable(fc), System.LoggingLevel.ERROR); RollupLogger.Instance.save(); } } + + private void recurseThroughConductors() { + if (this.cabooses.isEmpty() == false) { + RollupAsyncProcessor conductor = this.cabooses.remove(0); + RollupLogger.Instance.log('Starting up new conductor', conductor, System.LoggingLevel.INFO); + String potentialProcessId = conductor.runCalc(); + if (potentialProcessId == conductor.getNoProcessId() && this.cabooses.isEmpty() == false) { + this.recurseThroughConductors(); + } + } + } } diff --git a/rollup/core/classes/RollupFlowBulkProcessor.cls b/rollup/core/classes/RollupFlowBulkProcessor.cls index 67586677..ab7bff99 100644 --- a/rollup/core/classes/RollupFlowBulkProcessor.cls +++ b/rollup/core/classes/RollupFlowBulkProcessor.cls @@ -84,7 +84,7 @@ global without sharing class RollupFlowBulkProcessor { List rollupMetadata = Rollup.getMetadataFromCache(Rollup__mdt.SObjectType); // for some reason, lists passed from Flow to Apex report their SObjectType as null. womp. Schema.SObjectType sObjectType = flowInput.recordsToRollup?.get(0).getSObjectType(); - String childName = sObjectType?.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); + String childName = sObjectType?.toString(); for (Rollup__mdt meta : rollupMetadata) { if ( meta.IsRollupStartedFromParent__c || diff --git a/rollup/core/classes/RollupFullBatchRecalculator.cls b/rollup/core/classes/RollupFullBatchRecalculator.cls index 18efbd4c..5e0d8dae 100644 --- a/rollup/core/classes/RollupFullBatchRecalculator.cls +++ b/rollup/core/classes/RollupFullBatchRecalculator.cls @@ -1,5 +1,9 @@ -public without sharing virtual class RollupFullBatchRecalculator extends RollupFullRecalcProcessor implements Database.Stateful, Database.RaisesPlatformEvents { +public without sharing virtual class RollupFullBatchRecalculator extends RollupFullRecalcProcessor { private final RollupState state = new RollupState(); + private Database.Cursor cursor; + private Integer currentPosition = 0; + + private static final Integer DEFAULT_CHUNK_SIZE = 500; public RollupFullBatchRecalculator( String queryString, @@ -10,6 +14,7 @@ public without sharing virtual class RollupFullBatchRecalculator extends RollupF RollupFullRecalcProcessor postProcessor ) { super(RollupQueryBuilder.Current.getAllRowSafeQuery(calcItemType, queryString), invokePoint, rollupMetas, calcItemType, recordIds, postProcessor); + this.rollupControl.BatchChunkSize__c = this.rollupControl.BatchChunkSize__c ?? DEFAULT_CHUNK_SIZE; } public override RollupState getState() { @@ -36,6 +41,38 @@ public without sharing virtual class RollupFullBatchRecalculator extends RollupF } protected virtual override String startAsyncWork() { - return this.startBatchProcessor(); + if (this.finalizer != null) { + while (this.cabooses.isEmpty() == false) { + this.finalizer.addCaboose(this.cabooses.remove(0)); + } + } + this.cursor = this.cursor ?? this.preStart().getCursor(); + + Integer countOfRecordsToReturn = this.rollupControl.BatchChunkSize__c.intValue(); + if (countOfRecordsToReturn + this.currentPosition > this.cursor.getNumRecords()) { + countOfRecordsToReturn = this.cursor.getNumRecords() - this.currentPosition; + } + this.calcItems = this.cursor.fetch(this.currentPosition, countOfRecordsToReturn); + this.currentPosition += countOfRecordsToReturn; + return super.startAsyncWork(); + } + + protected override RollupFinalizer getFinalizer() { + return new FullRecalcFinalizer(this); + } + + private class FullRecalcFinalizer extends RollupFinalizer { + private final RollupFullBatchRecalculator conductor; + public FullRecalcFinalizer(RollupFullBatchRecalculator conductor) { + this.conductor = conductor; + } + + public override void handleSuccess() { + if (this.conductor.currentPosition < this.conductor.cursor.getNumRecords()) { + this.conductor.startAsyncWork(); + } else { + this.conductor.finish(); + } + } } } diff --git a/rollup/core/classes/RollupFullRecalcProcessor.cls b/rollup/core/classes/RollupFullRecalcProcessor.cls index 2849fb5d..23d6fd18 100644 --- a/rollup/core/classes/RollupFullRecalcProcessor.cls +++ b/rollup/core/classes/RollupFullRecalcProcessor.cls @@ -5,10 +5,15 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs private final RollupFullRecalcProcessor postProcessor; private final Map parentRecordsToClear = new Map(); - private final List cabooses = new List(); + protected final List cabooses = new List(); + private final Set jobIds = new Set(); private Map> typeToOldIntermediateGrandparents; private Boolean hasProcessedParentRecords = false; + private RollupFullRecalcProcessor(InvocationPoint invokePoint) { + super(invokePoint); + } + protected RollupFullRecalcProcessor( String queryString, InvocationPoint invokePoint, @@ -68,21 +73,27 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs } public void finish() { + this.jobIds.add(this.jobId); if (this.cabooses.isEmpty() == false) { RollupFullRecalcProcessor conductor = this.cabooses.remove(0); + conductor.jobIds.add(this.jobId); for (Rollup__mdt meta : conductor.rollupMetas) { conductor.storeUniqueParentFields(meta); } for (RollupFullRecalcProcessor caboose : this.cabooses) { - conductor.addCaboose(caboose); + conductor.finalizer.addCaboose(caboose); + } + if (this.isBatch()) { + conductor.startAsyncWork(); + } else { + this.finalizer.addCaboose(conductor); + } + } else { + if (this.postProcessor != null) { + this.logger.log('Post processor added as caboose', this.postProcessor, System.LoggingLevel.INFO); + this.finalizer.addCaboose(this.postProcessor); } - this.setCurrentJobId(conductor.startAsyncWork()); - } else if (this.postProcessor != null) { - this.logger.log('Starting post-full recalc processor', this.postProcessor, System.LoggingLevel.INFO); - // chain jobs together so that if recalc job is being tracked within the Recalc Rollups app, - // job continuity is established between the full recalc and then any downstream job that runs - // (as the postProcessor) - this.setCurrentJobId(this.postProcessor.runCalc()); + this.getState()?.cleanup(this.jobIds); } if (this.hasProcessedParentRecords == false) { List parentRecords = new List(); @@ -109,18 +120,19 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs } this.hasProcessedParentRecords = true; Map relatedParentRecordsMap = new Map(relatedParentRecords); - for (SObject parentRecordToReset : this.parentRecordsToClear.values()) { - SObject relatedParentRecord = relatedParentRecordsMap.containsKey(parentRecordToReset.Id) - ? relatedParentRecordsMap.get(parentRecordToReset.Id) - : RollupCurrencyInfo.createNewRecord(parentRecordToReset); - - for (Rollup__mdt meta : this.rollupMetas) { - if (relatedParentRecord.getSObjectType().getDescribe().getName() == meta.LookupObject__c) { - relatedParentRecord.put(meta.RollupFieldOnLookupObject__c, null); + for (Rollup__mdt meta : this.rollupMetas) { + Object defaultVal = this.getDefaultValue(meta); + for (SObject parentRecordToReset : this.parentRecordsToClear.values()) { + if (parentRecordToReset.getSObjectType().toString() != meta.LookupObject__c) { + continue; + } + SObject relatedParentRecord = relatedParentRecordsMap.get(parentRecordToReset.Id); + if (relatedParentRecord == null) { + relatedParentRecord = RollupCurrencyInfo.createNewRecord(parentRecordToReset); + relatedParentRecordsMap.put(parentRecordToReset.Id, relatedParentRecord); } + relatedParentRecord.put(meta.RollupFieldOnLookupObject__c, defaultVal); } - - relatedParentRecordsMap.put(relatedParentRecord.Id, relatedParentRecord); } relatedParentRecords.clear(); relatedParentRecords.addAll(relatedParentRecordsMap.values()); @@ -146,6 +158,11 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs } } RollupAsyncProcessor processor = this.getAsyncRollup(this.rollupMetas, this.calcItemType, calcItems, new Map(), null, this.invokePoint); + for (Rollup innerRoll : this.rollups) { + if (innerRoll.getTypeName() == this.getTypeName()) { + this.cabooses.add((RollupFullRecalcProcessor) innerRoll); + } + } for (RollupAsyncProcessor innerRoll : processor.rollups) { innerRoll.fullRecalcProcessor = this; innerRoll.isFullRecalc = true; @@ -157,10 +174,11 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs protected virtual override Map customizeToStringEntries(Map props) { super.customizeToStringEntries(props); - this.addToMap(props, 'Rollup Metadata', this.rollupMetas); + Integer numberOfRollups = this.rollupMetas?.size(); + this.addToMap(props, 'Rollup Metadata', numberOfRollups > 5 ? (Object) numberOfRollups : (Object) this.rollupMetas); this.addToMap(props, 'Query String', this.queryString); this.addToMap(props, 'Caboose Count', this.cabooses.size()); - this.addToMap(props, 'Inner rollups', this.rollupMetas?.size()); + this.addToMap(props, 'Inner rollups', numberOfRollups); return props; } diff --git a/rollup/core/classes/RollupLogger.cls b/rollup/core/classes/RollupLogger.cls index 0f49627c..d83285d7 100644 --- a/rollup/core/classes/RollupLogger.cls +++ b/rollup/core/classes/RollupLogger.cls @@ -1,7 +1,7 @@ global without sharing virtual class RollupLogger implements ILogger { @TestVisible // this gets updated via the pipeline as the version number gets incremented - private static final String CURRENT_VERSION_NUMBER = 'v1.6.37'; + private static final String CURRENT_VERSION_NUMBER = 'v1.7.0'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final RollupPlugin PLUGIN = new RollupPlugin(); diff --git a/rollup/core/classes/RollupParentResetProcessor.cls b/rollup/core/classes/RollupParentResetProcessor.cls index 2e2d5277..8e7ec980 100644 --- a/rollup/core/classes/RollupParentResetProcessor.cls +++ b/rollup/core/classes/RollupParentResetProcessor.cls @@ -2,6 +2,7 @@ public without sharing class RollupParentResetProcessor extends RollupFullBatchR @TestVisible private static Integer maxQueryRows = Limits.getLimitQueryRows() / 2; private static Boolean isValidRun = false; + private static Boolean isRecursiveRun = false; private Integer countOfItems { get { @@ -16,9 +17,19 @@ public without sharing class RollupParentResetProcessor extends RollupFullBatchR private QueueableResetProcessor(RollupParentResetProcessor processor) { super(processor.invokePoint); this.processor = processor; + this.finalizer = processor.finalizer; + } + + public override String getTypeName() { + return QueueableResetProcessor.class.getName(); } protected override void performWork() { + for (RollupAsyncProcessor roll : this.processor.rollups) { + if (roll instanceof RollupFullRecalcProcessor) { + this.finalizer.addCaboose((RollupFullRecalcProcessor) roll); + } + } if (this.processor.countOfItems > maxQueryRows) { Database.executeBatch(this.processor, this.processor.rollupControl.BatchChunkSize__c.intValue()); } else { @@ -46,19 +57,24 @@ public without sharing class RollupParentResetProcessor extends RollupFullBatchR getRefinedQueryString(this.queryString, this.rollupMetas, this.invokePoint); this.objIds.addAll(this.recordIds); String processId = this.getNoProcessId(); - if (isValidRun == false || this.rollupControl.ShouldSkipResettingParentFields__c == true) { + if (isValidRun == false || this.rollupControl.ShouldSkipResettingParentFields__c == true || this.countOfItems <= 0) { this.logger.log('Parent reset processor no-op', System.LoggingLevel.INFO); return processId; } Boolean isOverLimit = this.countOfItems > maxQueryRows; - if (isOverLimit && this.isBatch() == false) { - // avoids: System.AsyncException: Database.executeBatch cannot be called from a batch start, batch execute, or future method - processId = super.startAsyncWork(); - } else if (isOverLimit && Limits.getLimitQueueableJobs() > Limits.getQueueableJobs()) { + Integer previouslyQueuedJobs = Limits.getQueueableJobs(); + if (isOverLimit && System.isQueueable() && previouslyQueuedJobs == 1 && this.finalizer != null) { + this.finalizer.addCaboose(new QueueableResetProcessor(this)); + } else if (isOverLimit && Limits.getLimitQueueableJobs() > previouslyQueuedJobs) { // avoids System.LimitException: Too many queueable jobs added to the queue: { output of Limits.getQueueableJobs() } // down the rabbit hole we go again processId = this.startAsyncWork(); } else if (this.countOfItems > 0) { + isRecursiveRun = true; + for (RollupAsyncProcessor proc : this.rollups) { + proc.runCalc(); + } + isRecursiveRun = false; this.runSync(); } @@ -69,25 +85,28 @@ public without sharing class RollupParentResetProcessor extends RollupFullBatchR if (parentItems.isEmpty()) { return; } - this.logger.log('resetting parent fields for: ' + parentItems.size() + ' items', System.LoggingLevel.INFO); Map parentFields = parentItems.get(0).getSObjectType().getDescribe().fields.getMap(); - for (SObject parentItem : parentItems) { - for (Rollup__mdt rollupMeta : this.rollupMetas) { - if ( - rollupMeta.LookupObject__c == this.calcItemType.getDescribe().getName() && - this.parentRollupFieldHasBeenReset( - rollupMeta.LookupFieldOnLookupObject__c, - parentItem, - rollupMeta.LookupObject__c, - rollupMeta.RollupFieldOnLookupObject__c - ) == false && - parentFields.containsKey(rollupMeta.RollupFieldOnLookupObject__c) - ) { - parentItem.put(rollupMeta.RollupFieldOnLookupObject__c, this.getDefaultValue(rollupMeta)); - } + for (Rollup__mdt rollupMeta : this.rollupMetas) { + Schema.SObjectField parentFieldToken = parentFields.get(rollupMeta.RollupFieldOnLookupObject__c); + if ( + rollupMeta.LookupObject__c != this.calcItemType.toString() || + this.parentRollupFieldHasBeenReset(rollupMeta.LookupObject__c, rollupMeta.RollupFieldOnLookupObject__c) || + parentFieldToken == null + ) { + continue; } + Object defaultVal = this.getDefaultValue(rollupMeta); + this.logger.log( + 'resetting parent fields to: ' + defaultVal + ' for field: ' + parentFieldToken + ' for ' + parentItems.size() + ' items', + System.LoggingLevel.DEBUG + ); + for (SObject parentItem : parentItems) { + this.getDML().updateField(parentFieldToken, parentItem, defaultVal); + } + } + if (isRecursiveRun == false) { + this.getDML().updateRecords(); } - this.getDML().doUpdate(parentItems); } protected override String getTypeName() { diff --git a/rollup/core/classes/RollupRelationshipFieldFinder.cls b/rollup/core/classes/RollupRelationshipFieldFinder.cls index e124b0c3..7908a074 100644 --- a/rollup/core/classes/RollupRelationshipFieldFinder.cls +++ b/rollup/core/classes/RollupRelationshipFieldFinder.cls @@ -221,8 +221,8 @@ public without sharing class RollupRelationshipFieldFinder { // if we're only going one relationship up, we need to validate that the // parent's relationship name doesn't differ from its SObject name if (this.relationshipParts.size() == 1 && this.isFirstRun) { - SObjectField parentField = this.getField(baseSObjectType.getDescribe().fields.getMap(), this.ultimateParent.getDescribe().getName()); - this.relationshipParts.add(0, parentField.getDescribe().getName()); + SObjectField parentField = this.getField(baseSObjectType.getDescribe().fields.getMap(), this.ultimateParent.toString()); + this.relationshipParts.add(0, parentField.toString()); } if (baseSObjectType == this.ultimateParent && this.isFinishedWithHierarchyTraversal(records)) { @@ -259,11 +259,11 @@ public without sharing class RollupRelationshipFieldFinder { SObjectField field = fieldMap.get(key); if (field.getDescribe().getRelationshipName() == relationshipPart) { return field; - } else if (field.getDescribe().getName() == relationshipPart) { + } else if (field.toString() == relationshipPart) { return field; } else if (field.getDescribe().isNamePointing()) { for (SObjectType potentialMatch : field.getDescribe().getReferenceTo()) { - if (potentialMatch.getDescribe().getName() == relationshipPart) { + if (potentialMatch.toString() == relationshipPart) { return field; } } @@ -392,7 +392,7 @@ public without sharing class RollupRelationshipFieldFinder { this.populateHierarchicalLookupFields(fieldNames, this.getField(fieldMap, this.metadata.UltimateParentLookup__c)); } } else { - fieldNames.add(field.getDescribe().getName()); + fieldNames.add(field.toString()); } // NB - we only support one route through polymorphic fields such as Task.WhoId and Task.WhatId for this sort of thing @@ -542,8 +542,8 @@ public without sharing class RollupRelationshipFieldFinder { } List copiedFieldNamesPerRelationship = new List(queryFieldNames); String relationshipName = hierarchyToken.getDescribe().getRelationshipName(); - String hierarchyName = hierarchyToken.getDescribe().getName(); - queryFieldNames.add(hierarchyToken.getDescribe().getName()); + String hierarchyName = hierarchyToken.toString(); + queryFieldNames.add(hierarchyToken.toString()); for (Integer index = 0; index < MAX_FOREIGN_KEY_RELATIONSHIP_HOPS; index++) { String baseOfHierarchy = relationshipName.repeat(index).replace(relationshipName, relationshipName + '.'); String repeatedHierarchy = baseOfHierarchy + hierarchyName; @@ -780,7 +780,7 @@ public without sharing class RollupRelationshipFieldFinder { private String getChildRelationshipFromType(Schema.SObjectType sObjectType, String relationshipName) { for (Schema.ChildRelationship child : sObjectType.getDescribe(SObjectDescribeOptions.FULL).getChildRelationships()) { if (child.getRelationshipName() == relationshipName) { - return child.getChildSObject().getDescribe().getName(); + return child.getChildSObject().toString(); } } return null; diff --git a/rollup/core/classes/RollupRepository.cls b/rollup/core/classes/RollupRepository.cls index 18147540..adac329d 100644 --- a/rollup/core/classes/RollupRepository.cls +++ b/rollup/core/classes/RollupRepository.cls @@ -46,6 +46,11 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb return this; } + public Database.Cursor getCursor() { + this.createQueryLog('Getting cursor'); + return Database.getCursorWithBinds(this.args.query, this.args.bindVars, this.accessLevel); + } + public Database.QueryLocator getLocator() { this.createQueryLog('Getting query locator'); return Database.getQueryLocatorWithBinds(this.args.query, this.args.bindVars, this.accessLevel); diff --git a/rollup/core/classes/RollupSObjectUpdater.cls b/rollup/core/classes/RollupSObjectUpdater.cls index 62b8bdb8..a930f6a1 100644 --- a/rollup/core/classes/RollupSObjectUpdater.cls +++ b/rollup/core/classes/RollupSObjectUpdater.cls @@ -15,6 +15,7 @@ global without sharing virtual class RollupSObjectUpdater { private Boolean forceSyncUpdate = false; private RollupControl__mdt rollupControl; + private RollupFinalizer finalizer; global interface IDispatcher { void dispatch(List records); @@ -42,6 +43,11 @@ global without sharing virtual class RollupSObjectUpdater { this.forceSyncUpdate = true; } + public RollupSObjectUpdater setFinalizer(RollupFinalizer finalizer) { + this.finalizer = finalizer; + return this; + } + public void updateRecords() { List recordsToUpdate = RECORDS_TO_UPDATE.values(); // static map has to be cleared BEFORE calling "doUpdate" @@ -167,7 +173,12 @@ global without sharing virtual class RollupSObjectUpdater { private void performAsyncUpdate(List recordsToUpdate) { if (Limits.getLimitQueueableJobs() > Limits.getQueueableJobs() && recordsToUpdate.isEmpty() == false) { - System.enqueueJob(new RollupAsyncSaver(recordsToUpdate, this.rollupControl)); + RollupAsyncSaver saver = new RollupAsyncSaver(this, recordsToUpdate); + if (this.finalizer == null) { + saver.runCalc(); + } else { + this.finalizer.addCaboose(saver); + } } } @@ -220,20 +231,36 @@ global without sharing virtual class RollupSObjectUpdater { } } - private class RollupAsyncSaver implements System.Queueable { - private final List records; - private final RollupControl__mdt control; - public RollupAsyncSaver(List records, RollupControl__mdt control) { - this.records = records; - this.control = control; + private without sharing class RollupAsyncSaver extends RollupAsyncProcessor.QueueableProcessor { + private final RollupSObjectUpdater saver; + private RollupAsyncSaver(RollupSObjectUpdater saver, List itemsToUpdate) { + super(null, itemsToUpdate, null); + this.finalizer = saver.finalizer; + this.isNoOp = itemsToUpdate.isEmpty(); + saver.rollupControl.MaxParentRowsUpdatedAtOnce__c = 500; + this.saver = saver; + } + + public override String getTypeName() { + return RollupAsyncSaver.class.getName(); + } + + public override String runCalc() { + return System.enqueueJob(this); + } + + public override Map customizeToStringEntries(Map props) { + props.remove('Invocation Point'); + props.remove('Is Full Recalc'); + props.remove('Is Conductor'); + props.remove('Inner rollups'); + props.put('Async update size', '' + this.calcItems.size()); + return props; } - public void execute(System.QueueableContext qc) { - System.attachFinalizer(new RollupFinalizer()); - RollupLogger.Instance.log('Deferred async saving starting for parent records', new Map(this.records).keySet(), System.LoggingLevel.INFO); - RollupSObjectUpdater updater = new RollupSObjectUpdater(); - updater.addRollupControl(this.control); - new RollupSObjectUpdater().doUpdate(this.records); + protected override void performWork() { + RollupLogger.Instance.log('Deferred async saving starting for parent records', new Map(this.calcItems).keySet(), System.LoggingLevel.INFO); + this.saver.doUpdate(this.calcItems); RollupLogger.Instance.log('Saving finished', System.LoggingLevel.INFO); RollupLogger.Instance.save(); } diff --git a/rollup/core/classes/RollupState.cls b/rollup/core/classes/RollupState.cls index a903242d..b816db5f 100644 --- a/rollup/core/classes/RollupState.cls +++ b/rollup/core/classes/RollupState.cls @@ -1,39 +1,71 @@ -public without sharing virtual class RollupState { - private final Map> keyToState { +public without sharing virtual class RollupState implements System.Queueable, System.Finalizer, RollupLogger.ToStringObject { + protected transient String key; + protected transient Integer keyLength; + protected transient String recordId; + protected transient String typeName; + protected final Set jobIds = new Set(); + + private Long commitCount = 1; + private Set statefulPreviouslyRetrievedStateIds = new Set(); + + @TestVisible + private static Integer maxBodyLength = 131072; + @TestVisible + private static Integer maxRelatedKeysLength = 255; + + private static final Map KEY_TO_STATE { get { - this.keyToState = this.keyToState ?? new Map>(); - return this.keyToState; + KEY_TO_STATE = KEY_TO_STATE ?? new Map(); + return KEY_TO_STATE; } set; } - public RollupState getState(String firstKey, Type secondKey) { - return this.keyToState.get(firstKey)?.get(secondKey); + private static final Map> CACHED_STATES { + get { + CACHED_STATES = CACHED_STATES ?? new Map>(); + return CACHED_STATES; + } + set; } - public virtual Type getType() { - return RollupState.class; + public RollupState getState(String key, Rollup__mdt meta, Type initializingType) { + String trueKey = (key + getMetadataKey(meta)); + RollupState possibleState = KEY_TO_STATE.get(trueKey); + if (possibleState == null) { + possibleState = (RollupState) initializingType.newInstance(); + possibleState.recordId = key; + // we only pass the original key's length because even though state is keyed by the trueKey + // the original key's length gives us access to the record key when committing state values to the + // various RelatedRecordKey{n}__c fields + this.setState(trueKey, key.length(), possibleState); + } + return possibleState; } - public void setState(String key, RollupState state) { - Map firstKeyStates = this.keyToState.get(key); - if (firstKeyStates == null) { - firstKeyStates = new Map(); - this.keyToState.put(key, firstKeyStates); - } - firstKeyStates.put(state.getType(), state); + public void setState(String key, Integer keyLength, RollupState state) { + state.commitCount = this.commitCount; + state.key = key; + state.keyLength = keyLength; + KEY_TO_STATE.put(state.key, state); + } + + public override String toString() { + return '' + this.getUntypedState(); + } + + public virtual Map getUntypedState() { + throw new SerializationException('Should not make it here'); + } + + public virtual Boolean isEmpty() { + throw new IllegalArgumentException('Should not make it here'); } public class AverageInfo extends RollupState { public Decimal denominator = 0; public Decimal numerator = 0; - public Set distinctNumerators { - get { - this.distinctNumerators = this.distinctNumerators ?? new Set(); - return this.distinctNumerators; - } - private set; - } + public Set distinctNumerators = new Set(); public void increment(Decimal value) { this.numerator += value; @@ -41,20 +73,42 @@ public without sharing virtual class RollupState { this.distinctNumerators.add(value); } - public override Type getType() { - return AverageInfo.class; + public override Boolean isEmpty() { + return this.denominator == 0; + } + + public override Map getUntypedState() { + return new Map{ + 'denominator' => this.denominator, + 'distinctNumerators' => this.distinctNumerators, + 'key' => this.key, + 'keyLength' => this.keyLength, + 'numerator' => this.numerator, + 'typeName' => AverageInfo.class.getName() + }; } } - public class MostInfo extends RollupState { + public class MostInfo extends GenericInfo { public Integer largestPointCounter = -1; - public void setLargestPointCounter(Integer newWinner) { + public void setValues(Integer newWinner, Object val) { this.largestPointCounter = newWinner; + this.value = val; + } + + public override Boolean isEmpty() { + return this.largestPointCounter == -1; } - public override Type getType() { - return MostInfo.class; + public override Map getUntypedState() { + return new Map{ + 'largestPointCounter' => this.largestPointCounter, + 'key' => this.key, + 'keyLength' => this.keyLength, + 'typeName' => MostInfo.class.getName(), + 'value' => this.value + }; } } @@ -65,8 +119,286 @@ public without sharing virtual class RollupState { this.item = item; } - public override Type getType() { - return SObjectInfo.class; + public override Boolean isEmpty() { + return this.item == null; + } + + public override Map getUntypedState() { + return new Map{ + 'item' => this.item, + 'itemType' => '' + this.item.getSObjectType(), + 'key' => this.key, + 'keyLength' => this.keyLength, + 'typeName' => SObjectInfo.class.getName() + }; } } + + /** + * In theory, GenericInfo is overkill for what it does - any rollup operation that can be reduced to a single value + * COULD simply take whatever the current value on the parent record is as the source of truth. That being said, the + * prior implementation with `Database.Stateful` shamefully split the logic between `RollupAsyncProcessor` and `RollupState` + * when managing prior values, and duplicating _some_ data here (as far as what eventually gets persisted to the database as `RollupState__c` records) + * seems vastly preferable as opposed to the logic living in two different places + */ + public virtual class GenericInfo extends RollupState { + public Object value; + + public void setValue(Object newValue) { + this.value = newValue; + } + + public virtual override Boolean isEmpty() { + return this.value == null; + } + + public virtual override Map getUntypedState() { + return new Map{ 'key' => this.key, 'keyLength' => this.keyLength, 'typeName' => GenericInfo.class.getName(), 'value' => this.value }; + } + } + + public void loadState(String jobId, Set relatedRecordKeys) { + this.jobIds.add(jobId); + List matchingState = this.loadOrRetrieveCachedState(jobId, relatedRecordKeys); + for (RollupState__c state : matchingState) { + if (this.statefulPreviouslyRetrievedStateIds.contains(state.Id) == false && state.Body0__c != null) { + List localUncastStates = (List) new DataWeaveScriptResource.jsonToRollupState() + .execute(new Map{ 'records' => '[' + state.Body0__c + ']' }) + .getValue(); + for (Object uncastState : localUncastStates) { + RollupState castState = (RollupState) uncastState; + KEY_TO_STATE.put(castState.key, castState); + } + } + this.statefulPreviouslyRetrievedStateIds.add(state.Id); + } + } + + public void commitState(String jobId) { + this.jobIds.add(jobId); + this.populateRelatedRecordStates(jobId); + } + + public String cleanup(Set jobIds) { + this.jobIds.addAll(jobIds); + RollupLogger.Instance.log('about to clean up rollup state', this.jobIds, System.LoggingLevel.INFO); + return System.enqueueJob(this); + } + + @SuppressWarnings('PMD.ApexCrudViolation') + public void execute(System.QueueableContext qc) { + System.attachFinalizer(this); + List matchingState = [ + SELECT Id + FROM RollupState__c + WHERE RelatedJobId__c = :this.jobIds + LIMIT :Limits.getLimitDmlRows() - Limits.getDmlRows() + ]; + Database.delete(matchingState, false, System.AccessLevel.SYSTEM_MODE); + } + + public void execute(System.FinalizerContext fc) { + if ([SELECT COUNT() FROM RollupState__c WHERE Id = :this.jobIds LIMIT 1] > 0) { + new RollupState().cleanup(this.jobIds); + } + } + + @SuppressWarnings('PMD.ApexCrudViolation') + private List loadOrRetrieveCachedState(String jobId, Set relatedRecordKeys) { + String cacheKey = String.join(relatedRecordKeys, ''); + List states = CACHED_STATES.get(cacheKey); + if (relatedRecordKeys.isEmpty() || states != null) { + RollupLogger.Instance.log('Returning state from cache for record size: ' + (states?.size() ?? 0), System.LoggingLevel.DEBUG); + return new List(); + } + List quotedRecordKeys = new List(); + for (String recordKey : relatedRecordKeys) { + quotedRecordKeys.add('%' + recordKey + '%'); + } + states = [ + SELECT Id, Body0__c + FROM RollupState__c + WHERE + (RelatedRecordKeys0__c LIKE :quotedRecordKeys + OR RelatedRecordKeys1__c LIKE :quotedRecordKeys + OR RelatedRecordKeys2__c LIKE :quotedRecordKeys + OR RelatedRecordKeys3__c LIKE :quotedRecordKeys + OR RelatedRecordKeys4__c LIKE :quotedRecordKeys + OR RelatedRecordKeys5__c LIKE :quotedRecordKeys + OR RelatedRecordKeys6__c LIKE :quotedRecordKeys + OR RelatedRecordKeys7__c LIKE :quotedRecordKeys + OR RelatedRecordKeys8__c LIKE :quotedRecordKeys + OR RelatedRecordKeys9__c LIKE :quotedRecordKeys + OR RelatedRecordKeys10__c LIKE :quotedRecordKeys) + AND RelatedJobId__c = :jobId + AND IsDeleted = FALSE + AND Id != :this.statefulPreviouslyRetrievedStateIds + ORDER BY CreatedDate DESC + ]; + CACHED_STATES.put(cacheKey, states); + Database.delete(states, false, System.AccessLevel.SYSTEM_MODE); + return states; + } + + @SuppressWarnings('PMD.ApexCrudViolation, PMD.AvoidDeeplyNestedIfStmts') + private void populateRelatedRecordStates(String jobId) { + RollupState__c currentStateToInsert = new RollupState__c(RelatedJobId__c = jobId); + List statesToInsert = new List{ currentStateToInsert }; + Integer numberOfStates = KEY_TO_STATE.size(); + + // mutable tracking fields + Set allRecordKeys = new Set(); + Integer relatedRecordKeyLength = 0; + Integer currentRelatedRecordFieldIndex = 0; + Double currentLength = 0; + Integer stateCounter = 0; + List> untypedStates = new List>(); + Schema.SObjectField relatedKeysFieldToken = RollupState__c.RelatedRecordKeys0__c; + for (String key : KEY_TO_STATE.keySet()) { + RollupState state = KEY_TO_STATE.get(key); + stateCounter++; + if (state.isEmpty()) { + continue; + } + + String recordKey = key.substring(0, state.keyLength); + String currentRelatedKeys = (String) currentStateToInsert.get(relatedKeysFieldToken); + + if (allRecordKeys.contains(recordKey) == false) { + allRecordKeys.add(recordKey); + String newKeys = currentRelatedKeys != null ? currentRelatedKeys + ',' + recordKey : recordKey; + currentStateToInsert.put(relatedKeysFieldToken, newKeys); + relatedRecordKeyLength = newKeys.length(); + } + Map untypedState = state.getUntypedState(); + // 1.1 is enough of a buffer for the serialized version with quoted characters and the + 1 accounts for commas as the delimiter between state objects + currentLength += (untypedState.toString().length() * 1.1) + 1; + untypedStates.add(untypedState); + + // if the next key would overflow the current related keys field, either step to the new field + // or add a new state record to the list if we're out of key fields + if (relatedRecordKeyLength + state.keyLength + 1 > maxRelatedKeysLength) { + currentRelatedRecordFieldIndex++; + TokenSentinel sentinel = getRelatedRecordKeySentinel(currentRelatedRecordFieldIndex); + Boolean isMissingCurrentKey = allRecordKeys.contains(recordKey) == false; + if (relatedKeysFieldToken == RollupState__c.RelatedRecordKeys10__c) { + if (isMissingCurrentKey) { + untypedStates.remove(untypedStates.size() - 1); + } + allRecordKeys = new Set(); + currentStateToInsert.Body0__c = getJoinedBody(untypedStates); + relatedKeysFieldToken = RollupState__c.RelatedRecordKeys0__c; + currentRelatedRecordFieldIndex = 0; + currentLength = 0; + currentStateToInsert = new RollupState__c(RelatedJobId__c = jobId); + statesToInsert.add(currentStateToInsert); + if (isMissingCurrentKey) { + untypedStates.add(untypedState); + currentStateToInsert.RelatedRecordKeys0__c = recordKey; + allRecordKeys.add(recordKey); + relatedRecordKeyLength = state.keyLength; + } + } else { + relatedKeysFieldToken = sentinel.token; + relatedRecordKeyLength = 0; + } + } + // There's some undocumented soft limit to the amount of data that can be stored in a long text area + // so we use another slight buffer to avoid running into the actual limit + else if ((currentLength + 1100) >= maxBodyLength) { + allRecordKeys = new Set(); + currentStateToInsert.Body0__c = getJoinedBody(untypedStates); + currentRelatedRecordFieldIndex = 0; + relatedRecordKeyLength = 0; + relatedKeysFieldToken = RollupState__c.RelatedRecordKeys0__c; + currentStateToInsert = new RollupState__c(RelatedJobId__c = jobId); + statesToInsert.add(currentStateToInsert); + currentLength = 0; + } + } + if (stateCounter == numberOfStates && untypedStates.isEmpty() == false) { + currentStateToInsert.Body0__c = getJoinedBody(untypedStates); + } + + for (Integer reverseIndex = statesToInsert.size() - 1; reverseIndex >= 0; reverseIndex--) { + RollupState__c state = statesToInsert[reverseIndex]; + if (state.Body0__c == null) { + statesToInsert.remove(reverseIndex); + } + } + + Database.insert(statesToInsert, System.AccessLevel.SYSTEM_MODE); + RollupLogger.Instance.log( + 'Finished inserting ' + statesToInsert.size() + ' states (for batch number: ' + this.commitCount + ')', + System.LoggingLevel.DEBUG + ); + KEY_TO_STATE.clear(); + CACHED_STATES.clear(); + this.commitCount++; + } + + private class TokenSentinel implements RollupLogger.ToStringObject { + public Boolean shouldReset = false; + public Schema.SObjectField token; + public Integer currentTokenIndex; + + public TokenSentinel(Integer currentTokenIndex) { + this.currentTokenIndex = currentTokenIndex; + } + } + + private static TokenSentinel getRelatedRecordKeySentinel(Integer currentIndex) { + TokenSentinel sentinel = new TokenSentinel(currentIndex); + + switch on currentIndex { + when 0 { + sentinel.token = RollupState__c.RelatedRecordKeys0__c; + } + when 1 { + sentinel.token = RollupState__c.RelatedRecordKeys1__c; + } + when 2 { + sentinel.token = RollupState__c.RelatedRecordKeys2__c; + } + when 3 { + sentinel.token = RollupState__c.RelatedRecordKeys3__c; + } + when 4 { + sentinel.token = RollupState__c.RelatedRecordKeys4__c; + } + when 5 { + sentinel.token = RollupState__c.RelatedRecordKeys5__c; + } + when 6 { + sentinel.token = RollupState__c.RelatedRecordKeys6__c; + } + when 7 { + sentinel.token = RollupState__c.RelatedRecordKeys7__c; + } + when 8 { + sentinel.token = RollupState__c.RelatedRecordKeys8__c; + } + when 9 { + sentinel.token = RollupState__c.RelatedRecordKeys9__c; + } + when 10 { + sentinel.token = RollupState__c.RelatedRecordKeys10__c; + } + when else { + sentinel.token = RollupState__c.RelatedRecordKeys0__c; + sentinel.shouldReset = true; + } + } + return sentinel; + } + + private static String getJoinedBody(List> untypedStates) { + String joinedBody = JSON.serialize(untypedStates).removeStart('[').removeEnd(']'); + untypedStates.clear(); + return joinedBody; + } + + private static String getMetadataKey(Rollup__mdt meta) { + return meta.DeveloperName ?? (meta.RollupOperation__c + meta.LookupObject__c + meta.RollupFieldOnLookupObject__c + meta.LookupFieldOnLookupObject__c); + } } diff --git a/rollup/core/dw/jsonToRollupState.dwl b/rollup/core/dw/jsonToRollupState.dwl new file mode 100644 index 00000000..3aa5bd9a --- /dev/null +++ b/rollup/core/dw/jsonToRollupState.dwl @@ -0,0 +1,19 @@ +%dw 2.0 +input records application/json +output application/apex + +// if the attributes property, which only exists on serialized SObjects, is present when trying to deserialize +// it leads to the following error: System.DataWeaveScriptException: Error writing item: Invalid field "attributes" for type "{your SObject Type}" +var getCompliantSObject = (item) -> item filterObject (value, key) -> (("" ++ key) != "attributes") +--- +// String coercion used to avoid errors like: +// Invalid type: "org.mule.weave.v2.model.values.MaterializedAttributeDelegateValue" +records map (record) -> "" ++ record.typeName match { + // regex here handles namespaced versions of the class name + case matches /(.*\.|)RollupState\.SObjectInfo/ -> { + key: record.key, + keyLength: record.keyLength, + item: getCompliantSObject(record.item) as Object { class: "" ++ record.itemType }, + } as Object { class: $[0] } + else -> record as Object { class: $ } + } \ No newline at end of file diff --git a/rollup/core/dw/jsonToRollupState.dwl-meta.xml b/rollup/core/dw/jsonToRollupState.dwl-meta.xml new file mode 100644 index 00000000..020f5c94 --- /dev/null +++ b/rollup/core/dw/jsonToRollupState.dwl-meta.xml @@ -0,0 +1,7 @@ + + + + 62.0 + false + false + diff --git a/rollup/core/flexipages/Rollup_State.flexipage-meta.xml b/rollup/core/flexipages/Rollup_State.flexipage-meta.xml new file mode 100644 index 00000000..fddaa69c --- /dev/null +++ b/rollup/core/flexipages/Rollup_State.flexipage-meta.xml @@ -0,0 +1,263 @@ + + + + + + + uiBehavior + none + + Record.Name + RecordNameField + + + + + + uiBehavior + none + + Record.RelatedJobId__c + RecordRelatedJobId__cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys0__c + RecordRelatedRecordKeys0_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys1__c + RecordRelatedRecordKeys1_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys2__c + RecordRelatedRecordKeys2_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys3__c + RecordRelatedRecordKeys3_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys4__c + RecordRelatedRecordKeys4_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys5__c + RecordRelatedRecordKeys5_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys6__c + RecordRelatedRecordKeys6_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys7__c + RecordRelatedRecordKeys7_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys8__c + RecordRelatedRecordKeys8_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys9__c + RecordRelatedRecordKeys9_cField + + + + + + uiBehavior + none + + Record.RelatedRecordKeys10__c + RecordRelatedRecordKeys10_cField + + + + + + uiBehavior + none + + Record.Body0__c + RecordBody0_cField + + + Facet-fd8f2d5f-4dbc-4758-85d1-0d980062cc8d + Facet + + + + + + body + Facet-fd8f2d5f-4dbc-4758-85d1-0d980062cc8d + + flexipage:column + flexipage_column + + + Facet-4f989d27-d9f3-4535-b1b4-8861dc8f1d1d + Facet + + + + + + uiBehavior + none + + Record.CreatedById + RecordCreatedByIdField + + + Facet-22e74c7b-821d-43ed-9616-4c25a5a4600f + Facet + + + + + + uiBehavior + none + + Record.LastModifiedById + RecordLastModifiedByIdField + + + Facet-54d38f1e-d16e-4c2f-b6c0-39d4e9697441 + Facet + + + + + + body + Facet-22e74c7b-821d-43ed-9616-4c25a5a4600f + + flexipage:column + flexipage_column2 + + + + + + body + Facet-54d38f1e-d16e-4c2f-b6c0-39d4e9697441 + + flexipage:column + flexipage_column3 + + + Facet-1be8da15-daa5-4827-89e1-606bcc6ca7ec + Facet + + + + + + columns + Facet-4f989d27-d9f3-4535-b1b4-8861dc8f1d1d + + + horizontalAlignment + false + + + label + State Info + + flexipage:fieldSection + flexipage_fieldSection + + + + + + columns + Facet-1be8da15-daa5-4827-89e1-606bcc6ca7ec + + + horizontalAlignment + false + + + label + System Info + + flexipage:fieldSection + flexipage_fieldSection2 + + + main + Region + + Rollup State + RollupState__c + + RecordPage + diff --git a/rollup/core/objects/RollupState__c/RollupState__c.object-meta.xml b/rollup/core/objects/RollupState__c/RollupState__c.object-meta.xml new file mode 100644 index 00000000..03058b6a --- /dev/null +++ b/rollup/core/objects/RollupState__c/RollupState__c.object-meta.xml @@ -0,0 +1,61 @@ + + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Action override created by Lightning App Builder during activation. + Rollup_State + Large + false + Flexipage + + + View + Action override created by Lightning App Builder during activation. + Rollup_State + Small + false + Flexipage + + + View + Default + + false + SYSTEM + Deployed + Keeps track of stateful info between batch chunks for batch full recalc rollup operations + false + true + false + false + false + true + true + true + true + Private + + + RS-{00000000} + + AutoNumber + + Rollup States + + ReadWrite + Public + diff --git a/rollup/core/objects/RollupState__c/fields/Body0__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/Body0__c.field-meta.xml new file mode 100644 index 00000000..9f436655 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/Body0__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Body0__c + Serialized state: 0 + Serialized state: 0 + + 131072 + false + LongTextArea + 3 + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedJobId__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedJobId__c.field-meta.xml new file mode 100644 index 00000000..3e500c0f --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedJobId__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedJobId__c + The corresponding (parent) AsyncApexJob Id that started the batch(es) + false + The corresponding (parent) AsyncApexJob Id that started the batch(es) + + 18 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys0__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys0__c.field-meta.xml new file mode 100644 index 00000000..8d67e643 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys0__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys0__c + Comma separated list of parent records with any rollup state associated with them 0 + + Comma separated list of parent records with any rollup state associated with them 0 + false + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys10__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys10__c.field-meta.xml new file mode 100644 index 00000000..88dde973 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys10__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys10__c + Comma separated list of parent records with any rollup state associated with them 10 + false + Comma separated list of parent records with any rollup state associated with them 10 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys1__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys1__c.field-meta.xml new file mode 100644 index 00000000..25068749 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys1__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys1__c + Comma separated list of parent records with any rollup state associated with them 1 + + Comma separated list of parent records with any rollup state associated with them 1 + false + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys2__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys2__c.field-meta.xml new file mode 100644 index 00000000..b968da1e --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys2__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys2__c + Comma separated list of parent records with any rollup state associated with them 2 + + Comma separated list of parent records with any rollup state associated with them 2 + false + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys3__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys3__c.field-meta.xml new file mode 100644 index 00000000..101a89e9 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys3__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys3__c + Comma separated list of parent records with any rollup state associated with them 3 + false + Comma separated list of parent records with any rollup state associated with them 3 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys4__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys4__c.field-meta.xml new file mode 100644 index 00000000..5f8e3743 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys4__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys4__c + Comma separated list of parent records with any rollup state associated with them 4 + false + Comma separated list of parent records with any rollup state associated with them 4 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys5__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys5__c.field-meta.xml new file mode 100644 index 00000000..3315553b --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys5__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys5__c + Comma separated list of parent records with any rollup state associated with them 5 + false + Comma separated list of parent records with any rollup state associated with them 5 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys6__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys6__c.field-meta.xml new file mode 100644 index 00000000..c90aea00 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys6__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys6__c + Comma separated list of parent records with any rollup state associated with them 6 + false + Comma separated list of parent records with any rollup state associated with them 6 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys7__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys7__c.field-meta.xml new file mode 100644 index 00000000..c781b2d1 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys7__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys7__c + Comma separated list of parent records with any rollup state associated with them 7 + false + Comma separated list of parent records with any rollup state associated with them 7 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys8__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys8__c.field-meta.xml new file mode 100644 index 00000000..a1a05e6d --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys8__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys8__c + Comma separated list of parent records with any rollup state associated with them 8 + false + Comma separated list of parent records with any rollup state associated with them 8 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys9__c.field-meta.xml b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys9__c.field-meta.xml new file mode 100644 index 00000000..ed936243 --- /dev/null +++ b/rollup/core/objects/RollupState__c/fields/RelatedRecordKeys9__c.field-meta.xml @@ -0,0 +1,13 @@ + + + RelatedRecordKeys9__c + Comma separated list of parent records with any rollup state associated with them 9 + false + Comma separated list of parent records with any rollup state associated with them 9 + + 255 + false + false + Text + false + diff --git a/rollup/core/objects/RollupState__c/listViews/All.listView-meta.xml b/rollup/core/objects/RollupState__c/listViews/All.listView-meta.xml new file mode 100644 index 00000000..eabf3e70 --- /dev/null +++ b/rollup/core/objects/RollupState__c/listViews/All.listView-meta.xml @@ -0,0 +1,21 @@ + + + All + NAME + RelatedJobId__c + RelatedRecordKeys0__c + RelatedRecordKeys1__c + RelatedRecordKeys2__c + RelatedRecordKeys3__c + RelatedRecordKeys4__c + RelatedRecordKeys5__c + RelatedRecordKeys6__c + RelatedRecordKeys7__c + RelatedRecordKeys8__c + RelatedRecordKeys9__c + RelatedRecordKeys10__c + CREATED_DATE + CREATEDBY_USER + Everything + + diff --git a/rollup/core/objects/RollupState__c/listViews/MyRollupStates.listView-meta.xml b/rollup/core/objects/RollupState__c/listViews/MyRollupStates.listView-meta.xml new file mode 100644 index 00000000..3d18fd74 --- /dev/null +++ b/rollup/core/objects/RollupState__c/listViews/MyRollupStates.listView-meta.xml @@ -0,0 +1,21 @@ + + + MyRollupStates + NAME + RelatedJobId__c + RelatedRecordKeys0__c + RelatedRecordKeys1__c + RelatedRecordKeys2__c + RelatedRecordKeys3__c + RelatedRecordKeys4__c + RelatedRecordKeys5__c + RelatedRecordKeys6__c + RelatedRecordKeys7__c + RelatedRecordKeys8__c + RelatedRecordKeys9__c + RelatedRecordKeys10__c + CREATED_DATE + CREATEDBY_USER + Mine + + diff --git a/rollup/core/profiles/Admin.profile-meta.xml b/rollup/core/profiles/Admin.profile-meta.xml index 9780dd76..5f99e8b2 100644 --- a/rollup/core/profiles/Admin.profile-meta.xml +++ b/rollup/core/profiles/Admin.profile-meta.xml @@ -105,6 +105,71 @@ true RollupPluginParameter__mdt + + true + RollupState__c.Body0__c + true + + + true + RollupState__c.RelatedJobId__c + true + + + true + RollupState__c.RelatedRecordKeys0__c + true + + + true + RollupState__c.RelatedRecordKeys1__c + true + + + true + RollupState__c.RelatedRecordKeys2__c + true + + + true + RollupState__c.RelatedRecordKeys3__c + true + + + true + RollupState__c.RelatedRecordKeys4__c + true + + + true + RollupState__c.RelatedRecordKeys5__c + true + + + true + RollupState__c.RelatedRecordKeys6__c + true + + + true + RollupState__c.RelatedRecordKeys7__c + true + + + true + RollupState__c.RelatedRecordKeys8__c + true + + + true + RollupState__c.RelatedRecordKeys9__c + true + + + true + RollupState__c.RelatedRecordKeys10__c + true + RollupControl__mdt-Rollup Control Layout @@ -120,5 +185,14 @@ RollupPluginParameter__mdt-Rollup Plugin Parameter Layout + + true + true + true + true + true + RollupState__c + true + Salesforce diff --git a/scripts/convert-dlrs-rules-namespaced.apex b/scripts/convert-dlrs-rules-namespaced.apex index fa035ae1..d82fede3 100644 --- a/scripts/convert-dlrs-rules-namespaced.apex +++ b/scripts/convert-dlrs-rules-namespaced.apex @@ -4,14 +4,10 @@ static final please__RollupControl__mdt ROLLUP_CONTROL = please__RollupControl__mdt.getInstance('please__Org_Defaults'); // Prepare the converted Rollup__mdt CMDT records for deployment -String customMetadataTypePrefix = Schema.please__Rollup__mdt.SObjectType.getDescribe().getName().replace('__mdt', ''); +String customMetadataTypePrefix = Schema.please__Rollup__mdt.SObjectType.toString().replace('__mdt', ''); Metadata.DeployContainer deployment = new Metadata.DeployContainer(); -Set objectsUnsupportedUsingEntityDefinition = new Set{ - Event.SObjectType.getDescribe().getName(), - Task.SObjectType.getDescribe().getName(), - User.SObjectType.getDescribe().getName() -}; +Set objectsUnsupportedUsingEntityDefinition = new Set{ Event.SObjectType.toString(), Task.SObjectType.toString(), User.SObjectType.toString() }; Boolean shouldDeploy = false; for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt.getAll().values()) { @@ -65,20 +61,20 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. // This code uses instances of Metadata.CustomMetadataValue for the deployment - not instances of Rollup__mdt // So, use a map & field tokens to store the expected values - Salesforce will store the data as Rollup__mdt records when deployed Map fieldValuesToCopy = new Map{ - calcObject.getDescribe().getName() => dlrsRule.dlrs__ChildObject__c, - please__Rollup__mdt.please__CalcItemWhereClause__c.getDescribe().getName() => dlrsRule.dlrs__RelationshipCriteria__c, - please__Rollup__mdt.please__ConcatDelimiter__c.getDescribe().getName() => operation.startsWith('CONCAT') ? dlrsRule.dlrs__ConcatenateDelimiter__c : null, - please__Rollup__mdt.please__Description__c.getDescribe().getName() => dlrsRule.dlrs__Description__c, - please__Rollup__mdt.please__LimitAmount__c.getDescribe().getName() => dlrsRule.dlrs__RowLimit__c, - lookupFieldCalcItem.getDescribe().getName() => dlrsRule.dlrs__RelationshipField__c, - lookupFieldParent.getDescribe().getName() => 'Id', - lookupObject.getDescribe().getName() => dlrsRule.dlrs__ParentObject__c, - please__Rollup__mdt.please__OrderByFirstLast__c.getDescribe().getName() => dlrsRule.dlrs__FieldToOrderBy__c, - please__Rollup__mdt.please__RollupControl__c.getDescribe().getName() => ROLLUP_CONTROL.DeveloperName, - rollupFieldCalcItem.getDescribe().getName() => dlrsRule.dlrs__FieldToAggregate__c, - rollupFieldParent.getDescribe().getName() => dlrsRule.dlrs__AggregateResultField__c, - please__Rollup__mdt.please__RollupOperation__c.getDescribe().getName() => operation.toUpperCase(), - please__Rollup__mdt.please__SharingMode__c.getDescribe().getName() => dlrsRule.dlrs__CalculationSharingMode__c + calcObject.toString() => dlrsRule.dlrs__ChildObject__c, + please__Rollup__mdt.please__CalcItemWhereClause__c.toString() => dlrsRule.dlrs__RelationshipCriteria__c, + please__Rollup__mdt.please__ConcatDelimiter__c.toString() => operation.startsWith('CONCAT') ? dlrsRule.dlrs__ConcatenateDelimiter__c : null, + please__Rollup__mdt.please__Description__c.toString() => dlrsRule.dlrs__Description__c, + please__Rollup__mdt.please__LimitAmount__c.toString() => dlrsRule.dlrs__RowLimit__c, + lookupFieldCalcItem.toString() => dlrsRule.dlrs__RelationshipField__c, + lookupFieldParent.toString() => 'Id', + lookupObject.toString() => dlrsRule.dlrs__ParentObject__c, + please__Rollup__mdt.please__OrderByFirstLast__c.toString() => dlrsRule.dlrs__FieldToOrderBy__c, + please__Rollup__mdt.please__RollupControl__c.toString() => ROLLUP_CONTROL.DeveloperName, + rollupFieldCalcItem.toString() => dlrsRule.dlrs__FieldToAggregate__c, + rollupFieldParent.toString() => dlrsRule.dlrs__AggregateResultField__c, + please__Rollup__mdt.please__RollupOperation__c.toString() => operation.toUpperCase(), + please__Rollup__mdt.please__SharingMode__c.toString() => dlrsRule.dlrs__CalculationSharingMode__c // Additional DLRS fields that are not supported/used by Rollup // dlrs__AggregateAllRows__c @@ -90,7 +86,7 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. for (String fieldName : fieldValuesToCopy.keySet()) { Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue(); customField.field = fieldName; - if (fieldName == please__Rollup__mdt.please__Description__c.getDescribe().getName()) { + if (fieldName == please__Rollup__mdt.please__Description__c.toString()) { customField.value = 'Generated by migration script:\n' + fieldValuesToCopy.get(fieldName); } else { customField.value = fieldValuesToCopy.get(fieldName); diff --git a/scripts/convert-dlrs-rules.apex b/scripts/convert-dlrs-rules.apex index 08e4e4eb..ecda53b4 100644 --- a/scripts/convert-dlrs-rules.apex +++ b/scripts/convert-dlrs-rules.apex @@ -4,7 +4,7 @@ static final RollupControl__mdt ROLLUP_CONTROL = RollupControl__mdt.getInstance('Org_Defaults'); // Prepare the converted Rollup__mdt CMDT records for deployment -String customMetadataTypePrefix = Schema.Rollup__mdt.SObjectType.getDescribe().getName().replace('__mdt', ''); +String customMetadataTypePrefix = Schema.Rollup__mdt.SObjectType.toString().replace('__mdt', ''); Metadata.DeployContainer deployment = new Metadata.DeployContainer(); List objectsUnsupportedUsingEntityDefinition = new List{ 'Event', 'Task', 'User' }; @@ -61,20 +61,20 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. // This code uses instances of Metadata.CustomMetadataValue for the deployment - not instances of Rollup__mdt // So, use a map & field tokens to store the expected values - Salesforce will store the data as Rollup__mdt records when deployed Map fieldValuesToCopy = new Map{ - calcObject.getDescribe().getName() => dlrsRule.dlrs__ChildObject__c, - Schema.Rollup__mdt.CalcItemWhereClause__c.getDescribe().getName() => dlrsRule.dlrs__RelationshipCriteria__c, - Schema.Rollup__mdt.ConcatDelimiter__c.getDescribe().getName() => operation.startsWith('CONCAT') ? dlrsRule.dlrs__ConcatenateDelimiter__c : null, - Schema.Rollup__mdt.Description__c.getDescribe().getName() => dlrsRule.dlrs__Description__c, - Schema.Rollup__mdt.LimitAmount__c.getDescribe().getName() => dlrsRule.dlrs__RowLimit__c, - lookupFieldCalcItem.getDescribe().getName() => dlrsRule.dlrs__RelationshipField__c, - lookupFieldParent.getDescribe().getName() => 'Id', - lookupObject.getDescribe().getName() => dlrsRule.dlrs__ParentObject__c, - Schema.Rollup__mdt.OrderByFirstLast__c.getDescribe().getName() => dlrsRule.dlrs__FieldToOrderBy__c, - Schema.Rollup__mdt.RollupControl__c.getDescribe().getName() => ROLLUP_CONTROL.DeveloperName, - rollupFieldCalcItem.getDescribe().getName() => dlrsRule.dlrs__FieldToAggregate__c, - rollupFieldParent.getDescribe().getName() => dlrsRule.dlrs__AggregateResultField__c, - Schema.Rollup__mdt.RollupOperation__c.getDescribe().getName() => operation.toUpperCase(), - Schema.Rollup__mdt.SharingMode__c.getDescribe().getName() => dlrsRule.dlrs__CalculationSharingMode__c + calcObject.toString() => dlrsRule.dlrs__ChildObject__c, + Schema.Rollup__mdt.CalcItemWhereClause__c.toString() => dlrsRule.dlrs__RelationshipCriteria__c, + Schema.Rollup__mdt.ConcatDelimiter__c.toString() => operation.startsWith('CONCAT') ? dlrsRule.dlrs__ConcatenateDelimiter__c : null, + Schema.Rollup__mdt.Description__c.toString() => dlrsRule.dlrs__Description__c, + Schema.Rollup__mdt.LimitAmount__c.toString() => dlrsRule.dlrs__RowLimit__c, + lookupFieldCalcItem.toString() => dlrsRule.dlrs__RelationshipField__c, + lookupFieldParent.toString() => 'Id', + lookupObject.toString() => dlrsRule.dlrs__ParentObject__c, + Schema.Rollup__mdt.OrderByFirstLast__c.toString() => dlrsRule.dlrs__FieldToOrderBy__c, + Schema.Rollup__mdt.RollupControl__c.toString() => ROLLUP_CONTROL.DeveloperName, + rollupFieldCalcItem.toString() => dlrsRule.dlrs__FieldToAggregate__c, + rollupFieldParent.toString() => dlrsRule.dlrs__AggregateResultField__c, + Schema.Rollup__mdt.RollupOperation__c.toString() => operation.toUpperCase(), + Schema.Rollup__mdt.SharingMode__c.toString() => dlrsRule.dlrs__CalculationSharingMode__c // Additional DLRS fields that are not supported/used by Rollup // dlrs__AggregateAllRows__c @@ -86,7 +86,7 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. for (String fieldName : fieldValuesToCopy.keySet()) { Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue(); customField.field = fieldName; - if (fieldName == Schema.Rollup__mdt.Description__c.getDescribe().getName()) { + if (fieldName == Schema.Rollup__mdt.Description__c.toString()) { customField.value = 'Generated by migration script:\n' + fieldValuesToCopy.get(fieldName); } else { customField.value = fieldValuesToCopy.get(fieldName); diff --git a/scripts/deactivate-converted-dlrs-rules-namespaced.apex b/scripts/deactivate-converted-dlrs-rules-namespaced.apex index 899f1de9..779391da 100644 --- a/scripts/deactivate-converted-dlrs-rules-namespaced.apex +++ b/scripts/deactivate-converted-dlrs-rules-namespaced.apex @@ -2,7 +2,7 @@ // This assumes that the records in dlrs__LookupRollupSummary2__mdt and please__Rollup__mdt have the same DeveloperName Set rollupRecordDeveloperNames = please__Rollup__mdt.getAll().keySet(); -String dlrsCustomMetadataTypePrefix = Schema.dlrs__LookupRollupSummary2__mdt.SObjectType.getDescribe().getName().replace('__mdt', ''); +String dlrsCustomMetadataTypePrefix = Schema.dlrs__LookupRollupSummary2__mdt.SObjectType.toString().replace('__mdt', ''); Metadata.DeployContainer deployment = new Metadata.DeployContainer(); for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt.getAll().values()) { @@ -12,7 +12,7 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. } Metadata.CustomMetadataValue dlrsIsActiveField = new Metadata.CustomMetadataValue(); - dlrsIsActiveField.field = Schema.dlrs__LookupRollupSummary2__mdt.dlrs__Active__c.getDescribe().getName(); + dlrsIsActiveField.field = Schema.dlrs__LookupRollupSummary2__mdt.dlrs__Active__c.toString(); dlrsIsActiveField.value = false; Metadata.CustomMetadata dlrsCustomMetadataRecord = new Metadata.CustomMetadata(); diff --git a/scripts/deactivate-converted-dlrs-rules.apex b/scripts/deactivate-converted-dlrs-rules.apex index 1b66eb01..6de8c281 100644 --- a/scripts/deactivate-converted-dlrs-rules.apex +++ b/scripts/deactivate-converted-dlrs-rules.apex @@ -2,7 +2,7 @@ // This assumes that the records in dlrs__LookupRollupSummary2__mdt and Rollup__mdt have the same DeveloperName Set rollupRecordDeveloperNames = Rollup__mdt.getAll().keySet(); -String dlrsCustomMetadataTypePrefix = Schema.dlrs__LookupRollupSummary2__mdt.SObjectType.getDescribe().getName().replace('__mdt', ''); +String dlrsCustomMetadataTypePrefix = Schema.dlrs__LookupRollupSummary2__mdt.SObjectType.toString().replace('__mdt', ''); Metadata.DeployContainer deployment = new Metadata.DeployContainer(); for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt.getAll().values()) { @@ -12,7 +12,7 @@ for (dlrs__LookupRollupSummary2__mdt dlrsRule : dlrs__LookupRollupSummary2__mdt. } Metadata.CustomMetadataValue dlrsIsActiveField = new Metadata.CustomMetadataValue(); - dlrsIsActiveField.field = Schema.dlrs__LookupRollupSummary2__mdt.dlrs__Active__c.getDescribe().getName(); + dlrsIsActiveField.field = Schema.dlrs__LookupRollupSummary2__mdt.dlrs__Active__c.toString(); dlrsIsActiveField.value = false; Metadata.CustomMetadata dlrsCustomMetadataRecord = new Metadata.CustomMetadata(); diff --git a/sfdx-project.json b/sfdx-project.json index 01e89546..1476c772 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -5,8 +5,8 @@ "package": "apex-rollup", "path": "rollup", "scopeProfiles": true, - "versionName": "Fixes mutually exclusive polymorphic where clauses replacing children in RollupCalcItemReplacer", - "versionNumber": "1.6.37.0", + "versionName": "New RollupState implementation to stop running out of memory on large full recalcs", + "versionNumber": "1.7.0.0", "versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.", "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest", "unpackagedMetadata": { @@ -69,12 +69,12 @@ "package": "Apex Rollup - Extra Code Coverage", "versionDescription": "This plugin adds code coverage for Apex Rollup if you need additional code coverage for the base package", "versionName": "Updating code coverage", - "versionNumber": "0.0.24.0", + "versionNumber": "0.0.25.0", "default": false, "scopeProfiles": true, "dependencies": [ { - "package": "apex-rollup@1.6.34" + "package": "apex-rollup@1.7.0" } ] }, @@ -93,6 +93,7 @@ "Apex Rollup - Extra Code Coverage": "0Ho6g000000GnCWCA0", "Apex Rollup - Extra Code Coverage@0.0.23": "04t6g000008ObVmAAK", "Apex Rollup - Extra Code Coverage@0.0.24": "04t6g000008ObgCAAS", + "Apex Rollup - Extra Code Coverage@0.0.25": "04t6g000008OfSiAAK", "Apex Rollup - Nebula Logger": "0Ho6g000000Gn8PCAS", "Apex Rollup - Nebula Logger@0.0.7-0": "04t6g000008b0O7AAI", "Apex Rollup - Nebula Logger@0.0.8-0": "04t6g000007zM6tAAE", @@ -101,12 +102,10 @@ "Apex Rollup - Rollup Callback@0.0.3-0": "04t6g000008Sis0AAC", "Nebula Logger - Core@4.14.4-optionally-auto-call-lightning-logger-lwc": "04t5Y0000015oRNQAY", "apex-rollup": "0Ho6g000000TNcOCAW", - "apex-rollup@1.6.31": "04t6g000008ObblAAC", - "apex-rollup@1.6.32": "04t6g000008ObbvAAC", - "apex-rollup@1.6.33": "04t6g000008ObeQAAS", "apex-rollup@1.6.34": "04t6g000008OfJfAAK", "apex-rollup@1.6.35": "04t6g000008OfKiAAK", "apex-rollup@1.6.36": "04t6g000008OfMeAAK", - "apex-rollup@1.6.37": "04t6g000008OfSEAA0" + "apex-rollup@1.6.37": "04t6g000008OfSEAA0", + "apex-rollup@1.7.0": "04t6g000008OfTvAAK" } }