From ae8f20e9ddb32df54cd15742e50b850465ebf318 Mon Sep 17 00:00:00 2001 From: Piotr Gajek Date: Tue, 12 Sep 2023 11:53:56 +0200 Subject: [PATCH 01/10] GROUP BY CUBE + Exception (#82) * GROUP BY CUBE + Exception * Fix groupBy with default fields --- force-app/main/default/classes/SOQL.cls | 63 +++++++++++++++++--- force-app/main/default/classes/SOQL_Test.cls | 35 +++++++++++ website/docs/api/soql.md | 25 +++++++- website/docs/docs/design.md | 1 + 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index 5dc1a585..f74ba61e 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -81,6 +81,7 @@ public virtual inherited sharing class SOQL implements Queryable { // GROUP BY Queryable groupBy(SObjectField field); Queryable groupByRollup(SObjectField field); + Queryable groupByCube(SObjectField field); // ORDER BY Queryable orderBy(String field); Queryable orderBy(String field, String direction); @@ -403,11 +404,19 @@ public virtual inherited sharing class SOQL implements Queryable { public SOQL groupBy(SObjectField field) { builder.groupBy.with(field); + builder.fields.addAsGroupedOrAggregated(field); return this; } public SOQL groupByRollup(SObjectField field) { builder.groupBy.rollup(field); + builder.fields.addAsGroupedOrAggregated(field); + return this; + } + + public SOQL groupByCube(SObjectField field) { + builder.groupBy.cube(field); + builder.fields.addAsGroupedOrAggregated(field); return this; } @@ -685,32 +694,41 @@ public virtual inherited sharing class SOQL implements Queryable { private class QFields implements QueryClause { private Set fields = new Set(); private Set counts = new Set(); + private Set groupedOrAggregated = new Set(); public void count() { // COUNT() must be the only element in the SELECT list. + clearAllFields(); count('COUNT()'); } public void count(SObjectField field) { + addAsGroupedOrAggregated(field); count('COUNT(' + field + ')'); } public void count(SObjectField field, String alias) { + addAsGroupedOrAggregated(field); count('COUNT(' + field + ') ' + alias); } - public void count(String countSoql) { - // Clear all default fields to avoid "Field must be grouped or aggregated" - clearAllFields(); + private void count(String countSoql) { counts.add(countSoql); } public void with(SObjectField field, String alias) { - // Only aggregate expressions use field aliasing. Clear all default fields to avoid "Field must be grouped or aggregated" - clearAllFields(); + addAsGroupedOrAggregated(field + ' ' + alias); fields.add(field + ' ' + alias); } + public void addAsGroupedOrAggregated(SObjectField field) { + addAsGroupedOrAggregated(field.getDescribe().getName()); + } + + public void addAsGroupedOrAggregated(String field) { + groupedOrAggregated.add(field); + } + public void with(String stringFields) { // To avoid field duplicates in query fields.addAll(stringFields.deleteWhitespace().split(',')); @@ -745,6 +763,8 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { + clearNotGroupedOrAggregatedFields(); + if (fields.isEmpty() && counts.isEmpty()) { return 'SELECT Id'; } @@ -756,6 +776,18 @@ public virtual inherited sharing class SOQL implements Queryable { return 'SELECT ' + String.join(selectStatement, ', '); } + + public void clearNotGroupedOrAggregatedFields() { + if (groupedOrAggregated.isEmpty()) { + return; + } + // Clear not grouped or aggregated fields to avoid "Field must be grouped or aggregated" error + for (String field : fields) { + if (!groupedOrAggregated.contains(field)) { + fields.remove(field); + } + } + } } private class QSubQuery implements SubQuery { @@ -1274,17 +1306,34 @@ public virtual inherited sharing class SOQL implements Queryable { private class QGroupBy implements QueryClause { private Set groupByFields = new Set(); + private String groupByFunction = ''; public void with(SObjectField field) { + setGroupByFunction('{0}'); groupByFields.add(field.getDescribe().getName()); } public void rollup(SObjectField field) { - groupByFields.add('ROLLUP(' + field + ')'); + setGroupByFunction('ROLLUP({0})'); + groupByFields.add(field.getDescribe().getName()); + } + + public void cube(SObjectField field) { + setGroupByFunction('CUBE({0})'); + groupByFields.add(field.getDescribe().getName()); + } + + public void setGroupByFunction(String newGroupByFunction) { + if (String.isNotEmpty(groupByFunction) && groupByFunction != newGroupByFunction) { + QueryException e = new QueryException(); + e.setMessage('You cant combine GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE syntax in the same statement.'); + throw e; + } + this.groupByFunction = newGroupByFunction; } public override String toString() { - return 'GROUP BY ' + String.join(groupByFields, ', '); + return 'GROUP BY ' + String.format(groupByFunction, new List { String.join(groupByFields, ', ') }); } } diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 40d634bf..48b8d5a3 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1297,6 +1297,41 @@ private class SOQL_Test { Assert.areEqual('SELECT COUNT(Name) cnt, LeadSource FROM Lead GROUP BY ROLLUP(LeadSource)', soql); } + @IsTest + static void groupByCube() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Type) + .groupByCube(Account.Type) + .toString(); + + // Verify + Assert.areEqual('SELECT Type FROM Account GROUP BY CUBE(Type)', soql); + } + + @IsTest + static void differentGroupByFunctionsException() { + // Setup + Exception queryException = null; + + // Test + try { + String soql = SOQL.of(Account.SObjectType) + .with(Account.Type) + .groupBy(Account.Type) + .groupByCube(Account.Type) + .toString(); + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual( + 'You cant combine GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE syntax in the same statement.', + queryException.getMessage() + ); + } + @IsTest static void orderByString() { // Test diff --git a/website/docs/api/soql.md b/website/docs/api/soql.md index 78cbb19e..80b3fb72 100644 --- a/website/docs/api/soql.md +++ b/website/docs/api/soql.md @@ -62,6 +62,7 @@ The following are methods for `SOQL`. - [`groupBy(SObjectField field)`](#group-by) - [`groupByRollup(SObjectField field)`](#groupbyrollup) +- [`groupByCube(SObjectField field)`](#groupbycube) [**ORDER BY**](#order-by) @@ -726,13 +727,35 @@ FROM Lead GROUP BY ROLLUP(LeadSource) ``` ```apex -QS.of(Lead.SObjectType) +SOQL.of(Lead.SObjectType) .with(Lead.LeadSource) .count(Lead.Name, 'cnt') .groupByRollup(Lead.LeadSource) .toAggregated(); ``` +### groupByCube + +**Signature** + +```apex +SOQL groupByCube(SObjectField field) +``` + +**Example** + +```sql +SELECT Type +FROM Account +GROUP BY ROLLUP(Type) +``` +```apex +SOQL.of(Account.SObjectType) + .with(Account.Type) + .groupByCube(Account.Type) + .toAggregated(); +``` + ## ORDER BY [ORDER BY](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_orderby.htm) diff --git a/website/docs/docs/design.md b/website/docs/docs/design.md index ba47ccde..cdf991b5 100644 --- a/website/docs/docs/design.md +++ b/website/docs/docs/design.md @@ -81,6 +81,7 @@ public interface Queryable { Queryable groupBy(SObjectField field); Queryable groupByRollup(SObjectField field); + Queryable groupByCube(SObjectField field); Queryable orderBy(String field); // ASC, NULLS FIRST by default Queryable orderBy(String field, String direction); // dynamic order by, NULLS FIRST by default From e546fecd34c64e85afbbb2d90794af13a7566e2c Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Tue, 12 Sep 2023 12:09:17 +0200 Subject: [PATCH 02/10] Improve naming --- force-app/main/default/classes/SOQL.cls | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index f74ba61e..9baa9b82 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -404,19 +404,19 @@ public virtual inherited sharing class SOQL implements Queryable { public SOQL groupBy(SObjectField field) { builder.groupBy.with(field); - builder.fields.addAsGroupedOrAggregated(field); + builder.fields.withAggregatedField(field); return this; } public SOQL groupByRollup(SObjectField field) { builder.groupBy.rollup(field); - builder.fields.addAsGroupedOrAggregated(field); + builder.fields.withAggregatedField(field); return this; } public SOQL groupByCube(SObjectField field) { builder.groupBy.cube(field); - builder.fields.addAsGroupedOrAggregated(field); + builder.fields.withAggregatedField(field); return this; } @@ -694,7 +694,7 @@ public virtual inherited sharing class SOQL implements Queryable { private class QFields implements QueryClause { private Set fields = new Set(); private Set counts = new Set(); - private Set groupedOrAggregated = new Set(); + private Set aggregatedFields = new Set(); public void count() { // COUNT() must be the only element in the SELECT list. @@ -703,12 +703,12 @@ public virtual inherited sharing class SOQL implements Queryable { } public void count(SObjectField field) { - addAsGroupedOrAggregated(field); + withAggregatedField(field); count('COUNT(' + field + ')'); } public void count(SObjectField field, String alias) { - addAsGroupedOrAggregated(field); + withAggregatedField(field); count('COUNT(' + field + ') ' + alias); } @@ -717,16 +717,16 @@ public virtual inherited sharing class SOQL implements Queryable { } public void with(SObjectField field, String alias) { - addAsGroupedOrAggregated(field + ' ' + alias); + withAggregatedField(field + ' ' + alias); fields.add(field + ' ' + alias); } - public void addAsGroupedOrAggregated(SObjectField field) { - addAsGroupedOrAggregated(field.getDescribe().getName()); + public void withAggregatedField(SObjectField field) { + withAggregatedField(field.getDescribe().getName()); } - public void addAsGroupedOrAggregated(String field) { - groupedOrAggregated.add(field); + public void withAggregatedField(String field) { + aggregatedFields.add(field); } public void with(String stringFields) { @@ -763,7 +763,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - clearNotGroupedOrAggregatedFields(); + removeNotAggregatedFieldsFromAggregateSoql(); if (fields.isEmpty() && counts.isEmpty()) { return 'SELECT Id'; @@ -777,13 +777,13 @@ public virtual inherited sharing class SOQL implements Queryable { return 'SELECT ' + String.join(selectStatement, ', '); } - public void clearNotGroupedOrAggregatedFields() { - if (groupedOrAggregated.isEmpty()) { + public void removeNotAggregatedFieldsFromAggregateSoql() { + if (aggregatedFields.isEmpty()) { return; } // Clear not grouped or aggregated fields to avoid "Field must be grouped or aggregated" error for (String field : fields) { - if (!groupedOrAggregated.contains(field)) { + if (!aggregatedFields.contains(field)) { fields.remove(field); } } From 88029f85df0c67af545fc0fde183d963cbc8c33f Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Thu, 14 Sep 2023 19:51:20 +0200 Subject: [PATCH 03/10] formatting --- force-app/main/default/classes/SOQL.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index 9baa9b82..d45a6400 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -1333,7 +1333,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - return 'GROUP BY ' + String.format(groupByFunction, new List { String.join(groupByFields, ', ') }); + return 'GROUP BY ' + String.format(groupByFunction, new List{ String.join(groupByFields, ', ') }); } } From 031b69d0508e51fa764869835d1ca2ae1143ab04 Mon Sep 17 00:00:00 2001 From: Piotr Gajek Date: Thu, 14 Sep 2023 20:38:35 +0200 Subject: [PATCH 04/10] grouping (#84) * grouping * refactoring --- force-app/main/default/classes/SOQL.cls | 40 +++++++++++--------- force-app/main/default/classes/SOQL_Test.cls | 16 ++++++++ website/docs/api/soql.md | 34 +++++++++++++++++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index d45a6400..ab8594b1 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -65,6 +65,8 @@ public virtual inherited sharing class SOQL implements Queryable { Queryable count(); Queryable count(SObjectField field); Queryable count(SObjectField field, String alias); + // GROUPING + Queryable grouping(SObjectField field, String alias); // USING SCOPE Queryable delegatedScope(); Queryable mineScope(); @@ -347,6 +349,11 @@ public virtual inherited sharing class SOQL implements Queryable { return this; } + public SOQL grouping(SObjectField field, String alias) { + builder.fields.grouping(field, alias); + return this; + } + public SOQL delegatedScope() { builder.scope.delegated(); return this; @@ -539,7 +546,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public Integer toInteger() { - if (builder.fields.areCountsEmpty()) { + if (!builder.fields.hasCount()) { count(); } return executor.toInteger(builder.toString(), binder.getBindingMap()); @@ -693,27 +700,31 @@ public virtual inherited sharing class SOQL implements Queryable { private class QFields implements QueryClause { private Set fields = new Set(); - private Set counts = new Set(); private Set aggregatedFields = new Set(); + private Boolean hasCount = false; public void count() { - // COUNT() must be the only element in the SELECT list. - clearAllFields(); count('COUNT()'); } public void count(SObjectField field) { - withAggregatedField(field); count('COUNT(' + field + ')'); } public void count(SObjectField field, String alias) { - withAggregatedField(field); count('COUNT(' + field + ') ' + alias); + fields.add('COUNT(' + field + ') ' + alias); } - private void count(String countSoql) { - counts.add(countSoql); + private void count(String count) { + this.hasCount = true; + withAggregatedField(count); + fields.add(count); + } + + public void grouping(SObjectField field, String alias) { + withAggregatedField('GROUPING(' + field + ') ' + alias); + fields.add('GROUPING(' + field + ') ' + alias); } public void with(SObjectField field, String alias) { @@ -758,23 +769,18 @@ public virtual inherited sharing class SOQL implements Queryable { fields.clear(); } - public Boolean areCountsEmpty() { - return counts.isEmpty(); + public Boolean hasCount() { + return hasCount; } public override String toString() { removeNotAggregatedFieldsFromAggregateSoql(); - if (fields.isEmpty() && counts.isEmpty()) { + if (fields.isEmpty()) { return 'SELECT Id'; } - List selectStatement = new List(); - - selectStatement.addAll(counts); - selectStatement.addAll(fields); - - return 'SELECT ' + String.join(selectStatement, ', '); + return 'SELECT ' + String.join(fields, ', '); } public void removeNotAggregatedFieldsFromAggregateSoql() { diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 48b8d5a3..41063914 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -62,6 +62,22 @@ private class SOQL_Test { Assert.areEqual('SELECT COUNT(Name) names FROM Account', soql); } + @IsTest + static void grouping() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource, Lead.Rating) + .grouping(Lead.LeadSource, 'grpLS') + .grouping(Lead.Rating, 'grpRating') + .count(Lead.Name, 'cnt') + .groupByRollup(Lead.LeadSource) + .groupByRollup(Lead.Rating) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, Rating, GROUPING(LeadSource) grpLS, GROUPING(Rating) grpRating, COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(LeadSource, Rating)', soql); + } + @IsTest static void withField() { // Test diff --git a/website/docs/api/soql.md b/website/docs/api/soql.md index 80b3fb72..de38c5b9 100644 --- a/website/docs/api/soql.md +++ b/website/docs/api/soql.md @@ -38,6 +38,10 @@ The following are methods for `SOQL`. - [`count(SObjectField field)`](#count-field) - [`count(SObjectField field, String alias)`](#count-with-alias) +[**GROUPING**](#grouping) + +- [`grouping(SObjectField field, String alias)`](#grouping) + [**SUBQUERY**](#sub-query) - [`with(SubQuery subQuery)`](#with-subquery) @@ -437,6 +441,36 @@ SOQL.of(Account.SObjectType) .toAggregated(); ``` +## GROUPING + +### grouping + +**Signature** + +```apex +grouping(SObjectField field, String alias) +``` + +**Example** + +```sql +SELECT LeadSource, Rating, + GROUPING(LeadSource) grpLS, GROUPING(Rating) grpRating, + COUNT(Name) cnt +FROM Lead +GROUP BY ROLLUP(LeadSource, Rating) +``` +```apex +SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource, Lead.Rating) + .grouping(Lead.LeadSource, 'grpLS') + .grouping(Lead.Rating, 'grpRating') + .count(Lead.Name, 'cnt') + .groupByRollup(Lead.LeadSource) + .groupByRollup(Lead.Rating) + .toAggregated(); +``` + ## USING SCOPE [USING SCOPE](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_using_scope.htm) From 020a5182091a69758eac7122525421d9acdbf228 Mon Sep 17 00:00:00 2001 From: Piotr Gajek Date: Sun, 24 Sep 2023 11:20:11 +0200 Subject: [PATCH 05/10] toMap with custom key (#85) * toMap with custom key * toMap with custom key and type * toAggregatedMap * refactoring * toMap and toAggregatedMap * refactoring * Fix toMap cast * documentation update --- force-app/main/default/classes/SOQL.cls | 39 +++++++++++++++- force-app/main/default/classes/SOQL_Test.cls | 47 +++++++++++++++++++- website/docs/api/soql.md | 30 +++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index ab8594b1..d4b43ed4 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -126,6 +126,8 @@ public virtual inherited sharing class SOQL implements Queryable { List toList(); List toAggregated(); Map toMap(); + Map toMap(SObjectField keyField); + Map> toAggregatedMap(SObjectField keyField); Database.QueryLocator toQueryLocator(); } @@ -246,6 +248,8 @@ public virtual inherited sharing class SOQL implements Queryable { private QueryBuilder builder; private Executor executor; + private String ofObject; + public static SOQL of(SObjectType ofObject) { return new SOQL(ofObject); } @@ -259,6 +263,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public SOQL(String ofObject) { + this.ofObject = ofObject; binder = new Binder(); executor = new Executor(); builder = new QueryBuilder(ofObject); @@ -565,7 +570,39 @@ public virtual inherited sharing class SOQL implements Queryable { } public Map toMap() { - return new Map(toList()); + Map idToSObject = (Map) Type.forName('Map').newInstance(); + idToSObject.putAll(toList()); + return idToSObject; + } + + public Map toMap(SObjectField keyField) { + with(keyField); + + Map cutomKeyToRecord = (Map) Type.forName('Map').newInstance(); + + for (SObject record : toList()) { + cutomKeyToRecord.put(record.get(keyField) + '', record); + } + + return cutomKeyToRecord; + } + + public Map> toAggregatedMap(SObjectField keyField) { + with(keyField); + + Map> cutomKeyToRecords = (Map>) Type.forName('Map>').newInstance(); + + for (SObject record : toList()) { + String customKey = record.get(keyField) + ''; + + if (!cutomKeyToRecords.containsKey(customKey)) { + cutomKeyToRecords.put(customKey, new List()); + } + + cutomKeyToRecords.get(customKey).add(record); + } + + return cutomKeyToRecords; } public Database.QueryLocator toQueryLocator() { diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 41063914..45f8b5f3 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1882,15 +1882,60 @@ private class SOQL_Test { List accounts = insertAccounts(); // Test - Map result = SOQL.of(Account.SObjectType).toMap(); + Map result = (Map) SOQL.of(Account.SObjectType).toMap(); // Verify Assert.areEqual(accounts.size(), result.size()); + for (Account acc : accounts) { Assert.isNotNull(result.get(acc.Id)); } } + @IsTest + static void toMapWithCustomKey() { + // Setup + List accounts = insertAccounts(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).toMap(Account.Name); + + // Verify + Assert.areEqual(accounts.size(), result.size()); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Name)); + } + } + + @IsTest + static void toAggregatedMapWithCustomKey() { + // Setup + List accounts = insertAccounts(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Name); + + // Verify + Assert.areEqual(accounts.size(), result.size()); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Name)); + } + } + + @IsTest + static void toAggregatedMapWithEmptyCustomKey() { + // Setup + List accounts = insertAccounts(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry); + + // Verify + Assert.areEqual(1, result.size()); // grouped by empty Industry + } + @IsTest static void toQueryLocator() { // Test diff --git a/website/docs/api/soql.md b/website/docs/api/soql.md index de38c5b9..e1541c33 100644 --- a/website/docs/api/soql.md +++ b/website/docs/api/soql.md @@ -128,6 +128,8 @@ The following are methods for `SOQL`. - [`toList()`](#tolist) - [`toAggregated()`](#toaggregated) - [`toMap()`](#tomap) +- [`toMap(SObjectField keyField)`](#tomap-with-custom-key) +- [`toAggregatedMap(SObjectField keyField)`](#toaggregatedmap) - [`toQueryLocator()`](#toquerylocator) ## INIT @@ -1480,6 +1482,34 @@ Map toMap() SOQL.of(Account.SObjectType).toMap(); ``` +### toMap with custom key + +**Signature** + +```apex +Map toMap(SObjectField keyField) +``` + +**Example** + +```apex +Map nameToAccount = (Map) SOQL.of(Account.SObjectType).toMap(Account.Name); +``` + +### toAggregatedMap + +**Signature** + +```apex +Map> toAggregatedMap(SObjectField keyField) +``` + +**Example** + +```apex +Map industryToAccounts = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry); +``` + ### toQueryLocator **Signature** From c1d68433216e631bac091a52eb6777f11805a5f4 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Sun, 24 Sep 2023 18:25:13 +0200 Subject: [PATCH 06/10] error message rename + formatting --- force-app/main/default/classes/SOQL.cls | 6 +++--- force-app/main/default/classes/SOQL_Test.cls | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index d4b43ed4..cb297321 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -1312,10 +1312,10 @@ public virtual inherited sharing class SOQL implements Queryable { public override String toString() { if (skipBinding) { - return String.format(wrapper, new List { field + ' ' + comperator + ' ' + value }); + return String.format(wrapper, new List{ field + ' ' + comperator + ' ' + value }); } - return String.format(wrapper, new List { field + ' ' + comperator + ' :' + binder.bind(value) }); + return String.format(wrapper, new List{ field + ' ' + comperator + ' :' + binder.bind(value) }); } } @@ -1369,7 +1369,7 @@ public virtual inherited sharing class SOQL implements Queryable { public void setGroupByFunction(String newGroupByFunction) { if (String.isNotEmpty(groupByFunction) && groupByFunction != newGroupByFunction) { QueryException e = new QueryException(); - e.setMessage('You cant combine GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE syntax in the same statement.'); + e.setMessage('You cant use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.'); throw e; } this.groupByFunction = newGroupByFunction; diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 45f8b5f3..4f25d740 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1343,7 +1343,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'You cant combine GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE syntax in the same statement.', + 'You cant use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.', queryException.getMessage() ); } From 68ebda59a8fc868ffcb33b18f2f07691dd7c0244 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Tue, 26 Sep 2023 16:51:14 +0200 Subject: [PATCH 07/10] groupByWithDefaultFields --- force-app/main/default/classes/SOQL.cls | 1 + force-app/main/default/classes/SOQL_Test.cls | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index cb297321..0044e1e3 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -547,6 +547,7 @@ public virtual inherited sharing class SOQL implements Queryable { public Set toValuesOf(SObjectField fieldToExtract) { // https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result + builder.fields.clearAllFields(); // other fields not needed return new Map(with(fieldToExtract, 'Id').groupBy(fieldToExtract).toAggregated()).keySet(); } diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 4f25d740..eff70234 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1348,6 +1348,19 @@ private class SOQL_Test { ); } + @IsTest + static void groupByWithDefaultFields() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.FirstName, Lead.LastName, Lead.Email) + .with(Lead.LeadSource) + .groupBy(Lead.LeadSource) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource', soql); + } + @IsTest static void orderByString() { // Test From d5ac02a930aafd7b9f86bfd365f7115ba8589e5e Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Tue, 26 Sep 2023 16:55:29 +0200 Subject: [PATCH 08/10] refactoring --- force-app/main/default/classes/SOQL.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index 0044e1e3..64bb6d3e 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -582,7 +582,7 @@ public virtual inherited sharing class SOQL implements Queryable { Map cutomKeyToRecord = (Map) Type.forName('Map').newInstance(); for (SObject record : toList()) { - cutomKeyToRecord.put(record.get(keyField) + '', record); + cutomKeyToRecord.put(String.valueOf(record.get(keyField)), record); } return cutomKeyToRecord; @@ -594,7 +594,7 @@ public virtual inherited sharing class SOQL implements Queryable { Map> cutomKeyToRecords = (Map>) Type.forName('Map>').newInstance(); for (SObject record : toList()) { - String customKey = record.get(keyField) + ''; + String customKey = String.valueOf(record.get(keyField)); if (!cutomKeyToRecords.containsKey(customKey)) { cutomKeyToRecords.put(customKey, new List()); From fd7b31ef7a658b5f1faa37ee1b297e4dc01d12be Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Mon, 2 Oct 2023 10:03:14 +0200 Subject: [PATCH 09/10] Refactoring --- force-app/main/default/classes/SOQL_Test.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index eff70234..1f4e8106 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1940,7 +1940,7 @@ private class SOQL_Test { @IsTest static void toAggregatedMapWithEmptyCustomKey() { // Setup - List accounts = insertAccounts(); + insertAccounts(); // Test Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry); From 6387c51bde84f2257e04d79f3e3e2e79606543fc Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Mon, 9 Oct 2023 17:26:33 +0200 Subject: [PATCH 10/10] toMap methods --- force-app/main/default/classes/SOQL.cls | 39 ++++++++++++++++++++ force-app/main/default/classes/SOQL_Test.cls | 34 ++++++++++++++++- website/docs/api/soql.md | 33 ++++++++++++++++- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/force-app/main/default/classes/SOQL.cls b/force-app/main/default/classes/SOQL.cls index 64bb6d3e..4e86e88a 100644 --- a/force-app/main/default/classes/SOQL.cls +++ b/force-app/main/default/classes/SOQL.cls @@ -127,7 +127,9 @@ public virtual inherited sharing class SOQL implements Queryable { List toAggregated(); Map toMap(); Map toMap(SObjectField keyField); + Map toMap(SObjectField keyField, SObjectField valueField); Map> toAggregatedMap(SObjectField keyField); + Map> toAggregatedMap(SObjectField keyField, SObjectField valueField); Database.QueryLocator toQueryLocator(); } @@ -588,6 +590,23 @@ public virtual inherited sharing class SOQL implements Queryable { return cutomKeyToRecord; } + public Map toMap(SObjectField keyField, SObjectField valueField) { + builder.fields.clearAllFields(); // other fields not needed + + with(keyField, valueField); + + Map cutomKeyToCustomValue = new Map(); + + for (SObject record : toList()) { + cutomKeyToCustomValue.put( + String.valueOf(record.get(keyField)), + String.valueOf(record.get(valueField)) + ); + } + + return cutomKeyToCustomValue; + } + public Map> toAggregatedMap(SObjectField keyField) { with(keyField); @@ -606,6 +625,26 @@ public virtual inherited sharing class SOQL implements Queryable { return cutomKeyToRecords; } + public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { + builder.fields.clearAllFields(); // other fields not needed + + with(keyField, valueField); + + Map> customKeyToValues = new Map>(); + + for (SObject record : toList()) { + String customKey = String.valueOf(record.get(keyField)); + + if (!customKeyToValues.containsKey(customKey)) { + customKeyToValues.put(customKey, new List()); + } + + customKeyToValues.get(customKey).add(String.valueOf(record.get(valueField))); + } + + return customKeyToValues; + } + public Database.QueryLocator toQueryLocator() { return executor.toQueryLocator(builder.toString(), binder.getBindingMap()); } diff --git a/force-app/main/default/classes/SOQL_Test.cls b/force-app/main/default/classes/SOQL_Test.cls index 1f4e8106..677f90f9 100644 --- a/force-app/main/default/classes/SOQL_Test.cls +++ b/force-app/main/default/classes/SOQL_Test.cls @@ -1921,6 +1921,22 @@ private class SOQL_Test { } } + @IsTest + static void toMapWithCustomKeyAndCustomValue() { + // Setup + List accounts = insertAccounts(); + + // Test + Map result = SOQL.of(Account.SObjectType).toMap(Account.Name, Account.Id); + + // Verify + Assert.areEqual(accounts.size(), result.size()); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Name)); + } + } + @IsTest static void toAggregatedMapWithCustomKey() { // Setup @@ -1933,7 +1949,7 @@ private class SOQL_Test { Assert.areEqual(accounts.size(), result.size()); for (Account acc : accounts) { - Assert.isNotNull(result.get(acc.Name)); + Assert.isFalse(result.get(acc.Name).isEmpty()); } } @@ -1949,6 +1965,22 @@ private class SOQL_Test { Assert.areEqual(1, result.size()); // grouped by empty Industry } + @IsTest + static void toAggregatedMapWithCustomKeyAndCustomValue() { + // Setup + List accounts = insertAccounts(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Name, Account.Id); + + // Verify + Assert.areEqual(accounts.size(), result.size()); + + for (Account acc : accounts) { + Assert.isFalse(result.get(acc.Name).isEmpty()); + } + } + @IsTest static void toQueryLocator() { // Test diff --git a/website/docs/api/soql.md b/website/docs/api/soql.md index e1541c33..f3fd609e 100644 --- a/website/docs/api/soql.md +++ b/website/docs/api/soql.md @@ -129,7 +129,9 @@ The following are methods for `SOQL`. - [`toAggregated()`](#toaggregated) - [`toMap()`](#tomap) - [`toMap(SObjectField keyField)`](#tomap-with-custom-key) +- [`toMap(SObjectField keyField, SObjectField valueField)`](#tomap-with-custom-key-and-value) - [`toAggregatedMap(SObjectField keyField)`](#toaggregatedmap) +- [`toAggregatedMap(SObjectField keyField, SObjectField valueField)`](#toaggregatedmap-with-custom-value) - [`toQueryLocator()`](#toquerylocator) ## INIT @@ -1479,7 +1481,7 @@ Map toMap() **Example** ```apex -SOQL.of(Account.SObjectType).toMap(); +Map idToAccount = (Map) SOQL.of(Account.SObjectType).toMap(); ``` ### toMap with custom key @@ -1496,6 +1498,21 @@ Map toMap(SObjectField keyField) Map nameToAccount = (Map) SOQL.of(Account.SObjectType).toMap(Account.Name); ``` +### toMap with custom key and value + +**Signature** + +```apex +Map toMap(SObjectField keyField, , SObjectField valueField) +``` + +**Example** + +```apex +Map nameToAccount = SOQL.of(Account.SObjectType).toMap(Account.Name, Account.Industry); +``` + + ### toAggregatedMap **Signature** @@ -1510,6 +1527,20 @@ Map> toAggregatedMap(SObjectField keyField) Map industryToAccounts = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry); ``` +### toAggregatedMap with custom value + +**Signature** + +```apex +Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) +``` + +**Example** + +```apex +Map> industryToAccounts = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry, Account.Name); +``` + ### toQueryLocator **Signature**