Skip to content

v6.12.0

Choose a tag to compare

@pgajek2 pgajek2 released this 15 Aug 21:29

v6.12.0 - 15-Aug-2026

Scope

  • New Feature: SOQL.Logger — Automatic Query Error Logging
  • New Feature: SubQuery Related Field Overloads (with(relationshipName, field1 - field5))
  • Enhancement: SOQLCache — Filtering by Non-Unique Fields

New Feature: SOQL.Logger — Automatic Query Error Logging

Added SOQL.Logger and SOQL.Error interfaces (#306). Create a top-level class that implements SOQL.Logger and SOQL Lib will discover it automatically — no registration needed. The logger is invoked whenever query execution fails (toList, toObject, toInteger, toAggregated, toQueryLocator, toCursor, toPaginationCursor, and every result method built on them), after which the original exception is rethrown.

New public surface

global interface Logger {
    void log(Error error);
}

global interface Error {
    Exception getException();
    String getQuery();
    Map<String, Object> getBinding();
    AccessLevel getAccessLevel();
}

Usage

public class SOQLLogger implements SOQL.Logger {
    public void log(SOQL.Error error) {
        System.debug(LoggingLevel.ERROR, 'Query failed: ' + error.getException().getMessage() + '\n' + error.getQuery());
        // or use your own logging framework here (e.g. Nebula Logger)
    }
}

Notes

  • The implementation is discovered via ApexTypeImplementor (namespace-aware) and instantiated lazily using a no-arg constructor — the first failing query in a transaction triggers the discovery.
  • Exactly one implementation is honored. Keep a single class implementing SOQL.Logger in your org.
  • The class must be top-level and concrete — inner classes are not discoverable by ApexTypeImplementor.
  • The logger must not throw — an exception thrown inside log() replaces the original query error.
  • Mocked exceptions (SOQL.mock(...).throwException()) are not logged.
  • The original query exception is always rethrown after logging.

New Feature: SubQuery Related Field Overloads

Aligned SOQL.SubQuery with SOQL.Queryable (#310): parent (relationship) fields can now be added to a sub-query with the same explicit overloads that are available on the main query.

New public surface

SubQuery with(String relationshipName, SObjectField field);
SubQuery with(String relationshipName, SObjectField field1, SObjectField field2);
SubQuery with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3);
SubQuery with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
SubQuery with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);

Example

SELECT Id, (
    SELECT CreatedBy.Id, CreatedBy.Name
    FROM Contacts
) FROM Account
SOQL.of(Account.SObjectType)
    .with(SOQL.SubQuery.of('Contacts')
        .with('CreatedBy', User.Id, User.Name)
    )
    .toList();

Enhancement: SOQLCache — Filtering by Non-Unique Fields

Previously, every whereEqual condition on a cached query had to be based on Id, Name, DeveloperName, or a unique field. Now a cached query is valid when at least one condition is based on such a field — additional non-unique conditions can be combined with it to narrow the result.

Before

// throws: 'A cached query can be filtered only by Id, Name, DeveloperName, or a unique field. (...)'
SOQLCache.of(Profile.SObjectType)
    .with(Profile.Id, Profile.Name, Profile.UserType)
    .whereEqual(Profile.Name, 'System Administrator')
    .whereEqual(Profile.UserType, 'Standard')
    .toObject();

After

// works: Name is a unique condition, UserType narrows the result
SOQLCache.of(Profile.SObjectType)
    .with(Profile.Id, Profile.Name, Profile.UserType)
    .whereEqual(Profile.Name, 'System Administrator')
    .whereEqual(Profile.UserType, 'Standard')
    .toObject();

Notes

  • A query with no unique condition at all now throws: A cached query must have at least one condition based on Id, Name, DeveloperName, or a unique field. You can ignore this validation by calling allowFilteringByNonUniqueFields().
  • A cached record that does not yet contain a condition field counts as a match — the field-completeness check then forces a refresh from the database, so results stay correct.
  • allowFilteringByNonUniqueFields() still bypasses the validation entirely.

Contributors

Thank you @nwcm !