Skip to content

Releases: beyond-the-cloud-dev/soql-lib

v4.2.0

03 Feb 18:19
7e0c67f

Choose a tag to compare

03-February-2025

Scope

SOQL

  • Queryable toLabel(SObjectField field, String alias);
  • Queryable toLabel(String field, String alias);
  • Queryable sort(String direction);
  • Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);
  • Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);
  • Issued queries counter
  • Code refactoring

SOQL.SubQuery

  • SubQuery with(String fields);
  • SubQuery orderBy(String field);
  • SubQuery sort(String direction);

SOQL.FilterGroup

  • FilterGroup add(List<Filter> filters)
  • FilterGroup add(List<String> dynamicConditions)

SOQLCache

  • removeFromCache(List<SObject> records)

SOQL

toLabel(SObjectField field, String alias)

Enhancement: #147

You can specify the toLabel(field) alias to avoid the error: “duplicate field selected: field”.

Signature

Queryable toLabel(SObjectField field, String alias);
Queryable toLabel(String field, String alias);

Example

SELECT Status, toLabel(Status) leadStatus FROM Lead
SOQL.of(Lead.SObjectType)
    .with(Lead.Status)
    .toLabel(Lead.Status, 'leadStatus')
    .toList();
SELECT Company, toLabel(Recordtype.Name) recordTypeName FROM Lead
SOQL.of(Lead.SObjectType)
    .with(Lead.Company)
    .toLabel('Recordtype.Name', 'recordTypeName')
    .toList();

Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField)

Enhancement: #144

Signature

Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);

Example

Map<String, Account> parentCreatedByEmailToAccount = new Map<String, Account>();

for (Account account : [SELECT Id, Parent.CreatedBy.Email FROM Account]) {
   parentCreatedByEmailToAccount.put(account.Parent.CreatedBy.Email, account);
}

Map<String, Account> parentCreatedByEmailToAccount = 
   (Map<String, Account>) SOQL.of(Account.SObjectType).toMap('Parent.CreatedBy', User.Email);

Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);

Enhancement: #144

Signature

Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);

Example

Map<String, List<Account>> parentCreatedByEmailToAccount = new Map<String, List<Account>>();

for (Account account : [SELECT Id, Parent.CreatedBy.Email FROM Account]) {
   if (!parentCreatedByEmailToAccount.containsKey(account.Parent.CreatedBy.Email)) {
      parentCreatedByEmailToAccount.put(account.Parent.CreatedBy.Email, new List<Account>());
   }
   
   parentCreatedByEmailToAccount.get(account.Parent.CreatedBy.Email).add(account);
}

Map<String, List<Account>> parentCreatedByEmailToAccounts = 
   (Map<String, List<Account>>) SOQL.of(Account.SObjectType).toAggregatedMap('Parent.CreatedBy', User.Email);

Issued queries counter

Mocked queries are now counted by SOQL Lib, and if the number of issued queries exceeds 100, a new QueryException('Too many SOQL queries.') will be thrown. This allows you to catch the issue in your unit tests.

SOQL.SubQuery

Thanks, @salberski for the collaboration!

SubQuery with(String fields);

Signature

SubQuery with(String fields)

Example

SELECT Id, (
    SELECT Id, Name, Phone, RecordTypeId, Title, Salutation
    FROM Contacts
) FROM Account
SOQL.of(Account.SObjectType)
    .with(SOQL.SubQuery.of('Contacts')
        .with('Id, Name, Phone, RecordTypeId, Title, Salutation')
    )
    .toList();

SubQuery orderBy(String field);

Signature

SubQuery orderBy(String field)

Example

SELECT Id, (
    SELECT Id
    FROM Contacts
    ORDER BY Name
) FROM Account
SOQL.of(Account.SObjectType)
    .with(SOQL.SubQuery.of('Contacts')
        .orderBy('Name')
    )
    .toList();

SubQuery sort(String direction);

Signature

SubQuery sort(String direction)

Example

SELECT Id, (
    SELECT Id
    FROM Contacts
    ORDER BY Name DESC
) FROM Account
SOQL.of(Account.SObjectType)
    .with(SOQL.SubQuery.of('Contacts')
        .orderBy('Name')
        .sort('DESC')
    )
    .toList();

SOQL.FilterGroup

Thanks, @salberski for the collaboration!

Signature

