Repository navigation
v6.12.0
v6.12.0 - 15-Aug-2026
Scope
- New Feature:
SOQL.Logger— Automatic Query Error Logging - New Feature:
SubQueryRelated 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.Loggerin 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 AccountSOQL.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 !