Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #216 & #240 - add order by support of multiple fields, ASC/DESC and NULLS FIRST/LAST & fix to ensure recalc when order by field changes #238

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 101 additions & 19 deletions rolluptool/src/classes/LREngine.cls
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public class LREngine {
// #0 token : SOQL projection
String soqlProjection = ctx.lookupField.getName();
List<String> orderByFields = new List<String>();
orderByFields.add(ctx.lookupField.getName()); // ensure details records are ordered by parent record
orderByFields.add(new Ordering(ctx.lookupField).toString()); // ensure details records are ordered by parent record

// k: detail field name, v: master field name
Integer exprIdx = 0;
Expand Down Expand Up @@ -163,11 +163,12 @@ public class LREngine {
}
// create order by projections
// i.e. Amount ASC NULLS FIRST
String orderByField =
rsf.detailOrderBy!=null ? rsf.detailOrderBy.getName() : rsf.detail.getName();
if(!orderByFieldsSet.contains(orderByField)) {
orderByFields.add(orderByField);
orderByFieldsSet.add(orderByField);
for (Ordering orderByField :(rsf.detailOrderBy == null || rsf.detailOrderBy.isEmpty()) ? new List<Ordering> { new Ordering(rsf.detail) } : rsf.detailOrderBy) {
String fieldName = orderByField.getField().getName();
if(!orderByFieldsSet.contains(fieldName)) {
orderByFields.add(orderByField.toString());
orderByFieldsSet.add(fieldName);
}
}
}
}
Expand Down Expand Up @@ -344,7 +345,68 @@ public class LREngine {
return String.join(listOfString, delimiter == null ? '' : delimiter);
}
}


/**
Sort Order
*/
public enum SortOrder {ASCENDING, DESCENDING}

/**
Represents a single portion of the Order By clause for SOQL statement
*/
public class Ordering{
private SortOrder direction;
private Boolean nullsLast;
private Schema.DescribeFieldResult field;
private Boolean directionSpecified; // if direction was specified during construction
private Boolean nullsLastSpecified; // if nullsLast was specified during construction

/**
* Construct a new ordering instance
**/
public Ordering(Schema.DescribeFieldResult field) {
this(field, null);
}
public Ordering(Schema.DescribeFieldResult field, SortOrder direction) {
this(field, direction, null);
}
public Ordering(Schema.DescribeFieldResult field, SortOrder direction, Boolean nullsLast) {
// field must be specified
if (field == null) {
throw new BadOrderingStateException('field cannot be null.');
}

this.field = field;
this.directionSpecified = direction != null;
this.nullsLastSpecified = nullsLast != null;
this.direction = this.directionSpecified ? direction : SortOrder.ASCENDING; //SOQL docs ASC is default behavior
this.nullsLast = this.nullsLastSpecified ? nullsLast : false; //SOQL docs state NULLS FIRST is default behavior
}
public Schema.DescribeFieldResult getField(){
return field;
}
public SortOrder getDirection() {
return direction;
}
public Boolean getNullsLast() {
return nullsLast;
}
public override String toString() {
return field.getName() + ' ' + (direction == SortOrder.ASCENDING ? 'ASC' : 'DESC') + ' ' + (nullsLast ? 'NULLS LAST' : 'NULLS FIRST');
}
public String toAsSpecifiedString() {
// emit order by using describe info with the direction and nullsLast
// that was provided during construction. This allows to regurgitate
// the proper SOQL order by using exactly what was passed in
return field.getName() + (directionSpecified ? (direction == SortOrder.ASCENDING ? ' ASC' : ' DESC') : '') + (nullsLastSpecified ? (nullsLast ? ' NULLS LAST' : ' NULLS FIRST') : '');
}
}

/**
Exception thrown if Ordering is in bad state
*/
public class BadOrderingStateException extends Exception {}