FilterGroup add(List<Filter> filters);
FilterGroup add(List<String> dynamicConditions);

Example

// SELECT Id FROM Account WHERE (Name = 'Test' AND BillingCity = 'Krakow')

SOQL.of(Account.SObjectType)
    .whereAre(SOQL.FilterGroup
        .add(new List<SOQL.Filter> {
            SOQL.Filter.with(Account.Name).equal('Test'),
            SOQL.Filter.with(Account.BillingCity).equal('Krakow')
        })
    ).toList();
// SELECT Id FROM Account WHERE (Name = 'Test' AND BillingCity = 'Krakow')

SOQL.of(Account.SObjectType)
    .whereAre(SOQL.FilterGroup
        .add(new List<String> {
            'Name = \'Test\'',
            'BillingCity = \'Krakow\''
        })
    ).toList();

SOQLCache

removeFromCache(List<SObject> records)

The removeFromCache method clears records from the cache, triggering an automatic refresh the next time the query is executed.

Thanks, @patrykacc for the collaboration!

Signature

Cacheable removeFromCache(List<SObject> records);

Example

trigger SomeObjectTrigger on SomeObject (after update, after delete) {
    SOQLCache.removeFromCache(Trigger.new);
}

New Contributors

v4.0.0

07 Jan 09:00
e0f4531

Choose a tag to compare

07-January-2025

SOQL Caching

API: https://soql.beyondthecloud.dev/api/cached-soql/soql-cache
Examples: https://soql.beyondthecloud.dev/examples/cached-soql/select
Cached Selector: https://soql.beyondthecloud.dev/docs/build-cached-selector

SOQL caching is more complex than it seems. From our perspective, it is much more important to make the code predictable, bug-proof, and intuitive rather than adding numerous features that could confuse developers.

To achieve our goals, we made the following assumptions:

Limited Interface

The Cached SOQL Selector should have a limited interface that represents only the operations that can be performed on cached records. Methods that exist in the SOQL Library are not relevant when retrieving cached records. For example, it is impossible to check sharing rules for cached records, so methods like withSharing() or withoutSharing() are not applicable.

For instance, the following query:

SOQL.of(Profile.SObjectType)
    .with(Profile.Id, Profile.Name, Profile.UserType)
    .myTerritoryScope()
    .forView()
    .withSharing()
    .toList();

doesn’t make sense when working with cached records. We cannot apply scope, forView, or withSharing to cached records.

From the very beginning, we envisioned the cache SOQL as a standalone module, not logic tightly coupled with the SOQL Library. Each company could potentially have its own caching system. Therefore, we decided to introduce a new module called SOQLCache.cls.

public interface Cacheable {
    // CONFIG
    Cacheable cacheInApexTransaction();
    Cacheable cacheInOrgCache();
    Cacheable cacheInSessionCache();
    // SELECT
    Cacheable with(SObjectField field);
    Cacheable with(SObjectField field1, SObjectField field2);
    Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3);
    Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
    Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
    Cacheable with(List<SObjectField> fields);
    Cacheable with(String fields);
    // WHERE
    Cacheable whereEqual(SObjectField field, Object value);
    Cacheable whereEqual(String field, Object value);
    // FIELD-LEVEL SECURITY
    Cacheable stripInaccessible();
    Cacheable stripInaccessible(AccessType accessType);
    // MOCKING
    Cacheable mockId(String queryIdentifier);
    // DEBUGGING
    Cacheable preview();
    // PREDEFINED
    Cacheable byId(SObject record);
    Cacheable byId(Id recordId);
    // RESULT
    Id toId();
    Boolean doExist();
    SObject toObject();
    Object toValueOf(SObjectField fieldToExtract);
}

Cached Selectors

To avoid interfering with existing selectors and to keep the logic clean and simple, the best approach we identified is using Cached Selectors.

This approach provides immediate feedback to developers reading the code, indicating that the following query is cached. For instance:
SOQL_Profile.query().byName('System Administrator').toObject(); retrieves records from the database, whereas
SOQL_ProfileCache.query().byName('System Administrator').toObject(); retrieves records from the cache.

This clear distinction gives architects full control over which records should be cached and which should not, addressing potential issues that might arise with a .saveInCache() method.

Additionally, Cached Selectors look slightly different and include cache-specific methods:

