From d89e6150ef68fc69ef78ce7160dbc9532948aed3 Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:13:40 -0700 Subject: [PATCH] v1.7.1 - Scheduled Rollup Updates & Date literal comparisons (#641) * Fixes #640 by properly ensuring RollupDateLiteral comparisons occur in local time since they are always based off of the running user and their time zone * Fixes an issue brought up in #639 where scheduling a rollup can inadvertently exceed the max query rows --- .prettierrc | 4 ++ README.md | 6 +- .../classes/RollupDateLiteralTests.cls | 34 +++++++-- .../classes/RollupIntegrationTests.cls | 72 ++++++++++++++++++- package.json | 2 +- rollup-namespaced/README.md | 4 +- rollup-namespaced/sfdx-project.json | 7 +- rollup/core/classes/Rollup.cls | 41 +++++++---- rollup/core/classes/RollupDateLiteral.cls | 12 ++-- .../classes/RollupFullBatchRecalculator.cls | 10 +++ .../classes/RollupFullRecalcProcessor.cls | 8 ++- rollup/core/classes/RollupLogger.cls | 2 +- rollup/core/classes/RollupRepository.cls | 3 + sfdx-project.json | 7 +- 14 files changed, 172 insertions(+), 40 deletions(-) diff --git a/.prettierrc b/.prettierrc index a3223d26..5c665af8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -16,6 +16,10 @@ { "files": "*.{cls, apex}", "options": { "parser": "apex" } + }, + { + "files": "**/*.xml", + "options": { "parser": "xml" } } ], "plugins": ["prettier-plugin-apex", "@prettier/plugin-xml"], diff --git a/README.md b/README.md index 97e2bf6e..87370b2b 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ As well, don't miss [the Wiki](../../wiki), which includes even more info for co ## Deployment & Setup - + Deploy to Salesforce - + Deploy to Salesforce Sandbox
@@ -358,7 +358,7 @@ Rollup.schedule( 'My example job name', 'my cron expression, like 0 0 0 * * ?', 'my SOQL query, like SELECT Id, Amount FROM Opportunity WHERE CreatedDate > YESTERDAY', - 'The API name of the SObject associated with Rollup__mdt records configuring the rollup operation', + 'The API name of the Child SObject associated with Rollup__mdt records for this schedule', null ); ``` diff --git a/extra-tests/classes/RollupDateLiteralTests.cls b/extra-tests/classes/RollupDateLiteralTests.cls index 4ed69395..7965100b 100644 --- a/extra-tests/classes/RollupDateLiteralTests.cls +++ b/extra-tests/classes/RollupDateLiteralTests.cls @@ -169,7 +169,6 @@ private class RollupDateLiteralTests { System.assertNotEquals(true, matchingCpcs.isEmpty()); for (ContactPointConsent cpc : matchingCpcs) { assert(literalUnderTest, cpc.CaptureDate, '='); - assert(literalUnderTest, cpc.CaptureDate.dateGmt(), '='); } if (sentinelPostCpc != null) { assert(literalUnderTest, sentinelPostCpc.CaptureDate, '>'); @@ -191,6 +190,12 @@ private class RollupDateLiteralTests { ); } + static User australiaUser { + get { + return [SELECT Id FROM User WHERE LastName = 'Australia User']; + } + } + @IsTest static void shouldWorkForYesterday() { runTestForLiteral('YESTERDAY'); @@ -199,32 +204,46 @@ private class RollupDateLiteralTests { @IsTest static void shouldWorkForToday() { runTestForLiteral('TODAY'); + System.runAs(australiaUser) { + runTestForLiteral('TODAY'); + } } @IsTest static void shouldWorkForTomorrow() { runTestForLiteral('TOMORROW'); + System.runAs(australiaUser) { + runTestForLiteral('TOMORROW'); + } } @IsTest static void shouldWorkForLastWeek() { runTestForLiteral('LAST_WEEK'); + System.runAs(australiaUser) { + runTestForLiteral('LAST_WEEK'); + } } @IsTest static void shouldWorkForThisWeek() { runTestForLiteral('THIS_WEEK'); + System.runAs(australiaUser) { + runTestForLiteral('THIS_WEEK'); + } } @IsTest static void shouldWorkForNextWeek() { runTestForLiteral('NEXT_WEEK'); + System.runAs(australiaUser) { + runTestForLiteral('NEXT_WEEK'); + } } @IsTest static void shouldWorkForLastMonth() { runTestForLiteral('LAST_MONTH'); - User australiaUser = [SELECT Id FROM User WHERE LastName = 'Australia User']; System.runAs(australiaUser) { runTestForLiteral('LAST_MONTH'); } @@ -233,7 +252,6 @@ private class RollupDateLiteralTests { @IsTest static void shouldWorkForThisMonth() { runTestForLiteral('THIS_MONTH'); - User australiaUser = [SELECT Id FROM User WHERE LastName = 'Australia User']; System.runAs(australiaUser) { runTestForLiteral('THIS_MONTH'); } @@ -242,7 +260,6 @@ private class RollupDateLiteralTests { @IsTest static void shouldWorkForNextMonth() { runTestForLiteral('NEXT_MONTH'); - User australiaUser = [SELECT Id FROM User WHERE LastName = 'Australia User']; System.runAs(australiaUser) { runTestForLiteral('NEXT_MONTH'); } @@ -392,7 +409,14 @@ private class RollupDateLiteralTests { @IsTest static void shouldWorkForNextNFiscalQuarters() { + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 1'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 2'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 3'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 4'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 5'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 6'); runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 7'); + runTestForLiteral('NEXT_N_FISCAL_QUARTERS: 8'); } @IsTest @@ -592,4 +616,4 @@ private class RollupDateLiteralTests { System.assertEquals(false, weekInYear.matches(oneWeekAfter, '=')); System.assertEquals(false, weekInYear.matches(comparisonDate, '!=')); } -} +} \ No newline at end of file diff --git a/extra-tests/classes/RollupIntegrationTests.cls b/extra-tests/classes/RollupIntegrationTests.cls index 922227b0..45a61aec 100644 --- a/extra-tests/classes/RollupIntegrationTests.cls +++ b/extra-tests/classes/RollupIntegrationTests.cls @@ -1956,7 +1956,7 @@ private class RollupIntegrationTests { Rollup.schedule('Test bad query', '0 0 0 0 0', veryBadQuery, 'Account', null); Assert.fail('Exception should be thrown above'); } catch (Exception ex) { - Assert.isTrue(ex.getMessage().contains('field ActivityDate does not support aggregate operator MAX')); + Assert.isTrue(ex.getMessage().containsIgnoreCase('field ActivityDate does not support aggregate operator MAX'), ex.getMessage()); } } @@ -1968,4 +1968,74 @@ private class RollupIntegrationTests { System.assertNotEquals(null, jobId); } + + @IsTest + static void usesFullBatchRecalculatorForLargerQueries() { + List accounts = [SELECT Id FROM Account]; + RollupAsyncProcessor.stubParentRecords = accounts; + Rollup.defaultControl = new RollupControl__mdt(MaxQueryRows__c = 0); + Rollup.onlyUseMockMetadata = true; + Rollup.rollupMetadata = new List{ + new Rollup__mdt( + RollupFieldOnCalcItem__c = 'Id', + LookupObject__c = 'Account', + LookupFieldOnCalcItem__c = 'AccountId', + LookupFieldOnLookupObject__c = 'Id', + RollupFieldOnLookupObject__c = 'AnnualRevenue', + RollupOperation__c = 'COUNT', + CalcItem__c = 'Contact' + ) + }; + insert new Contact(AccountId = accounts[0].Id, LastName = 'Does Not Reset'); + + String jobId = Rollup.schedule('Does not Reset ' + System.now(), '0 0 0 * * ?', 'SELECT Id, AccountId FROM Contact', 'Account', null); + + CronTrigger job = [SELECT CronJobDetail.Name FROM CronTrigger WHERE Id = :jobId]; + Assert.isTrue(job.CronJobDetail.Name.contains(RollupFullBatchRecalculator.class.getName())); + } + + @IsTest + static void doesNotResetParentRecordValuesForLargeScheduledQueries() { + Account acc = [SELECT Id FROM Account]; + acc.AnnualRevenue = 1; + update acc; + + Rollup.onlyUseMockMetadata = true; + Rollup.rollupMetadata = new List{ + new Rollup__mdt( + RollupFieldOnCalcItem__c = 'Id', + LookupObject__c = 'Account', + LookupFieldOnCalcItem__c = 'AccountId', + LookupFieldOnLookupObject__c = 'Id', + RollupFieldOnLookupObject__c = 'AnnualRevenue', + RollupOperation__c = 'COUNT', + CalcItem__c = 'Contact' + ) + }; + insert new Contact(AccountId = acc.Id, LastName = 'Does Not Reset'); + + Test.startTest(); + new RollupFullBatchRecalculator.NonResettingBulkFullRecalc( + 'SELECT Id, AccountId\nFROM Contact', + Rollup.InvocationPoint.FROM_SCHEDULED, + new List{ + new Rollup__mdt( + RollupFieldOnCalcItem__c = 'Id', + LookupObject__c = 'Account', + LookupFieldOnCalcItem__c = 'AccountId', + LookupFieldOnLookupObject__c = 'Id', + RollupFieldOnLookupObject__c = 'AnnualRevenue', + RollupOperation__c = 'COUNT', + CalcItem__c = 'Contact', + CalcItemWhereClause__c = RollupCurrencyInfo.isMultiCurrency() ? RollupCurrencyInfo.CURRENCY_ISO_CODE_FIELD_NAME + ' != null' : null + ) + }, + Contact.SObjectType + ) + .runCalc(); + Test.stopTest(); + + acc = [SELECT AnnualRevenue FROM Account WHERE Id = :acc.Id LIMIT 1]; + Assert.areEqual(2, acc.AnnualRevenue); + } } diff --git a/package.json b/package.json index 4a7a79d3..ff4b72c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apex-rollup", - "version": "1.7.0", + "version": "1.7.1", "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/rollup-namespaced/README.md b/rollup-namespaced/README.md index 24a4c628..9d8a5e85 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 dbdb0df5..c4eb03ec 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": "New RollupState implementation to stop running out of memory on large full recalcs", - "versionNumber": "1.2.0.0", + "versionName": "Scheduled Rollup updates, RollupDateLiteral updates", + "versionNumber": "1.2.1.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": { @@ -25,6 +25,7 @@ "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.2.0": "04t6g000008OfU0AAK" + "apex-rollup-namespaced@1.2.0": "04t6g000008OfU0AAK", + "apex-rollup-namespaced@1.2.1": "04t6g000008OfWVAA0" } } diff --git a/rollup/core/classes/Rollup.cls b/rollup/core/classes/Rollup.cls index f573e532..34a5b76b 100644 --- a/rollup/core/classes/Rollup.cls +++ b/rollup/core/classes/Rollup.cls @@ -917,23 +917,38 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje } global static Id schedule(String jobName, String cronExp, String query, String rollupObjectName, Evaluator eval) { - List localCalcItems; + Rollup rollToSchedule; + query = query.toUpperCase(); + if (query.contains('\nFROM ') == false) { + query = query.substringBeforeLast(' FROM ') + '\nFROM ' + query.substringAfterLast(' FROM '); + } try { - localCalcItems = new RollupRepository(RollupRepository.RunAsMode.SYSTEM_LEVEL).setQuery(query).get(); + RollupRepository repo = new RollupRepository(RollupRepository.RunAsMode.SYSTEM_LEVEL).setQuery(query); + Integer queryCount = repo.getCount(); + if (queryCount < CACHED_DEFAULT.MaxQueryRows__c) { + List localCalcItems = repo.get(); + rollToSchedule = getRollup( + getRollupMetadataBySObject(localCalcItems.getSObjectType()), + localCalcItems.getSObjectType(), + localCalcItems, + new Map(), + eval, + InvocationPoint.FROM_SCHEDULED + ); + } else { + Schema.SObjectType childType = RollupFieldInitializer.Current.getDescribeFromName(rollupObjectName).getSObjectType(); + rollToSchedule = new RollupFullBatchRecalculator.NonResettingBulkFullRecalc( + query, + InvocationPoint.FROM_SCHEDULED, + getRollupMetadataBySObject(childType), + childType + ); + } } catch (QueryException ex) { throw new QueryException('There\'s a problem with your query: ' + ex.getMessage() + '\n' + ex.getStackTraceString()); } - SObjectType calcItemType = localCalcItems.getSObjectType(); - Rollup roll = getRollup( - getRollupMetadataBySObject(calcItemType), - calcItemType, - localCalcItems, - new Map(), - eval, - InvocationPoint.FROM_SCHEDULED - ); - RollupSchedulable scheduledRollup = new RollupSchedulable(roll); - return System.schedule(jobName, cronExp, scheduledRollup); + RollupSchedulable scheduledRollup = new RollupSchedulable(rollToSchedule); + return System.schedule(jobName + ' for ' + rollToSchedule.getTypeName(), cronExp, scheduledRollup); } global static void batch(Rollup rollup, Rollup secondRollup) { diff --git a/rollup/core/classes/RollupDateLiteral.cls b/rollup/core/classes/RollupDateLiteral.cls index 54b43dad..8575b8ba 100644 --- a/rollup/core/classes/RollupDateLiteral.cls +++ b/rollup/core/classes/RollupDateLiteral.cls @@ -258,7 +258,7 @@ public without sharing abstract class RollupDateLiteral { protected virtual Boolean isEqualTo(Object val) { if (val instanceof Date) { Date dateVal = (Date) val; - return this.ref.dateGmt() <= dateVal && dateVal <= this.bound.dateGmt(); + return this.ref.date() <= dateVal && dateVal <= this.bound.date(); } Datetime datetimeVal = (Datetime) val; return this.ref <= datetimeVal && datetimeVal <= this.bound; @@ -266,7 +266,7 @@ public without sharing abstract class RollupDateLiteral { protected virtual Boolean isGreaterThan(Object val) { if (val instanceof Date) { Date dateVal = (Date) val; - return dateVal > this.bound.dateGmt() && dateVal > this.ref.dateGmt(); + return dateVal > this.bound.date() && dateVal > this.ref.date(); } Datetime datetimeVal = (Datetime) val; return datetimeVal > this.bound && datetimeVal > this.ref; @@ -274,7 +274,7 @@ public without sharing abstract class RollupDateLiteral { protected virtual Boolean isLessThan(Object val) { if (val instanceof Date) { Date dateVal = (Date) val; - return dateVal < this.bound.dateGmt() && dateVal < this.ref.dateGmt(); + return dateVal < this.bound.date() && dateVal < this.ref.date(); } Datetime datetimeVal = (Datetime) val; return datetimeVal < this.bound && datetimeVal < this.ref; @@ -333,7 +333,7 @@ public without sharing abstract class RollupDateLiteral { private class ThisWeekLiteral extends RollupDateLiteral { public ThisWeekLiteral() { this.ref = getRelativeDatetime(System.today().toStartOfWeek(), START_TIME); - this.bound = getRelativeDatetime(this.ref.addDays(6).dateGmt(), END_TIME); + this.bound = getRelativeDatetime(this.ref.addDays(6).date(), END_TIME); } } @@ -344,7 +344,7 @@ public without sharing abstract class RollupDateLiteral { private class NextWeekLiteral extends RollupDateLiteral { public NextWeekLiteral() { this.ref = getRelativeDatetime(System.today().toStartOfWeek().addDays(7), START_TIME); - this.bound = getRelativeDatetime(this.ref.addDays(6).dateGmt(), END_TIME); + this.bound = getRelativeDatetime(this.ref.addDays(6).date(), END_TIME); } } @@ -397,7 +397,7 @@ public without sharing abstract class RollupDateLiteral { private class Last90DaysLiteral extends RollupDateLiteral { public Last90DaysLiteral() { this.bound = getRelativeDatetime(START_OF_TODAY.date(), END_TIME); - this.ref = getRelativeDatetime(this.bound.addDays(-91).dateGmt(), START_TIME); + this.ref = getRelativeDatetime(this.bound.addDays(-91).date(), START_TIME); } } diff --git a/rollup/core/classes/RollupFullBatchRecalculator.cls b/rollup/core/classes/RollupFullBatchRecalculator.cls index 5e0d8dae..d27c70cc 100644 --- a/rollup/core/classes/RollupFullBatchRecalculator.cls +++ b/rollup/core/classes/RollupFullBatchRecalculator.cls @@ -5,6 +5,16 @@ public without sharing virtual class RollupFullBatchRecalculator extends RollupF private static final Integer DEFAULT_CHUNK_SIZE = 500; + public class NonResettingBulkFullRecalc extends RollupFullBatchRecalculator { + public NonResettingBulkFullRecalc(String queryString, InvocationPoint invokePoint, List rollupMetas, SObjectType calcItemType) { + super(queryString, invokePoint, rollupMetas, calcItemType, new Set(), null); + } + + public override Boolean getShouldResetParentRecordsDuringRecalculation() { + return false; + } + } + public RollupFullBatchRecalculator( String queryString, InvocationPoint invokePoint, diff --git a/rollup/core/classes/RollupFullRecalcProcessor.cls b/rollup/core/classes/RollupFullRecalcProcessor.cls index 23d6fd18..13fee11d 100644 --- a/rollup/core/classes/RollupFullRecalcProcessor.cls +++ b/rollup/core/classes/RollupFullRecalcProcessor.cls @@ -165,13 +165,17 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs } for (RollupAsyncProcessor innerRoll : processor.rollups) { innerRoll.fullRecalcProcessor = this; - innerRoll.isFullRecalc = true; + innerRoll.isFullRecalc = this.getShouldResetParentRecordsDuringRecalculation(); innerRoll.calcItems = calcItems; } this.fullRecalcProcessor = this; return processor.rollups; } + protected virtual Boolean getShouldResetParentRecordsDuringRecalculation() { + return true; + } + protected virtual override Map customizeToStringEntries(Map props) { super.customizeToStringEntries(props); Integer numberOfRollups = this.rollupMetas?.size(); @@ -207,4 +211,4 @@ global abstract without sharing class RollupFullRecalcProcessor extends RollupAs this.runAsMode = RollupMetaPicklists.getAccessLevel(meta); } } -} +} \ No newline at end of file diff --git a/rollup/core/classes/RollupLogger.cls b/rollup/core/classes/RollupLogger.cls index d83285d7..7d02cb3c 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.7.0'; + private static final String CURRENT_VERSION_NUMBER = 'v1.7.1'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final RollupPlugin PLUGIN = new RollupPlugin(); diff --git a/rollup/core/classes/RollupRepository.cls b/rollup/core/classes/RollupRepository.cls index adac329d..e1c072df 100644 --- a/rollup/core/classes/RollupRepository.cls +++ b/rollup/core/classes/RollupRepository.cls @@ -62,10 +62,12 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb } public Integer getCount() { + String originalQuery = this.args.query; if (this.args.query.contains(RollupQueryBuilder.ALL_ROWS)) { this.args.query = this.args.query.replace(RollupQueryBuilder.ALL_ROWS, ''); } this.args.query = this.args.query.replaceFirst('SELECT.+\n', 'SELECT Count()\n'); + this.createQueryLog('Getting count'); Integer countAmount; try { @@ -77,6 +79,7 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb countAmount = SENTINEL_COUNT_VALUE; } this.createQueryLog('Returned amount: ' + countAmount); + this.args.query = originalQuery; return countAmount; } diff --git a/sfdx-project.json b/sfdx-project.json index 1476c772..8d89ff52 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -5,8 +5,8 @@ "package": "apex-rollup", "path": "rollup", "scopeProfiles": true, - "versionName": "New RollupState implementation to stop running out of memory on large full recalcs", - "versionNumber": "1.7.0.0", + "versionName": "Scheduled Rollup updates, RollupDateLiteral updates", + "versionNumber": "1.7.1.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": { @@ -106,6 +106,7 @@ "apex-rollup@1.6.35": "04t6g000008OfKiAAK", "apex-rollup@1.6.36": "04t6g000008OfMeAAK", "apex-rollup@1.6.37": "04t6g000008OfSEAA0", - "apex-rollup@1.7.0": "04t6g000008OfTvAAK" + "apex-rollup@1.7.0": "04t6g000008OfTvAAK", + "apex-rollup@1.7.1": "04t6g000008OfWQAA0" } }