Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions package/main/default/classes/standard-soql/SOQL.cls
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ global virtual inherited sharing class SOQL implements Queryable {
Queryable query();
}

global interface PostProcessor {
List<SObject> process(List<SObject> records);
}

global static Queryable of(SObjectType ofObject) {
return new SOQL(ofObject);
}
Expand Down Expand Up @@ -469,6 +473,7 @@ global virtual inherited sharing class SOQL implements Queryable {
private SoqlBuilder builder;
private Executor executor;
private Converter converter;
private Boolean skipPostProcessing = false;

global SOQL(SObjectType ofObject) {
this(ofObject.toString());
Expand Down Expand Up @@ -960,6 +965,31 @@ global virtual inherited sharing class SOQL implements Queryable {
return this;
}

global SOQL skipPostProcessors() {
this.skipPostProcessing = true;
return this;
}

@TestVisible
protected virtual List<PostProcessor> getPostProcessors() {
return null;
}

private List<SObject> toListProcessed() {
List<SObject> records = this.executor.toList();
if (this.skipPostProcessing) {
return records;
}
List<PostProcessor> processors = this.getPostProcessors();
if (processors == null || processors.isEmpty()) {
return records;
}
for (PostProcessor p : processors) {
records = p.process(records);
}
return records;
}

global Map<String, Object> binding() {
return binder.getBindingMap();
}
Expand Down Expand Up @@ -1033,11 +1063,18 @@ global virtual inherited sharing class SOQL implements Queryable {
}

global SObject toObject() {
return this.executor.toObject();
List<SObject> records = this.toListProcessed();
if (records.isEmpty()) {
return null;
}
if (records.size() > 1) {
throw new QueryException('List has more than 1 row for assignment to SObject');
}
return records[0];
}

global List<SObject> toList() {
return this.executor.toList();
return this.toListProcessed();
}

global List<AggregateResult> toAggregated() {
Expand All @@ -1053,77 +1090,77 @@ global virtual inherited sharing class SOQL implements Queryable {
}

global Map<Id, SObject> toMap() {
return this.converter.transform(this.executor.toList()).toMap();
return this.converter.transform(this.toListProcessed()).toMap();
}

global Map<Id, SObject> toIdMapBy(SObjectField field) {
this.with(field);
this.whereAre(Filter.with(field).isNotNull());
return this.converter.transform(this.executor.toList()).toIdMapBy(field);
return this.converter.transform(this.toListProcessed()).toIdMapBy(field);
}

global Map<Id, SObject> toIdMapBy(String relationshipName, SObjectField targetKeyField) {
this.with(relationshipName + '.' + targetKeyField.toString());
this.whereAre(Filter.with(relationshipName, targetKeyField).isNotNull());
return this.converter.transform(this.executor.toList()).toIdMapBy(relationshipName, targetKeyField);
return this.converter.transform(this.toListProcessed()).toIdMapBy(relationshipName, targetKeyField);
}

global Map<Id, List<SObject>> toAggregatedIdMapBy(SObjectField keyField) {
this.with(keyField);
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedIdMapBy(keyField);
return this.converter.transform(this.toListProcessed()).toAggregatedIdMapBy(keyField);
}

global Map<Id, List<SObject>> toAggregatedIdMapBy(String relationshipName, SObjectField targetKeyField) {
this.with(relationshipName + '.' + targetKeyField.toString());
this.whereAre(Filter.with(relationshipName, targetKeyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedIdMapBy(relationshipName, targetKeyField);
return this.converter.transform(this.toListProcessed()).toAggregatedIdMapBy(relationshipName, targetKeyField);
}

global Map<String, SObject> toMap(SObjectField keyField) {
this.with(keyField);
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toMap(keyField);
return this.converter.transform(this.toListProcessed()).toMap(keyField);
}

global Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField) {
this.with(relationshipName + '.' + targetKeyField.toString());
this.whereAre(Filter.with(relationshipName, targetKeyField).isNotNull());
return this.converter.transform(this.executor.toList()).toMap(relationshipName, targetKeyField);
return this.converter.transform(this.toListProcessed()).toMap(relationshipName, targetKeyField);
}

global Map<String, String> toMap(SObjectField keyField, SObjectField valueField) {
this.builder.fields.clearAllFields(); // other fields not needed
this.with(keyField, valueField);
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toMap(keyField, valueField);
return this.converter.transform(this.toListProcessed()).toMap(keyField, valueField);
}

global Map<String, List<SObject>> toAggregatedMap(SObjectField keyField) {
this.with(keyField);
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField);
return this.converter.transform(this.toListProcessed()).toAggregatedMap(keyField);
}

global Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField) {
this.with(relationshipName + '.' + targetKeyField.toString());
this.whereAre(Filter.with(relationshipName, targetKeyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedMap(relationshipName, targetKeyField);
return this.converter.transform(this.toListProcessed()).toAggregatedMap(relationshipName, targetKeyField);
}

global Map<String, List<String>> toAggregatedMap(SObjectField keyField, SObjectField valueField) {
this.builder.fields.clearAllFields(); // other fields not needed
this.with(keyField, valueField);
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField, valueField);
return this.converter.transform(this.toListProcessed()).toAggregatedMap(keyField, valueField);
}

global Map<String, List<String>> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField) {
this.builder.fields.clearAllFields(); // other fields not needed
this.with(keyField);
this.with(relationshipName + '.' + targetKeyField.toString());
this.whereAre(Filter.with(keyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField, relationshipName, targetKeyField);
return this.converter.transform(this.toListProcessed()).toAggregatedMap(keyField, relationshipName, targetKeyField);
}

global Database.QueryLocator toQueryLocator() {
Expand Down
116 changes: 116 additions & 0 deletions package/main/default/classes/standard-soql/SOQL_Full_Test.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5266,6 +5266,122 @@ private class SOQL_Full_Test {
SOQL.of(Account.SObjectType).count().preview().toInteger();
}

// PostProcessor — infrastructure tests

private class TestableSOQL extends SOQL {
public TestableSOQL() {
super(Account.SObjectType);
with(Account.Name);
}
protected override List<SOQL.PostProcessor> getPostProcessors() {
return new List<SOQL.PostProcessor>{ new NamePrefixFilter('Keep') };
}
}

private class NamePrefixFilter implements SOQL.PostProcessor {
private final String prefix;
public NamePrefixFilter(String p) { this.prefix = p; }
public List<SObject> process(List<SObject> records) {
List<SObject> kept = new List<SObject>();
for (SObject r : records) {
if (((Account) r).Name.startsWith(this.prefix)) {
kept.add(r);
}
}
return kept;
}
}

@IsTest
static void postProcessorFiltersToList() {
// Setup
SOQL.mock(Account.SObjectType).thenReturn(new List<Account>{
new Account(Name = 'Keep Me'),
new Account(Name = 'Drop Me')
});

// Test
List<SObject> result = new TestableSOQL().toList();

// Verify
Assert.areEqual(1, result.size(), 'Post-processor should filter non-matching records.');
Assert.areEqual('Keep Me', ((Account) result[0]).Name, 'Remaining record should be the kept one.');
}

@IsTest
static void postProcessorFiltersToObject() {
// Setup
SOQL.mock(Account.SObjectType).thenReturn(new List<Account>{
new Account(Name = 'Keep Me')
});

// Test
Account result = (Account) new TestableSOQL().toObject();

// Verify
Assert.areEqual('Keep Me', result.Name, 'Post-processor should pass a matching record through.');
}

@IsTest
static void postProcessorFiltersToMap() {
// Setup
Id keepId = SOQL.IdGenerator.get(Account.SObjectType);
Id dropId = SOQL.IdGenerator.get(Account.SObjectType);
SOQL.mock(Account.SObjectType).thenReturn(new List<Account>{
new Account(Id = keepId, Name = 'Keep Me'),
new Account(Id = dropId, Name = 'Drop Me')
});

// Test
Map<Id, SObject> result = new TestableSOQL().toMap();

// Verify
Assert.areEqual(1, result.size(), 'Post-processor should filter before map conversion.');
Assert.isTrue(result.containsKey(keepId), 'Map should only contain the kept record.');
}

@IsTest
static void skipPostProcessorsReturnsFull() {
// Setup
SOQL.mock(Account.SObjectType).thenReturn(new List<Account>{
new Account(Name = 'Keep Me'),
new Account(Name = 'Drop Me')
});

// Test
List<SObject> result = new TestableSOQL().skipPostProcessors().toList();

// Verify
Assert.areEqual(2, result.size(), 'skipPostProcessors() should bypass all processors.');
}

@IsTest
static void noPostProcessorHasZeroCost() {
// Setup
SOQL.mock(Account.SObjectType).thenReturn(new List<Account>{
new Account(Name = 'Account A'),
new Account(Name = 'Account B')
});

// Test — plain SOQL.of() with no override should be unaffected
List<SObject> result = SOQL.of(Account.SObjectType).toList();

// Verify
Assert.areEqual(2, result.size(), 'Non-overriding path should return all records unchanged.');
}

@IsTest
static void postProcessorDoesNotAffectToInteger() {
// Setup
SOQL.mock(Account.SObjectType).thenReturn(5);

// Test
Integer result = new TestableSOQL().toInteger();

// Verify
Assert.areEqual(5, result, 'toInteger() must not be affected by post-processors.');
}

static List<Account> insertAccounts() {
List<Account> accounts = new List<Account>{
new Account(Name = 'Test 1'),
Expand Down