public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
    public static SOQL_ProfileCache query() {
        return new SOQL_ProfileCache();
    }

    private SOQL_ProfileCache() {
        super(Profile.SObjectType);
        cacheInOrgCache();
        with(Profile.Id, Profile.Name, Profile.UserType);
    }

    public override SOQL.Queryable initialQuery() {
        return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
    }

    public SOQL_ProfileCache byName(String profileName) {
        whereEqual(Profile.Name, profileName);
        return thisl
    }
}

Developers can specify the type of storage using the cacheIn...() method. The available storage types are: Apex Transaction (cacheInApexTransaction()), Org Cache (cacheInOrgCache()), or Session Cache (cacheInSessionCache()).

Additionally, the with(...) method provides information about which fields are cached in the specified storage.

Last but not least, there is the initialQuery() method, which allows records to be prepopulated in the storage. For example, SOQL_ProfileCache will prepopulate all Profiles in the storage, so calling byName(String profileName) will not execute a query but will retrieve the records directly from the cache.

Cache Manager

Inspired by the Iteratively Building a Flexible Caching System for Apex post, we decided to build our own Cache Manager. This can be used not only for the SOQL Lib but also for caching other crucial items in your Salesforce org.

CacheManager.cls is required for using cached selectors and it's part of this repository. Already have your own cache manager?

No worries! You can use it by simply replacing SOQLCache.CacheStorageProxy with your own cache manager.

Records are stored as a List

Records are stored in the cache as a List, with the cache key being the SObjectType. This approach helps avoid record duplication, which is crucial given the limited storage capacity.

Key: Profile

Records:

Id Name UserType
00e3V000000DhteQAC Standard Guest Guest
00e3V000000DhtfQAC Community Profile Guest
00e3V000000NmefQAC Standard User Standard
00e3V000000Nme3QAC System Administrator Standard
00e3V000000NmeAQAS Standard Platform User Standard
00e3V000000NmeHQAS Customer Community Plus User PowerCustomerSuccess
00e3V000000NmeNQAS Customer Community Plus Login User PowerCustomerSuccess

Why not just cache by SOQL String?

SELECT Id, Name FROM Profile => List of Profiles.

There are several reasons why caching by SOQL string is not the best approach:

  1. Identical Queries Treated Differently

Even the same SOQL query can be treated as different due to variations in the string format.
For instance, SELECT Id, Name FROM Profile and SELECT Name, Id FROM Profile are identical in intent but differ in query string format. This issue becomes even more complex when dealing with WHERE conditions.

  1. Duplicate Entries and Inefficient Record Retrieval

A simple query like SELECT Id, Name FROM Profile is a subset of a more complex query like SELECT Id, Name, UserType FROM Profile.
If the results of SELECT Id, Name, UserType FROM Profile are already cached, the simpler query should ideally retrieve the relevant data from the cache. However, if the cache uses the query string as the key, this won’t work, leading to duplication and inefficiency.

  1. Cache Storage Limitations

Cache storage is limited. If different variations of queries continually add new entries to the cache, it can quickly fill up with redundant or overlapping records, reducing overall effectiveness.

  1. Condition binding

The SOQL library uses variable binding (queryWithBinds). Using the query as a key becomes highly complex when there are many binding variables. Developers aim to use cached records to boost performance, but relying on the query as a key can be performance-intensive and counterproductive.

Cached query required single condition

A query requires a single condition, and that condition must filter by a unique field.

To ensure that cached records are aligned with the database, a single condition is required.
A query without a condition cannot guarantee that the number of records in the cache matches the database.

For example, let’s assume a developer makes the query: SELECT Id, Name FROM Profile. Cached records will be returned, but they may differ from the records in the database.

The filter field should be unique. Consistency issues can arise when the field is not unique. For instance, the query:
SELECT Id, Name FROM Profile WHERE UserType = 'Standard'
may return some records, but the number of records in the cache may differ from those in the database.

Using a unique field ensures that if a record is not found in the cache, the SOQL library can look it up in the database.

Example

Cached Records:

Id Name UserType
00e3V000000Nme3QAC System Administrator Standard
00e3V000000NmeAQAS Standard Platform User Standard
00e3V000000NmeHQAS Customer Community Plus User PowerCustomerSuccess

Database Records:

Id Name UserType
00e3V000000Nme3QAC System Administrator Standard
00e3V000000NmeAQAS Standard Platform User Standard
00e3V000000NmeZQAS Read Only Standard
00e3V000000NmeYQAS Solution Manager Standard
00e3V000000NmeHQAS Customer Community Plus User PowerCustomerSuccess

