Skip to content

Commit

Permalink
Adding SOSL in/not in support since there is no queryWithBinds suppor…
Browse files Browse the repository at this point in the history
…t within SOSL at the moment
  • Loading branch information
jamessimone committed May 1, 2024
1 parent b45ba98 commit 39169a2
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 96 deletions.
70 changes: 42 additions & 28 deletions force-app/repository/Query.cls
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ public virtual class Query {

public Query usingParent(List<SObjectField> parentFields) {
parentFields.add(this.fieldToken);
return new ParentQuery(parentFields, this);
return new ParentQuery(parentFields, this.operator, this.predicate);
}

public static Query subquery(Schema.SObjectField field, Schema.SObjectField innerMatchingField, Query subcondition) {
public static Query subquery(
Schema.SObjectField field,
Schema.SObjectField innerMatchingField,
Query subcondition
) {
return subquery(field, innerMatchingField.getDescribe().getSObjectType(), innerMatchingField, subcondition);
}

Expand Down Expand Up @@ -93,6 +97,17 @@ public virtual class Query {
return new AndQuery(innerQueries);
}

public static String getBuiltUpParentFieldName(List<Schema.SObjectField> parentFields) {
String builtUpFieldName = '';
for (Integer index = 0; index < parentFields.size(); index++) {
Schema.DescribeFieldResult parentFieldDescribe = parentFields[index].getDescribe();
builtUpFieldName += index == parentFields.size() - 1
? parentFieldDescribe.getName()
: parentFieldDescribe.getRelationshipName() + '.';
}
return builtUpFieldName;
}

private class SubQuery extends Query {
private final Schema.SObjectField field;
private final Schema.SObjectType objectType;
Expand Down Expand Up @@ -169,25 +184,18 @@ public virtual class Query {
}

private class ParentQuery extends Query {
private ParentQuery(List<SObjectField> parentFields, Query innerQuery) {
super(innerQuery);
this.field = getBuiltUpParentFieldName(parentFields);
private ParentQuery(List<SObjectField> parentFields, Operator operator, Object predicate) {
super(getBuiltUpParentFieldName(parentFields), operator, predicate);
}
}

protected Query() {
}

protected Query(Query innerQuery) {
this.operator = innerQuery.operator;
this.predicate = innerQuery.predicate;
this.bindVars.putAll(innerQuery.getBindVars());
}

protected Query(String fieldName, Operator operator, Object predicate) {
this.field = fieldName;
this.operator = operator;
this.predicate = this.getPredicate(predicate);
this.predicate = predicate;
}

private Query(SObjectField fieldToken, Operator operator, Object predicate) {
Expand All @@ -200,19 +208,36 @@ public virtual class Query {
}

public virtual override String toString() {
String predicateValue = this.getPredicate(this.predicate);
if (this.operator == Query.Operator.NOT_LIKE) {
// who knows why this is the format they wanted
return String.format(this.getOperator(), new List<String>{ this.field, this.predicate.toString() });
return String.format(this.getOperator(), new List<String>{ this.field }) + ' ' + predicateValue;
}
return this.field + ' ' + this.getOperator() + ' ' + this.predicate;
return this.field + ' ' + this.getOperator() + ' ' + predicateValue;
}

public String toSoslString() {
String startingString = this.toString();
for (String key : this.bindVars.keySet()) {
startingString = startingString.replace(':' + key, this.getSoslPredicate(this.bindVars.get(key)));
}
startingString = startingString.replaceAll('= \\(', 'IN \\(').replaceAll('!= \\(', 'NOT IN \\(');
if (this.predicate instanceof Iterable<Object>) {
String operatorToReplace;
String newOperator;
switch on this.operator {
when EQUALS {
operatorToReplace = '=';
newOperator = 'IN';
}
when NOT_EQUALS {
operatorToReplace = '!=';
newOperator = 'NOT IN';
}
}
if (operatorToReplace != null) {
startingString = startingString.replace(operatorToReplace, newOperator);
}
}
return startingString;
}

Expand Down Expand Up @@ -249,10 +274,10 @@ public virtual class Query {
returnVal = '>=';
}
when ALIKE {
returnVal = 'like';
returnVal = 'LIKE';
}
when NOT_LIKE {
returnVal = '(not {0} like {1})';
returnVal = 'NOT {0} LIKE';
}
}
return returnVal;
Expand All @@ -268,17 +293,6 @@ public virtual class Query {
return ':' + predicateKey;
}

private static String getBuiltUpParentFieldName(List<SObjectField> parentFields) {
String builtUpFieldName = '';
for (Integer index = 0; index < parentFields.size(); index++) {
Schema.DescribeFieldResult parentFieldDescribe = parentFields[index].getDescribe();
builtUpFieldName += index == parentFields.size() - 1
? parentFieldDescribe.getName()
: parentFieldDescribe.getRelationshipName() + '.';
}
return builtUpFieldName;
}

private String getSoslPredicate(Object predicate) {
if (predicate == null) {
return 'null';
Expand Down
60 changes: 39 additions & 21 deletions force-app/repository/QueryTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ private class QueryTests {
Id nullId = null;
String expectedQuery = '(Id = null OR Id != null)';

Query orQuery = Query.orQuery(Query.equals(Account.Id, nullId), Query.notEquals(Account.Id, nullId));
Query orQuery = Query.orQuery(
Query.equals(Account.Id, nullId),
Query.notEquals(Account.Id, nullId)
);

System.assertEquals(expectedQuery, orQuery.toString());
}
Expand All @@ -97,7 +100,10 @@ private class QueryTests {
Query.equals(Contact.LastName, 'asb'),
Query.andQuery(
Query.equals(Contact.FirstName, 'John'),
Query.orQuery(Query.notEquals(Contact.LastName, 'a'), Query.notEquals(Contact.LastName, 'b'))
Query.orQuery(
Query.notEquals(Contact.LastName, 'a'),
Query.notEquals(Contact.LastName, 'b')
)
)
}
)
Expand All @@ -111,7 +117,7 @@ private class QueryTests {

Query likeQuery = Query.likeQuery(Account.Name, expectedName);

System.assertEquals('Name like :bindVar0', likeQuery.toString());
System.assertEquals('Name LIKE :bindVar0', likeQuery.toString());
System.assertEquals(expectedName, likeQuery.getBindVars().get('bindVar0'));
}

Expand All @@ -121,7 +127,7 @@ private class QueryTests {

Query notLike = Query.notLike(Account.Name, expectedName);

System.assertEquals('(not Name like :bindVar0)', notLike.toString());
System.assertEquals('NOT Name LIKE :bindVar0', notLike.toString());
System.assertEquals(expectedName, notLike.getBindVars().get('bindVar0'));
}

Expand All @@ -133,25 +139,10 @@ private class QueryTests {

Query notLike = Query.notLike(Account.Name, values);

System.assertEquals('(not Name like :bindVar0)', notLike.toString());
System.assertEquals('NOT Name LIKE :bindVar0', notLike.toString());
System.assertEquals(values, notLike.getBindVars().get('bindVar0'));
}

@IsTest
static void it_should_allow_multiple_not_like_statements() {
String someName = '%someName%';
String otherName = '%otherName%';

Query multipleNotLike = Query.andQuery(new List<Query> {
Query.notLike(Account.Name, someName),
Query.notLike(Account.Name, otherName)
});

System.assertEquals('((not Name like :bindVar0) AND (not Name like :bindVar1))', multipleNotLike.toString());
System.assertEquals(someName, multipleNotLike.getBindVars().get('bindVar0'));
System.assertEquals(otherName, multipleNotLike.getBindVars().get('bindVar1'));
}

@IsTest
static void it_should_allow_parent_fields_for_filtering() {
Query parentQuery = Query.equals(Group.DeveloperName, 'SOME_CONSTANT.DeveloperName')
Expand Down Expand Up @@ -189,7 +180,10 @@ private class QueryTests {
Query.equals(Account.AnnualRevenue, 50),
Query.equals(Account.Industry, 'Tech'),
Query.orQuery(
new List<Query>{ Query.equals(Account.NumberOfEmployees, 1), Query.equals(Account.Site, 'web3') }
new List<Query>{
Query.equals(Account.NumberOfEmployees, 1),
Query.equals(Account.Site, 'web3')
}
)
}
)
Expand All @@ -200,4 +194,28 @@ private class QueryTests {
subquery.toString()
);
}

@IsTest
static void it_works_with_collections_for_sosl_queries_not_in() {
List<Id> fakeAccountIds = new List<Id>{
TestingUtils.generateId(Account.SObjectType),
TestingUtils.generateId(Account.SObjectType)
};

Query query = Query.notEquals(Account.Id, fakeAccountIds);

Assert.areEqual('Id NOT IN (\'' + String.join(fakeAccountIds, '\',\'') + '\')', query.toSoslString());
}

@IsTest
static void it_works_with_collections_for_sosl_queries_in() {
List<Id> fakeAccountIds = new List<Id>{
TestingUtils.generateId(Account.SObjectType),
TestingUtils.generateId(Account.SObjectType)
};

Query query = Query.equals(Account.Id, fakeAccountIds);

Assert.areEqual('Id IN (\'' + String.join(fakeAccountIds, '\',\'') + '\')', query.toSoslString());
}
}
55 changes: 28 additions & 27 deletions force-app/repository/Repository.cls
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
public virtual without sharing class Repository implements IRepository {
private final Map<Schema.SObjectField, String> childToRelationshipNames;
private final IDML dml;
private final List<Schema.SObjectField> queryFields;
private final Set<String> selectFields = new Set<String>();
private final Map<Schema.SObjectField, String> childToRelationshipNames;

protected final Schema.SObjectType repoType;
protected final Map<String, Object> bindVars = new Map<String, Object>();

protected System.AccessLevel accessLevel = System.AccessLevel.SYSTEM_MODE;
protected final Map<String, RepositorySortOrder> fieldToSortOrder = new Map<String, RepositorySortOrder>();

Expand All @@ -15,7 +16,11 @@ public virtual without sharing class Repository implements IRepository {
private Integer limitAmount;
private SearchGroup soslSearchGroup = SearchGroup.ALL_FIELDS;

public Repository(Schema.SObjectType repoType, List<Schema.SObjectField> queryFields, RepoFactory repoFactory) {
public Repository(
Schema.SObjectType repoType,
List<Schema.SObjectField> queryFields,
RepoFactory repoFactory
) {
this.dml = repoFactory.getDml();
this.queryFields = queryFields;
this.repoType = repoType;
Expand All @@ -27,17 +32,6 @@ public virtual without sharing class Repository implements IRepository {
return this.getQueryLocator(queries, this.shouldAddChildFields);
}

/**
* It's recommended that subqueries be removed from `Database.QueryLocator` instances (docs snippet below). This framework gives you the option
* of ignoring that advice, but it'd be my recommendation that callers to `Repository.getQueryLocator` always pass `false` as the second argument.
*
* From the docs:
*
* > Batch Apex jobs run faster when the start method returns a `QueryLocator` object that doesn't include related records via a subquery.
* > Avoiding relationship subqueries in a `QueryLocator` allows batch jobs to run using a faster, chunked implementation.
* > If the start method returns an `Iterable` or a `QueryLocator` object with a relationship subquery, the batch job uses a slower, non-chunking, implementation
*
*/
public Database.QueryLocator getQueryLocator(List<Query> queries, Boolean shouldAddChildFields) {
Boolean originalValue = this.shouldAddChildFields;
this.shouldAddChildFields = shouldAddChildFields;
Expand All @@ -46,7 +40,7 @@ public virtual without sharing class Repository implements IRepository {
this.bindVars,
this.accessLevel
);
this.clearState();
this.bindVars.clear();
this.shouldAddChildFields = originalValue;

return locator;
Expand Down Expand Up @@ -75,7 +69,18 @@ public virtual without sharing class Repository implements IRepository {
return this;
}

public Repository addParentFields(List<Schema.SObjectField> parentTypes, List<Schema.SObjectField> parentFields) {
public Repository addSortOrder(
List<Schema.SObjectField> parentFieldChain,
RepositorySortOrder sortOrder
) {
this.fieldToSortOrder.put(Query.getBuiltUpParentFieldName(parentFieldChain), sortOrder);
return this;
}

public Repository addParentFields(
List<Schema.SObjectField> parentTypes,
List<Schema.SObjectField> parentFields
) {
String parentBase = '';
for (SObjectField parentId : parentTypes) {
parentBase += parentId.getDescribe().getRelationshipName() + '.';
Expand Down Expand Up @@ -103,7 +108,7 @@ public virtual without sharing class Repository implements IRepository {
Map<String, RepositorySortOrder> fieldToSortOrder,
Integer limitAmount
) {
if (this.shouldAddChildFields == false || this.childToRelationshipNames.containsKey(childFieldToken) == false) {
if (this.childToRelationshipNames.containsKey(childFieldToken) == false || this.shouldAddChildFields == false) {
return this;
}

Expand All @@ -122,10 +127,7 @@ public virtual without sharing class Repository implements IRepository {
this.selectFields.add(
String.format(
baseSubselect,
new List<String>{
String.join(new List<String>(childFieldNames), ','),
this.childToRelationshipNames.get(childFieldToken)
}
new List<String>{ String.join(childFieldNames, ','), this.childToRelationshipNames.get(childFieldToken) }
)
);
return this;
Expand Down Expand Up @@ -172,7 +174,7 @@ public virtual without sharing class Repository implements IRepository {
localSelectFields.addAll(this.selectFields);
this.baseSelectUsed = false;
}
return 'SELECT ' + String.join(new List<String>(localSelectFields), ', ') + '\nFROM ' + this.repoType;
return 'SELECT ' + String.join(localSelectFields, ', ') + '\nFROM ' + this.repoType;
}

private String addWheres(List<Query> queries) {
Expand All @@ -185,10 +187,10 @@ public virtual without sharing class Repository implements IRepository {
}

private List<SObject> performQuery(String finalQuery) {
System.debug('Query:\n' + finalQuery);
System.debug(finalQuery);
List<SObject> results = Database.queryWithBinds(finalQuery, this.bindVars, this.accessLevel);
this.clearState();
System.debug('Number of results: ' + results.size() + '\nResults: \n' + results);
System.debug(System.LoggingLevel.FINER, 'Number of results: ' + results.size() + '\nResults: \n' + results);
return results;
}

Expand All @@ -197,8 +199,8 @@ public virtual without sharing class Repository implements IRepository {
if (sortOrders.isEmpty() == false) {
orderByString += ' \nORDER BY ';
String separator = ', ';
for (String fieldName : sortOrders.keySet()) {
orderByString += fieldName + ' ' + sortOrders.get(fieldName).toString() + separator;
for (String field : sortOrders.keySet()) {
orderByString += field + ' ' + sortOrders.get(field).toString() + separator;
}
orderByString = orderByString.removeEnd(separator);
}
Expand Down Expand Up @@ -231,7 +233,7 @@ public virtual without sharing class Repository implements IRepository {
')';
System.debug('Search query:\n' + searchQuery);
List<List<SObject>> results = Search.query(searchQuery, this.accessLevel);
System.debug('Number of results: ' + results.size() + '\nResults: \n' + results);
System.debug(System.LoggingLevel.FINER, 'Number of results: ' + results.size() + '\nResults: \n' + results);
this.clearState();
this.isSosl = false;
return results;
Expand All @@ -242,7 +244,6 @@ public virtual without sharing class Repository implements IRepository {
return this;
}

// DML
public Database.SaveResult doInsert(SObject record) {
return this.dml.doInsert(record);
}
Expand Down
Loading

0 comments on commit 39169a2

Please sign in to comment.