From 00d196d91a62a4d7fd35404553d27c41323674db Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 28 Apr 2024 22:26:56 -0400 Subject: [PATCH 01/25] Bumped API versions to 60.0 (Spring '24) --- .../main/classes/AggregateQuery.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/Query.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/SOQL.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/SOSL.cls-meta.xml | 2 +- .../tests/classes/AggregateQuery_Tests.cls-meta.xml | 2 +- nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml | 2 +- .../tests/classes/RecordSearch_Tests.cls-meta.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/main/classes/Query.cls-meta.xml b/nebula-query-and-search/main/classes/Query.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/main/classes/Query.cls-meta.xml +++ b/nebula-query-and-search/main/classes/Query.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml +++ b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active diff --git a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml index 133fce1e..df13efa8 100644 --- a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 60.0 Active From 82c6d683d3915c316ef4e5e5105e3ecc7a5482b9 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 28 Apr 2024 22:32:51 -0400 Subject: [PATCH 02/25] Added support for using System.AccessLevel (#36) --- .../main/classes/AggregateQuery.cls | 7 +- .../main/classes/Query.cls | 5 ++ .../main/classes/RecordSearch.cls | 5 ++ nebula-query-and-search/main/classes/SOQL.cls | 17 ++++- nebula-query-and-search/main/classes/SOSL.cls | 13 +++- .../tests/classes/AggregateQuery_Tests.cls | 60 +++++++++++++++++ .../tests/classes/Query_Tests.cls | 60 +++++++++++++++++ .../tests/classes/RecordSearch_Tests.cls | 64 +++++++++++++++++++ 8 files changed, 226 insertions(+), 5 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 47b98f9e..e32be0a4 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -106,6 +106,11 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery withAccessLevel(System.AccessLevel mode) { + super.doWithAccessLevel(mode); + return this.setHasChanged(); + } + global AggregateQuery orderByField(Schema.SObjectField field) { return this.orderByField(field, null); } @@ -220,7 +225,7 @@ global class AggregateQuery extends SOQL { super.doGetOrderByString() + super.doGetLimitCountString() + super.doGetOffetString(); - return Database.countQuery(countQuery); + return Database.countQuery(countQuery, this.doGetAccessLevel()); } global AggregateResult getFirstResult() { diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 2025858f..9b8dcc23 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -239,6 +239,11 @@ global class Query extends SOQL { //return this.setHasChanged(); //} + global Query withAccessLevel(System.AccessLevel mode) { + super.doWithAccessLevel(mode); + return this.setHasChanged(); + } + global Query orderByField(Schema.SObjectField field) { return this.orderByField(new SOQL.QueryField(field)); } diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls b/nebula-query-and-search/main/classes/RecordSearch.cls index f3fbdb4a..3ac09920 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls +++ b/nebula-query-and-search/main/classes/RecordSearch.cls @@ -64,6 +64,11 @@ global class RecordSearch extends SOSL { return this.setHasChanged(); } + global RecordSearch withAccessLevel(System.AccessLevel accessLevel) { + this.accessLevel = accessLevel; + return this.setHasChanged(); + } + global RecordSearch updateArticleReporting(SOSL.ArticleReporting articleReporting) { this.articleReporting = articleReporting; return this.setHasChanged(); diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index a2b0f802..78b10168 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -181,6 +181,7 @@ global abstract class SOQL implements Comparable { protected Set excludedQueryFields; protected Scope scope; protected List whereFilters; + protected System.AccessLevel accessLevel; protected List orderByFieldApiNames; protected Integer limitCount; protected Integer offset; @@ -198,6 +199,7 @@ global abstract class SOQL implements Comparable { this.excludedQueryFields = new Set(); this.whereFilters = new List(); this.orderByFieldApiNames = new List(); + this.accessLevel = System.AccessLevel.SYSTEM_MODE; this.cacheResults = false; this.hasChanged = false; } @@ -271,6 +273,10 @@ global abstract class SOQL implements Comparable { this.doSetHasChanged(); } + protected void doWithAccessLevel(System.AccessLevel accessLevel) { + this.accessLevel = accessLevel; + } + protected void doOrderBy(SOQL.QueryField queryField, SOQL.SortOrder sortOrder, Boolean sortNullsFirst) { this.doOrderBy(queryField.toString(), sortOrder, sortNullsFirst); } @@ -305,7 +311,7 @@ global abstract class SOQL implements Comparable { if (this.cacheResults) { return this.getCachedResults(); } else { - return Database.query(this.getQuery()); + return Database.query(this.getQuery(), this.doGetAccessLevel()); } } @@ -361,6 +367,10 @@ global abstract class SOQL implements Comparable { return this.whereFilters.isEmpty() ? '' : ' WHERE ' + String.join(this.whereFilters, ' AND '); } + protected System.AccessLevel doGetAccessLevel() { + return this.accessLevel ?? System.AccessLevel.SYSTEM_MODE; + } + protected String doGetOrderByString() { return this.orderByFieldApiNames.isEmpty() ? '' : ' ORDER BY ' + String.join(this.orderByFieldApiNames, ', '); } @@ -383,7 +393,10 @@ global abstract class SOQL implements Comparable { Boolean isCached = cachedResultsByHashCode.containsKey(hashCode); if (!isCached) { - cachedResultsByHashCode.put(hashCode, Database.query(query)); + cachedResultsByHashCode.put( + hashCode, + Database.query(query, this.doGetAccessLevel()) + ); } // Always return a deep clone so the original cached version is never modified diff --git a/nebula-query-and-search/main/classes/SOSL.cls b/nebula-query-and-search/main/classes/SOSL.cls index 918c5b1f..c7d29cc9 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls +++ b/nebula-query-and-search/main/classes/SOSL.cls @@ -42,6 +42,7 @@ global abstract class SOSL { protected SOSL.ArticleReporting articleReporting; protected List withClauses; protected List withDataCategoryClauses; + protected System.AccessLevel accessLevel; protected SOSL.SearchGroup searchGroup; protected SOSL(String searchTerm, Query sobjectQuery) { @@ -57,6 +58,7 @@ global abstract class SOSL { this.searchGroup = SOSL.SearchGroup.ALL_FIELDS; this.withClauses = new List(); this.withDataCategoryClauses = new List(); + this.accessLevel = System.AccessLevel.SYSTEM_MODE; } public Set getSObjectTypes() { @@ -88,7 +90,7 @@ global abstract class SOSL { if (this.cacheResults) { return this.getCachedResults(); } else { - return System.Search.query(this.getSearch()); + return System.Search.query(this.getSearch(), this.doGetAccessLevel()); } } @@ -119,6 +121,10 @@ global abstract class SOSL { return this.withClauses.isEmpty() ? '' : ' WITH ' + String.join(this.withClauses, ' WITH '); } + protected System.AccessLevel doGetAccessLevel() { + return this.accessLevel ?? System.AccessLevel.SYSTEM_MODE; + } + protected String doGetUpdateArticleReportingString() { return this.articleReporting == null ? '' : ' UPDATE ' + this.articleReporting.name(); } @@ -129,7 +135,10 @@ global abstract class SOSL { Boolean isCached = cachedSearchResultsByHashCode.containsKey(hashCode); if (!isCached) { - cachedSearchResultsByHashCode.put(hashCode, Search.query(searchQuery)); + cachedSearchResultsByHashCode.put( + hashCode, + System.Search.query(searchQuery, this.doGetAccessLevel()) + ); } // Always return a deep clone so the original cached version is never modified diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index 8e4a2a86..b2017c67 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -129,6 +129,52 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_run_with_system_mode() { + String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; + + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .addAggregate(SOQL.Aggregate.COUNT, Opportunity.Id) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = aggregateQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Query should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; + + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .addAggregate(SOQL.Aggregate.COUNT, Opportunity.Id) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = aggregateQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Query should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'Opportunity\' is not supported'), 'Query should throw exception when run in User Mode'); + } + @IsTest static void it_should_build_a_ridiculous_query_string() { String expectedQueryString = @@ -168,4 +214,18 @@ private class AggregateQuery_Tests { List returnedResults = aggregateQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); + } } diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 051ac675..4521ce46 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -193,6 +193,52 @@ private class Query_Tests { List accounts = accountQuery.getResults(); } + @IsTest + static void it_should_run_with_system_mode() { + String expectedQueryString = 'SELECT Id, Name FROM Account LIMIT 1'; + + Query accountQuery = new Query(Schema.Account.SObjectType) + .limitTo(1) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = accountQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Query should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedQueryString = 'SELECT Id, Name FROM Account LIMIT 1'; + + Query accountQuery = new Query(Schema.Account.SObjectType) + .limitTo(1) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = accountQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Query should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'Account\' is not supported'), 'Query should throw exception when run in User Mode'); + } + @IsTest static void it_includes_order_by_statement_for_single_field() { String expectedQueryString = 'SELECT Id, Name FROM Lead ORDER BY CreatedDate ASC NULLS FIRST'; @@ -277,4 +323,18 @@ private class Query_Tests { System.Test.stopTest(); } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); + } } diff --git a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls index 53805d8a..66a7a428 100644 --- a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls +++ b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls @@ -127,6 +127,56 @@ private class RecordSearch_Tests { List userSearchResults = userSearch.getFirstResults(); } + @IsTest + static void it_should_run_with_system_mode() { + String expectedSearchQueryString = 'FIND \'' + System.UserInfo.getUserEmail() + '\' IN ALL FIELDS RETURNING Contact(Id, Name)'; + + Query contactQuery = new Query(Schema.Contact.SObjectType); + RecordSearch contactSearch = new RecordSearch(System.UserInfo.getUserEmail(), contactQuery) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.assertEquals(expectedSearchQueryString, contactSearch.getSearch()); + List contactSearchResults = contactSearch.getFirstResults(); + + List> expectedResults = Search.query(expectedSearchQueryString); + List> returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = contactSearch.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Search should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedSearchQueryString = 'FIND \'' + System.UserInfo.getUserEmail() + '\' IN ALL FIELDS RETURNING Contact(Id, Name)'; + + Query contactQuery = new Query(Schema.Contact.SObjectType); + RecordSearch contactSearch = new RecordSearch(System.UserInfo.getUserEmail(), contactQuery) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.assertEquals(expectedSearchQueryString, contactSearch.getSearch()); + List contactSearchResults = contactSearch.getFirstResults(); + + List> expectedResults = Search.query(expectedSearchQueryString); + List> returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = contactSearch.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Search should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'contact\' is not supported'), 'Search should throw exception when run in User Mode'); + } + @IsTest static void it_should_cache_search_results_when_enabled() { Integer loops = 4; @@ -150,4 +200,18 @@ private class RecordSearch_Tests { System.Test.stopTest(); } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); + } } From 100d090233c9f327f710adea4ecc7893b7247068 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 6 May 2024 22:51:23 -0400 Subject: [PATCH 03/25] Added support for using bind variables with new Database query methods (#35) --- .../main/classes/AggregateQuery.cls | 76 +++++- .../main/classes/Query.cls | 28 +++ nebula-query-and-search/main/classes/SOQL.cls | 54 ++++- .../tests/classes/AggregateQuery_Tests.cls | 227 +++++++++++++++++- .../tests/classes/Query_Tests.cls | 169 +++++++++++++ 5 files changed, 540 insertions(+), 14 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index e32be0a4..9b4b992f 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -16,6 +16,7 @@ global class AggregateQuery extends SOQL { private SOQL.GroupingDimension groupingDimension; private List aggregateFields; private List havingConditions; + private String countQuery; global AggregateQuery(Schema.SObjectType sobjectType) { super(sobjectType, false); @@ -79,8 +80,29 @@ global class AggregateQuery extends SOQL { return this.havingAggregate(aggregateFunction, new SOQL.QueryField(field), operator, value); } + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.havingAggregate(aggregateFunction, new SOQL.QueryField(field), operator, value, bindWithKey); + } + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value) { - this.havingConditions.add(aggregateFunction.name() + '(' + queryField + ') ' + SOQL.getOperatorValue(operator) + ' ' + value); + return this.havingAggregate(aggregateFunction, queryField, operator, value, null); + } + + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + this.havingConditions.add( + String.format( + '{0}({1}) {2} {3}', + new List { + aggregateFunction.name(), + queryField.toString(), + SOQL.getOperatorValue(operator), + (String.isNotBlank(bindWithKey) ? ':' + bindWithKey : new QueryArgument(value).toString()) + } + ) + ); + if (String.isNotBlank(bindWithKey)) { + this.bindsMap.put(bindWithKey, value); + } return this.setHasChanged(); } @@ -88,10 +110,18 @@ global class AggregateQuery extends SOQL { return this.filterWhere(new SOQL.QueryField(field), operator, value); } + global AggregateQuery filterWhere(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryField(field), operator, value, bindWithKey); + } + global AggregateQuery filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value) { return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value)); } + global AggregateQuery filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value, bindWithKey)); + } + global AggregateQuery filterWhere(SOQL.QueryFilter filter) { return this.filterWhere(new List{ filter }); } @@ -171,6 +201,26 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery setBind(String key, Object value) { + super.doSetBind(key, value); + return this.setHasChanged(); + } + + global AggregateQuery setBinds(Map binds) { + super.doSetBinds(binds); + return this.setHasChanged(); + } + + global AggregateQuery removeBind(String key) { + super.doRemoveBind(key); + return this.setHasChanged(); + } + + global AggregateQuery clearBinds() { + super.doClearBinds(); + return this.setHasChanged(); + } + // TODO decide if this should be global public AggregateQuery cacheResults() { super.doCacheResults(); @@ -211,10 +261,12 @@ global class AggregateQuery extends SOQL { return this.query; } - // TODO consider renaming to getCountResult() - @SuppressWarnings('PMD.ApexSOQLInjection') - global Integer getResultCount() { - String countQuery = + global String getCountQuery() { + if (this.countQuery != null && !this.hasChanged) { + return this.countQuery; + } + + this.countQuery = 'SELECT COUNT()' + ' FROM ' + this.sobjectType + @@ -225,7 +277,19 @@ global class AggregateQuery extends SOQL { super.doGetOrderByString() + super.doGetLimitCountString() + super.doGetOffetString(); - return Database.countQuery(countQuery, this.doGetAccessLevel()); + + System.debug(System.LoggingLevel.FINEST, this.countQuery); + return this.countQuery; + } + + // TODO consider renaming to getCountResult() + @SuppressWarnings('PMD.ApexSOQLInjection') + global Integer getResultCount() { + return Database.countQueryWithBinds( + this.getCountQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ); } global AggregateResult getFirstResult() { diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 9b8dcc23..621ffb0d 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -199,10 +199,18 @@ global class Query extends SOQL { return this.filterWhere(new SOQL.QueryField(field), operator, value); } + global Query filterWhere(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryField(field), operator, value, bindWithKey); + } + global Query filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value) { return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value)); } + global Query filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value, bindWithKey)); + } + global Query filterWhere(SOQL.QueryFilter filter) { return this.filterWhere(new List{ filter }); } @@ -294,6 +302,26 @@ global class Query extends SOQL { return this.setHasChanged(); } + global Query setBind(String key, Object value) { + super.doSetBind(key, value); + return this.setHasChanged(); + } + + global Query setBinds(Map binds) { + super.doSetBinds(binds); + return this.setHasChanged(); + } + + global Query removeBind(String key) { + super.doRemoveBind(key); + return this.setHasChanged(); + } + + global Query clearBinds() { + super.doClearBinds(); + return this.setHasChanged(); + } + // TODO decide if this should be global public Query cacheResults() { super.doCacheResults(); diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 78b10168..178c201d 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -187,7 +187,7 @@ global abstract class SOQL implements Comparable { protected Integer offset; protected Boolean hasChanged; protected Boolean sortQueryFields; - + protected Map bindsMap; protected Boolean cacheResults; protected SOQL(Schema.SObjectType sobjectType, Boolean sortQueryFields) { @@ -200,6 +200,7 @@ global abstract class SOQL implements Comparable { this.whereFilters = new List(); this.orderByFieldApiNames = new List(); this.accessLevel = System.AccessLevel.SYSTEM_MODE; + this.bindsMap = new Map(); this.cacheResults = false; this.hasChanged = false; } @@ -248,12 +249,15 @@ global abstract class SOQL implements Comparable { } protected void doFilterWhere(List filters) { - if (filters == null || filters.isEmpty()) { + if (filters?.isEmpty() != false) { return; } for (SOQL.QueryFilter filter : filters) { this.whereFilters.add(filter.toString()); + if (String.isNotBlank(filter.bindKey)) { + this.bindsMap.put(filter.bindKey, filter.value); + } } this.doSetHasChanged(); } @@ -302,6 +306,22 @@ global abstract class SOQL implements Comparable { this.offset = offset; } + protected void doSetBind(String key, Object value) { + this.bindsMap.put(key, value); + } + + protected void doSetBinds(Map binds) { + this.bindsMap.putAll(binds); + } + + protected void doRemoveBind(String key) { + this.bindsMap.remove(key); + } + + protected void doClearBinds() { + this.bindsMap.clear(); + } + protected SObject doGetFirstResult() { List results = this.doGetResults(); return results == null || results.isEmpty() ? null : results[0]; @@ -311,7 +331,11 @@ global abstract class SOQL implements Comparable { if (this.cacheResults) { return this.getCachedResults(); } else { - return Database.query(this.getQuery(), this.doGetAccessLevel()); + return Database.queryWithBinds( + this.getQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ); } } @@ -383,6 +407,10 @@ global abstract class SOQL implements Comparable { return this.offset == null ? '' : ' OFFSET ' + this.offset; } + protected Map doGetBindsMap() { + return this.bindsMap ?? new Map(); + } + private void doSetHasChanged() { this.hasChanged = true; } @@ -395,7 +423,11 @@ global abstract class SOQL implements Comparable { if (!isCached) { cachedResultsByHashCode.put( hashCode, - Database.query(query, this.doGetAccessLevel()) + Database.queryWithBinds( + this.getQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ) ); } @@ -555,16 +587,26 @@ global abstract class SOQL implements Comparable { private Object value; private String formattedValue; private String filterString; + private String bindKey; global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value) { this(new QueryField(field), operator, value); } + global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindKey) { + this(new QueryField(field), operator, value, bindKey); + } + global QueryFilter(QueryField queryField, SOQL.Operator operator, Object value) { + this(queryField, operator, value, null); + } + + global QueryFilter(QueryField queryField, SOQL.Operator operator, Object value, String bindKey) { this.queryField = queryField; this.operator = operator; this.value = value; - this.formattedValue = new QueryArgument(value).toString(); + this.formattedValue = (String.isNotBlank(bindKey) ? ':' + bindKey : new QueryArgument(value).toString()); + this.bindKey = bindKey; this.filterString = queryField + ' ' + SOQL.getOperatorValue(operator) + ' ' + formattedValue; } @@ -618,7 +660,7 @@ global abstract class SOQL implements Comparable { public class SOQLException extends Exception { } - private class QueryArgument { + public class QueryArgument { private String value; public QueryArgument(Object valueToFormat) { diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index b2017c67..a8535619 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -8,6 +8,35 @@ ) @IsTest(IsParallel=true) private class AggregateQuery_Tests { + + @IsTest + static void it_should_construct_a_count_query_without_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != null'; + + // TEST + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null)); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + } + + @IsTest + static void it_should_construct_a_count_query_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != :accountIdFilter'; + + // TEST + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + } + @IsTest static void it_should_be_usable_after_construction() { String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; @@ -34,6 +63,53 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_return_count_result_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != :accountIdFilter'; + Integer expectedResult = Database.countQueryWithBinds( + expectedQueryString, + new Map { + 'accountIdFilter' => null + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // TEST + Integer returnedResult = aggregateQuery.getResultCount(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + System.Assert.areEqual(expectedResult, returnedResult); + } + + @IsTest + static void it_should_return_results_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Type FROM Opportunity WHERE AccountId != :accountIdFilter GROUP BY Type'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'accountIdFilter' => null + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .groupByField(Schema.Opportunity.Type) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + @IsTest static void it_should_return_results_when_filtering_with_an_or_statement() { String expectedQueryString = 'SELECT Type, COUNT(Id) COUNT__Id FROM Account WHERE (AnnualRevenue = null OR Type = null) AND ParentId != null GROUP BY Type'; @@ -115,6 +191,31 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_group_by_having_aggregate_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Name, COUNT(Id) COUNT__Id FROM Account GROUP BY Name HAVING COUNT(Id) > :minCount'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minCount' => 2 + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .groupByField(Schema.Account.Name) + .addAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id) + .havingAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id, SOQL.Operator.GREATER_THAN, 2, 'minCount'); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + @IsTest static void it_should_group_by_a_date_function() { String expectedQueryString = 'SELECT CALENDAR_MONTH(CloseDate), COUNT(Id) COUNT__Id FROM Opportunity GROUP BY CALENDAR_MONTH(CloseDate)'; @@ -182,7 +283,7 @@ private class AggregateQuery_Tests { ' COUNT_DISTINCT(AccountId) COUNT_DISTINCT__AccountId, COUNT_DISTINCT(OwnerId) COUNT_DISTINCT__OwnerId, COUNT_DISTINCT(Type) COUNT_DISTINCT__Type,' + ' MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate, SUM(Amount) SUM__Amount' + ' FROM Opportunity' + - ' WHERE AccountId != null' + + ' WHERE AccountId != null AND CreatedDate >= :createdDateFilter' + ' GROUP BY Account.Type, StageName' + ' ORDER BY Account.Type ASC NULLS FIRST, StageName ASC NULLS FIRST, SUM(Amount) ASC NULLS FIRST,' + ' MIN(CloseDate) DESC NULLS FIRST, MAX(Account.LastActivityDate) ASC NULLS FIRST' + @@ -206,13 +307,135 @@ private class AggregateQuery_Tests { .orderByAggregate(SOQL.Aggregate.MIN, Schema.Opportunity.CloseDate, SOQL.SortOrder.DESCENDING) .orderByAggregate(SOQL.Aggregate.MAX, new SOQL.QueryField(new List{ Schema.Opportunity.AccountId, Schema.Account.LastActivityDate })) .filterWhere(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null) + .filterWhere(Schema.Opportunity.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today(), 'createdDateFilter') .limitTo(100) .offsetBy(0); System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); - List expectedResults = Database.query(expectedQueryString); + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'createdDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + List returnedResults = aggregateQuery.getResults(); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_will_set_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :dateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'dateFilter' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + aggregateQuery.setBind('dateFilter', Date.today().addDays(-1)); + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_set_multiple_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minDateFilter' => Date.today().addDays(-7), + 'maxDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .addAggregate(SOQL.Aggregate.MAX, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + aggregateQuery.setBind('minDateFilter', Date.today().addDays(-7)); + aggregateQuery.setBind('maxDateFilter', Date.today()); List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-7)); + System.Assert.isFalse((Date)returnedResults[0].get('MAX__CreatedDate') >= Date.today()); + } + } + + @IsTest + static void it_will_remove_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :dateFilter'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + aggregateQuery.removeBind('dateFilter'); + Exception caughtException; + try { + List returnedResults = aggregateQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + @IsTest + static void it_will_clear_all_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .addAggregate(SOQL.Aggregate.MAX, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + aggregateQuery.clearBinds(); + Exception caughtException; + try { + List returnedResults = aggregateQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); } static User minimumAccessUser() { diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 4521ce46..06e795bf 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -8,6 +8,35 @@ ) @IsTest(IsParallel=true) private class Query_Tests { + + @IsTest + static void it_should_construct_a_query_without_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= THIS_MONTH'; + + // TEST + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, new SOQL.DateLiteral(SOQL.FixedDateLiteral.THIS_MONTH))); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + } + + @IsTest + static void it_should_construct_a_query_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= :dateFilter'; + + // TEST + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + } + @IsTest static void it_should_return_results_for_a_simple_query() { String expectedQueryString = 'SELECT Id, Name FROM Account'; @@ -193,6 +222,29 @@ private class Query_Tests { List accounts = accountQuery.getResults(); } + @IsTest + static void it_should_return_results_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= :createdDateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'createdDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today(), 'createdDateFilter')); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + @IsTest static void it_should_run_with_system_mode() { String expectedQueryString = 'SELECT Id, Name FROM Account LIMIT 1'; @@ -324,6 +376,123 @@ private class Query_Tests { System.Test.stopTest(); } + @IsTest + static void it_will_set_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'dateFilter' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + accountQuery.setBind('dateFilter', Date.today().addDays(-1)); + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_set_multiple_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minDateFilter' => Date.today().addDays(-7), + 'maxDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + accountQuery.setBind('minDateFilter', Date.today().addDays(-7)); + accountQuery.setBind('maxDateFilter', Date.today()); + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-7)); + System.Assert.isFalse(returnedResults[0].CreatedDate >= Date.today()); + } + } + + @IsTest + static void it_will_remove_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter'; + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + accountQuery.removeBind('dateFilter'); + Exception caughtException; + try { + List returnedResults = accountQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + @IsTest + static void it_will_clear_all_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + accountQuery.clearBinds(); + Exception caughtException; + try { + List returnedResults = accountQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + static User minimumAccessUser() { return new User( Alias = 'newUser', From f183a76e2e93f9e51d524a370d2698de7233a54b Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 6 May 2024 23:15:13 -0400 Subject: [PATCH 04/25] Update project API version to 60.0 (Spring '24) --- sfdx-project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfdx-project.json b/sfdx-project.json index ea0452fd..7a80f50a 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -2,7 +2,7 @@ "name": "Nebula Query & Search", "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "58.0", + "sourceApiVersion": "60.0", "plugins": { "sfdx-plugin-prettier": { "enabled": true From 158db75414115b6248f5c1509a9ca3e09c6ee8ff Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 23 Jun 2024 23:31:03 -0400 Subject: [PATCH 05/25] Rename method parameter names for consistency --- nebula-query-and-search/main/classes/AggregateQuery.cls | 4 ++-- nebula-query-and-search/main/classes/Query.cls | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 9b4b992f..cbd619e9 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -136,8 +136,8 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } - global AggregateQuery withAccessLevel(System.AccessLevel mode) { - super.doWithAccessLevel(mode); + global AggregateQuery withAccessLevel(System.AccessLevel accessLevel) { + super.doWithAccessLevel(accessLevel); return this.setHasChanged(); } diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 621ffb0d..641f3b9f 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -247,8 +247,8 @@ global class Query extends SOQL { //return this.setHasChanged(); //} - global Query withAccessLevel(System.AccessLevel mode) { - super.doWithAccessLevel(mode); + global Query withAccessLevel(System.AccessLevel accessLevel) { + super.doWithAccessLevel(accessLevel); return this.setHasChanged(); } From f497090b8f19d458a7831a4ac4a380e059de5591 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 23 Jun 2024 23:32:31 -0400 Subject: [PATCH 06/25] Set SOSL access level in base class --- nebula-query-and-search/main/classes/RecordSearch.cls | 2 +- nebula-query-and-search/main/classes/SOSL.cls | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls b/nebula-query-and-search/main/classes/RecordSearch.cls index 3ac09920..143d0d04 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls +++ b/nebula-query-and-search/main/classes/RecordSearch.cls @@ -65,7 +65,7 @@ global class RecordSearch extends SOSL { } global RecordSearch withAccessLevel(System.AccessLevel accessLevel) { - this.accessLevel = accessLevel; + super.doWithAccessLevel(accessLevel); return this.setHasChanged(); } diff --git a/nebula-query-and-search/main/classes/SOSL.cls b/nebula-query-and-search/main/classes/SOSL.cls index c7d29cc9..32ad7489 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls +++ b/nebula-query-and-search/main/classes/SOSL.cls @@ -121,6 +121,10 @@ global abstract class SOSL { return this.withClauses.isEmpty() ? '' : ' WITH ' + String.join(this.withClauses, ' WITH '); } + protected void doWithAccessLevel(System.AccessLevel accessLevel) { + this.accessLevel = accessLevel; + } + protected System.AccessLevel doGetAccessLevel() { return this.accessLevel ?? System.AccessLevel.SYSTEM_MODE; } From 9a3f1c7fce5852218f307496fcc5aaf93c79ee94 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 23 Jun 2024 23:33:34 -0400 Subject: [PATCH 07/25] Bumped API versions to 61.0 (Summer '24) --- .../main/classes/AggregateQuery.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/Query.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/SOQL.cls-meta.xml | 2 +- nebula-query-and-search/main/classes/SOSL.cls-meta.xml | 2 +- .../tests/classes/AggregateQuery_Tests.cls-meta.xml | 2 +- nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml | 2 +- .../tests/classes/RecordSearch_Tests.cls-meta.xml | 2 +- sfdx-project.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/Query.cls-meta.xml b/nebula-query-and-search/main/classes/Query.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/main/classes/Query.cls-meta.xml +++ b/nebula-query-and-search/main/classes/Query.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml +++ b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml index df13efa8..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 60.0 + 61.0 Active diff --git a/sfdx-project.json b/sfdx-project.json index 7a80f50a..6644f90c 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -2,7 +2,7 @@ "name": "Nebula Query & Search", "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "60.0", + "sourceApiVersion": "61.0", "plugins": { "sfdx-plugin-prettier": { "enabled": true From b719a2c6819e74b4b02b82379fb38099d080c4f8 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 24 Jun 2024 00:05:39 -0400 Subject: [PATCH 08/25] Adds base properties and methods for auto-generating bind variable keys --- .../main/classes/AggregateQuery.cls | 5 +++++ nebula-query-and-search/main/classes/Query.cls | 5 +++++ nebula-query-and-search/main/classes/SOQL.cls | 12 ++++++++++++ 3 files changed, 22 insertions(+) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index cbd619e9..c2c8f1b5 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -221,6 +221,11 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery generateBindVariableKeys() { + super.doGenerateBindVariableKeys(); + return this; + } + // TODO decide if this should be global public AggregateQuery cacheResults() { super.doCacheResults(); diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 641f3b9f..9f345154 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -322,6 +322,11 @@ global class Query extends SOQL { return this.setHasChanged(); } + global Query generateBindVariableKeys() { + super.doGenerateBindVariableKeys(); + return this; + } + // TODO decide if this should be global public Query cacheResults() { super.doCacheResults(); diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 178c201d..15b02dbc 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -189,6 +189,8 @@ global abstract class SOQL implements Comparable { protected Boolean sortQueryFields; protected Map bindsMap; protected Boolean cacheResults; + private Boolean generateBindVariableKeys; + private Integer generatedBindVariableKeyCounter; protected SOQL(Schema.SObjectType sobjectType, Boolean sortQueryFields) { this.sobjectType = sobjectType; @@ -203,6 +205,8 @@ global abstract class SOQL implements Comparable { this.bindsMap = new Map(); this.cacheResults = false; this.hasChanged = false; + this.generateBindVariableKeys = false; + this.generatedBindVariableKeyCounter = 0; } global Schema.SObjectType getSObjectType() { @@ -322,6 +326,14 @@ global abstract class SOQL implements Comparable { this.bindsMap.clear(); } + protected void doGenerateBindVariableKeys() { + this.generateBindVariableKeys = true; + } + + protected String generateNextBindVariableKey() { + return this.generateBindVariableKeys != true ? null : 'bindVar' + this.generatedBindVariableKeyCounter++; + } + protected SObject doGetFirstResult() { List results = this.doGetResults(); return results == null || results.isEmpty() ? null : results[0]; From e25b73484013da0effe43c00ef9fc1d6a30b79f8 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 24 Jun 2024 00:09:31 -0400 Subject: [PATCH 09/25] Updates havingAggregate method to auto-generate bind variable key --- nebula-query-and-search/main/classes/AggregateQuery.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index c2c8f1b5..e2e69bf7 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -85,7 +85,7 @@ global class AggregateQuery extends SOQL { } global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value) { - return this.havingAggregate(aggregateFunction, queryField, operator, value, null); + return this.havingAggregate(aggregateFunction, queryField, operator, value, this.generateNextBindVariableKey()); } global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { From 5fefc202431bd0e42831757ad8d0c414967c7bdc Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 24 Jun 2024 00:10:40 -0400 Subject: [PATCH 10/25] Updates doFilterWhere and doOrFilterWhere methods to auto-generate bind variable key --- nebula-query-and-search/main/classes/SOQL.cls | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 15b02dbc..56abff2a 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -258,6 +258,7 @@ global abstract class SOQL implements Comparable { } for (SOQL.QueryFilter filter : filters) { + filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); this.whereFilters.add(filter.toString()); if (String.isNotBlank(filter.bindKey)) { this.bindsMap.put(filter.bindKey, filter.value); @@ -275,6 +276,7 @@ global abstract class SOQL implements Comparable { List orFilterPieces = new List(); for (SOQL.QueryFilter filter : filters) { + filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); orFilterPieces.add(filter.toString()); } this.whereFilters.add('(' + String.join(orFilterPieces, ' OR ') + ')'); From 8d7db9ea51d14ebf4d22f47f805e88b04d2ccbae Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 24 Jun 2024 00:15:41 -0400 Subject: [PATCH 11/25] Updates QueryFilter inner class to generate formattedValue and string output on-demand instead of during construction. --- nebula-query-and-search/main/classes/SOQL.cls | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 56abff2a..97440ee0 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -599,9 +599,11 @@ global abstract class SOQL implements Comparable { private SOQL.QueryField queryField; private SOQL.Operator operator; private Object value; - private String formattedValue; - private String filterString; private String bindKey; + private Schema.SObjectType childSObjectType; + private Query childQuery; + private Boolean inOrNotIn; + private Schema.SObjectField lookupFieldOnChildSObject; global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value) { this(new QueryField(field), operator, value); @@ -619,20 +621,21 @@ global abstract class SOQL implements Comparable { this.queryField = queryField; this.operator = operator; this.value = value; - this.formattedValue = (String.isNotBlank(bindKey) ? ':' + bindKey : new QueryArgument(value).toString()); this.bindKey = bindKey; - - this.filterString = queryField + ' ' + SOQL.getOperatorValue(operator) + ' ' + formattedValue; } global QueryFilter(Schema.SObjectType childSObjectType, Boolean inOrNotIn, Schema.SObjectField lookupFieldOnChildSObject) { this.operator = inOrNotIn ? SOQL.Operator.IS_IN : SOQL.Operator.IS_NOT_IN; - this.filterString = 'Id ' + SOQL.getOperatorValue(this.operator) + ' (SELECT ' + lookupFieldOnChildSObject + ' FROM ' + childSObjectType + ')'; + this.childSObjectType = childSObjectType; + this.inOrNotIn = inOrNotIn; + this.lookupFieldOnChildSObject = lookupFieldOnChildSObject; } global QueryFilter(Query childQuery, Boolean inOrNotIn, Schema.SObjectField lookupFieldOnChildSObject) { this.operator = inOrNotIn ? SOQL.Operator.IS_IN : SOQL.Operator.IS_NOT_IN; - this.filterString = 'Id ' + SOQL.getOperatorValue(this.operator) + ' ' + childQuery.getSubquery(lookupFieldOnChildSObject); + this.childQuery = childQuery; + this.inOrNotIn = inOrNotIn; + this.lookupFieldOnChildSObject = lookupFieldOnChildSObject; } global Integer compareTo(Object compareTo) { @@ -659,14 +662,33 @@ global abstract class SOQL implements Comparable { return this.value; } + global String getBindKey() { + return this.bindKey; + } + + global void setBindKey(String bindKey) { + if (this.queryField != null) + { + this.bindKey = bindKey; + } + } + // TODO decide if this should be global public Object getFormattedValue() { - return this.formattedValue; + return (String.isNotBlank(this.bindKey) ? ':' + this.bindKey : new QueryArgument(this.value).toString()); } // TODO decide if this should be global public override String toString() { - return this.filterString; + return ( + this.queryField != null ? + this.queryField + ' ' + SOQL.getOperatorValue(this.operator) + ' ' + this.getFormattedValue() : + ( + this.childSObjectType != null ? + 'Id ' + SOQL.getOperatorValue(this.operator) + ' (SELECT ' + this.lookupFieldOnChildSObject + ' FROM ' + this.childSObjectType + ')' : + 'Id ' + SOQL.getOperatorValue(this.operator) + ' ' + this.childQuery.getSubquery(this.lookupFieldOnChildSObject) + ) + ); } } From 12ee75bb2e3db6d6b9b504f7023db72ae2e54108 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 24 Jun 2024 00:16:09 -0400 Subject: [PATCH 12/25] Initial tests --- .../tests/classes/AggregateQuery_Tests.cls | 60 +++++++++++++++++++ .../tests/classes/Query_Tests.cls | 32 ++++++++++ 2 files changed, 92 insertions(+) diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index a8535619..a9eef050 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -451,4 +451,64 @@ private class AggregateQuery_Tests { UserName = 'newuser@testorg.com' ); } + + @IsTest + static void it_will_generate_a_bind_variable_for_a_having_filter() + { + // SETUP + String expectedQueryString = 'SELECT ParentId, MIN(CreatedDate) MIN__CreatedDate FROM Account GROUP BY ParentId HAVING MIN(CreatedDate) >= :bindVar0'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .groupByField(Account.ParentId) + .havingAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1)); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_generate_a_bind_variable_for_a_where_filter() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :bindVar0'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1))); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-1)); + } + } + } diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 06e795bf..4197f3b8 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -506,4 +506,36 @@ private class Query_Tests { UserName = 'newuser@testorg.com' ); } + + @IsTest + static void it_will_generate_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :bindVar0 ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1))) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + } From b94879b0b10c73e1753fdca33662c30eee7e14d4 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sat, 29 Jun 2024 00:05:37 -0400 Subject: [PATCH 13/25] Retain complete "WHERE" QueryFilter instances instead of strings, generate strings when query string is built --- nebula-query-and-search/main/classes/SOQL.cls | 71 +++++++++++--- .../tests/classes/Query_Tests.cls | 93 +++++++++++++++++++ 2 files changed, 149 insertions(+), 15 deletions(-) diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 97440ee0..ac554199 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -180,7 +180,8 @@ global abstract class SOQL implements Comparable { protected Map includedQueryFieldsAndCategory; protected Set excludedQueryFields; protected Scope scope; - protected List whereFilters; + protected List whereFilters; + protected String whereFilterConditionsString; protected System.AccessLevel accessLevel; protected List orderByFieldApiNames; protected Integer limitCount; @@ -199,7 +200,8 @@ global abstract class SOQL implements Comparable { this.sobjectDescribe = this.sobjectType.getDescribe(Schema.SObjectDescribeOptions.DEFERRED); this.includedQueryFieldsAndCategory = new Map(); this.excludedQueryFields = new Set(); - this.whereFilters = new List(); + this.whereFilters = new List(); + this.whereFilterConditionsString = ''; this.orderByFieldApiNames = new List(); this.accessLevel = System.AccessLevel.SYSTEM_MODE; this.bindsMap = new Map(); @@ -258,28 +260,26 @@ global abstract class SOQL implements Comparable { } for (SOQL.QueryFilter filter : filters) { - filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); - this.whereFilters.add(filter.toString()); - if (String.isNotBlank(filter.bindKey)) { - this.bindsMap.put(filter.bindKey, filter.value); - } + this.whereFilters.add(filter); + this.whereFilterConditionsString += (this.whereFilters.size() == 1 ? '1' : ' AND ' + this.whereFilters.size()); } this.doSetHasChanged(); } protected void doOrFilterWhere(List filters) { - if (filters == null || filters.isEmpty()) { + if (filters?.isEmpty() != false) { return; } filters.sort(); - List orFilterPieces = new List(); - for (SOQL.QueryFilter filter : filters) { - filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); - orFilterPieces.add(filter.toString()); + this.whereFilterConditionsString += (this.whereFilters.isEmpty() ? '' : ' AND ') + '('; + for (Integer i = 0; i < filters.size(); i++) { + SOQL.QueryFilter filter = filters[i]; + this.whereFilters.add(filter); + this.whereFilterConditionsString += (i == 0 ? '' : ' OR ') + this.whereFilters.size(); } - this.whereFilters.add('(' + String.join(orFilterPieces, ' OR ') + ')'); + this.whereFilterConditionsString += ')'; this.doSetHasChanged(); } @@ -401,8 +401,49 @@ global abstract class SOQL implements Comparable { } protected String doGetWhereClauseString() { - this.whereFilters.sort(); - return this.whereFilters.isEmpty() ? '' : ' WHERE ' + String.join(this.whereFilters, ' AND '); + List whereFilterStrings = new List(); + for (QueryFilter filter : this.whereFilters) + { + filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); + if (String.isNotBlank(filter.bindKey)) { + this.bindsMap.put(filter.bindKey, filter.value); + } + whereFilterStrings.add(filter.toString()); + } + if (String.isBlank(this.whereFilterConditionsString)) + { + return ''; + } + + List whereFilterConditionsParts = this.whereFilterConditionsString.split('\\s+'); + List whereFilterConditionsPartsReplaced = new List(); + for (String whereFilterConditionsPart : whereFilterConditionsParts) + { + Matcher m = Pattern.compile('\\d+').matcher(whereFilterConditionsPart); + Boolean indexFound = m.find(); + if (indexFound) + { + Integer whereFilterConditionsIndex = Integer.valueOf(m.group()); + try + { + whereFilterConditionsPartsReplaced.add( + whereFilterConditionsPart.replace( + m.group(), + whereFilterStrings[whereFilterConditionsIndex - 1] + ) + ); + } + catch (ListException e) + { + throw new QueryException('No query WHERE filter defined for index "' + whereFilterConditionsIndex + '" specified in filter conditions "' + this.whereFilterConditionsString + '"'); + } + } + else + { + whereFilterConditionsPartsReplaced.add(whereFilterConditionsPart); + } + } + return ' WHERE ' + String.join(whereFilterConditionsPartsReplaced, ' '); } protected System.AccessLevel doGetAccessLevel() { diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 4197f3b8..b0b2a515 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -538,4 +538,97 @@ private class Query_Tests { } } + @IsTest + static void it_will_generate_a_bind_key_when_instructed_before_filter_is_added() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :bindVar0 ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1))) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_generate_a_bind_key_when_instructed_after_filter_is_added() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :bindVar0 ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1))) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1) + .generateBindVariableKeys(); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_not_generate_a_bind_key_if_already_specified() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'dateFilter' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1) + .generateBindVariableKeys(); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + } From b4556d01e921c0f2a78a98fbc9458d484cd9007e Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sat, 29 Jun 2024 00:07:26 -0400 Subject: [PATCH 14/25] Fix removal of bind keys --- nebula-query-and-search/main/classes/SOQL.cls | 11 +++++++ .../tests/classes/AggregateQuery_Tests.cls | 28 +++------------- .../tests/classes/Query_Tests.cls | 32 ++++--------------- 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index ac554199..a95903e3 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -322,10 +322,21 @@ global abstract class SOQL implements Comparable { protected void doRemoveBind(String key) { this.bindsMap.remove(key); + for (QueryFilter filter : this.whereFilters) + { + if (filter.bindKey == key) + { + filter.bindKey = null; + } + } } protected void doClearBinds() { this.bindsMap.clear(); + for (QueryFilter filter : this.whereFilters) + { + filter.bindKey = null; + } } protected void doGenerateBindVariableKeys() { diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index a9eef050..92e300b8 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -390,52 +390,34 @@ private class AggregateQuery_Tests { static void it_will_remove_a_bind_variable() { // SETUP - String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :dateFilter'; + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= 2000-01-01T05:00:00Z'; AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Datetime.newInstance(2000, 1, 1), 'dateFilter')); // TEST aggregateQuery.removeBind('dateFilter'); - Exception caughtException; - try { - List returnedResults = aggregateQuery.getResults(); - } - catch (Exception e) { - caughtException = e; - } // VERIFY System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); - System.Assert.isInstanceOfType(caughtException, QueryException.class); - System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); } @IsTest static void it_will_clear_all_bind_variables() { // SETUP - String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= 2000-01-01T05:00:00Z AND CreatedDate < 2001-01-01T05:00:00Z'; AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) .addAggregate(SOQL.Aggregate.MAX, Schema.Account.CreatedDate) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Datetime.newInstance(2000, 1, 1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Datetime.newInstance(2001, 1, 1), 'maxDateFilter')); // TEST aggregateQuery.clearBinds(); - Exception caughtException; - try { - List returnedResults = aggregateQuery.getResults(); - } - catch (Exception e) { - caughtException = e; - } // VERIFY System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); - System.Assert.isInstanceOfType(caughtException, QueryException.class); - System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); } static User minimumAccessUser() { diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index b0b2a515..332945c7 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -443,54 +443,36 @@ private class Query_Tests { } @IsTest - static void it_will_remove_a_bind_variable() + static void it_will_remove_a_bind_key() { // SETUP - String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter'; + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= 2000-01-01T05:00:00Z'; Query accountQuery = new Query(Schema.Account.SObjectType) .addField(Schema.Account.CreatedDate) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Datetime.newInstance(2000, 1, 1), 'dateFilter')); // TEST accountQuery.removeBind('dateFilter'); - Exception caughtException; - try { - List returnedResults = accountQuery.getResults(); - } - catch (Exception e) { - caughtException = e; - } // VERIFY System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); - System.Assert.isInstanceOfType(caughtException, QueryException.class); - System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); } @IsTest - static void it_will_clear_all_bind_variables() + static void it_will_clear_all_bind_keys() { // SETUP - String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= 2000-01-01T05:00:00Z AND CreatedDate < 2001-01-01T05:00:00Z'; Query accountQuery = new Query(Schema.Account.SObjectType) .addField(Schema.Account.CreatedDate) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) - .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Datetime.newInstance(2000, 1, 1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Datetime.newInstance(2001, 1, 1), 'maxDateFilter')); // TEST accountQuery.clearBinds(); - Exception caughtException; - try { - List returnedResults = accountQuery.getResults(); - } - catch (Exception e) { - caughtException = e; - } // VERIFY System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); - System.Assert.isInstanceOfType(caughtException, QueryException.class); - System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); } static User minimumAccessUser() { From bb432087875ebecc2dbc3e769832994176093fa6 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sat, 29 Jun 2024 00:09:37 -0400 Subject: [PATCH 15/25] Update tests to account for "WHERE" filters no longer being sorted, use newer System.Assert methods --- .../tests/classes/AggregateQuery_Tests.cls | 8 ++-- .../tests/classes/Query_Tests.cls | 48 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index 92e300b8..029b9262 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -112,7 +112,7 @@ private class AggregateQuery_Tests { @IsTest static void it_should_return_results_when_filtering_with_an_or_statement() { - String expectedQueryString = 'SELECT Type, COUNT(Id) COUNT__Id FROM Account WHERE (AnnualRevenue = null OR Type = null) AND ParentId != null GROUP BY Type'; + String expectedQueryString = 'SELECT Type, COUNT(Id) COUNT__Id FROM Account WHERE ParentId != null AND (AnnualRevenue = null OR Type = null) GROUP BY Type'; AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) .groupByField(Schema.Account.Type) @@ -135,13 +135,13 @@ private class AggregateQuery_Tests { static void it_should_cache_results() { AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType); aggregateQuery.cacheResults(); - System.assertEquals(0, System.Limits.getQueries()); + System.Assert.areEqual(0, System.Limits.getQueries()); for (Integer i = 0; i < 3; i++) { aggregateQuery.getResults(); } - System.assertEquals(1, System.Limits.getQueries()); + System.Assert.areEqual(1, System.Limits.getQueries()); } @IsTest @@ -356,7 +356,7 @@ private class AggregateQuery_Tests { static void it_will_set_multiple_bind_variables() { // SETUP - String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :minDateFilter AND CreatedDate < :maxDateFilter'; List expectedResults = Database.queryWithBinds( expectedQueryString, new Map { diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 332945c7..afc4de72 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -43,7 +43,7 @@ private class Query_Tests { Query simpleAccountQuery = new Query(Schema.Account.SObjectType); - System.assertEquals(expectedQueryString, simpleAccountQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, simpleAccountQuery.getQuery()); List accounts = simpleAccountQuery.getResults(); } @@ -65,15 +65,15 @@ private class Query_Tests { String expectedQueryString = 'SELECT Alias, Email, Id, IsActive, Profile.Name, ProfileId' + ' FROM User USING SCOPE MINE' + - ' WHERE CreatedDate <= LAST_WEEK' + - ' AND Email != null' + - ' AND IsActive = true' + - ' AND LastLoginDate >= LAST_N_DAYS:3' + - ' AND LastModifiedDate <= ' + - now.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time') + + ' WHERE IsActive = true' + ' AND Profile.Id != \'' + System.UserInfo.getProfileId() + '\'' + + ' AND LastModifiedDate <= ' + + now.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time') + + ' AND LastLoginDate >= LAST_N_DAYS:3' + + ' AND CreatedDate <= LAST_WEEK' + + ' AND Email != null' + ' ORDER BY Profile.CreatedBy.LastModifiedDate ASC NULLS FIRST, Name ASC NULLS FIRST, Email ASC NULLS FIRST' + ' LIMIT 100 OFFSET 1 FOR VIEW'; List fieldsToQuery = new List{ Schema.User.IsActive, Schema.User.Alias }; @@ -100,7 +100,7 @@ private class Query_Tests { .offsetBy(1) .forView(); - System.assertEquals(expectedQueryString, userQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, userQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = userQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -118,7 +118,7 @@ private class Query_Tests { Query userQuery = new Query(Schema.User.SObjectType).addField(queryField).limitTo(1); - System.assertEquals(expectedQueryString, userQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, userQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = userQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -130,7 +130,7 @@ private class Query_Tests { Query accountQuery = new Query(Schema.Account.SObjectType).addField(new SOQL.QueryField(Schema.Account.OwnerId)); - System.assertEquals(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); List accounts = accountQuery.getResults(); } @@ -176,7 +176,7 @@ private class Query_Tests { System.Test.stopTest(); - System.assertEquals(expectedQuery, taskQuery.getQuery()); + System.Assert.areEqual(expectedQuery, taskQuery.getQuery()); } @IsTest @@ -185,7 +185,7 @@ private class Query_Tests { Query leadQuery = new Query(Schema.Lead.SObjectType).addField(new SOQL.QueryField(Schema.Lead.OwnerId)); - System.assertEquals(expectedQueryString, leadQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, leadQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = leadQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -201,7 +201,7 @@ private class Query_Tests { .includeRelatedRecords(Schema.Contact.AccountId, contactQuery) .addField(new SOQL.QueryField(Schema.Account.Type)); - System.assertEquals(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); List accounts = accountQuery.getResults(); } @@ -297,7 +297,7 @@ private class Query_Tests { Query leadQuery = new Query(Schema.Lead.SObjectType).orderByField(Schema.Lead.CreatedDate); - System.assertEquals(expectedQueryString, leadQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, leadQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = leadQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -309,7 +309,7 @@ private class Query_Tests { Query leadQuery = new Query(Schema.Lead.SObjectType).forReference(); - System.assertEquals(expectedQueryString, leadQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, leadQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = leadQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -321,7 +321,7 @@ private class Query_Tests { Query leadQuery = new Query(Schema.Lead.SObjectType).forUpdate(); - System.assertEquals(expectedQueryString, leadQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, leadQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = leadQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -333,7 +333,7 @@ private class Query_Tests { Query leadQuery = new Query(Schema.Lead.SObjectType).forView(); - System.assertEquals(expectedQueryString, leadQuery.getQuery()); + System.Assert.areEqual(expectedQueryString, leadQuery.getQuery()); List expectedResults = Database.query(expectedQueryString); List returnedResults = leadQuery.getResults(); System.Assert.areEqual(expectedResults, returnedResults); @@ -359,11 +359,11 @@ private class Query_Tests { Query userQuery = new Query(Schema.User.SObjectType).limitTo(1); // First, verify that caching is not enabled by default - System.assertEquals(0, System.Limits.getQueries()); + System.Assert.areEqual(0, System.Limits.getQueries()); for (Integer i = 0; i < loops; i++) { userQuery.getResults(); } - System.assertEquals(loops, System.Limits.getQueries()); + System.Assert.areEqual(loops, System.Limits.getQueries()); System.Test.startTest(); @@ -371,13 +371,13 @@ private class Query_Tests { for (Integer i = 0; i < loops; i++) { userQuery.getResults(); } - System.assertEquals(1, System.Limits.getQueries()); + System.Assert.areEqual(1, System.Limits.getQueries()); System.Test.stopTest(); } @IsTest - static void it_will_set_a_bind_variable() + static void it_will_set_a_bind_key() { // SETUP String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; @@ -408,10 +408,10 @@ private class Query_Tests { } @IsTest - static void it_will_set_multiple_bind_variables() + static void it_will_set_multiple_bind_keys() { // SETUP - String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :minDateFilter AND CreatedDate < :maxDateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; List expectedResults = Database.queryWithBinds( expectedQueryString, new Map { @@ -490,7 +490,7 @@ private class Query_Tests { } @IsTest - static void it_will_generate_a_bind_variable() + static void it_will_generate_a_bind_key() { // SETUP String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :bindVar0 ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; From 249c97187ef7e207be5d19482824a5f0c313c81e Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sat, 29 Jun 2024 00:15:12 -0400 Subject: [PATCH 16/25] Additional test --- .../tests/classes/Query_Tests.cls | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index afc4de72..13d01b6d 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -613,4 +613,36 @@ private class Query_Tests { } } + @IsTest + static void it_will_generate_a_bind_key_if_removed() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :bindVar0 ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'bindVar0' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1) + .removeBind('dateFilter') + .generateBindVariableKeys(); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + } From 169fb52a2691c096a36b87a4fd72cd21c6d1a8aa Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:02:50 -0400 Subject: [PATCH 17/25] Made SOQL.QueryFilter virtual and extended with new AggregateQuery.AggregateQueryFilter class --- .../main/classes/AggregateQuery.cls | 25 +++++++++++++++++++ nebula-query-and-search/main/classes/SOQL.cls | 20 +++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index e2e69bf7..9548b64c 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -361,4 +361,29 @@ global class AggregateQuery extends SOQL { return aggregateFunction.name() + '(' + fieldApiName + ') ' + fieldAlias; } } + + global class AggregateQueryFilter extends SOQL.QueryFilter { + private SOQL.Aggregate aggregateFunction; + + public AggregateQueryFilter(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindKey) + { + super(queryField, operator, value, bindKey); + this.aggregateFunction = aggregateFunction; + } + + public override String toString() + { + return String.format( + '{0}({1}) {2} {3}', + new List { + this.aggregateFunction.name(), + this.queryField.toString(), + SOQL.getOperatorValue(this.operator), + (String.isNotBlank(this.bindKey) ? ':' + this.bindKey : new QueryArgument(this.value).toString()) + } + ); + } + + } + } diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index a95903e3..de0dd62a 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -647,15 +647,15 @@ global abstract class SOQL implements Comparable { } } - global class QueryFilter implements Comparable { - private SOQL.QueryField queryField; - private SOQL.Operator operator; - private Object value; - private String bindKey; - private Schema.SObjectType childSObjectType; - private Query childQuery; - private Boolean inOrNotIn; - private Schema.SObjectField lookupFieldOnChildSObject; + global virtual class QueryFilter implements Comparable { + protected SOQL.QueryField queryField; + protected SOQL.Operator operator; + protected Object value; + protected String bindKey; + protected Schema.SObjectType childSObjectType; + protected Query childQuery; + protected Boolean inOrNotIn; + protected Schema.SObjectField lookupFieldOnChildSObject; global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value) { this(new QueryField(field), operator, value); @@ -731,7 +731,7 @@ global abstract class SOQL implements Comparable { } // TODO decide if this should be global - public override String toString() { + public virtual override String toString() { return ( this.queryField != null ? this.queryField + ' ' + SOQL.getOperatorValue(this.operator) + ' ' + this.getFormattedValue() : From ee03def514f98e20e6fb07ef0dde28522c5d488f Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:04:31 -0400 Subject: [PATCH 18/25] Updated AggregateQuery to handle HAVING filters with AggregateQueryFilters the same as WHERE filters --- .../main/classes/AggregateQuery.cls | 79 +++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 9548b64c..ca242eb3 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -15,14 +15,16 @@ global class AggregateQuery extends SOQL { private SOQL.GroupingDimension groupingDimension; private List aggregateFields; - private List havingConditions; + private List havingConditions; + protected String havingConditionsLogic; private String countQuery; global AggregateQuery(Schema.SObjectType sobjectType) { super(sobjectType, false); this.aggregateFields = new List(); - this.havingConditions = new List(); + this.havingConditions = new List(); + this.havingConditionsLogic = ''; } global AggregateQuery groupByField(Schema.SObjectField field) { @@ -90,19 +92,26 @@ global class AggregateQuery extends SOQL { global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { this.havingConditions.add( - String.format( - '{0}({1}) {2} {3}', - new List { - aggregateFunction.name(), - queryField.toString(), - SOQL.getOperatorValue(operator), - (String.isNotBlank(bindWithKey) ? ':' + bindWithKey : new QueryArgument(value).toString()) - } - ) + new AggregateQueryFilter(aggregateFunction, queryField, operator, value, bindWithKey) ); - if (String.isNotBlank(bindWithKey)) { - this.bindsMap.put(bindWithKey, value); + this.havingConditionsLogic += (this.havingConditions.size() == 1 ? '1' : ' AND ' + this.havingConditions.size()); + return this.setHasChanged(); + } + + global AggregateQuery havingOrAggregate(List filters) { + if (filters?.isEmpty() != false) { + return this; } + + filters.sort(); + + this.havingConditionsLogic += (this.havingConditions.isEmpty() ? '' : ' AND ') + '('; + for (Integer i = 0; i < filters.size(); i++) { + AggregateQueryFilter filter = filters[i]; + this.havingConditions.add(filter); + this.havingConditionsLogic += (i == 0 ? '' : ' OR ') + this.havingConditions.size(); + } + this.havingConditionsLogic += ')'; return this.setHasChanged(); } @@ -337,7 +346,49 @@ global class AggregateQuery extends SOQL { } private String getHavingString() { - return this.havingConditions.isEmpty() ? '' : ' HAVING ' + String.join(this.havingConditions, ', '); + List havingConditionStrings = new List(); + for (AggregateQueryFilter condition : this.havingConditions) + { + condition.setBindKey(condition.getBindKey() ?? this.generateNextBindVariableKey()); + if (String.isNotBlank(condition.getBindKey())) { + this.bindsMap.put(condition.getBindKey(), condition.getValue()); + } + havingConditionStrings.add(condition.toString()); + } + if (String.isBlank(this.havingConditionsLogic)) + { + return ''; + } + + List havingConditionsLogicParts = this.havingConditionsLogic.split('\\s+'); + List havingConditionsLogicPartsReplaced = new List(); + for (String havingConditionsLogicPart : havingConditionsLogicParts) + { + Matcher m = Pattern.compile('\\d+').matcher(havingConditionsLogicPart); + Boolean indexFound = m.find(); + if (indexFound) + { + Integer havingConditionsIndex = Integer.valueOf(m.group()); + try + { + havingConditionsLogicPartsReplaced.add( + havingConditionsLogicPart.replace( + m.group(), + havingConditionStrings[havingConditionsIndex - 1] + ) + ); + } + catch (ListException e) + { + throw new QueryException('No query HAVING filter defined for index "' + havingConditionsIndex + '" specified in filter conditions "' + this.havingConditionsLogic + '"'); + } + } + else + { + havingConditionsLogicPartsReplaced.add(havingConditionsLogicPart); + } + } + return ' HAVING ' + String.join(havingConditionsLogicPartsReplaced, ' '); } private class AggregateField { From 7045be0ea620688c5944856659b53295b851663c Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:18:42 -0400 Subject: [PATCH 19/25] Refactored HAVING and WHERE clause stringification into single common method --- .../main/classes/AggregateQuery.cls | 44 +------------------ nebula-query-and-search/main/classes/SOQL.cls | 40 +++++++++-------- 2 files changed, 23 insertions(+), 61 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index ca242eb3..39d06402 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -346,49 +346,7 @@ global class AggregateQuery extends SOQL { } private String getHavingString() { - List havingConditionStrings = new List(); - for (AggregateQueryFilter condition : this.havingConditions) - { - condition.setBindKey(condition.getBindKey() ?? this.generateNextBindVariableKey()); - if (String.isNotBlank(condition.getBindKey())) { - this.bindsMap.put(condition.getBindKey(), condition.getValue()); - } - havingConditionStrings.add(condition.toString()); - } - if (String.isBlank(this.havingConditionsLogic)) - { - return ''; - } - - List havingConditionsLogicParts = this.havingConditionsLogic.split('\\s+'); - List havingConditionsLogicPartsReplaced = new List(); - for (String havingConditionsLogicPart : havingConditionsLogicParts) - { - Matcher m = Pattern.compile('\\d+').matcher(havingConditionsLogicPart); - Boolean indexFound = m.find(); - if (indexFound) - { - Integer havingConditionsIndex = Integer.valueOf(m.group()); - try - { - havingConditionsLogicPartsReplaced.add( - havingConditionsLogicPart.replace( - m.group(), - havingConditionStrings[havingConditionsIndex - 1] - ) - ); - } - catch (ListException e) - { - throw new QueryException('No query HAVING filter defined for index "' + havingConditionsIndex + '" specified in filter conditions "' + this.havingConditionsLogic + '"'); - } - } - else - { - havingConditionsLogicPartsReplaced.add(havingConditionsLogicPart); - } - } - return ' HAVING ' + String.join(havingConditionsLogicPartsReplaced, ' '); + return this.doGetFilterableClauseString('HAVING', this.havingConditions, this.havingConditionsLogic); } private class AggregateField { diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index de0dd62a..78bc091b 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -411,50 +411,54 @@ global abstract class SOQL implements Comparable { return this.scope == null ? '' : ' USING SCOPE ' + this.scope.name(); } - protected String doGetWhereClauseString() { - List whereFilterStrings = new List(); - for (QueryFilter filter : this.whereFilters) + protected String doGetFilterableClauseString(String clause, LIST filters, String filterLogic) { + List filterStrings = new List(); + for (Soql.QueryFilter filter : filters) { filter.setBindKey(filter.getBindKey() ?? this.generateNextBindVariableKey()); - if (String.isNotBlank(filter.bindKey)) { - this.bindsMap.put(filter.bindKey, filter.value); + if (String.isNotBlank(filter.getBindKey())) { + this.bindsMap.put(filter.getBindKey(), filter.getValue()); } - whereFilterStrings.add(filter.toString()); + filterStrings.add(filter.toString()); } - if (String.isBlank(this.whereFilterConditionsString)) + if (String.isBlank(filterLogic)) { return ''; } - List whereFilterConditionsParts = this.whereFilterConditionsString.split('\\s+'); - List whereFilterConditionsPartsReplaced = new List(); - for (String whereFilterConditionsPart : whereFilterConditionsParts) + List filterLogicParts = filterLogic.split('\\s+'); + List filterLogicPartsReplaced = new List(); + for (String filterLogicPart : filterLogicParts) { - Matcher m = Pattern.compile('\\d+').matcher(whereFilterConditionsPart); + Matcher m = Pattern.compile('\\d+').matcher(filterLogicPart); Boolean indexFound = m.find(); if (indexFound) { - Integer whereFilterConditionsIndex = Integer.valueOf(m.group()); + Integer filterLogicIndex = Integer.valueOf(m.group()); try { - whereFilterConditionsPartsReplaced.add( - whereFilterConditionsPart.replace( + filterLogicPartsReplaced.add( + filterLogicPart.replace( m.group(), - whereFilterStrings[whereFilterConditionsIndex - 1] + filterStrings[filterLogicIndex - 1] ) ); } catch (ListException e) { - throw new QueryException('No query WHERE filter defined for index "' + whereFilterConditionsIndex + '" specified in filter conditions "' + this.whereFilterConditionsString + '"'); + throw new QueryException('No query ' + clause + ' filter defined for index "' + filterLogicIndex + '" specified in filter conditions "' + filterLogic + '"'); } } else { - whereFilterConditionsPartsReplaced.add(whereFilterConditionsPart); + filterLogicPartsReplaced.add(filterLogicPart); } } - return ' WHERE ' + String.join(whereFilterConditionsPartsReplaced, ' '); + return ' ' + clause + ' ' + String.join(filterLogicPartsReplaced, ' '); + } + + protected String doGetWhereClauseString() { + return this.doGetFilterableClauseString('WHERE', this.whereFilters, this.whereFilterConditionsString); } protected System.AccessLevel doGetAccessLevel() { From fccae9664c1fd0e29773dffbfcfde44f7f12a010 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:28:26 -0400 Subject: [PATCH 20/25] Rename variables and methods for clarity and consistency --- .../main/classes/AggregateQuery.cls | 30 +++++++++---------- nebula-query-and-search/main/classes/SOQL.cls | 26 ++++++++-------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 39d06402..84ed1285 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -15,16 +15,16 @@ global class AggregateQuery extends SOQL { private SOQL.GroupingDimension groupingDimension; private List aggregateFields; - private List havingConditions; - protected String havingConditionsLogic; + private List havingClauseFilters; + protected String havingClauseFilterLogic; private String countQuery; global AggregateQuery(Schema.SObjectType sobjectType) { super(sobjectType, false); this.aggregateFields = new List(); - this.havingConditions = new List(); - this.havingConditionsLogic = ''; + this.havingClauseFilters = new List(); + this.havingClauseFilterLogic = ''; } global AggregateQuery groupByField(Schema.SObjectField field) { @@ -91,27 +91,27 @@ global class AggregateQuery extends SOQL { } global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { - this.havingConditions.add( + this.havingClauseFilters.add( new AggregateQueryFilter(aggregateFunction, queryField, operator, value, bindWithKey) ); - this.havingConditionsLogic += (this.havingConditions.size() == 1 ? '1' : ' AND ' + this.havingConditions.size()); + this.havingClauseFilterLogic += (this.havingClauseFilters.size() == 1 ? '1' : ' AND ' + this.havingClauseFilters.size()); return this.setHasChanged(); } - global AggregateQuery havingOrAggregate(List filters) { + global AggregateQuery orHavingAggregate(List filters) { if (filters?.isEmpty() != false) { return this; } filters.sort(); - this.havingConditionsLogic += (this.havingConditions.isEmpty() ? '' : ' AND ') + '('; + this.havingClauseFilterLogic += (this.havingClauseFilters.isEmpty() ? '' : ' AND ') + '('; for (Integer i = 0; i < filters.size(); i++) { AggregateQueryFilter filter = filters[i]; - this.havingConditions.add(filter); - this.havingConditionsLogic += (i == 0 ? '' : ' OR ') + this.havingConditions.size(); + this.havingClauseFilters.add(filter); + this.havingClauseFilterLogic += (i == 0 ? '' : ' OR ') + this.havingClauseFilters.size(); } - this.havingConditionsLogic += ')'; + this.havingClauseFilterLogic += ')'; return this.setHasChanged(); } @@ -266,7 +266,7 @@ global class AggregateQuery extends SOQL { super.doGetUsingScopeString() + super.doGetWhereClauseString() + this.getGroupByString() + - this.getHavingString() + + this.getHavingClauseString() + super.doGetOrderByString() + super.doGetLimitCountString() + super.doGetOffetString(); @@ -287,7 +287,7 @@ global class AggregateQuery extends SOQL { super.doGetUsingScopeString() + super.doGetWhereClauseString() + this.getGroupByString() + - this.getHavingString() + + this.getHavingClauseString() + super.doGetOrderByString() + super.doGetLimitCountString() + super.doGetOffetString(); @@ -345,8 +345,8 @@ global class AggregateQuery extends SOQL { return String.isEmpty(queryFieldString) ? '' : groupByTextString + queryFieldString + groupingDimensionClosingString; } - private String getHavingString() { - return this.doGetFilterableClauseString('HAVING', this.havingConditions, this.havingConditionsLogic); + private String getHavingClauseString() { + return this.doGetFilterableClauseString('HAVING', this.havingClauseFilters, this.havingClauseFilterLogic); } private class AggregateField { diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 78bc091b..4746292a 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -180,8 +180,8 @@ global abstract class SOQL implements Comparable { protected Map includedQueryFieldsAndCategory; protected Set excludedQueryFields; protected Scope scope; - protected List whereFilters; - protected String whereFilterConditionsString; + protected List whereClauseFilters; + protected String whereClauseFilterLogic; protected System.AccessLevel accessLevel; protected List orderByFieldApiNames; protected Integer limitCount; @@ -200,8 +200,8 @@ global abstract class SOQL implements Comparable { this.sobjectDescribe = this.sobjectType.getDescribe(Schema.SObjectDescribeOptions.DEFERRED); this.includedQueryFieldsAndCategory = new Map(); this.excludedQueryFields = new Set(); - this.whereFilters = new List(); - this.whereFilterConditionsString = ''; + this.whereClauseFilters = new List(); + this.whereClauseFilterLogic = ''; this.orderByFieldApiNames = new List(); this.accessLevel = System.AccessLevel.SYSTEM_MODE; this.bindsMap = new Map(); @@ -260,8 +260,8 @@ global abstract class SOQL implements Comparable { } for (SOQL.QueryFilter filter : filters) { - this.whereFilters.add(filter); - this.whereFilterConditionsString += (this.whereFilters.size() == 1 ? '1' : ' AND ' + this.whereFilters.size()); + this.whereClauseFilters.add(filter); + this.whereClauseFilterLogic += (this.whereClauseFilters.size() == 1 ? '1' : ' AND ' + this.whereClauseFilters.size()); } this.doSetHasChanged(); } @@ -273,13 +273,13 @@ global abstract class SOQL implements Comparable { filters.sort(); - this.whereFilterConditionsString += (this.whereFilters.isEmpty() ? '' : ' AND ') + '('; + this.whereClauseFilterLogic += (this.whereClauseFilters.isEmpty() ? '' : ' AND ') + '('; for (Integer i = 0; i < filters.size(); i++) { SOQL.QueryFilter filter = filters[i]; - this.whereFilters.add(filter); - this.whereFilterConditionsString += (i == 0 ? '' : ' OR ') + this.whereFilters.size(); + this.whereClauseFilters.add(filter); + this.whereClauseFilterLogic += (i == 0 ? '' : ' OR ') + this.whereClauseFilters.size(); } - this.whereFilterConditionsString += ')'; + this.whereClauseFilterLogic += ')'; this.doSetHasChanged(); } @@ -322,7 +322,7 @@ global abstract class SOQL implements Comparable { protected void doRemoveBind(String key) { this.bindsMap.remove(key); - for (QueryFilter filter : this.whereFilters) + for (QueryFilter filter : this.whereClauseFilters) { if (filter.bindKey == key) { @@ -333,7 +333,7 @@ global abstract class SOQL implements Comparable { protected void doClearBinds() { this.bindsMap.clear(); - for (QueryFilter filter : this.whereFilters) + for (QueryFilter filter : this.whereClauseFilters) { filter.bindKey = null; } @@ -458,7 +458,7 @@ global abstract class SOQL implements Comparable { } protected String doGetWhereClauseString() { - return this.doGetFilterableClauseString('WHERE', this.whereFilters, this.whereFilterConditionsString); + return this.doGetFilterableClauseString('WHERE', this.whereClauseFilters, this.whereClauseFilterLogic); } protected System.AccessLevel doGetAccessLevel() { From 39a9dd47fcbf480a0430486804b88b7591eb1e72 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:36:52 -0400 Subject: [PATCH 21/25] Refactored OR filter methods into single common method --- .../main/classes/AggregateQuery.cls | 14 +------------- nebula-query-and-search/main/classes/SOQL.cls | 17 +++++++++++------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 84ed1285..d82f19e0 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -99,19 +99,7 @@ global class AggregateQuery extends SOQL { } global AggregateQuery orHavingAggregate(List filters) { - if (filters?.isEmpty() != false) { - return this; - } - - filters.sort(); - - this.havingClauseFilterLogic += (this.havingClauseFilters.isEmpty() ? '' : ' AND ') + '('; - for (Integer i = 0; i < filters.size(); i++) { - AggregateQueryFilter filter = filters[i]; - this.havingClauseFilters.add(filter); - this.havingClauseFilterLogic += (i == 0 ? '' : ' OR ') + this.havingClauseFilters.size(); - } - this.havingClauseFilterLogic += ')'; + this.havingClauseFilterLogic = this.doOrFilter(this.havingClauseFilters, this.havingClauseFilterLogic); return this.setHasChanged(); } diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 4746292a..ac756119 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -266,21 +266,26 @@ global abstract class SOQL implements Comparable { this.doSetHasChanged(); } - protected void doOrFilterWhere(List filters) { + protected String doOrFilter(List filters, String filterLogic) { if (filters?.isEmpty() != false) { - return; + return filterLogic; } filters.sort(); - this.whereClauseFilterLogic += (this.whereClauseFilters.isEmpty() ? '' : ' AND ') + '('; + filterLogic += (filters.isEmpty() ? '' : ' AND ') + '('; for (Integer i = 0; i < filters.size(); i++) { SOQL.QueryFilter filter = filters[i]; - this.whereClauseFilters.add(filter); - this.whereClauseFilterLogic += (i == 0 ? '' : ' OR ') + this.whereClauseFilters.size(); + filters.add(filter); + filterLogic += (i == 0 ? '' : ' OR ') + filters.size(); } - this.whereClauseFilterLogic += ')'; + filterLogic += ')'; this.doSetHasChanged(); + return filterLogic; + } + + protected void doOrFilterWhere(List filters) { + this.whereClauseFilterLogic = this.doOrFilter(this.whereClauseFilters, this.whereClauseFilterLogic); } protected void doWithAccessLevel(System.AccessLevel accessLevel) { From e7fa0b21fa27f74b496cc7b2dc3977e02cb1ce4a Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Sun, 30 Jun 2024 00:42:49 -0400 Subject: [PATCH 22/25] Updated "setHasChanged" methods to invoke corresponding base method --- nebula-query-and-search/main/classes/AggregateQuery.cls | 2 +- nebula-query-and-search/main/classes/Query.cls | 2 +- nebula-query-and-search/main/classes/SOQL.cls | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index d82f19e0..2411e718 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -303,7 +303,7 @@ global class AggregateQuery extends SOQL { } private AggregateQuery setHasChanged() { - this.hasChanged = true; + this.doSetHasChanged(); return this; } diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 9f345154..d557b85b 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -450,7 +450,7 @@ global class Query extends SOQL { } private Query setHasChanged() { - this.hasChanged = true; + this.doSetHasChanged(); return this; } diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index ac756119..003da247 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -263,7 +263,6 @@ global abstract class SOQL implements Comparable { this.whereClauseFilters.add(filter); this.whereClauseFilterLogic += (this.whereClauseFilters.size() == 1 ? '1' : ' AND ' + this.whereClauseFilters.size()); } - this.doSetHasChanged(); } protected String doOrFilter(List filters, String filterLogic) { @@ -280,7 +279,6 @@ global abstract class SOQL implements Comparable { filterLogic += (i == 0 ? '' : ' OR ') + filters.size(); } filterLogic += ')'; - this.doSetHasChanged(); return filterLogic; } @@ -486,7 +484,7 @@ global abstract class SOQL implements Comparable { return this.bindsMap ?? new Map(); } - private void doSetHasChanged() { + protected void doSetHasChanged() { this.hasChanged = true; } From 198fd42e42cb51d9624c970e7beeb85ca6a4d63b Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 1 Jul 2024 01:01:22 -0400 Subject: [PATCH 23/25] Fix OR filter methods refactor --- .../main/classes/AggregateQuery.cls | 2 +- nebula-query-and-search/main/classes/SOQL.cls | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 2411e718..ac321f33 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -99,7 +99,7 @@ global class AggregateQuery extends SOQL { } global AggregateQuery orHavingAggregate(List filters) { - this.havingClauseFilterLogic = this.doOrFilter(this.havingClauseFilters, this.havingClauseFilterLogic); + this.havingClauseFilterLogic = this.doOrFilter(filters, this.havingClauseFilters, this.havingClauseFilterLogic); return this.setHasChanged(); } diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index 003da247..e1a232cf 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -265,25 +265,25 @@ global abstract class SOQL implements Comparable { } } - protected String doOrFilter(List filters, String filterLogic) { - if (filters?.isEmpty() != false) { + protected String doOrFilter(List newFilters, List clauseFilters, String filterLogic) { + if (newFilters?.isEmpty() != false) { return filterLogic; } - filters.sort(); + newFilters.sort(); - filterLogic += (filters.isEmpty() ? '' : ' AND ') + '('; - for (Integer i = 0; i < filters.size(); i++) { - SOQL.QueryFilter filter = filters[i]; - filters.add(filter); - filterLogic += (i == 0 ? '' : ' OR ') + filters.size(); + filterLogic += (clauseFilters.isEmpty() ? '' : ' AND ') + '('; + for (Integer i = 0; i < newFilters.size(); i++) { + SOQL.QueryFilter filter = newFilters[i]; + clauseFilters.add(filter); + filterLogic += (i == 0 ? '' : ' OR ') + clauseFilters.size(); } filterLogic += ')'; return filterLogic; } protected void doOrFilterWhere(List filters) { - this.whereClauseFilterLogic = this.doOrFilter(this.whereClauseFilters, this.whereClauseFilterLogic); + this.whereClauseFilterLogic = this.doOrFilter(filters, this.whereClauseFilters, this.whereClauseFilterLogic); } protected void doWithAccessLevel(System.AccessLevel accessLevel) { From bb0c8f2c56eaf2b878323a3c01846344fdb33bb4 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 1 Jul 2024 01:02:16 -0400 Subject: [PATCH 24/25] Add methods to set custom HAVING and WHERE clause logic --- .../main/classes/AggregateQuery.cls | 10 ++++++++++ nebula-query-and-search/main/classes/Query.cls | 5 +++++ nebula-query-and-search/main/classes/SOQL.cls | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index ac321f33..3793fb0b 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -103,6 +103,11 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery setHavingFilterLogic(String filterLogic) { + this.havingClauseFilterLogic = filterLogic; + return this.setHasChanged(); + } + global AggregateQuery filterWhere(Schema.SObjectField field, SOQL.Operator operator, Object value) { return this.filterWhere(new SOQL.QueryField(field), operator, value); } @@ -133,6 +138,11 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery setWhereFilterLogic(String filterLogic) { + super.doSetWhereFilterLogic(filterLogic); + return this.setHasChanged(); + } + global AggregateQuery withAccessLevel(System.AccessLevel accessLevel) { super.doWithAccessLevel(accessLevel); return this.setHasChanged(); diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index d557b85b..97e4d09c 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -225,6 +225,11 @@ global class Query extends SOQL { return this.setHasChanged(); } + global Query setWhereFilterLogic(String filterLogic) { + super.doSetWhereFilterLogic(filterLogic); + return this.setHasChanged(); + } + //global Query filterWhereInSubquery(Schema.SObjectType childSObjectType, Schema.SObjectField lookupFieldOnChildSObject) { //this.whereFilters.add('Id IN (SELECT ' + lookupFieldOnChildSObject + ' FROM ' + childSObjectType + ')'); //return this.setHasChanged(); diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index e1a232cf..c444a18c 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -282,6 +282,11 @@ global abstract class SOQL implements Comparable { return filterLogic; } + protected void doSetWhereFilterLogic(String filterLogic) + { + this.whereClauseFilterLogic = filterLogic; + } + protected void doOrFilterWhere(List filters) { this.whereClauseFilterLogic = this.doOrFilter(filters, this.whereClauseFilters, this.whereClauseFilterLogic); } From d565b32d208ea1f63c326248bb4e3982b1205153 Mon Sep 17 00:00:00 2001 From: Ken Louf Date: Mon, 1 Jul 2024 01:02:26 -0400 Subject: [PATCH 25/25] Tests for custom HAVING and WHERE clause logic --- .../tests/classes/AggregateQuery_Tests.cls | 48 +++++++++++++++++++ .../tests/classes/Query_Tests.cls | 24 ++++++++++ 2 files changed, 72 insertions(+) diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index 029b9262..f318bc76 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -493,4 +493,52 @@ private class AggregateQuery_Tests { } } + @IsTest + static void it_will_construct_a_where_clause_based_on_custom_logic() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE (CreatedDate = :bindVar1 OR LastModifiedDate = :bindVar0) AND (Name LIKE :bindVar3 OR Parent.Name LIKE :bindVar2)'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.LastModifiedDate, SOQL.Operator.EQUALS, Date.today())) + .orFilterWhere( + new List { + new SOQL.QueryFilter(new SOQL.QueryField(Schema.Account.getSObjectType(), 'Parent.Name'), SOQL.Operator.IS_LIKE, 'Test%'), + new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.EQUALS, Date.today().addDays(-1)) + } + ) + .filterWhere(new SOQL.QueryFilter(Schema.Account.Name, SOQL.Operator.IS_LIKE, 'Smith%')) + .setWhereFilterLogic('(2 OR 1) AND (4 OR 3)'); + + // TEST + String actualQueryString = aggregateQuery.getQuery(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, actualQueryString); + } + + @IsTest + static void it_will_construct_a_having_clause_based_on_custom_logic() + { + // SETUP + String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id, MIN(CreatedDate) MIN__CreatedDate, MIN(LastModifiedDate) MIN__LastModifiedDate FROM Account HAVING (MIN(CreatedDate) = :bindVar2 OR MIN(LastModifiedDate) = :bindVar1) AND (COUNT(Id) = :bindVar3 OR COUNT(Id) = :bindVar0)'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .generateBindVariableKeys() + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.LastModifiedDate) + .addAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id) + .havingAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id, SOQL.Operator.EQUALS, 10) + .havingAggregate(SOQL.Aggregate.MIN, Schema.Account.LastModifiedDate, SOQL.Operator.EQUALS, Datetime.newInstance(2000, 1, 1)) + .havingAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate, SOQL.Operator.EQUALS, Datetime.newInstance(2021, 12, 31)) + .havingAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id, SOQL.Operator.EQUALS, 100) + .setHavingFilterLogic('(3 OR 2) AND (4 OR 1)'); + + // TEST + String actualQueryString = aggregateQuery.getQuery(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, actualQueryString); + } + } diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 13d01b6d..fec1f9d1 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -645,4 +645,28 @@ private class Query_Tests { } } + @IsTest + static void it_will_construct_a_where_clause_based_on_custom_logic() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE (CreatedDate = :bindVar1 OR LastModifiedDate = :bindVar0) AND (Name LIKE :bindVar3 OR Parent.Name LIKE :bindVar2)'; + Query accountQuery = new Query(Schema.Account.SObjectType) + .generateBindVariableKeys() + .filterWhere(new SOQL.QueryFilter(Schema.Account.LastModifiedDate, SOQL.Operator.EQUALS, Date.today())) + .orFilterWhere( + new List { + new SOQL.QueryFilter(new SOQL.QueryField(Schema.Account.getSObjectType(), 'Parent.Name'), SOQL.Operator.IS_LIKE, 'Test%'), + new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.EQUALS, Date.today().addDays(-1)) + } + ) + .filterWhere(new SOQL.QueryFilter(Schema.Account.Name, SOQL.Operator.IS_LIKE, 'Smith%')) + .setWhereFilterLogic('(2 OR 1) AND (4 OR 3)'); + + // TEST + String actualQueryString = accountQuery.getQuery(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, actualQueryString); + } + }