Let’s...

Read more

v3.4.0

04 Jan 14:07
9c13430

Choose a tag to compare

04-January-2025

New

  • toId()
  • API 62.0 update

toId()

A new predefined method that simplifies your code.

Signature

Id toId();

Example

new User (
    // ...
    ProfileId = SOQL_Profile.query().byName('System Administrator').toId()
);

More details:

API 62.0 Update

Resolved the issue System.FinalException: Cannot modify a collection while it is being iterated.
The SOQL library now supports API version 62.0.

v3.3.0

06 Oct 16:07
555311b

Choose a tag to compare

06-October-2024

New

  • HAVING support

HAVING support

HAVING is an optional clause that can be used in a SOQL query to filter results that aggregate functions return.

You can use a HAVING clause with a GROUP BY clause to filter the results returned by aggregate functions, such as SUM(). The HAVING clause is similar to a WHERE clause. The difference is that you can include aggregate functions in a HAVING clause, but not in a WHERE clause.

Signature

Queryable have(HavingFilterGroup filterGroup);
Queryable have(HavingFilter filter);
Queryable have(String dynamicCondition);
Queryable havingConditionLogic(String order);
Queryable anyHavingConditionMatching();

Example

SOQL.of(Lead.SObjectType)
    .with(Lead.LeadSource)
    .count(Lead.Name)
    .groupBy(Lead.City)
    .groupBy(Lead.LeadSource)
    .have(SOQL.HavingFilterGroup
        .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100))
        .add(SOQL.HavingFilter.with(Lead.City).startsWith('San'))
    )
    .toAggregated();
SOQL.of(Lead.SObjectType)
    .sum(Lead.AnnualRevenue)
    .groupBy(Lead.LeadSource)
    .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(1000000))
    .toAggregated();

More details:

release/v3.2.4

01 Jul 06:57
32645c1

Choose a tag to compare

01-July-2024

New

  • userMode()
  • API Update 61.0

userMode

Execution mode in which the object permissions, field-level security, and sharing rules of the current user are enforced.

By default, all queries are executed WITH USER_MODE. However, developers can override this. For more details, check Field-Level Security and Sharing Rules.

The userMode() method can be useful to override the systemMode() provided by the selector as in the example below.

Signature

Queryable userMode()

Example

public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
    public static final String MOCK_ID = 'SOQL_Account';

    public static SOQL_Account query() {
        return new SOQL_Account();
    }

    private SOQL_Account() {
        super(Account.SObjectType);
        // default settings
        with(Account.Id, Account.Name, Account.Type);
        systemMode();
        withoutSharing();
        mockId(MOCK_ID);
    }
}
SOQL_Account.query()
    .userMode() // systemMode() from SOQL_Account.query will be overridden by userMode() method
    .toList();

v3.2.3

25 Apr 07:00
ac25ba2

Choose a tag to compare

25-April-2024

New

  • groupBy(String relationshipName, SObjectField field)
  • groupByRollup(String relationshipName, SObjectField field)
  • groupByCube(String relationshipName, SObjectField field)

Refactoring

  • Iterable for SubQuery with(Iterable<SObjectField> fields)
  • Iterable for Filter containsSome(Iterable<String> values)
  • Code refactoring

groupBy related

Thanks to @jessewheeler 's help, we have a groupBy related.

Signature

Queryable groupBy(String relationshipName, SObjectField field)

Example

SELECT COUNT(Name) count
FROM OpportunityLineItem
GROUP BY OpportunityLineItem.Opportunity.Account.Id
SOQL.of(OpportunityLineItem.SObjectType)
    .count(OpportunityLineItem.Name, 'count')
    .groupBy('OpportunityLineItem.Opportunity.Account', Account.Id)
    .toAggregated();

groupByRollup related

Signature

Queryable groupByRollup(String relationshipName, SObjectField field)

Example

SELECT COUNT(Name) cnt
FROM Lead
GROUP BY ROLLUP(ConvertedOpportunity.StageName)
SOQL.of(Lead.SObjectType)
    .count(Lead.Name, 'cnt')
    .groupByRollup('ConvertedOpportunity', Opportunity.StageName)
    .toAggregated();

groupByCube related

Signature