/**
Exception throwed if Rollup Summary field is in bad state
*/
Expand All @@ -367,7 +429,7 @@ public class LREngine {
public class RollupSummaryField {
public Schema.Describefieldresult master;
public Schema.Describefieldresult detail;
public Schema.Describefieldresult detailOrderBy;
public List<Ordering> detailOrderBy;
public RollupOperation operation;
public String concatenateDelimiter;

Expand All @@ -383,14 +445,21 @@ public class LREngine {

public RollupSummaryField(Schema.Describefieldresult m,
Schema.Describefieldresult d, RollupOperation op) {
this(m, d, null, op, null);
this(m, d, (Schema.Describefieldresult)null, op, null);
}

public RollupSummaryField(Schema.Describefieldresult m,
Schema.Describefieldresult d,
Schema.Describefieldresult detailOrderBy,
RollupOperation op,
String concatenateDelimiter) {
this(m, d, (detailOrderBy == null ? null : new List<Ordering> { new Ordering(detailOrderBy) }), op, concatenateDelimiter);
}
public RollupSummaryField(Schema.Describefieldresult m,
Schema.Describefieldresult d,
List<Ordering> detailOrderBy,
RollupOperation op,
String concatenateDelimiter) {
this.master = m;
this.detail = d;
this.detailOrderBy = detailOrderBy;
Expand Down Expand Up @@ -442,6 +511,11 @@ public class LREngine {
if (isMasterTypeDateOrTime && (RollupOperation.Sum == operation || RollupOperation.Avg == operation)) {
throw new BadRollUpSummaryStateException('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.');
}

// If we had the SObjectType of Detail field here we could
// iterate the detailOrderBy SObjectFields and ensure they belong to the
// detail SObject but there is no way to determine SObjectType from DescribeFieldResult
// therefore we cannot validate the order by in any way
}

boolean isText (Schema.Displaytype dt) {
Expand Down Expand Up @@ -469,19 +543,11 @@ public class LREngine {
}

public boolean isAggregateBasedRollup() {
return operation == RollupOperation.Sum ||
operation == RollupOperation.Min ||
operation == RollupOperation.Max ||
operation == RollupOperation.Avg ||
operation == RollupOperation.Count ||
operation == RollupOperation.Count_Distinct;
return isAggregateBasedRollup(operation);
}

public boolean isQueryBasedRollup() {
return operation == RollupOperation.Concatenate ||
operation == RollupOperation.Concatenate_Distinct ||
operation == RollupOperation.First ||
operation == RollupOperation.Last;
return isQueryBasedRollup(operation);
}
}

Expand All @@ -490,6 +556,22 @@ public class LREngine {
System_x
}

public static boolean isAggregateBasedRollup(RollupOperation operation) {
return operation == RollupOperation.Sum ||
operation == RollupOperation.Min ||
operation == RollupOperation.Max ||
operation == RollupOperation.Avg ||
operation == RollupOperation.Count ||
operation == RollupOperation.Count_Distinct;
}

public static boolean isQueryBasedRollup(RollupOperation operation) {
return operation == RollupOperation.Concatenate ||
operation == RollupOperation.Concatenate_Distinct ||
operation == RollupOperation.First ||
operation == RollupOperation.Last;
}

/**
Context having all the information about the rollup to be done.
Please note : This class encapsulates many rollup summary fields with different operations.
Expand Down
59 changes: 43 additions & 16 deletions rolluptool/src/classes/RollupService.cls
Original file line number Diff line number Diff line change
Expand Up @@ -539,12 +539,41 @@ global with sharing class RollupService
// Set of field names from the child used in the rollup to search for changes on
Set<String> fieldsToSearchForChanges = new Set<String>();
Set<String> relationshipFields = new Set<String>();
// keep track of fields that should trigger a rollup to be processed
// this avoids having to re-parse RelationshipCriteria & OrderBy fields during field change detection
Map<Id, Set<String>> fieldsInvolvedInLookup = new Map<Id, Set<String>>();
for(LookupRollupSummary__c lookup : lookups)
{
fieldsToSearchForChanges.add(lookup.FieldToAggregate__c);
if(lookup.RelationshipCriteriaFields__c!=null)
for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n'))
fieldsToSearchForChanges.add(criteriaField);
Set<String> lookupFields = new Set<String>();
lookupFields.add(lookup.FieldToAggregate__c);
if(!String.isBlank(lookup.RelationshipCriteriaFields__c)) {
for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n')) {
lookupFields.add(criteriaField);
}
}
// only include order by fields when query based rollup (concat, first, last, etc.) since changes to them
// will not impact the outcome of an aggregate based rollup (sum, count, etc.)
if(LREngine.isQueryBasedRollup(RollupSummaries.OPERATION_PICKLIST_TO_ENUMS.get(lookup.AggregateOperation__c)) && !String.isBlank(lookup.FieldToOrderBy__c)) {
List<LREngine.Ordering> orderByFields = Utilities.parseOrderByClause(lookup.FieldToOrderBy__c, sObjectType.getDescribe().fields.getMap());
if (orderByFields != null && !orderByFields.isEmpty()) {
for (LREngine.Ordering orderByField :orderByFields) {
lookupFields.add(orderByField.getField().getName());
}
}
}

// add all lookup fields to our master list of fields to search for
fieldsToSearchForChanges.addAll(lookupFields);

// add relationshipfield to fields for this lookup
// this comes after adding to fieldsToSearchForChanges because we handle
// change detection separately for non-relationship fields and relationship fields
lookupFields.add(lookup.RelationShipField__c);

// add to map for later use
fieldsInvolvedInLookup.put(lookup.Id, lookupFields);

// add relationship field to master list of relationship fields
relationshipFields.add(lookup.RelationShipField__c);
}

Expand Down Expand Up @@ -624,16 +653,14 @@ global with sharing class RollupService
for(LookupRollupSummary__c lookup : lookups)
{
// Are any of the changed fields used by this lookup?
Boolean processLookup = false;
if(fieldsChanged.contains(lookup.FieldToAggregate__c) ||
fieldsChanged.contains(lookup.RelationShipField__c))
processLookup = true;
if(lookup.RelationshipCriteriaFields__c!=null)
for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n'))
if(fieldsChanged.contains(criteriaField))
processLookup = true;
if(processLookup)
lookupsToProcess.add(lookup);
Set<String> lookupFields = fieldsInvolvedInLookup.get(lookup.Id);
for (String lookupField :lookupFields) {
if (fieldsChanged.contains(lookupField)) {
// add lookup to be processed and exit for loop since we have our answer
lookupsToProcess.add(lookup);
break;
}
}
}
lookups = lookupsToProcess;

Expand Down Expand Up @@ -776,7 +803,7 @@ global with sharing class RollupService
if(childFields==null)
gdFields.put(childObjectType, ((childFields = childObjectType.getDescribe().fields.getMap())));
SObjectField fieldToAggregate = childFields.get(lookup.FieldToAggregate__c);
SObjectField fieldToOrderBy = lookup.FieldToOrderBy__c!=null ? childFields.get(lookup.FieldToOrderBy__c) : null;
List<LREngine.Ordering> fieldsToOrderBy = Utilities.parseOrderByClause(lookup.FieldToOrderBy__c, childFields);
SObjectField relationshipField = childFields.get(lookup.RelationshipField__c);
SObjectField aggregateResultField = parentFields.get(lookup.AggregateResultField__c);
if(fieldToAggregate==null || relationshipField==null || aggregateResultField==null)
Expand All @@ -787,7 +814,7 @@ global with sharing class RollupService
new LREngine.RollupSummaryField(
aggregateResultField.getDescribe(),
fieldToAggregate.getDescribe(),
fieldToOrderBy !=null ? fieldToOrderBy.getDescribe() : null, // field to order by on child
fieldsToOrderBy, // field to order by on child
RollupSummaries.OPERATION_PICKLIST_TO_ENUMS.get(lookup.AggregateOperation__c),
lookup.ConcatenateDelimiter__c);

Expand Down
Loading