Queryable groupByCube(String relationshipName, SObjectField field)

Example

SELECT COUNT(Name) cnt
FROM Lead
GROUP BY CUBE(ConvertedOpportunity.StageName)
SOQL.of(Lead.SObjectType)
    .count(Lead.Name, 'cnt')
    .groupByCube('ConvertedOpportunity', Opportunity.StageName)
    .toAggregated();

Code refactoring

Applied the Iterable interface where possible for a small code refactoring to simplify the code.

v3.2.2

27 Mar 09:21
c408443

Choose a tag to compare

27-March-2024

New

  • withFieldSet(String fieldSetName)
  • byRecordType(String recordTypeDeveloperName)

Refactoring

  • API 60 Update
  • Iterable for with(String relationshipName, Iterable<SObjectField> fields)
  • Code refactoring

withFieldSet

Pass FieldSet name to get dynamic fields. It can be very useful to have a dynamic UI.

Signature

Queryable withFieldSet(String fieldSetName)

Example

SELECT
    Id,
    Name,
    Industry
FROM Account
SOQL.of(Account.SObjectType)
    .withFieldSet('AccountFieldSet')
    .toList();

byRecordType

Query record by RecordType.DeveloperName. To do that, you can use the byRecordType method.

🚨 If you already have a method called byRecordType in your selector, you may encounter the following issues:
Non-virtual, non-abstract methods cannot be overridden: YourSelector YourSelector.byRecordType(String)

You have two options:

  1. You can remove the byRecordType from your selector and use the predefined method from the SOQL library.
  2. You can skip that update and still use byRecordType from your selector.

Signature

Queryable byRecordType(String recordTypeDeveloperName)

Example

SELECT Id
FROM Account
WHERE RecordType.DeveloperName = 'Partner'
SOQL.of(Account.SObjectType)
    .byRecordType('Partner')
    .toList();

API 60 Update

🚨Do not forget to update ALL selectors to API 60.

Selectors with API < 60 that are using the isIn method will encounter the following error:
Method does not exist or incorrect signature: void isIn(List<String>) from the type SOQL.Filter

Salesforce has made some changes, and this update is necessary!

Iterable for with(String relationshipName, Iterable<SObjectField> fields)

Now you can pass List<SObjectField> as well as Set<SObjectField> to with(String relationshipName, Iterable<SObjectField> fields).

Code refactoring

Unused lines of code were removed. Null Coalescing Operator was applied.

v3.2.1

16 Feb 08:49
f84441a

Choose a tag to compare

16-February-2024

Update of SOQL_Test

We replaced the Standard User Profile with Minimum Access - Salesforce, ensuring that the stripInaccessibleToObject and stripInaccessibleToList tests will execute successfully regardless of the CRUD settings in the Salesforce org.

From:

 static User standardUser() {
        return new User(
            Alias = 'newUser',
            Email = '[email protected]',
            EmailEncodingKey = 'UTF-8',
            LastName = 'Testing',
            LanguageLocaleKey = 'en_US',
            LocaleSidKey = 'en_US',
            ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User'].Id,
            TimeZoneSidKey = 'America/Los_Angeles',
            UserName = '[email protected]'
        );
}

To:

 static User minimumAccessUser() {
        return new User(
            Alias = 'newUser',
            Email = '[email protected]',
            EmailEncodingKey = 'UTF-8',
            LastName = 'Testing',
            LanguageLocaleKey = 'en_US',
            LocaleSidKey = 'en_US',
            ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id,
            TimeZoneSidKey = 'America/Los_Angeles',
            UserName = '[email protected]'
        );
 }

v3.2.0

19 Dec 12:20
7796c88

Choose a tag to compare

19-December-2023

  • Changes
    • COUNT related
    • AVG related
    • COUNT_DISTINCT related
    • MIN related
    • MAX related
    • SUM related
    • toLabel
    • format
    • Code refactoring

COUNT

Introducing the count method for related fields.

Queryable count(String relationshipName, SObjectField field);
Queryable count(String relationshipName, SObjectField field, String alias);
// SELECT COUNT(Account.Name) 
// FROM Contact

SOQL.of(Contact.SObjectType)
    .count('Account', Account.Name)
    .toAggregated();
// SELECT COUNT(Account.Name) names 
// FROM Contact

SOQL.of(Contact.SObjectType)
    .count('Account', Account.Name, 'names')
    .toAggregated();

AVG

Introducing the avg method for related fields.

Queryable avg(String relationshipName, SObjectField field);
Queryable avg(String relationshipName, SObjectField field, String alias);
// SELECT AVG(Opportunity.Amount) 
// FROM OpportunityLineItem

SOQL.of(OpportunityLineItem.SObjectType)
    .avg('Opportunity', Opportunity.Amount)
    .toAggregate();
// SELECT AVG(Opportunity.Amount) amount 
// FROM OpportunityLineItem

SOQL.of(OpportunityLineItem.SObjectType)
    .avg('Opportunity', Opportunity.Amount, 'amount')
    .toAggregate();

COUNT_DISTINCT

Introducing the countDistinct method for related fields.

Queryable countDistinct(String relationshipName, SObjectField field);
Queryable countDistinct(String relationshipName, SObjectField field, String alias);
// SELECT COUNT_DISTINCT(Lead.Company) 
// FROM CampaignMember

SOQL.of(CampaignMember.SObjectType)
    .countDistinct('Lead', Lead.Company)
    .toAggregate();
// SELECT COUNT_DISTINCT(Lead.Company) company 
// FROM CampaignMember

SOQL.of(CampaignMember.SObjectType)
    .countDistinct('Lead', Lead.Company, 'company')
    .toAggregate();

MIN

Introducing the min method for related fields.

Queryable min(String relationshipName, SObjectField field);
Queryable min(String relationshipName, SObjectField field, String alias);
// SELECT MIN(Account.CreatedDate) 
// FROM Contact

SOQL.of(Contact.SObjectType)
    .min('Account', Account.CreatedDate)
    .toAggregate();
// SELECT MIN(Account.CreatedDate) createdDate 
// FROM Contact

SOQL.of(Contact.SObjectType)
    .min('Account', Account.CreatedDate, 'createdDate')
    .toAggregate();

MAX

Introducing the max method for related fields.

Queryable max(String relationshipName, SObjectField field);
Queryable max(String relationshipName, SObjectField field, String alias);
// SELECT MAX(Campaign.BudgetedCost) 
// FROM CampaignMember

SOQL.of(CampaignMember.SObjectType)
    .max('Campaign', Campaign.BudgetedCost)
    .toAggregate();
// SELECT MAX(Campaign.BudgetedCost) budgetedCost 
// FROM CampaignMember

SOQL.of(CampaignMember.SObjectType)
    .max('Campaign', Campaign.BudgetedCost, 'budgetedCost')
    .toAggregate();

SUM

Introducing the sum method for related fields.

Queryable sum(String relationshipName, SObjectField field);
Queryable sum(String relationshipName, SObjectField field, String alias);
// SELECT SUM(Opportunity.Amount)
// FROM OpportunityLineItem

SOQL.of(OpportunityLineItem.SObjectType)
    .sum('Opportunity', Opportunity.Amount)
    .toAggregate();
// SELECT SUM(Opportunity.Amount) amount 
// FROM OpportunityLineItem

SOQL.of(OpportunityLineItem.SObjectType)
    .sum('Opportunity', Opportunity.Amount, 'amount')
    .toAggregate();

toLabel

toLabel()

To translate SOQL query results into the language of the user who submits the query, use the toLabel method.

Queryable toLabel(SObjectField field);
Queryable toLabel(String field);
// SELECT Company, toLabel(Status) 
// FROM Lead

SOQL.of(Lead.SObjectType)
    .with(Lead.Company)
    .toLabel(Lead.Status)
    .toList();
// SELECT Company, toLabel(Recordtype.Name) 
// FROM Lead

SOQL.of(Lead.SObjectType)
    .with(Lead.Company)
    .toLabel('Recordtype.Name')
    .toList();

format

FORMAT()

Use FORMAT with the SELECT clause to apply localized formatting to standard and custom number, date, time, and currency fields.

Queryable format(SObjectField field);
Queryable format(SObjectField field, String alias);
// SELECT FORMAT(Amount)
// FROM Opportunity

SOQL.of(Opportunity.SObjectType)
    .format(Opportunity.Amount)
    .toList();
// SELECT FORMAT(Amount) amt 
// FROM Opportunity

SOQL.of(Opportunity.SObjectType)
    .format(Opportunity.Amount, 'amt')
    .toList();

v3.1.0

08 Nov 09:59
ff606d6

Choose a tag to compare

08-November-2023

  • Changes
    • AVG
    • COUNT_DISTINCT
    • MIN
    • MAX
    • SUM
    • ignoreWhen for FilterGroup
    • Code refactoring

AVG

Introducing the avg method.

Queryable avg(SObjectField field);
Queryable avg(SObjectField field, String alias);
// SELECT CampaignId, AVG(Amount) amount 
// FROM Opportunity 
// GROUP BY CampaignId

SOQL.of(Opportunity.SObjectType)
     .with(Opportunity.CampaignId)
     .avg(Opportunity.Amount)
     .groupBy(Opportunity.CampaignId)
     .toAggregated();
// SELECT CampaignId, AVG(Amount) amount 
// FROM Opportunity 
// GROUP BY CampaignId

SOQL.of(Opportunity.SObjectType)
    .with(Opportunity.CampaignId)
    .avg(Opportunity.Amount, 'amount')
    .groupBy(Opportunity.CampaignId)
    .toAggregated();

COUNT_DISTINCT

Introducing the countDistinct method.

Queryable countDistinct(SObjectField field);
Queryable countDistinct(SObjectField field, String alias);
// SELECT COUNT_DISTINCT(Company) FROM Lead

SOQL.of(Lead.SObjectType)
    .countDistinct(Lead.Company)
    .toAggregated();
// SELECT COUNT_DISTINCT(Company) company FROM Lead

SOQL.of(Lead.SObjectType)
    .countDistinct(Lead.Company, 'company')
    .toAggregated();

MIN

Introducing the min method.

Queryable min(SObjectField field);
Queryable min(SObjectField field, String alias);
// SELECT FirstName, LastName, MIN(CreatedDate) 
// FROM Contact 
// GROUP BY FirstName, LastName

SOQL.of(Contact.SObjectType)
    .with(Contact.FirstName, Contact.LastName)
    .min(Contact.CreatedDate)
    .groupBy(Contact.FirstName)
    .groupBy(Contact.LastName)
    .toAggregated();
// SELECT FirstName, LastName, MIN(CreatedDate) createdDate
// FROM Contact 
// GROUP BY FirstName, LastName

SOQL.of(Contact.SObjectType)
    .with(Contact.FirstName, Contact.LastName)
    .min(Contact.CreatedDate, 'createdDate')
    .groupBy(Contact.FirstName)
     groupBy(Contact.LastName)
    .toAggregated();

MAX

Introducing the max method.

Queryable max(SObjectField field);
Queryable max(SObjectField field, String alias);
// SELECT Name, MAX(BudgetedCost) 
// FROM Campaign 
// GROUP BY Name

SOQL.of(Campaign.SObjectType)
    .with(Campaign.Name)
    .max(Campaign.BudgetedCost)
    .groupBy(Campaign.Name)
    .toAggregated();
// SELECT Name, MAX(BudgetedCost) budgetedCost
// FROM Campaign 
// GROUP BY Name

SOQL.of(Campaign.SObjectType)
    .with(Campaign.Name)
    .max(Campaign.BudgetedCost, 'budgetedCost')
    .groupBy(Campaign.Name)
    .toAggregated();

SUM

Introducing the sum method.

Queryable sum(SObjectField field);
Queryable sum(SObjectField field, String alias);
// SELECT SUM(Amount) FROM Opportunity

SOQL.of(Opportunity.SObjectType)
    .sum(Opportunity.Amount)
    .toAggregated()
// SELECT SUM(Amount) amount FROM Opportunity

SOQL.of(Opportunity.SObjectType)
    .sum(Opportunity.Amount, 'amount')
    .toAggregated()

ignoreWhen for FilterGroup

The ignoreWhen method was previously available only for Filter, but now you can use it for FilterGroup.

Boolean isPartnerUser = false;

SOQL.of(Account.SObjectType)
    .whereAre(SOQL.FilterGroup
        .add(SOQL.FilterGroup
            .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow'))
            .add(SOQL.Filter.with(Account.BillingCity).equal('Warsaw'))
            .anyConditionMatching()
            .ignoreWhen(!isPartnerUser) // <===
        )
        .add(SOQL.FilterGroup
            .add(SOQL.Filter.with(Account.Industry).equal('IT'))
            .add(SOQL.Filter.name().contains('MyAcccount'))
        )
    )
    .toList();