diff --git a/force-app/main/default/classes/main/cached-soql/CacheManager.cls b/force-app/main/default/classes/main/cached-soql/CacheManager.cls index f93f18c5..aa06b542 100644 --- a/force-app/main/default/classes/main/cached-soql/CacheManager.cls +++ b/force-app/main/default/classes/main/cached-soql/CacheManager.cls @@ -9,126 +9,126 @@ **/ @SuppressWarnings('PMD.CognitiveComplexity,PMD.PropertyNamingConventions,PMD.FieldNamingConventions') public with sharing class CacheManager { - public interface Cacheable { - Boolean contains(String key); - Set getKeys(); - Object get(String key); - void put(String key, Object value); - void remove(String key); - } - - public final static Cacheable ApexTransaction = new ApexTransactionCache(); - - public final static Cacheable SOQLOrgCache { - get { return getOrgCache('SOQL'); } - } - - public final static Cacheable SOQLSessionCache { - get { return getSessionCache('SOQL'); } - } - - // Implementation - - private enum CacheType { ORG, SESSION } - - private final static Map> CACHE_MAP = new Map>{ - CacheType.ORG => new Map(), - CacheType.SESSION => new Map() - }; - - private static Cacheable getOrgCache(String partitionName) { - if (!CACHE_MAP.get(CacheType.ORG).containsKey(partitionName)) { - CACHE_MAP.get(CacheType.ORG).put(partitionName, new OrgPlatformCache(partitionName)); - } - return CACHE_MAP.get(CacheType.ORG).get(partitionName); - } - - private static Cacheable getSessionCache(String partitionName) { - if (!CACHE_MAP.get(CacheType.SESSION).containsKey(partitionName)) { - CACHE_MAP.get(CacheType.SESSION).put(partitionName, new SessionPlatformCache(partitionName)); - } - return CACHE_MAP.get(CacheType.SESSION).get(partitionName); - } - - private static void validateKey(String key) { - if (!Pattern.compile('^[a-zA-Z0-9]+$').matcher(key).matches()) { - throw new IllegalArgumentException('Key must be alphanumeric, received key: ' + key); - } - } - - private class ApexTransactionCache implements Cacheable { - private final Map TRANSACTION_CACHE = new Map(); - - public Boolean contains(String key) { - return this.TRANSACTION_CACHE.containsKey(key); - } - - public Set getKeys() { - return TRANSACTION_CACHE.keySet(); - } - - public Object get(String key) { - return this.TRANSACTION_CACHE.get(key); - } - - public void put(String key, Object value) { - validateKey(key); - this.TRANSACTION_CACHE.put(key, value); - } - - public void remove(String key) { - this.TRANSACTION_CACHE.remove(key); - } - } - - private abstract class PlatformCache implements Cacheable { - private Cache.Partition platformCachePartition; - - public PlatformCache(String partitionName) { - this.platformCachePartition = getPartition(partitionName); - } - - protected abstract Cache.Partition getPartition(String partitionName); - - public Boolean contains(String key) { - return this.platformCachePartition.contains(key); - } - - public Set getKeys() { - return this.platformCachePartition.getKeys(); - } - - public Object get(String key) { - return this.platformCachePartition.get(key); - } - - public void remove(String key) { - this.platformCachePartition.remove(key); - } - - public void put(String key, Object value) { - validateKey(key); - this.platformCachePartition.put(key, value); - } - } - - private class OrgPlatformCache extends PlatformCache { - public OrgPlatformCache(String partitionName) { - super(partitionName); - } - - public override Cache.Partition getPartition(String partitionName) { - return Cache.Org.getPartition(partitionName); - } - } - - private class SessionPlatformCache extends PlatformCache { - public SessionPlatformCache(String partitionName) { - super(partitionName); - } - - public override Cache.Partition getPartition(String partitionName) { - return Cache.Session.getPartition(partitionName); - } - } + public interface Cacheable { + Boolean contains(String key); + Set getKeys(); + Object get(String key); + void put(String key, Object value); + void remove(String key); + } + + public final static Cacheable ApexTransaction = new ApexTransactionCache(); + + public final static Cacheable SOQLOrgCache { + get { return getOrgCache('SOQL'); } + } + + public final static Cacheable SOQLSessionCache { + get { return getSessionCache('SOQL'); } + } + + // Implementation + + private enum CacheType { ORG, SESSION } + + private final static Map> CACHE_MAP = new Map>{ + CacheType.ORG => new Map(), + CacheType.SESSION => new Map() + }; + + private static Cacheable getOrgCache(String partitionName) { + if (!CACHE_MAP.get(CacheType.ORG).containsKey(partitionName)) { + CACHE_MAP.get(CacheType.ORG).put(partitionName, new OrgPlatformCache(partitionName)); + } + return CACHE_MAP.get(CacheType.ORG).get(partitionName); + } + + private static Cacheable getSessionCache(String partitionName) { + if (!CACHE_MAP.get(CacheType.SESSION).containsKey(partitionName)) { + CACHE_MAP.get(CacheType.SESSION).put(partitionName, new SessionPlatformCache(partitionName)); + } + return CACHE_MAP.get(CacheType.SESSION).get(partitionName); + } + + private static void validateKey(String key) { + if (!Pattern.compile('^[a-zA-Z0-9]+$').matcher(key).matches()) { + throw new IllegalArgumentException('Key must be alphanumeric, received key: ' + key); + } + } + + private class ApexTransactionCache implements Cacheable { + private final Map TRANSACTION_CACHE = new Map(); + + public Boolean contains(String key) { + return this.TRANSACTION_CACHE.containsKey(key); + } + + public Set getKeys() { + return TRANSACTION_CACHE.keySet(); + } + + public Object get(String key) { + return this.TRANSACTION_CACHE.get(key); + } + + public void put(String key, Object value) { + validateKey(key); + this.TRANSACTION_CACHE.put(key, value); + } + + public void remove(String key) { + this.TRANSACTION_CACHE.remove(key); + } + } + + private abstract class PlatformCache implements Cacheable { + private Cache.Partition platformCachePartition; + + public PlatformCache(String partitionName) { + this.platformCachePartition = getPartition(partitionName); + } + + protected abstract Cache.Partition getPartition(String partitionName); + + public Boolean contains(String key) { + return this.platformCachePartition.contains(key); + } + + public Set getKeys() { + return this.platformCachePartition.getKeys(); + } + + public Object get(String key) { + return this.platformCachePartition.get(key); + } + + public void remove(String key) { + this.platformCachePartition.remove(key); + } + + public void put(String key, Object value) { + validateKey(key); + this.platformCachePartition.put(key, value); + } + } + + private class OrgPlatformCache extends PlatformCache { + public OrgPlatformCache(String partitionName) { + super(partitionName); + } + + public override Cache.Partition getPartition(String partitionName) { + return Cache.Org.getPartition(partitionName); + } + } + + private class SessionPlatformCache extends PlatformCache { + public SessionPlatformCache(String partitionName) { + super(partitionName); + } + + public override Cache.Partition getPartition(String partitionName) { + return Cache.Session.getPartition(partitionName); + } + } } diff --git a/force-app/main/default/classes/main/cached-soql/CacheManagerTest.cls b/force-app/main/default/classes/main/cached-soql/CacheManagerTest.cls index 901f2b31..1ec36c62 100644 --- a/force-app/main/default/classes/main/cached-soql/CacheManagerTest.cls +++ b/force-app/main/default/classes/main/cached-soql/CacheManagerTest.cls @@ -4,225 +4,225 @@ **/ @IsTest private class CacheManagerTest { - @IsTest - static void apexTransactionContains() { - // Setup - CacheManager.ApexTransaction.put('TestKey', 'Test Value'); - - // Test - Boolean hasKey = CacheManager.ApexTransaction.contains('TestKey'); - - // Verify - Assert.isTrue(hasKey, 'Key should exist'); - } - - @IsTest - static void apexTransactionGet() { - // Setup - CacheManager.ApexTransaction.put('TestKey', 'Test Value'); - - // Test - String cachedValue = (String) CacheManager.ApexTransaction.get('TestKey'); - - // Verify - Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); - } - - @IsTest - static void apexTransactionPut() { - // Test - CacheManager.ApexTransaction.put('TestKey', 'Test Value'); - - // Verify - Assert.areEqual('Test Value', CacheManager.ApexTransaction.get('TestKey'), 'Cached value should be the same'); - } - - @IsTest - static void apexTransactionRemove() { - // Setup - CacheManager.ApexTransaction.put('TestKey', 'Test Value'); - - // Test - CacheManager.ApexTransaction.remove('TestKey'); - - // Verify - Assert.areEqual(null, CacheManager.ApexTransaction.get('TestKey'), 'Cached value should not exist.'); - } - - @IsTest - static void apexTransactionGetKeys() { - // Setup - CacheManager.ApexTransaction.put('TestKey', 'Test Value'); - CacheManager.ApexTransaction.put('TestKey2', 'Test Value 2'); - - // Test - Set keys = CacheManager.ApexTransaction.getKeys(); - - // Verify - Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); - Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); - } - - @IsTest - static void apexTransactionInvalidKey() { - IllegalArgumentException keyException = null; - - // Test - try { - CacheManager.ApexTransaction.put('Test Key', 'Test Value'); - } catch (IllegalArgumentException e) { - keyException = e; - } - - // Verify - Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); - } - - @IsTest - static void orgCacheContains() { - // Setup - CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); - - // Test - Boolean hasKey = CacheManager.SOQLOrgCache.contains('TestKey'); - - // Verify - Assert.isTrue(hasKey, 'Key should exist'); - } - - @IsTest - static void orgCacheGet() { - // Setup - CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); - - // Test - String cachedValue = (String) CacheManager.SOQLOrgCache.get('TestKey'); - - // Verify - Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); - } - - @IsTest - static void orgCachePut() { - // Test - CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); - - // Verify - Assert.areEqual('Test Value', CacheManager.SOQLOrgCache.get('TestKey'), 'Cached value should be the same'); - } - - @IsTest - static void orgCacheRemove() { - // Setup - CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); - - // Test - CacheManager.SOQLOrgCache.remove('TestKey'); - - // Verify - Assert.areEqual(null, CacheManager.SOQLOrgCache.get('TestKey'), 'Cached value should not exist.'); - } - - @IsTest - static void orgCacheGetKeys() { - // Setup - CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); - CacheManager.SOQLOrgCache.put('TestKey2', 'Test Value 2'); - - // Test - Set keys = CacheManager.SOQLOrgCache.getKeys(); - - // Verify - Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); - Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); - } - - @IsTest - static void orgCacheInvalidKey() { - IllegalArgumentException keyException = null; - - // Test - try { - CacheManager.SOQLOrgCache.put('Test Key', 'Test Value'); - } catch (IllegalArgumentException e) { - keyException = e; - } - - // Verify - Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); - } - - @IsTest - static void sessionCacheContains() { - // Setup - CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); - - // Test - Boolean hasKey = CacheManager.SOQLSessionCache.contains('TestKey'); - - // Verify - Assert.isTrue(hasKey, 'Key should exist'); - } - - @IsTest - static void sessionCacheGet() { - // Setup - CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); - - // Test - String cachedValue = (String) CacheManager.SOQLSessionCache.get('TestKey'); - - // Verify - Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); - } - - @IsTest - static void sessionCachePut() { - // Test - CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); - - // Verify - Assert.areEqual('Test Value', CacheManager.SOQLSessionCache.get('TestKey'), 'Cached value should be the same'); - } - - @IsTest - static void sessionCacheRemove() { - // Setup - CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); - - // Test - CacheManager.SOQLSessionCache.remove('TestKey'); - - // Verify - Assert.areEqual(null, CacheManager.SOQLSessionCache.get('TestKey'), 'Cached value should not exist.'); - } - - @IsTest - static void sessionCacheGetKeys() { - // Setup - CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); - CacheManager.SOQLSessionCache.put('TestKey2', 'Test Value 2'); - - // Test - Set keys = CacheManager.SOQLSessionCache.getKeys(); - - // Verify - Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); - Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); - } - - @IsTest - static void sessionCacheInvalidKey() { - IllegalArgumentException keyException = null; - - // Test - try { - CacheManager.SOQLSessionCache.put('Test Key', 'Test Value'); - } catch (IllegalArgumentException e) { - keyException = e; - } - - // Verify - Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); - } + @IsTest + static void apexTransactionContains() { + // Setup + CacheManager.ApexTransaction.put('TestKey', 'Test Value'); + + // Test + Boolean hasKey = CacheManager.ApexTransaction.contains('TestKey'); + + // Verify + Assert.isTrue(hasKey, 'Key should exist'); + } + + @IsTest + static void apexTransactionGet() { + // Setup + CacheManager.ApexTransaction.put('TestKey', 'Test Value'); + + // Test + String cachedValue = (String) CacheManager.ApexTransaction.get('TestKey'); + + // Verify + Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); + } + + @IsTest + static void apexTransactionPut() { + // Test + CacheManager.ApexTransaction.put('TestKey', 'Test Value'); + + // Verify + Assert.areEqual('Test Value', CacheManager.ApexTransaction.get('TestKey'), 'Cached value should be the same'); + } + + @IsTest + static void apexTransactionRemove() { + // Setup + CacheManager.ApexTransaction.put('TestKey', 'Test Value'); + + // Test + CacheManager.ApexTransaction.remove('TestKey'); + + // Verify + Assert.areEqual(null, CacheManager.ApexTransaction.get('TestKey'), 'Cached value should not exist.'); + } + + @IsTest + static void apexTransactionGetKeys() { + // Setup + CacheManager.ApexTransaction.put('TestKey', 'Test Value'); + CacheManager.ApexTransaction.put('TestKey2', 'Test Value 2'); + + // Test + Set keys = CacheManager.ApexTransaction.getKeys(); + + // Verify + Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); + Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); + } + + @IsTest + static void apexTransactionInvalidKey() { + IllegalArgumentException keyException = null; + + // Test + try { + CacheManager.ApexTransaction.put('Test Key', 'Test Value'); + } catch (IllegalArgumentException e) { + keyException = e; + } + + // Verify + Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); + } + + @IsTest + static void orgCacheContains() { + // Setup + CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); + + // Test + Boolean hasKey = CacheManager.SOQLOrgCache.contains('TestKey'); + + // Verify + Assert.isTrue(hasKey, 'Key should exist'); + } + + @IsTest + static void orgCacheGet() { + // Setup + CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); + + // Test + String cachedValue = (String) CacheManager.SOQLOrgCache.get('TestKey'); + + // Verify + Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); + } + + @IsTest + static void orgCachePut() { + // Test + CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); + + // Verify + Assert.areEqual('Test Value', CacheManager.SOQLOrgCache.get('TestKey'), 'Cached value should be the same'); + } + + @IsTest + static void orgCacheRemove() { + // Setup + CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); + + // Test + CacheManager.SOQLOrgCache.remove('TestKey'); + + // Verify + Assert.areEqual(null, CacheManager.SOQLOrgCache.get('TestKey'), 'Cached value should not exist.'); + } + + @IsTest + static void orgCacheGetKeys() { + // Setup + CacheManager.SOQLOrgCache.put('TestKey', 'Test Value'); + CacheManager.SOQLOrgCache.put('TestKey2', 'Test Value 2'); + + // Test + Set keys = CacheManager.SOQLOrgCache.getKeys(); + + // Verify + Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); + Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); + } + + @IsTest + static void orgCacheInvalidKey() { + IllegalArgumentException keyException = null; + + // Test + try { + CacheManager.SOQLOrgCache.put('Test Key', 'Test Value'); + } catch (IllegalArgumentException e) { + keyException = e; + } + + // Verify + Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); + } + + @IsTest + static void sessionCacheContains() { + // Setup + CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); + + // Test + Boolean hasKey = CacheManager.SOQLSessionCache.contains('TestKey'); + + // Verify + Assert.isTrue(hasKey, 'Key should exist'); + } + + @IsTest + static void sessionCacheGet() { + // Setup + CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); + + // Test + String cachedValue = (String) CacheManager.SOQLSessionCache.get('TestKey'); + + // Verify + Assert.areEqual('Test Value', cachedValue, 'Cached value should be the same'); + } + + @IsTest + static void sessionCachePut() { + // Test + CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); + + // Verify + Assert.areEqual('Test Value', CacheManager.SOQLSessionCache.get('TestKey'), 'Cached value should be the same'); + } + + @IsTest + static void sessionCacheRemove() { + // Setup + CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); + + // Test + CacheManager.SOQLSessionCache.remove('TestKey'); + + // Verify + Assert.areEqual(null, CacheManager.SOQLSessionCache.get('TestKey'), 'Cached value should not exist.'); + } + + @IsTest + static void sessionCacheGetKeys() { + // Setup + CacheManager.SOQLSessionCache.put('TestKey', 'Test Value'); + CacheManager.SOQLSessionCache.put('TestKey2', 'Test Value 2'); + + // Test + Set keys = CacheManager.SOQLSessionCache.getKeys(); + + // Verify + Assert.isTrue(keys.contains('TestKey'), 'Key should exist.'); + Assert.isTrue(keys.contains('TestKey2'), 'Key should exist.'); + } + + @IsTest + static void sessionCacheInvalidKey() { + IllegalArgumentException keyException = null; + + // Test + try { + CacheManager.SOQLSessionCache.put('Test Key', 'Test Value'); + } catch (IllegalArgumentException e) { + keyException = e; + } + + // Verify + Assert.isNotNull(keyException, 'Exception should be thrown for not alphanumeric key.'); + } } diff --git a/force-app/main/default/classes/main/cached-soql/SOQLCache.cls b/force-app/main/default/classes/main/cached-soql/SOQLCache.cls index 7668741d..b223f00a 100644 --- a/force-app/main/default/classes/main/cached-soql/SOQLCache.cls +++ b/force-app/main/default/classes/main/cached-soql/SOQLCache.cls @@ -2,7 +2,7 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query @@ -16,749 +16,749 @@ **/ @SuppressWarnings('PMD.ExcessivePublicCount,PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.PropertyNamingConventions,PMD.FieldDeclarationsShouldBeAtStart,PMD.ApexDoc,PMD.ExcessiveParameterList') public virtual inherited sharing class SOQLCache implements Cacheable { - public interface Selector { - Cacheable query(); - } - - public static Cacheable of(SObjectType ofObject) { - return new SOQLCache(ofObject); - } - - public static Cacheable of(String ofObject) { - return new SOQLCache(ofObject); - } - - public interface Cacheable { - // CONFIG - Cacheable cacheInApexTransaction(); - Cacheable cacheInOrgCache(); - Cacheable cacheInSessionCache(); - Cacheable maxHoursWithoutRefresh(Integer hours); - Cacheable allowFilteringByNonUniqueFields(); - Cacheable allowQueryWithoutConditions(); - // 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 fields); - Cacheable with(String fields); - Cacheable with(String relationshipName, SObjectField field); - Cacheable with(String relationshipName, SObjectField field1, SObjectField field2); - Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3); - Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Cacheable with(String relationshipName, Iterable 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(); - Id toIdOf(SObjectField field); - Boolean doExist(); - SObject toObject(); - Object toValueOf(SObjectField fieldToExtract); - } - - protected virtual SOQL.Queryable initialQuery() { - return null; - } - - public virtual List additionalAllowedConditionFields() { - return new List(); - } - - // Mocking - - public interface Mockable { - // SObject - void thenReturn(SObject record); - } - - @TestVisible - private static Mockable mock(String mockId) { - if (!SOQLCache.queryIdToMock.containsKey(mockId)) { - SOQLCache.queryIdToMock.put(mockId, new List()); - } - SOQLCache.queryIdToMock.get(mockId).add(new SoqlMock()); - return SOQLCache.queryIdToMock.get(mockId).get(SOQLCache.queryIdToMock.get(mockId).size() - 1); - } - - public static void removeFromCache(List records) { - if (records.isEmpty()) { - return; - } - - String ofObject = records[0].getSObjectType().toString(); - CacheStorageProxy proxy = new CacheStorageProxy(ofObject); - - // Record deletion will trigger an automatic cache refresh when the query is executed. - proxy.apexTransaction().removeRecordsFromCache(records); - proxy.orgCache().removeRecordsFromCache(records); - proxy.sessionCache().removeRecordsFromCache(records); - } - - // Backward support - - @TestVisible // deprecated - private static void setMock(String mockId, SObject record) { - SOQLCache.queryIdToMock.put(mockId, new List{ new SoqlMock() }); - SOQLCache.queryIdToMock.get(mockId).get(SOQLCache.queryIdToMock.get(mockId).size() - 1).thenReturn(record); - } - - // Implementation - - private static Map> queryIdToMock = new Map>(); - - private Executor executor; - private Cache cache; - - public SOQL.Queryable initialQuery = null; - public SOQL.Queryable currentQuery = null; - - protected SOQLCache(SObjectType ofObject) { - this(ofObject.toString()); - } - - protected SOQLCache(String ofObject) { - this.initialQuery = this.initialQuery()?.systemMode()?.withoutSharing(); - this.currentQuery = SOQL.of(ofObject).systemMode().withoutSharing(); - - this.cache = new Cache(ofObject); - this.cache.filterGroup.setAdditionalAllowedConditionFields(this.additionalAllowedConditionFields()); - this.executor = new Executor(this.currentQuery, this.cache); - - this.with('Id'); - } - - public Cacheable cacheInApexTransaction() { - this.cache.storage.apexTransaction(); - return this; - } - - public Cacheable cacheInOrgCache() { - this.cache.storage.orgCache(); - return this; - } - - public Cacheable cacheInSessionCache() { - this.cache.storage.sessionCache(); - return this; - } - - public Cacheable maxHoursWithoutRefresh(Integer hours) { - this.cache.expiration.maxRecordAgeInHours(hours); - return this; - } - - public Cacheable allowFilteringByNonUniqueFields() { - this.cache.filterGroup.allowFilteringByNonUniqueFields(); - return this; - } - - public Cacheable allowQueryWithoutConditions() { - this.cache.filterGroup.allowQueryWithoutConditions(); - return this; - } - - public Cacheable with(SObjectField field) { - this.initialQuery?.with(field); - this.currentQuery.with(field); - this.cache.fields.with(field); - return this; - } - - public Cacheable with(SObjectField field1, SObjectField field2) { - return this.with(field1).with(field2); - } - - public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3) { - return this.with(field1, field2).with(field3); - } - - public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.with(field1, field2, field3).with(field4); - } - - public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.with(field1, field2, field3, field4).with(field5); - } - - public Cacheable with(List fields) { - this.initialQuery?.with(fields); - this.currentQuery.with(fields); - this.cache.fields.with(fields); - return this; - } - - public Cacheable with(String fields) { - this.initialQuery?.with(fields); - this.currentQuery.with(fields); - this.cache.fields.with(fields); - return this; - } - - public Cacheable with(String relationshipName, SObjectField field) { - this.initialQuery?.with(relationshipName, field); - this.currentQuery.with(relationshipName, field); - this.cache.fields.with(relationshipName, field); - return this; - } - - public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2) { - return this.with(relationshipName, new List{ field1, field2 }); - } - - public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3) { - return this.with(relationshipName, new List{ field1, field2, field3 }); - } - - public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.with(relationshipName, new List{ field1, field2, field3, field4 }); - } - - public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.with(relationshipName, new List{ field1, field2, field3, field4, field5 }); - } - - public Cacheable with(String relationshipName, Iterable fields) { - this.initialQuery?.with(relationshipName, fields); - this.currentQuery.with(relationshipName, fields); - this.cache.fields.with(relationshipName, fields); - return this; - } - - public Cacheable whereEqual(SObjectField field, Object value) { - return this.whereEqual(field.toString(), value); - } - - public Cacheable whereEqual(String field, Object value) { - this.with(field); - this.cache.filterGroup.add(new CacheFilter().with(field).equal(value)); - this.currentQuery.whereAre(SOQL.Filter.with(field).equal(value)); - return this; - } - - public Cacheable stripInaccessible() { - return this.stripInaccessible(AccessType.READABLE); - } - - public Cacheable stripInaccessible(AccessType accessType) { - this.currentQuery.stripInaccessible(accessType); - this.executor.stripInaccessible(accessType); - return this; - } - - public Cacheable mockId(String queryIdentifier) { - this.currentQuery.mockId(queryIdentifier); - this.executor.mock(queryIdToMock.get(queryIdentifier)); - return this; - } - - public Cacheable preview() { - this.currentQuery.preview(); - return this; - } - - public Id toId() { - return this.toObject()?.Id; - } - - public Id toIdOf(SObjectField field) { - return (Id) this.toObject()?.get(field); - } - - public Boolean doExist() { - return this.toObject() != null; - } - - public Object toValueOf(SObjectField fieldToExtract) { - this.with(fieldToExtract); - return this.toObject()?.get(fieldToExtract); - } - - public SObject toObject() { - this.cache.filterGroup.validateIfQueryHasAtLeastOneCondition(); - this.executeInitialQuery(); - return this.executor.toObject(); - } - - private void executeInitialQuery() { - if (this.cache.storage.hasCachedRecords() || this.initialQuery == null) { - return; - } - this.cache.storage.putInitialRecordsToCache(this.initialQuery.toList()); - } - - public Cacheable byId(SObject record) { - return byId(record.Id); - } - - public Cacheable byId(Id recordId) { - whereEqual('Id', recordId); - return this; - } - - private class CacheStorageProxy { - private CacheManager.Cacheable storage = CacheManager.ApexTransaction; - private String cacheKey; - - public CacheStorageProxy(String ofObject) { - this.cacheKey = this.getAlphanumericKeyFromObjectApiName(ofObject); - } - - private String getAlphanumericKeyFromObjectApiName(String ofObject) { - return Pattern.compile('[^a-zA-Z0-9]').matcher(ofObject).replaceAll(''); - } - - public CacheStorageProxy apexTransaction() { - this.storage = CacheManager.ApexTransaction; - return this; - } - - public CacheStorageProxy orgCache() { - this.storage = CacheManager.SOQLOrgCache; - return this; - } - - public CacheStorageProxy sessionCache() { - this.storage = CacheManager.SOQLSessionCache; - return this; - } - - public void putInitialRecordsToCache(List records) { - List cacheRecords = new List(); - - for (SObject record : records) { - cacheRecords.add(new CacheItem(record)); - } - - this.putRecordsToCache(cacheRecords); - } - - public void putRecordsToCache(List records) { - this.storage.put(this.cacheKey, records); - } - - public Boolean hasCachedRecords() { - return this.storage.contains(this.cacheKey); - } - - public List getCachedRecords() { - return (List) (this.storage.get(this.cacheKey) ?? new List()); - } - - public void addRecordsToCache(List recordsToAdd) { - List allCachedRecords = getCachedRecords(); - - for (SObject databaseRecord : recordsToAdd) { - allCachedRecords.add(new CacheItem(databaseRecord)); - } - - putRecordsToCache(allCachedRecords); - } - - public void updateRecordsInCache(List recordsToUpdate) { - List allCachedRecords = getCachedRecords(); - - Map updatedRecordsById = new Map(recordsToUpdate); - - for (CacheItem cachedRecord : allCachedRecords) { - if (updatedRecordsById.containsKey(cachedRecord.id)) { - cachedRecord.record = updatedRecordsById.get(cachedRecord.id); - cachedRecord.cachedDate = System.now(); - } - } - - putRecordsToCache(allCachedRecords); - } - - public void removeRecordsFromCache(List recordsToRemove) { - Set recordsToRemoveIds = new Map(recordsToRemove).keySet(); - - List filteredCachedItems = new List(); - - for (CacheItem cachedRecord : getCachedRecords()) { - if (!recordsToRemoveIds.contains(cachedRecord.id)) { - filteredCachedItems.add(cachedRecord); - } - } - - putRecordsToCache(filteredCachedItems); - } - } - - private class Cache { - public CacheFields fields; - public CacheExpiration expiration; - public CacheFilterGroup filterGroup; - public CacheStorageProxy storage; - - public Cache(String ofObject) { - this.filterGroup = new CacheFilterGroup(ofObject); - this.storage = new CacheStorageProxy(ofObject); - this.fields = new CacheFields(); - this.expiration = new CacheExpiration(); - } - - private List cachedItemsThatMeetCriteria { - get { - if (this.cachedItemsThatMeetCriteria == null) { - this.cachedItemsThatMeetCriteria = this.filterGroup.filter(this.storage.getCachedRecords()); - } - return this.cachedItemsThatMeetCriteria; - } - private set; - } - - public List toList() { - List records = new List(); - - for (CacheItem cachedRecord : this.cachedItemsThatMeetCriteria) { - records.add(cachedRecord.record); - } - - return records; - } - - public void save(List databaseRecords) { - if (this.isRecordMissingFromCache()) { - storage.addRecordsToCache(databaseRecords); - } else if (databaseRecords.isEmpty()) { // record does not exist in database anymore - storage.removeRecordsFromCache(this.toList()); - } else if (this.areRequestedFieldsMissing() || this.areRecordsOutdated()) { - storage.updateRecordsInCache(databaseRecords); - } - } - - public Boolean isRecordMissingFromCache() { - return this.cachedItemsThatMeetCriteria.isEmpty(); - } - - public Boolean areRequestedFieldsMissing() { - return !this.fields.haveAllRequestedFields(this.cachedItemsThatMeetCriteria); - } - - public Boolean areRecordsOutdated() { - return !this.expiration.areRecordsRecentEnough(this.cachedItemsThatMeetCriteria); - } - } - - @TestVisible - private class CacheItem { - public Id id; - public DateTime cachedDate; - public SObject record; - - public CacheItem(SObject record) { - this.id = record.Id; - this.cachedDate = System.now(); - this.record = record; - } - } - - private class CacheFilterGroup { - private String ofObject; - - private List filters = new List(); - private List additionalAllowedConditionFields = new List(); - - private Boolean allowFilteringByNonUniqueFields = false; - private Boolean allowQueryWithoutConditions = false; - - public CacheFilterGroup(String ofObject) { - this.ofObject = ofObject; - } - - public void add(CacheFilter filter) { - this.filters.add(filter); - } - - public void setAdditionalAllowedConditionFields(List additionalAllowedConditionFields) { - for (SObjectField field : additionalAllowedConditionFields) { - this.additionalAllowedConditionFields.add(field.toString()); - } - } - - public void allowFilteringByNonUniqueFields() { - this.allowFilteringByNonUniqueFields = true; - } - - public void allowQueryWithoutConditions() { - this.allowQueryWithoutConditions = true; - } - - public void validateUniqueCondition(CacheFilter filter) { - if (this.allowFilteringByNonUniqueFields) { - return; - } - - if (!(new List{ 'Id', 'Name', 'DeveloperName' }.contains(filter.field) || this.additionalAllowedConditionFields.contains(filter.field) || this.isFieldUnique(filter.field))) { - throw new SoqlCacheException('A cached query can be filtered only by Id, Name, DeveloperName, or a unique field. You can ignore this validation by calling allowFilteringByNonUniqueFields()'); - } - } - - public void validateIfQueryHasAtLeastOneCondition() { - if (!this.allowQueryWithoutConditions && this.filters.isEmpty()) { - throw new SoqlCacheException('A condition is missing. Please provide a filter to retrieve the cached record. You can ignore this validation by calling allowQueryWithoutConditions()'); - } - } - - private void validateConditions() { - for (CacheFilter filter : this.filters) { - this.validateUniqueCondition(filter); - } - } - - private Boolean isFieldUnique(String field) { - return Schema.getGlobalDescribe().get(this.ofObject).getDescribe().fields.getMap().get(field).getDescribe().isUnique(); - } - - public List filter(List cachedItems) { - this.validateConditions(); - - List cacheItemsThatMeetCriteria = new List(); - - for (CacheItem cacheItem : cachedItems) { - Boolean isCachedItemsMatchingAllCriteria = true; - - for (CacheFilter filter : this.filters) { - isCachedItemsMatchingAllCriteria &= filter.isMatching(cacheItem); - } - - if (isCachedItemsMatchingAllCriteria) { - cacheItemsThatMeetCriteria.add(cacheItem); - } - } - - return cacheItemsThatMeetCriteria; - } - } - - private class CacheFilter { - public String field; - private Object value; - - public CacheFilter with(String field) { - this.field = field; - return this; - } - - public CacheFilter equal(Object value) { - this.value = value; - return this; - } - - public Boolean isMatching(CacheItem cachedItem) { - return cachedItem.record.get(this.field) == this.value; - } - } - - private class CacheFields { - private Set cachedFields = new Set(); - private Set cachedRelationshipFields = new Set(); - - public void with(String commaSeparatedFields) { - for (String field : commaSeparatedFields.split(',')) { - this.classifyField(field); - } - } - - private void classifyField(String field) { - String trimmedField = field.trim(); - - if (trimmedField.contains('.')) { - this.cachedRelationshipFields.add(trimmedField); - } else { - this.cachedFields.add(trimmedField); - } - } - - public void with(SObjectField field) { - this.cachedFields.add(field.toString()); - } - - public void with(List fields) { - for (SObjectField field : fields) { - this.cachedFields.add(field.toString()); - } - } - - public void with(String relationshipName, SObjectField field) { - this.cachedRelationshipFields.add(relationshipName + '.' + field.toString()); - } - - public void with(String relationshipName, Iterable fields) { - for (SObjectField field : fields) { - this.with(relationshipName, field); - } - } - - public Boolean haveAllRequestedFields(List cachedItems) { - for (CacheItem cachedItem : cachedItems) { - if (!this.hasAllRequestedPlainFields(cachedItem.record) || - !this.hasAllRequestedRelationshipFields(cachedItem.record)) { - return false; - } - } - return true; - } - - private Boolean hasAllRequestedPlainFields(SObject record) { - for (String field : this.cachedFields) { - if (!record.isSet(field)) { - return false; - } - } - - return true; - } - - private Boolean hasAllRequestedRelationshipFields(SObject record) { - for (String relationship : this.cachedRelationshipFields) { - if (!this.isRelationshipFieldSet(record, relationship)) { - return false; - } - } - - return true; - } - - private Boolean isRelationshipFieldSet(SObject record, String relationship) { - List relationshipPath = relationship.split('\\.'); - - String targetField = relationshipPath.remove(relationshipPath.size() - 1); - SObject targetRelationshipSObject = this.getTargetRelationshipSObject(record, relationshipPath); - - return targetRelationshipSObject == null ? false : targetRelationshipSObject.isSet(targetField); - } - - private SObject getTargetRelationshipSObject(SObject record, List relationshipPath) { - SObject currentRecord = record; - - for (String relationshipField : relationshipPath) { - try { - currentRecord = currentRecord.getSObject(relationshipField); - } catch (System.SObjectException e) { - // Calling getSObject() on a SObject throws an exception if the field is not queried - // We need to return null to avoid what means that the relationship field is not set - return null; - } - } - - return currentRecord; - } - } - - private class CacheExpiration { - private Integer maxRecordAgeInHours = 48; - - public void maxRecordAgeInHours(Integer hours) { - this.maxRecordAgeInHours = hours; - } - - public Boolean areRecordsRecentEnough(List cachedItems) { - for (CacheItem cachedItem : cachedItems) { - if (!isRecentEnough(cachedItem)) { - return false; - } - } - - return true; - } - - public Boolean isRecentEnough(CacheItem cachedItem) { - Decimal recordAgeInHoursSinceCached = (System.now().getTime() - cachedItem.cachedDate.getTime()) / 3600000; - return recordAgeInHoursSinceCached <= this.maxRecordAgeInHours; - } - } - - public class SoqlMock implements Mockable { - private List mockedRecords = new List(); - - public void thenReturn(SObject record) { - this.mockedRecords.add(record); - } - - public List getMockedResult() { - return mockedRecords; - } - } - - private inherited sharing class Executor { - private SOQL.Queryable currentQuery; - private Cache cache; - private AccessType accessType = null; - private List mocks = new List(); - - public Executor(SOQL.Queryable currentQuery, Cache cache) { - this.currentQuery = currentQuery; - this.cache = cache; - } - - public void stripInaccessible(AccessType type) { - this.accessType = type; - } - - public void mock(List mocks) { - this.mocks = mocks ?? new List(); - } - - public SObject toObject() { - List records = this.toList(); - - 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]; - } - - private List toList() { - if (!this.mocks.isEmpty()) { - return this.getMockedList(); - } - - List records = this.cache.toList(); - - if ( - this.cache.isRecordMissingFromCache() || - this.cache.areRequestedFieldsMissing() || - this.cache.areRecordsOutdated() - ) { - records = this.currentQuery.toList(); // SOQL query execution - cache.save(records); - } - - if (this.accessType == null) { - return records; - } - - return System.Security.stripInaccessible(this.accessType, records).getRecords(); - } - - private List getMockedList() { - if (this.mocks.size() == 1) { - return this.mocks[0].getMockedResult(); - } - return this.mocks.remove(0).getMockedResult(); - } - } - - public class SoqlCacheException extends Exception {} + public interface Selector { + Cacheable query(); + } + + public static Cacheable of(SObjectType ofObject) { + return new SOQLCache(ofObject); + } + + public static Cacheable of(String ofObject) { + return new SOQLCache(ofObject); + } + + public interface Cacheable { + // CONFIG + Cacheable cacheInApexTransaction(); + Cacheable cacheInOrgCache(); + Cacheable cacheInSessionCache(); + Cacheable maxHoursWithoutRefresh(Integer hours); + Cacheable allowFilteringByNonUniqueFields(); + Cacheable allowQueryWithoutConditions(); + // 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 fields); + Cacheable with(String fields); + Cacheable with(String relationshipName, SObjectField field); + Cacheable with(String relationshipName, SObjectField field1, SObjectField field2); + Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3); + Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); + Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); + Cacheable with(String relationshipName, Iterable 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(); + Id toIdOf(SObjectField field); + Boolean doExist(); + SObject toObject(); + Object toValueOf(SObjectField fieldToExtract); + } + + protected virtual SOQL.Queryable initialQuery() { + return null; + } + + public virtual List additionalAllowedConditionFields() { + return new List(); + } + + // Mocking + + public interface Mockable { + // SObject + void thenReturn(SObject record); + } + + @TestVisible + private static Mockable mock(String mockId) { + if (!SOQLCache.queryIdToMock.containsKey(mockId)) { + SOQLCache.queryIdToMock.put(mockId, new List()); + } + SOQLCache.queryIdToMock.get(mockId).add(new SoqlMock()); + return SOQLCache.queryIdToMock.get(mockId).get(SOQLCache.queryIdToMock.get(mockId).size() - 1); + } + + public static void removeFromCache(List records) { + if (records.isEmpty()) { + return; + } + + String ofObject = records[0].getSObjectType().toString(); + CacheStorageProxy proxy = new CacheStorageProxy(ofObject); + + // Record deletion will trigger an automatic cache refresh when the query is executed. + proxy.apexTransaction().removeRecordsFromCache(records); + proxy.orgCache().removeRecordsFromCache(records); + proxy.sessionCache().removeRecordsFromCache(records); + } + + // Backward support + + @TestVisible // deprecated + private static void setMock(String mockId, SObject record) { + SOQLCache.queryIdToMock.put(mockId, new List{ new SoqlMock() }); + SOQLCache.queryIdToMock.get(mockId).get(SOQLCache.queryIdToMock.get(mockId).size() - 1).thenReturn(record); + } + + // Implementation + + private static Map> queryIdToMock = new Map>(); + + private Executor executor; + private Cache cache; + + public SOQL.Queryable initialQuery = null; + public SOQL.Queryable currentQuery = null; + + protected SOQLCache(SObjectType ofObject) { + this(ofObject.toString()); + } + + protected SOQLCache(String ofObject) { + this.initialQuery = this.initialQuery()?.systemMode()?.withoutSharing(); + this.currentQuery = SOQL.of(ofObject).systemMode().withoutSharing(); + + this.cache = new Cache(ofObject); + this.cache.filterGroup.setAdditionalAllowedConditionFields(this.additionalAllowedConditionFields()); + this.executor = new Executor(this.currentQuery, this.cache); + + this.with('Id'); + } + + public Cacheable cacheInApexTransaction() { + this.cache.storage.apexTransaction(); + return this; + } + + public Cacheable cacheInOrgCache() { + this.cache.storage.orgCache(); + return this; + } + + public Cacheable cacheInSessionCache() { + this.cache.storage.sessionCache(); + return this; + } + + public Cacheable maxHoursWithoutRefresh(Integer hours) { + this.cache.expiration.maxRecordAgeInHours(hours); + return this; + } + + public Cacheable allowFilteringByNonUniqueFields() { + this.cache.filterGroup.allowFilteringByNonUniqueFields(); + return this; + } + + public Cacheable allowQueryWithoutConditions() { + this.cache.filterGroup.allowQueryWithoutConditions(); + return this; + } + + public Cacheable with(SObjectField field) { + this.initialQuery?.with(field); + this.currentQuery.with(field); + this.cache.fields.with(field); + return this; + } + + public Cacheable with(SObjectField field1, SObjectField field2) { + return this.with(field1).with(field2); + } + + public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3) { + return this.with(field1, field2).with(field3); + } + + public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { + return this.with(field1, field2, field3).with(field4); + } + + public Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { + return this.with(field1, field2, field3, field4).with(field5); + } + + public Cacheable with(List fields) { + this.initialQuery?.with(fields); + this.currentQuery.with(fields); + this.cache.fields.with(fields); + return this; + } + + public Cacheable with(String fields) { + this.initialQuery?.with(fields); + this.currentQuery.with(fields); + this.cache.fields.with(fields); + return this; + } + + public Cacheable with(String relationshipName, SObjectField field) { + this.initialQuery?.with(relationshipName, field); + this.currentQuery.with(relationshipName, field); + this.cache.fields.with(relationshipName, field); + return this; + } + + public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2) { + return this.with(relationshipName, new List{ field1, field2 }); + } + + public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3) { + return this.with(relationshipName, new List{ field1, field2, field3 }); + } + + public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { + return this.with(relationshipName, new List{ field1, field2, field3, field4 }); + } + + public Cacheable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { + return this.with(relationshipName, new List{ field1, field2, field3, field4, field5 }); + } + + public Cacheable with(String relationshipName, Iterable fields) { + this.initialQuery?.with(relationshipName, fields); + this.currentQuery.with(relationshipName, fields); + this.cache.fields.with(relationshipName, fields); + return this; + } + + public Cacheable whereEqual(SObjectField field, Object value) { + return this.whereEqual(field.toString(), value); + } + + public Cacheable whereEqual(String field, Object value) { + this.with(field); + this.cache.filterGroup.add(new CacheFilter().with(field).equal(value)); + this.currentQuery.whereAre(SOQL.Filter.with(field).equal(value)); + return this; + } + + public Cacheable stripInaccessible() { + return this.stripInaccessible(AccessType.READABLE); + } + + public Cacheable stripInaccessible(AccessType accessType) { + this.currentQuery.stripInaccessible(accessType); + this.executor.stripInaccessible(accessType); + return this; + } + + public Cacheable mockId(String queryIdentifier) { + this.currentQuery.mockId(queryIdentifier); + this.executor.mock(queryIdToMock.get(queryIdentifier)); + return this; + } + + public Cacheable preview() { + this.currentQuery.preview(); + return this; + } + + public Id toId() { + return this.toObject()?.Id; + } + + public Id toIdOf(SObjectField field) { + return (Id) this.toObject()?.get(field); + } + + public Boolean doExist() { + return this.toObject() != null; + } + + public Object toValueOf(SObjectField fieldToExtract) { + this.with(fieldToExtract); + return this.toObject()?.get(fieldToExtract); + } + + public SObject toObject() { + this.cache.filterGroup.validateIfQueryHasAtLeastOneCondition(); + this.executeInitialQuery(); + return this.executor.toObject(); + } + + private void executeInitialQuery() { + if (this.cache.storage.hasCachedRecords() || this.initialQuery == null) { + return; + } + this.cache.storage.putInitialRecordsToCache(this.initialQuery.toList()); + } + + public Cacheable byId(SObject record) { + return byId(record.Id); + } + + public Cacheable byId(Id recordId) { + whereEqual('Id', recordId); + return this; + } + + private class CacheStorageProxy { + private CacheManager.Cacheable storage = CacheManager.ApexTransaction; + private String cacheKey; + + public CacheStorageProxy(String ofObject) { + this.cacheKey = this.getAlphanumericKeyFromObjectApiName(ofObject); + } + + private String getAlphanumericKeyFromObjectApiName(String ofObject) { + return Pattern.compile('[^a-zA-Z0-9]').matcher(ofObject).replaceAll(''); + } + + public CacheStorageProxy apexTransaction() { + this.storage = CacheManager.ApexTransaction; + return this; + } + + public CacheStorageProxy orgCache() { + this.storage = CacheManager.SOQLOrgCache; + return this; + } + + public CacheStorageProxy sessionCache() { + this.storage = CacheManager.SOQLSessionCache; + return this; + } + + public void putInitialRecordsToCache(List records) { + List cacheRecords = new List(); + + for (SObject record : records) { + cacheRecords.add(new CacheItem(record)); + } + + this.putRecordsToCache(cacheRecords); + } + + public void putRecordsToCache(List records) { + this.storage.put(this.cacheKey, records); + } + + public Boolean hasCachedRecords() { + return this.storage.contains(this.cacheKey); + } + + public List getCachedRecords() { + return (List) (this.storage.get(this.cacheKey) ?? new List()); + } + + public void addRecordsToCache(List recordsToAdd) { + List allCachedRecords = getCachedRecords(); + + for (SObject databaseRecord : recordsToAdd) { + allCachedRecords.add(new CacheItem(databaseRecord)); + } + + putRecordsToCache(allCachedRecords); + } + + public void updateRecordsInCache(List recordsToUpdate) { + List allCachedRecords = getCachedRecords(); + + Map updatedRecordsById = new Map(recordsToUpdate); + + for (CacheItem cachedRecord : allCachedRecords) { + if (updatedRecordsById.containsKey(cachedRecord.id)) { + cachedRecord.record = updatedRecordsById.get(cachedRecord.id); + cachedRecord.cachedDate = System.now(); + } + } + + putRecordsToCache(allCachedRecords); + } + + public void removeRecordsFromCache(List recordsToRemove) { + Set recordsToRemoveIds = new Map(recordsToRemove).keySet(); + + List filteredCachedItems = new List(); + + for (CacheItem cachedRecord : getCachedRecords()) { + if (!recordsToRemoveIds.contains(cachedRecord.id)) { + filteredCachedItems.add(cachedRecord); + } + } + + putRecordsToCache(filteredCachedItems); + } + } + + private class Cache { + public CacheFields fields; + public CacheExpiration expiration; + public CacheFilterGroup filterGroup; + public CacheStorageProxy storage; + + public Cache(String ofObject) { + this.filterGroup = new CacheFilterGroup(ofObject); + this.storage = new CacheStorageProxy(ofObject); + this.fields = new CacheFields(); + this.expiration = new CacheExpiration(); + } + + private List cachedItemsThatMeetCriteria { + get { + if (this.cachedItemsThatMeetCriteria == null) { + this.cachedItemsThatMeetCriteria = this.filterGroup.filter(this.storage.getCachedRecords()); + } + return this.cachedItemsThatMeetCriteria; + } + private set; + } + + public List toList() { + List records = new List(); + + for (CacheItem cachedRecord : this.cachedItemsThatMeetCriteria) { + records.add(cachedRecord.record); + } + + return records; + } + + public void save(List databaseRecords) { + if (this.isRecordMissingFromCache()) { + storage.addRecordsToCache(databaseRecords); + } else if (databaseRecords.isEmpty()) { // record does not exist in database anymore + storage.removeRecordsFromCache(this.toList()); + } else if (this.areRequestedFieldsMissing() || this.areRecordsOutdated()) { + storage.updateRecordsInCache(databaseRecords); + } + } + + public Boolean isRecordMissingFromCache() { + return this.cachedItemsThatMeetCriteria.isEmpty(); + } + + public Boolean areRequestedFieldsMissing() { + return !this.fields.haveAllRequestedFields(this.cachedItemsThatMeetCriteria); + } + + public Boolean areRecordsOutdated() { + return !this.expiration.areRecordsRecentEnough(this.cachedItemsThatMeetCriteria); + } + } + + @TestVisible + private class CacheItem { + public Id id; + public Datetime cachedDate; + public SObject record; + + public CacheItem(SObject record) { + this.id = record.Id; + this.cachedDate = System.now(); + this.record = record; + } + } + + private class CacheFilterGroup { + private String ofObject; + + private List filters = new List(); + private List additionalAllowedConditionFields = new List(); + + private Boolean allowFilteringByNonUniqueFields = false; + private Boolean allowQueryWithoutConditions = false; + + public CacheFilterGroup(String ofObject) { + this.ofObject = ofObject; + } + + public void add(CacheFilter filter) { + this.filters.add(filter); + } + + public void setAdditionalAllowedConditionFields(List additionalAllowedConditionFields) { + for (SObjectField field : additionalAllowedConditionFields) { + this.additionalAllowedConditionFields.add(field.toString()); + } + } + + public void allowFilteringByNonUniqueFields() { + this.allowFilteringByNonUniqueFields = true; + } + + public void allowQueryWithoutConditions() { + this.allowQueryWithoutConditions = true; + } + + public void validateUniqueCondition(CacheFilter filter) { + if (this.allowFilteringByNonUniqueFields) { + return; + } + + if (!(new List{ 'Id', 'Name', 'DeveloperName' }.contains(filter.field) || this.additionalAllowedConditionFields.contains(filter.field) || this.isFieldUnique(filter.field))) { + throw new SoqlCacheException('A cached query can be filtered only by Id, Name, DeveloperName, or a unique field. You can ignore this validation by calling allowFilteringByNonUniqueFields()'); + } + } + + public void validateIfQueryHasAtLeastOneCondition() { + if (!this.allowQueryWithoutConditions && this.filters.isEmpty()) { + throw new SoqlCacheException('A condition is missing. Please provide a filter to retrieve the cached record. You can ignore this validation by calling allowQueryWithoutConditions()'); + } + } + + private void validateConditions() { + for (CacheFilter filter : this.filters) { + this.validateUniqueCondition(filter); + } + } + + private Boolean isFieldUnique(String field) { + return Schema.getGlobalDescribe().get(this.ofObject).getDescribe().fields.getMap().get(field).getDescribe().isUnique(); + } + + public List filter(List cachedItems) { + this.validateConditions(); + + List cacheItemsThatMeetCriteria = new List(); + + for (CacheItem cacheItem : cachedItems) { + Boolean isCachedItemsMatchingAllCriteria = true; + + for (CacheFilter filter : this.filters) { + isCachedItemsMatchingAllCriteria &= filter.isMatching(cacheItem); + } + + if (isCachedItemsMatchingAllCriteria) { + cacheItemsThatMeetCriteria.add(cacheItem); + } + } + + return cacheItemsThatMeetCriteria; + } + } + + private class CacheFilter { + public String field; + private Object value; + + public CacheFilter with(String field) { + this.field = field; + return this; + } + + public CacheFilter equal(Object value) { + this.value = value; + return this; + } + + public Boolean isMatching(CacheItem cachedItem) { + return cachedItem.record.get(this.field) == this.value; + } + } + + private class CacheFields { + private Set cachedFields = new Set(); + private Set cachedRelationshipFields = new Set(); + + public void with(String commaSeparatedFields) { + for (String field : commaSeparatedFields.split(',')) { + this.classifyField(field); + } + } + + private void classifyField(String field) { + String trimmedField = field.trim(); + + if (trimmedField.contains('.')) { + this.cachedRelationshipFields.add(trimmedField); + } else { + this.cachedFields.add(trimmedField); + } + } + + public void with(SObjectField field) { + this.cachedFields.add(field.toString()); + } + + public void with(List fields) { + for (SObjectField field : fields) { + this.cachedFields.add(field.toString()); + } + } + + public void with(String relationshipName, SObjectField field) { + this.cachedRelationshipFields.add(relationshipName + '.' + field.toString()); + } + + public void with(String relationshipName, Iterable fields) { + for (SObjectField field : fields) { + this.with(relationshipName, field); + } + } + + public Boolean haveAllRequestedFields(List cachedItems) { + for (CacheItem cachedItem : cachedItems) { + if (!this.hasAllRequestedPlainFields(cachedItem.record) || + !this.hasAllRequestedRelationshipFields(cachedItem.record)) { + return false; + } + } + return true; + } + + private Boolean hasAllRequestedPlainFields(SObject record) { + for (String field : this.cachedFields) { + if (!record.isSet(field)) { + return false; + } + } + + return true; + } + + private Boolean hasAllRequestedRelationshipFields(SObject record) { + for (String relationship : this.cachedRelationshipFields) { + if (!this.isRelationshipFieldSet(record, relationship)) { + return false; + } + } + + return true; + } + + private Boolean isRelationshipFieldSet(SObject record, String relationship) { + List relationshipPath = relationship.split('\\.'); + + String targetField = relationshipPath.remove(relationshipPath.size() - 1); + SObject targetRelationshipSObject = this.getTargetRelationshipSObject(record, relationshipPath); + + return targetRelationshipSObject == null ? false : targetRelationshipSObject.isSet(targetField); + } + + private SObject getTargetRelationshipSObject(SObject record, List relationshipPath) { + SObject currentRecord = record; + + for (String relationshipField : relationshipPath) { + try { + currentRecord = currentRecord.getSObject(relationshipField); + } catch (System.SObjectException e) { + // Calling getSObject() on a SObject throws an exception if the field is not queried + // We need to return null to avoid what means that the relationship field is not set + return null; + } + } + + return currentRecord; + } + } + + private class CacheExpiration { + private Integer maxRecordAgeInHours = 48; + + public void maxRecordAgeInHours(Integer hours) { + this.maxRecordAgeInHours = hours; + } + + public Boolean areRecordsRecentEnough(List cachedItems) { + for (CacheItem cachedItem : cachedItems) { + if (!isRecentEnough(cachedItem)) { + return false; + } + } + + return true; + } + + public Boolean isRecentEnough(CacheItem cachedItem) { + Decimal recordAgeInHoursSinceCached = (System.now().getTime() - cachedItem.cachedDate.getTime()) / 3600000; + return recordAgeInHoursSinceCached <= this.maxRecordAgeInHours; + } + } + + public class SoqlMock implements Mockable { + private List mockedRecords = new List(); + + public void thenReturn(SObject record) { + this.mockedRecords.add(record); + } + + public List getMockedResult() { + return mockedRecords; + } + } + + private inherited sharing class Executor { + private SOQL.Queryable currentQuery; + private Cache cache; + private AccessType accessType = null; + private List mocks = new List(); + + public Executor(SOQL.Queryable currentQuery, Cache cache) { + this.currentQuery = currentQuery; + this.cache = cache; + } + + public void stripInaccessible(AccessType type) { + this.accessType = type; + } + + public void mock(List mocks) { + this.mocks = mocks ?? new List(); + } + + public SObject toObject() { + List records = this.toList(); + + 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]; + } + + private List toList() { + if (!this.mocks.isEmpty()) { + return this.getMockedList(); + } + + List records = this.cache.toList(); + + if ( + this.cache.isRecordMissingFromCache() || + this.cache.areRequestedFieldsMissing() || + this.cache.areRecordsOutdated() + ) { + records = this.currentQuery.toList(); // SOQL query execution + cache.save(records); + } + + if (this.accessType == null) { + return records; + } + + return System.Security.stripInaccessible(this.accessType, records).getRecords(); + } + + private List getMockedList() { + if (this.mocks.size() == 1) { + return this.mocks[0].getMockedResult(); + } + return this.mocks.remove(0).getMockedResult(); + } + } + + public class SoqlCacheException extends Exception {} } diff --git a/force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls b/force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls index 45fc773b..8014f239 100644 --- a/force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls +++ b/force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls @@ -1,1196 +1,1197 @@ /** + * @description Tests for the SOQLCache class. * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class * - CognitiveComplexity: It is a library and we tried to put everything into ONE class - * - ApexDoc: Variable names are self-documented. **/ -@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ApexDoc') +@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity') @IsTest private class SOQLCache_Test { - private static final String INITIAL_QUERY_MOCK_ID = 'cachedProfile'; - private static final String SYSTEM_ADMINISTRATOR = 'System Administrator'; - private static final String STANDARD_USER = 'Standard User'; - - @IsTest - static void initialQuery() { - // Setup - List mockedProfiles = new List{ - new Profile(Name = SYSTEM_ADMINISTRATOR), - new Profile(Name = STANDARD_USER) - }; - - SOQL.mock(INITIAL_QUERY_MOCK_ID).thenReturn(mockedProfiles); - - // Test - new SOQL_ProfileCache().whereEqual('Name', SYSTEM_ADMINISTRATOR).toObject(); // initial query will be executed - List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); - - // Verify - Assert.areEqual(mockedProfiles.size(), cachedProfiles.size(), 'The cached profiles should be identical to those in the initial query.'); - } - - @IsTest - static void noInitialQuery() { - // Test - new SOQL_ProfileCacheDefault().whereEqual('Name', SYSTEM_ADMINISTRATOR).toObject(); - List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); - Profile cachedProfile = (Profile) cachedProfiles[0].record; - - // Verify - Assert.areEqual(1, cachedProfiles.size(), 'Only one record should be cached.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The System Administrator profile should be cached.'); - } - - @IsTest - static void additionalAllowedConditionFields() { - // Setup - SOQLCache.SoqlCacheException soqlException; - - // Test - try { - User user = (User) new SOQL_UserCache().whereEqual(User.Username, 'test@test.com').toObject(); - } catch (SOQLCache.SoqlCacheException e) { - soqlException = e; - } - - // Verify - Assert.isNull(soqlException, 'An exception should not be thrown, because Username is an additional allowed condition field.'); - } - - @IsTest - static void multipleSelectorInvocation() { - // Test - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'Only the first query should be executed to populate data in the cache.'); - } - - @IsTest - static void multipleSelectorInvocationWithDifferentConditions() { - // Test - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, STANDARD_USER).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); - SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, STANDARD_USER).toObject(); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Only the one query for each condition should be executed.'); - } - - @IsTest - static void multipleSelectorInvocationWhenPlainFieldsWereAlreadySet() { - // Test - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'Only the one query should be executed, because the first query set all necessary fields.'); - } - - @IsTest - static void multipleSelectorInvocationWhenPlainFieldsAreMissing() { - // Test - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name, Profile.UserType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be executed, because the first query haven\'t set all necessary fields.'); - } - - @IsTest - static void multipleSelectorInvocationWhenRelationshipFieldsWereAlreadySet() { - // Test - SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'Only the one query should be executed, because the first query set all necessary fields.'); - } - - @IsTest - static void multipleSelectorInvocationWhenRelationshipFieldsAreMissing() { - // Test - SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be executed, because the first query haven\'t set all necessary fields.'); - } - - @IsTest - static void ofString() { - // Test - Profile profile = (Profile) SOQLCache.of('Profile'). - with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void cacheInApexTransaction() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .cacheInApexTransaction() - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); - Profile cachedProfile = (Profile) cachedProfiles[0].record; - - // Verify - Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); - Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void cacheInOrgCache() { - // Test - SOQLCache.of(Profile.SObjectType) - .cacheInOrgCache() - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - List cachedProfiles = (List) CacheManager.SOQLOrgCache.get('Profile'); - Profile cachedProfile = (Profile) cachedProfiles[0].record; - - // Verify - Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); - Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void cacheInSessionCache() { - // Test - SOQLCache.of(Profile.SObjectType) - .cacheInSessionCache() - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - List cachedProfiles = (List) CacheManager.SOQLSessionCache.get('Profile'); - Profile cachedProfile = (Profile) cachedProfiles[0].record; - - // Verify - Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); - Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void maxHoursWithoutRefreshRecentRecord() { - // Setup - SOQLCache.CacheItem cachedItem = new SOQLCache.CacheItem([ - SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR LIMIT 1 - ]); - - CacheManager.ApexTransaction.put('Profile', new List{ cachedItem }); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .maxHoursWithoutRefresh(5) - .toObject(); - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'One queries should be issued. The second query should retrieve record from cache.'); - Assert.isNotNull(profile, 'Profile should be not null.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void maxHoursWithoutRefreshOldRecord() { - // Setup - SOQLCache.CacheItem cachedItem = new SOQLCache.CacheItem([ - SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR LIMIT 1 - ]); - cachedItem.cachedDate = DateTime.now().addHours(-6); - - CacheManager.ApexTransaction.put('Profile', new List{ cachedItem }); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .maxHoursWithoutRefresh(3) - .toObject(); - - List updatedCacheItems = (List) CacheManager.ApexTransaction.get('Profile'); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should update cached record.'); - Assert.areEqual(1, updatedCacheItems.size(), 'The Apex transaction cache should contain exactly one record.'); - Assert.isTrue(Math.abs((System.now().getTime() - updatedCacheItems[0].cachedDate.getTime()) / 1000) < 10, 'The cached record should be updated. The time difference should be less than 10 seconds.'); - Assert.isNotNull(profile, 'Profile should be not null.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void maxHoursWithoutRefreshRecordDoesNotExistAnymore() { - // Setup - SOQLCache.CacheItem cachedItem1 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'ProfileNotExistName')); - cachedItem1.cachedDate = DateTime.now().addHours(-6); - SOQLCache.CacheItem cachedItem2 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR)); - - CacheManager.ApexTransaction.put('Profile', new List{ cachedItem1, cachedItem2 }); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, 'ProfileNotExistName') - .maxHoursWithoutRefresh(3) - .toObject(); - - List updatedCacheItems = (List) CacheManager.ApexTransaction.get('Profile'); - Profile cachedProfile = (Profile) updatedCacheItems[0].record; - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'One queries should be issued.'); - Assert.areEqual(1, updatedCacheItems.size(), 'The Apex transaction cache should contain exactly one record.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void allowQueryWithoutConditions() { - // Setup - insert new Account(Name = 'Test Account'); - - // Test - Account account = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.Name) - .allowQueryWithoutConditions() - .toObject(); - - // Verify - Assert.isNotNull(account, 'The account should be not null'); - Assert.areEqual('Test Account', account.Name, 'The account name should be "Test Account"'); - } - - @IsTest - static void allowFilteringByNonUniqueFields() { - // Setup - insert new Account(Name = 'Test Account'); - - // Setup - Account account = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.Name) - .whereEqual(Account.Name, 'Test Account') - .allowFilteringByNonUniqueFields() - .toObject(); - - // Verify - Assert.isNotNull(account, 'The account should be not null'); - Assert.areEqual('Test Account', account.Name, 'The account name should be "Test Account"'); - } - - @IsTest - static void listHasMoreThanOneRowForAssignmentToSObject() { - // Setup - System.QueryException queryException = null; - - // Test - try { - SOQLCache.of(Profile.SObjectType) - .allowQueryWithoutConditions() - .toObject(); - } catch (System.QueryException e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'An exception should be thrown'); - Assert.areEqual(queryException.getMessage(), 'List has more than 1 row for assignment to SObject', 'The exception message should be "List has more than 1 row for assignment to SObject"'); - } - - @IsTest - static void withOneField() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be null.'); - } - - @IsTest - static void withTwoFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); - } - - - @IsTest - static void withThreeFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name, Profile.UserType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); - Assert.isTrue(profile.isSet('UserType'), 'The profile Name should not be set.'); - } - - - @IsTest - static void withFourFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name, Profile.UserType, Profile.UserLicenseId) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); - Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); - Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); - } - - @IsTest - static void withFiveFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name, Profile.UserType, Profile.Description, Profile.UserLicenseId) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); - Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); - Assert.isTrue(profile.isSet('Description'), 'The profile Description should not be set.'); - Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); - } - - @IsTest - static void withListOfSObjectFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(new List{ - Profile.Id, - Profile.Name, - Profile.UserType, - Profile.Description, - Profile.UserLicenseId - }) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); - Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); - Assert.isTrue(profile.isSet('Description'), 'The profile Description should not be set.'); - Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); - } - - @IsTest - static void withCommaSeparatedFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.Id, 'The profile Id should not be null.'); - Assert.isNotNull(profile.Name, 'The profile Name should not be null.'); - Assert.isNotNull(profile.UserType, 'The profile UserType should not be null.'); - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - } - - @IsTest - static void withOneRelationshipField() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - } - - @IsTest - static void withTwoRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - } - - @IsTest - static void withThreeRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); - } - - @IsTest - static void withFourRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel, UserLicense.LicenseDefinitionKey) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); - Assert.isNotNull(profile.UserLicense.LicenseDefinitionKey, 'The profile UserLicense.LicenseDefinitionKey should not be null.'); - } - - @IsTest - static void withFiveRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel, UserLicense.LicenseDefinitionKey, UserLicense.TotalLicenses) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); - Assert.isNotNull(profile.UserLicense.LicenseDefinitionKey, 'The profile UserLicense.LicenseDefinitionKey should not be null.'); - Assert.isNotNull(profile.UserLicense.TotalLicenses, 'The profile UserLicense.TotalLicenses should not be null.'); - } - - @IsTest - static void withListOfRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', new List{ - UserLicense.Name, - UserLicense.Status, - UserLicense.MasterLabel - }) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); - } - - @IsTest - static void withPlainAndRelationshipFields() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.isNotNull(profile.Id, 'The profile Id should not be null.'); - Assert.isNotNull(profile.Name, 'The profile Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); - Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); - Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); - } - - @IsTest - static void withInvalidRelationshipField() { - // Setup - System.QueryException queryException; - - // Test - try { - // First query to cache plain fields - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .with('UserLicense.InvalidPath', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - } catch (System.QueryException e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'An exception should be thrown when the relationship field is invalid.'); - } - - @IsTest - static void whereEqualSObjectField() { - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); - } - - @IsTest - static void whereEqualStringField() { - // Test - // First query to cache the record - Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual('Name', SYSTEM_ADMINISTRATOR) - .toObject(); - - // Second query to retrieve the cached record - Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual('Name', SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile1.Name, 'The cached profile record should be "System Administrator".'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile2.Name, 'The cached profile record should be "System Administrator".'); - Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); - } - - @IsTest - static void whereEqualIntegerField() { - insertAccount(); - - // Test - // First query to cache the record - Account account1 = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.Name) - .allowFilteringByNonUniqueFields() - .whereEqual(Account.NumberOfEmployees, 1000) - .toObject(); - - // Second query to retrieve the cached record - Account account2 = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.Name) - .allowFilteringByNonUniqueFields() - .whereEqual(Account.NumberOfEmployees, 1000) - .toObject(); - - // Verify - Assert.areEqual(1000, account1.NumberOfEmployees, 'The cached account record should be 1000.'); - Assert.areEqual(1000, account2.NumberOfEmployees, 'The cached account record should be 1000.'); - Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); - } - - @IsTest - static void whereEqualBooleanField() { - insertAccount(); - - // Test - // First query to cache the record - Account account1 = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.IsDeleted) - .allowFilteringByNonUniqueFields() - .whereEqual(Account.IsDeleted, FALSE) - .toObject(); - - // Second query to retrieve the cached record - Account account2 = (Account) SOQLCache.of(Account.SObjectType) - .with(Account.Id, Account.IsDeleted) - .allowFilteringByNonUniqueFields() - .whereEqual(Account.IsDeleted, FALSE) - .toObject(); - - // Verify - Assert.areEqual(FALSE, account1.IsDeleted, 'The cached account record should be FALSE.'); - Assert.areEqual(FALSE, account2.IsDeleted, 'The cached account record should be FALSE.'); - Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); - } - - @IsTest - static void whereEqualNotUniqueField() { - // Setup - SOQLCache.SoqlCacheException soqlException; - - // Test - try { - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.UserType, 'Standard') - .toObject(); - } catch (SOQLCache.SoqlCacheException e) { - soqlException = e; - } - - // Verify - Assert.isNotNull(soqlException, 'An exception should be thrown when the field is not Id, Name, DeveloperName, or a unique field.'); - Assert.areEqual('A cached query can be filtered only by Id, Name, DeveloperName, or a unique field. You can ignore this validation by calling allowFilteringByNonUniqueFields()', soqlException.getMessage(), 'The exception message should be "A cached query can be filtered only by Id, Name, DeveloperName, or a unique field."'); - } - - @IsTest - static void cachedQueryWithoutCondition() { - // Setup - SOQLCache.SoqlCacheException soqlException; - - // Test - try { - SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .toObject(); - } catch (SOQLCache.SoqlCacheException e) { - soqlException = e; - } - - // Verify - Assert.isNotNull(soqlException, 'An exception should be thrown when a condition is missing.'); - Assert.areEqual('A condition is missing. Please provide a filter to retrieve the cached record. You can ignore this validation by calling allowQueryWithoutConditions()', soqlException.getMessage(), 'The exception message should be "A condition is missing. Please provide a filter to retrieve the cached record."'); - } - - @IsTest - static void multipleConditions() { - // Setup - SOQLCache.SoqlCacheException soqlException = null; - - // Test - try { - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .whereEqual(Profile.Id, SOQL.IdGenerator.get(Profile.SObjectType)) - .toObject(); - } catch (SOQLCache.SoqlCacheException e) { - soqlException = e; - } - - // Verify - Assert.isNull(soqlException, 'An exception should not be thrown when more than one condition is introduced.'); - } - - @IsTest - static void stripInaccessible() { - // Setup - Task testTask = new Task(Subject = 'Test Task', Type = 'Other'); - insert testTask; - - System.runAs(minimumAccessUser()) { - // Test - Task cachedTask = (Task) SOQLCache.of(Task.SObjectType) - .with(Task.Id, Task.Type, Task.Subject) - .whereEqual(Task.Id, testTask.Id) - .stripInaccessible() - .toObject(); - - Exception queryException = null; - - try { - String inaccessibleFieldValue = cachedTask.Type; - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.areEqual( - 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', - queryException.getMessage(), - 'The stripInaccessible method should hide the inaccessible field value.' - ); - } - } - - @IsTest - static void mockId() { - // Setup - Id profileId = SOQL.IdGenerator.get(Profile.SObjectType); - SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = SYSTEM_ADMINISTRATOR)); - - // Test - Profile profile = (Profile) new SOQL_ProfileCache().query() - .mockId('ProfileQuery') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(profileId, profile.Id, 'The profile id should be the same as the id assigned to the profile cache.'); - Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - } - - @IsTest - static void mockStack() { - // Setup - SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 1')); - SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 2')); - SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 3')); - - // Test - SOQLCache.Cacheable query = SOQLCache.of(Profile.SObjectType) - .mockId('mockingQuery') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR); - - Profile profile1 = (Profile) query.toObject(); - Profile profile2 = (Profile) query.toObject(); - Profile profile3 = (Profile) query.toObject(); - Profile profile4 = (Profile) query.toObject(); - - // Verify - Assert.areEqual('Test 1', profile1.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - Assert.areEqual('Test 2', profile2.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - Assert.areEqual('Test 3', profile3.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - Assert.areEqual('Test 3', profile4.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - } - - @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') - @IsTest - static void preview() { - // Test - new SOQL_ProfileCache().query() - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .preview() - .toObject(); - - // Verify - impossible to verify system.debug - } - - @IsTest - static void byIdSObject() { - // Setup - Profile systemAdministratorProfile = [SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR]; - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .byId(systemAdministratorProfile) - .toObject(); - - // Verify - Assert.areEqual(systemAdministratorProfile.Id, profile.Id, 'The cached profile id should be equal to the systemAdministratorProfile id.'); - Assert.areEqual(systemAdministratorProfile.Name, profile.Name, 'The cached profile name should be equal to the systemAdministratorProfile name.'); - } - - @IsTest - static void byId() { - // Setup - Profile systemAdministratorProfile = [SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR]; - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .byId(systemAdministratorProfile.Id) - .toObject(); - - // Verify - Assert.areEqual(systemAdministratorProfile.Id, profile.Id, 'The cached profile id should be equal to the systemAdministratorProfile id.'); - Assert.areEqual(systemAdministratorProfile.Name, profile.Name, 'The cached profile name should be equal to the systemAdministratorProfile name.'); - } - - @IsTest - static void toId() { - // Test - Id systemAdministratorProfileId = SOQLCache.of(Profile.SObjectType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toId(); - - // Verify - Assert.isNotNull(systemAdministratorProfileId, 'The System Administrator profile Id must exist.'); - } - - @IsTest - static void toIdOf() { - // Test - Id systemAdministratorProfileId = SOQLCache.of(Profile.SObjectType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toIdOf(Profile.Id); - - // Verify - Assert.isNotNull(systemAdministratorProfileId, 'The System Administrator profile Id must exist.'); - } - - @IsTest - static void doExist() { - // Test - Boolean isProfileExist = SOQLCache.of(Profile.SObjectType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .doExist(); - - // Verify - Assert.isTrue(isProfileExist, 'The System Administrator profile must exist.'); - } - - @IsTest - static void doNotExist() { - // Test - Boolean isProfileExist = SOQLCache.of(Profile.SObjectType) - .whereEqual(Profile.Name, 'System Administrator NotExist') - .doExist(); - - // Verify - Assert.isFalse(isProfileExist, 'The System Administrator NotExist profile must not exist.'); - } - - @IsTest - static void toValueOf() { - // Test - Id profileId = (Id) SOQLCache.of(Profile.SObjectType) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toValueOf(Profile.Id); - - // Verify - Assert.isNotNull(profileId, 'The System Administrator profile Id must exist.'); - } - - @IsTest - static void toObjectWithMultipleRows() { - // Setup - SOQL.mock('ProfileQuery').thenReturn(new List{ - new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR), - new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR) - }); - - QueryException queryException = null; - - // Test - try { - SOQLCache.of(Profile.SObjectType) - .mockId('ProfileQuery') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - } catch (QueryException e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'QueryException should be thrown, because query has more than 1 row for assignment to SObject.'); - Assert.areEqual('List has more than 1 row for assignment to SObject', queryException.getMessage(), 'QueryException message should be "List has more than 1 row for assignment to SObject"'); - } - - @IsTest - static void recordNotFoundInCache() { - // Setup - Id profileId = SOQL.IdGenerator.get(Profile.SObjectType); - SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = 'Guest User')); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .mockId('ProfileQuery') - .whereEqual(Profile.Name, 'Guest User') - .toObject(); - - // Verify - Assert.areEqual(profileId, profile.Id, 'Record not found in cache should be retrieved from SOQL and cached.'); - Assert.areEqual('Guest User', profile.Name, 'Record not found in cache should be retrieved from SOQL and cached.'); - } - - @IsTest - static void recordNotFoundInCacheAndNotExistInDatabase() { - // Setup - SOQL.mock('ProfileQuery').thenReturn(new List()); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .mockId('ProfileQuery') - .whereEqual(Profile.Name, 'Profile That Not Exist') - .toObject(); - - // Verify - Assert.isNull(profile, 'The profile should be null.'); - } - - @IsTest - static void mockEmptyRecord() { - // Setup - SOQLCache.mock('ProfileQuery').thenReturn(null); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .mockId('ProfileQuery') - .whereEqual(Profile.Name, 'Profile That Not Exist') - .toObject(); - - // Verify - Assert.isNull(profile, 'The profile should be null.'); - Assert.areEqual(0, Limits.getQueries(), 'No query should be issued.'); - } - - @IsTest - static void cachedRecordDoesNotHaveNecessaryFields() { - // Test - Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name, Profile.UserType, Profile.UserLicenseId) // more fields - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing fields.'); - - Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); - Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); - - Assert.isTrue(profile1.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile1.isSet('Name'), 'The profile Name should not be set.'); - - Assert.isTrue(profile2.isSet('Id'), 'The profile Id should not be set.'); - Assert.isTrue(profile2.isSet('Name'), 'The profile Name should not be set.'); - Assert.isTrue(profile2.isSet('UserType'), 'The profile UserType should not be set.'); - Assert.isTrue(profile2.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); - } - - @IsTest - static void cachedRecordMissingRelationshipField() { - // Test - Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) - .with('UserLicense', UserLicense.Name, UserLicense.Status) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing fields.'); - - Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); - Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); - - Assert.isNotNull(profile1.UserLicense.Name, 'The profile1 UserLicense.Name should not be null.'); - - Assert.isNotNull(profile2.UserLicense.Name, 'The profile2 UserLicense.Name should not be null.'); - Assert.isNotNull(profile2.UserLicense.Status, 'The profile2 UserLicense.Status should not be null.'); - } - - @IsTest - static void cachedRecordDoesNotHaveRelationshipField() { - // Test - Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .with('UserLicense', UserLicense.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing relationship fields.'); - - Assert.isNotNull(profile1.Id, 'The profile1 Id should not be null.'); - Assert.isNotNull(profile1.Name, 'The profile1 Name should not be null.'); - - Assert.isNotNull(profile2.Id, 'The profile2 Id should not be null.'); - Assert.isNotNull(profile2.Name, 'The profile2 Name should not be null.'); - Assert.isNotNull(profile2.UserLicense.Name, 'The profile2 UserLicense.Name should not be null.'); - } - - @IsTest - static void withCommaSeparatedFieldsWereAlreadySet() { - // Test - Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) - .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) - .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .toObject(); - - // Verify - Assert.areEqual(1, Limits.getQueries(), 'Only one query should be issued. The second query should not be executed, because the first query set all necessary fields.'); - } - - @IsTest - static void recordsClearedFromCache() { - // Setup - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .cacheInOrgCache() - .toObject(); - - // Verify initial setup - Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should exist.'); - Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache item should be present.'); - - // Test - SOQLCache.removeFromCache(new List{ profile }); - - // Verify - Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should still exist.'); - Assert.isTrue(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache items should be empty.'); - } - - @IsTest - static void emptyRecordsClearedFromCache() { - // Setup - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) - .cacheInOrgCache() - .toObject(); - - // Verify initial setup - Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should exist.'); - Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache item should be present.'); - - // Test - SOQLCache.removeFromCache(new List()); - - // Verify - Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should still exist.'); - Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache items should be not empty.'); - } - - @IsTest - static void setMock() { - // Setup - SOQLCache.setMock('ProfileQuery', new Profile(Name = 'Random Profile Name')); - - // Test - Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) - .with(Profile.Id, Profile.Name) - .mockId('ProfileQuery') - .whereEqual(Profile.Name, 'Random Profile Name') - .toObject(); - - // Verify - Assert.areEqual('Random Profile Name', profile.Name, 'The profile name should be the same as the name assigned to the profile cache.'); - } - - static User minimumAccessUser() { - return new User( - Alias = 'newUser', - Email = 'newuser@testorg.com', - EmailEncodingKey = 'UTF-8', - LastName = 'Testing', - LanguageLocaleKey = 'en_US', - LocaleSidKey = 'en_US', - Profile = new Profile(Name = 'Minimum Access - Salesforce'), - TimeZoneSidKey = 'America/Los_Angeles', - UserName = 'queryselector@testorg.com' - ); - } - - static Account insertAccount() { - Account account = new Account(Name = 'Test 1', NumberOfEmployees = 1000); - insert account; - return account; - } - - public class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector { - public SOQL_ProfileCache query() { - return new SOQL_ProfileCache(); - } - - private SOQL_ProfileCache() { - super(Profile.SObjectType); - with(Profile.Id, Profile.Name); - } - - public override SOQL.Queryable initialQuery() { - return SOQL.of(Profile.SObjectType).mockId(INITIAL_QUERY_MOCK_ID).systemMode().withoutSharing(); - } - } - - public class SOQL_ProfileCacheDefault extends SOQLCache implements SOQLCache.Selector { - public SOQL_ProfileCacheDefault query() { - return new SOQL_ProfileCacheDefault(); - } - - private SOQL_ProfileCacheDefault() { - super(Profile.SObjectType); - with(Profile.Id, Profile.Name, Profile.UserType); - } - } - - public class SOQL_UserCache extends SOQLCache implements SOQLCache.Selector { - public SOQL_UserCache query() { - return new SOQL_UserCache(); - } - - private SOQL_UserCache() { - super(User.SObjectType); - with(User.Id, User.Name); - } - - public override List additionalAllowedConditionFields() { - return new List{ User.Username }; - } - } + private static final String INITIAL_QUERY_MOCK_ID = 'cachedProfile'; + private static final String SYSTEM_ADMINISTRATOR = 'System Administrator'; + private static final String STANDARD_USER = 'Standard User'; + + @IsTest + static void initialQuery() { + // Setup + List mockedProfiles = new List{ + new Profile(Name = SYSTEM_ADMINISTRATOR), + new Profile(Name = STANDARD_USER) + }; + + SOQL.mock(INITIAL_QUERY_MOCK_ID).thenReturn(mockedProfiles); + + // Test + new SOQL_ProfileCache().whereEqual('Name', SYSTEM_ADMINISTRATOR).toObject(); // initial query will be executed + List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); + + // Verify + Assert.areEqual(mockedProfiles.size(), cachedProfiles.size(), 'The cached profiles should be identical to those in the initial query.'); + } + + @IsTest + static void noInitialQuery() { + // Test + new SOQL_ProfileCacheDefault().whereEqual('Name', SYSTEM_ADMINISTRATOR).toObject(); + List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); + Profile cachedProfile = (Profile) cachedProfiles[0].record; + + // Verify + Assert.areEqual(1, cachedProfiles.size(), 'Only one record should be cached.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The System Administrator profile should be cached.'); + } + + @IsTest + static void additionalAllowedConditionFields() { + // Setup + SOQLCache.SoqlCacheException soqlException; + + // Test + try { + new SOQL_UserCache().whereEqual(User.Username, 'test@test.com').toObject(); + } catch (SOQLCache.SoqlCacheException e) { + soqlException = e; + } + + // Verify + Assert.isNull(soqlException, 'An exception should not be thrown, because Username is an additional allowed condition field.'); + } + + @IsTest + static void multipleSelectorInvocation() { + // Test + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'Only the first query should be executed to populate data in the cache.'); + } + + @IsTest + static void multipleSelectorInvocationWithDifferentConditions() { + // Test + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, STANDARD_USER).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR).toObject(); + SOQLCache.of(Profile.SObjectType).with(Profile.Id, Profile.Name).whereEqual(Profile.Name, STANDARD_USER).toObject(); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Only the one query for each condition should be executed.'); + } + + @IsTest + static void multipleSelectorInvocationWhenPlainFieldsWereAlreadySet() { + // Test + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'Only the one query should be executed, because the first query set all necessary fields.'); + } + + @IsTest + static void multipleSelectorInvocationWhenPlainFieldsAreMissing() { + // Test + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name, Profile.UserType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be executed, because the first query haven\'t set all necessary fields.'); + } + + @IsTest + static void multipleSelectorInvocationWhenRelationshipFieldsWereAlreadySet() { + // Test + SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'Only the one query should be executed, because the first query set all necessary fields.'); + } + + @IsTest + static void multipleSelectorInvocationWhenRelationshipFieldsAreMissing() { + // Test + SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be executed, because the first query haven\'t set all necessary fields.'); + } + + @IsTest + static void ofString() { + // Test + Profile profile = (Profile) SOQLCache.of('Profile'). + with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void cacheInApexTransaction() { + // Test + SOQLCache.of(Profile.SObjectType) + .cacheInApexTransaction() + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); + Profile cachedProfile = (Profile) cachedProfiles[0].record; + + // Verify + Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); + Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void cacheInOrgCache() { + // Test + SOQLCache.of(Profile.SObjectType) + .cacheInOrgCache() + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + List cachedProfiles = (List) CacheManager.SOQLOrgCache.get('Profile'); + Profile cachedProfile = (Profile) cachedProfiles[0].record; + + // Verify + Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); + Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void cacheInSessionCache() { + // Test + SOQLCache.of(Profile.SObjectType) + .cacheInSessionCache() + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + List cachedProfiles = (List) CacheManager.SOQLSessionCache.get('Profile'); + Profile cachedProfile = (Profile) cachedProfiles[0].record; + + // Verify + Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); + Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void maxHoursWithoutRefreshRecentRecord() { + // Setup + SOQLCache.CacheItem cachedItem = new SOQLCache.CacheItem([ + SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR LIMIT 1 + ]); + + CacheManager.ApexTransaction.put('Profile', new List{ cachedItem }); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .maxHoursWithoutRefresh(5) + .toObject(); + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'One queries should be issued. The second query should retrieve record from cache.'); + Assert.isNotNull(profile, 'Profile should be not null.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void maxHoursWithoutRefreshOldRecord() { + // Setup + SOQLCache.CacheItem cachedItem = new SOQLCache.CacheItem([ + SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR LIMIT 1 + ]); + cachedItem.cachedDate = Datetime.now().addHours(-6); + + CacheManager.ApexTransaction.put('Profile', new List{ cachedItem }); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .maxHoursWithoutRefresh(3) + .toObject(); + + List updatedCacheItems = (List) CacheManager.ApexTransaction.get('Profile'); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should update cached record.'); + Assert.areEqual(1, updatedCacheItems.size(), 'The Apex transaction cache should contain exactly one record.'); + Assert.isTrue(Math.abs((System.now().getTime() - updatedCacheItems[0].cachedDate.getTime()) / 1000) < 10, 'The cached record should be updated. The time difference should be less than 10 seconds.'); + Assert.isNotNull(profile, 'Profile should be not null.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void maxHoursWithoutRefreshRecordDoesNotExistAnymore() { + // Setup + SOQLCache.CacheItem cachedItem1 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'ProfileNotExistName')); + cachedItem1.cachedDate = Datetime.now().addHours(-6); + SOQLCache.CacheItem cachedItem2 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR)); + + CacheManager.ApexTransaction.put('Profile', new List{ cachedItem1, cachedItem2 }); + + // Test + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, 'ProfileNotExistName') + .maxHoursWithoutRefresh(3) + .toObject(); + + List updatedCacheItems = (List) CacheManager.ApexTransaction.get('Profile'); + Profile cachedProfile = (Profile) updatedCacheItems[0].record; + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'One queries should be issued.'); + Assert.areEqual(1, updatedCacheItems.size(), 'The Apex transaction cache should contain exactly one record.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, cachedProfile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void allowQueryWithoutConditions() { + // Setup + insert new Account(Name = 'Test Account'); + + // Test + Account account = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .allowQueryWithoutConditions() + .toObject(); + + // Verify + Assert.isNotNull(account, 'The account should be not null'); + Assert.areEqual('Test Account', account.Name, 'The account name should be "Test Account"'); + } + + @IsTest + static void allowFilteringByNonUniqueFields() { + // Setup + insert new Account(Name = 'Test Account'); + + // Setup + Account account = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .whereEqual(Account.Name, 'Test Account') + .allowFilteringByNonUniqueFields() + .toObject(); + + // Verify + Assert.isNotNull(account, 'The account should be not null'); + Assert.areEqual('Test Account', account.Name, 'The account name should be "Test Account"'); + } + + @IsTest + static void listHasMoreThanOneRowForAssignmentToSObject() { + // Setup + System.QueryException queryException = null; + + // Test + try { + SOQLCache.of(Profile.SObjectType) + .allowQueryWithoutConditions() + .toObject(); + } catch (System.QueryException e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'An exception should be thrown'); + Assert.areEqual(queryException.getMessage(), 'List has more than 1 row for assignment to SObject', 'The exception message should be "List has more than 1 row for assignment to SObject"'); + } + + @IsTest + static void withOneField() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be null.'); + } + + @IsTest + static void withTwoFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); + } + + + @IsTest + static void withThreeFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name, Profile.UserType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); + Assert.isTrue(profile.isSet('UserType'), 'The profile Name should not be set.'); + } + + + @IsTest + static void withFourFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name, Profile.UserType, Profile.UserLicenseId) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); + Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); + Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); + } + + @IsTest + static void withFiveFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name, Profile.UserType, Profile.Description, Profile.UserLicenseId) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); + Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); + Assert.isTrue(profile.isSet('Description'), 'The profile Description should not be set.'); + Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); + } + + @IsTest + static void withListOfSObjectFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(new List{ + Profile.Id, + Profile.Name, + Profile.UserType, + Profile.Description, + Profile.UserLicenseId + }) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isTrue(profile.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile.isSet('Name'), 'The profile Name should not be set.'); + Assert.isTrue(profile.isSet('UserType'), 'The profile UserType should not be set.'); + Assert.isTrue(profile.isSet('Description'), 'The profile Description should not be set.'); + Assert.isTrue(profile.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); + } + + @IsTest + static void withCommaSeparatedFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.Id, 'The profile Id should not be null.'); + Assert.isNotNull(profile.Name, 'The profile Name should not be null.'); + Assert.isNotNull(profile.UserType, 'The profile UserType should not be null.'); + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + } + + @IsTest + static void withOneRelationshipField() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + } + + @IsTest + static void withTwoRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + } + + @IsTest + static void withThreeRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); + } + + @IsTest + static void withFourRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel, UserLicense.LicenseDefinitionKey) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); + Assert.isNotNull(profile.UserLicense.LicenseDefinitionKey, 'The profile UserLicense.LicenseDefinitionKey should not be null.'); + } + + @IsTest + static void withFiveRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel, UserLicense.LicenseDefinitionKey, UserLicense.TotalLicenses) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); + Assert.isNotNull(profile.UserLicense.LicenseDefinitionKey, 'The profile UserLicense.LicenseDefinitionKey should not be null.'); + Assert.isNotNull(profile.UserLicense.TotalLicenses, 'The profile UserLicense.TotalLicenses should not be null.'); + } + + @IsTest + static void withListOfRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', new List{ + UserLicense.Name, + UserLicense.Status, + UserLicense.MasterLabel + }) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); + } + + @IsTest + static void withPlainAndRelationshipFields() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .with('UserLicense', UserLicense.Name, UserLicense.Status, UserLicense.MasterLabel) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.isNotNull(profile.Id, 'The profile Id should not be null.'); + Assert.isNotNull(profile.Name, 'The profile Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Name, 'The profile UserLicense.Name should not be null.'); + Assert.isNotNull(profile.UserLicense.Status, 'The profile UserLicense.Status should not be null.'); + Assert.isNotNull(profile.UserLicense.MasterLabel, 'The profile UserLicense.MasterLabel should not be null.'); + } + + @IsTest + static void withInvalidRelationshipField() { + // Setup + System.QueryException queryException; + + // Test + try { + // First query to cache plain fields + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .with('UserLicense.InvalidPath', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + } catch (System.QueryException e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'An exception should be thrown when the relationship field is invalid.'); + } + + @IsTest + static void whereEqualSObjectField() { + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The cached profile record should be "System Administrator".'); + } + + @IsTest + static void whereEqualStringField() { + // Test + // First query to cache the record + Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual('Name', SYSTEM_ADMINISTRATOR) + .toObject(); + + // Second query to retrieve the cached record + Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual('Name', SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile1.Name, 'The cached profile record should be "System Administrator".'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile2.Name, 'The cached profile record should be "System Administrator".'); + Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); + } + + @IsTest + static void whereEqualIntegerField() { + insertAccount(); + + // Test + // First query to cache the record + Account account1 = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .allowFilteringByNonUniqueFields() + .whereEqual(Account.NumberOfEmployees, 1000) + .toObject(); + + // Second query to retrieve the cached record + Account account2 = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .allowFilteringByNonUniqueFields() + .whereEqual(Account.NumberOfEmployees, 1000) + .toObject(); + + // Verify + Assert.areEqual(1000, account1.NumberOfEmployees, 'The cached account record should be 1000.'); + Assert.areEqual(1000, account2.NumberOfEmployees, 'The cached account record should be 1000.'); + Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); + } + + @IsTest + static void whereEqualBooleanField() { + insertAccount(); + + // Test + // First query to cache the record + Account account1 = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.IsDeleted) + .allowFilteringByNonUniqueFields() + .whereEqual(Account.IsDeleted, false) + .toObject(); + + // Second query to retrieve the cached record + Account account2 = (Account) SOQLCache.of(Account.SObjectType) + .with(Account.Id, Account.IsDeleted) + .allowFilteringByNonUniqueFields() + .whereEqual(Account.IsDeleted, false) + .toObject(); + + // Verify + Assert.isFalse(account1.IsDeleted, 'The cached account record should be false.'); + Assert.isFalse(account2.IsDeleted, 'The cached account record should be false.'); + Assert.areEqual(1, Limits.getQueries(), 'The number of queries should be 1.'); + } + + @IsTest + static void whereEqualNotUniqueField() { + // Setup + SOQLCache.SoqlCacheException soqlException; + + // Test + try { + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.UserType, 'Standard') + .toObject(); + } catch (SOQLCache.SoqlCacheException e) { + soqlException = e; + } + + // Verify + Assert.isNotNull(soqlException, 'An exception should be thrown when the field is not Id, Name, DeveloperName, or a unique field.'); + Assert.areEqual('A cached query can be filtered only by Id, Name, DeveloperName, or a unique field. You can ignore this validation by calling allowFilteringByNonUniqueFields()', soqlException.getMessage(), 'The exception message should be "A cached query can be filtered only by Id, Name, DeveloperName, or a unique field."'); + } + + @IsTest + static void cachedQueryWithoutCondition() { + // Setup + SOQLCache.SoqlCacheException soqlException; + + // Test + try { + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .toObject(); + } catch (SOQLCache.SoqlCacheException e) { + soqlException = e; + } + + // Verify + Assert.isNotNull(soqlException, 'An exception should be thrown when a condition is missing.'); + Assert.areEqual('A condition is missing. Please provide a filter to retrieve the cached record. You can ignore this validation by calling allowQueryWithoutConditions()', soqlException.getMessage(), 'The exception message should be "A condition is missing. Please provide a filter to retrieve the cached record."'); + } + + @IsTest + static void multipleConditions() { + // Setup + SOQLCache.SoqlCacheException soqlException = null; + + // Test + try { + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .whereEqual(Profile.Id, SOQL.IdGenerator.get(Profile.SObjectType)) + .toObject(); + } catch (SOQLCache.SoqlCacheException e) { + soqlException = e; + } + + // Verify + Assert.isNull(soqlException, 'An exception should not be thrown when more than one condition is introduced.'); + } + + @IsTest + static void stripInaccessible() { + // Setup + Task testTask = new Task(Subject = 'Test Task', Type = 'Other'); + insert testTask; + + System.runAs(minimumAccessUser()) { + // Test + Task cachedTask = (Task) SOQLCache.of(Task.SObjectType) + .with(Task.Id, Task.Type, Task.Subject) + .whereEqual(Task.Id, testTask.Id) + .stripInaccessible() + .toObject(); + + Exception queryException = null; + String inaccessibleFieldValue; + + try { + inaccessibleFieldValue = cachedTask.Type; + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual( + 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', + queryException.getMessage(), + 'The stripInaccessible method should hide the inaccessible field value.' + ); + } + } + + @IsTest + static void mockId() { + // Setup + Id profileId = SOQL.IdGenerator.get(Profile.SObjectType); + SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = SYSTEM_ADMINISTRATOR)); + + // Test + Profile profile = (Profile) new SOQL_ProfileCache().query() + .mockId('ProfileQuery') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(profileId, profile.Id, 'The profile id should be the same as the id assigned to the profile cache.'); + Assert.areEqual(SYSTEM_ADMINISTRATOR, profile.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + } + + @IsTest + static void mockStack() { + // Setup + SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 1')); + SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 2')); + SOQLCache.mock('mockingQuery').thenReturn(new Profile(Name = 'Test 3')); + + // Test + SOQLCache.Cacheable query = SOQLCache.of(Profile.SObjectType) + .mockId('mockingQuery') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR); + + Profile profile1 = (Profile) query.toObject(); + Profile profile2 = (Profile) query.toObject(); + Profile profile3 = (Profile) query.toObject(); + Profile profile4 = (Profile) query.toObject(); + + // Verify + Assert.areEqual('Test 1', profile1.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + Assert.areEqual('Test 2', profile2.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + Assert.areEqual('Test 3', profile3.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + Assert.areEqual('Test 3', profile4.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + } + + @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') + @IsTest + static void preview() { + // Test + new SOQL_ProfileCache().query() + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .preview() + .toObject(); + + // Verify - impossible to verify system.debug + } + + @IsTest + static void byIdSObject() { + // Setup + Profile systemAdministratorProfile = [SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR]; + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .byId(systemAdministratorProfile) + .toObject(); + + // Verify + Assert.areEqual(systemAdministratorProfile.Id, profile.Id, 'The cached profile id should be equal to the systemAdministratorProfile id.'); + Assert.areEqual(systemAdministratorProfile.Name, profile.Name, 'The cached profile name should be equal to the systemAdministratorProfile name.'); + } + + @IsTest + static void byId() { + // Setup + Profile systemAdministratorProfile = [SELECT Id, Name FROM Profile WHERE Name = :SYSTEM_ADMINISTRATOR]; + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .byId(systemAdministratorProfile.Id) + .toObject(); + + // Verify + Assert.areEqual(systemAdministratorProfile.Id, profile.Id, 'The cached profile id should be equal to the systemAdministratorProfile id.'); + Assert.areEqual(systemAdministratorProfile.Name, profile.Name, 'The cached profile name should be equal to the systemAdministratorProfile name.'); + } + + @IsTest + static void toId() { + // Test + Id systemAdministratorProfileId = SOQLCache.of(Profile.SObjectType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toId(); + + // Verify + Assert.isNotNull(systemAdministratorProfileId, 'The System Administrator profile Id must exist.'); + } + + @IsTest + static void toIdOf() { + // Test + Id systemAdministratorProfileId = SOQLCache.of(Profile.SObjectType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toIdOf(Profile.Id); + + // Verify + Assert.isNotNull(systemAdministratorProfileId, 'The System Administrator profile Id must exist.'); + } + + @IsTest + static void doExist() { + // Test + Boolean isProfileExist = SOQLCache.of(Profile.SObjectType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .doExist(); + + // Verify + Assert.isTrue(isProfileExist, 'The System Administrator profile must exist.'); + } + + @IsTest + static void doNotExist() { + // Test + Boolean isProfileExist = SOQLCache.of(Profile.SObjectType) + .whereEqual(Profile.Name, 'System Administrator NotExist') + .doExist(); + + // Verify + Assert.isFalse(isProfileExist, 'The System Administrator NotExist profile must not exist.'); + } + + @IsTest + static void toValueOf() { + // Test + Id profileId = (Id) SOQLCache.of(Profile.SObjectType) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toValueOf(Profile.Id); + + // Verify + Assert.isNotNull(profileId, 'The System Administrator profile Id must exist.'); + } + + @IsTest + static void toObjectWithMultipleRows() { + // Setup + SOQL.mock('ProfileQuery').thenReturn(new List{ + new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR), + new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = SYSTEM_ADMINISTRATOR) + }); + + QueryException queryException = null; + + // Test + try { + SOQLCache.of(Profile.SObjectType) + .mockId('ProfileQuery') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + } catch (QueryException e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'QueryException should be thrown, because query has more than 1 row for assignment to SObject.'); + Assert.areEqual('List has more than 1 row for assignment to SObject', queryException.getMessage(), 'QueryException message should be "List has more than 1 row for assignment to SObject"'); + } + + @IsTest + static void recordNotFoundInCache() { + // Setup + Id profileId = SOQL.IdGenerator.get(Profile.SObjectType); + SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = 'Guest User')); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .mockId('ProfileQuery') + .whereEqual(Profile.Name, 'Guest User') + .toObject(); + + // Verify + Assert.areEqual(profileId, profile.Id, 'Record not found in cache should be retrieved from SOQL and cached.'); + Assert.areEqual('Guest User', profile.Name, 'Record not found in cache should be retrieved from SOQL and cached.'); + } + + @IsTest + static void recordNotFoundInCacheAndNotExistInDatabase() { + // Setup + SOQL.mock('ProfileQuery').thenReturn(new List()); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .mockId('ProfileQuery') + .whereEqual(Profile.Name, 'Profile That Not Exist') + .toObject(); + + // Verify + Assert.isNull(profile, 'The profile should be null.'); + } + + @IsTest + static void mockEmptyRecord() { + // Setup + SOQLCache.mock('ProfileQuery').thenReturn(null); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .mockId('ProfileQuery') + .whereEqual(Profile.Name, 'Profile That Not Exist') + .toObject(); + + // Verify + Assert.isNull(profile, 'The profile should be null.'); + Assert.areEqual(0, Limits.getQueries(), 'No query should be issued.'); + } + + @IsTest + static void cachedRecordDoesNotHaveNecessaryFields() { + // Test + Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name, Profile.UserType, Profile.UserLicenseId) // more fields + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing fields.'); + + Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); + Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); + + Assert.isTrue(profile1.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile1.isSet('Name'), 'The profile Name should not be set.'); + + Assert.isTrue(profile2.isSet('Id'), 'The profile Id should not be set.'); + Assert.isTrue(profile2.isSet('Name'), 'The profile Name should not be set.'); + Assert.isTrue(profile2.isSet('UserType'), 'The profile UserType should not be set.'); + Assert.isTrue(profile2.isSet('UserLicenseId'), 'The profile UserLicenseId should not be set.'); + } + + @IsTest + static void cachedRecordMissingRelationshipField() { + // Test + Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) + .with('UserLicense', UserLicense.Name, UserLicense.Status) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + List cachedProfiles = (List) CacheManager.ApexTransaction.get('Profile'); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing fields.'); + + Assert.isFalse(cachedProfiles.isEmpty(), 'The Apex transaction cache should not be empty.'); + Assert.areEqual(1, cachedProfiles.size(), 'The Apex transaction cache should contain exactly one record.'); + + Assert.isNotNull(profile1.UserLicense.Name, 'The profile1 UserLicense.Name should not be null.'); + + Assert.isNotNull(profile2.UserLicense.Name, 'The profile2 UserLicense.Name should not be null.'); + Assert.isNotNull(profile2.UserLicense.Status, 'The profile2 UserLicense.Status should not be null.'); + } + + @IsTest + static void cachedRecordDoesNotHaveRelationshipField() { + // Test + Profile profile1 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + Profile profile2 = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .with('UserLicense', UserLicense.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(2, Limits.getQueries(), 'Two queries should be issued. The second query should retrieve the missing relationship fields.'); + + Assert.isNotNull(profile1.Id, 'The profile1 Id should not be null.'); + Assert.isNotNull(profile1.Name, 'The profile1 Name should not be null.'); + + Assert.isNotNull(profile2.Id, 'The profile2 Id should not be null.'); + Assert.isNotNull(profile2.Name, 'The profile2 Name should not be null.'); + Assert.isNotNull(profile2.UserLicense.Name, 'The profile2 UserLicense.Name should not be null.'); + } + + @IsTest + static void withCommaSeparatedFieldsWereAlreadySet() { + // Test + SOQLCache.of(Profile.SObjectType) + .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + SOQLCache.of(Profile.SObjectType) + .with('Id, Name, UserType, UserLicense.Name, UserLicense.Status') + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .toObject(); + + // Verify + Assert.areEqual(1, Limits.getQueries(), 'Only one query should be issued. The second query should not be executed, because the first query set all necessary fields.'); + } + + @IsTest + static void recordsClearedFromCache() { + // Setup + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .cacheInOrgCache() + .toObject(); + + // Verify initial setup + Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should exist.'); + Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache item should be present.'); + + // Test + SOQLCache.removeFromCache(new List{ profile }); + + // Verify + Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should still exist.'); + Assert.isTrue(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache items should be empty.'); + } + + @IsTest + static void emptyRecordsClearedFromCache() { + // Setup + SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .whereEqual(Profile.Name, SYSTEM_ADMINISTRATOR) + .cacheInOrgCache() + .toObject(); + + // Verify initial setup + Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should exist.'); + Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache item should be present.'); + + // Test + SOQLCache.removeFromCache(new List()); + + // Verify + Assert.isTrue(CacheManager.SOQLOrgCache.contains('Profile'), 'Key should still exist.'); + Assert.isFalse(((List) CacheManager.SOQLOrgCache.get('Profile')).isEmpty(), 'Cache items should be not empty.'); + } + + @IsTest + static void setMock() { + // Setup + SOQLCache.setMock('ProfileQuery', new Profile(Name = 'Random Profile Name')); + + // Test + Profile profile = (Profile) SOQLCache.of(Profile.SObjectType) + .with(Profile.Id, Profile.Name) + .mockId('ProfileQuery') + .whereEqual(Profile.Name, 'Random Profile Name') + .toObject(); + + // Verify + Assert.areEqual('Random Profile Name', profile.Name, 'The profile name should be the same as the name assigned to the profile cache.'); + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + Profile = new Profile(Name = 'Minimum Access - Salesforce'), + TimeZoneSidKey = 'America/Los_Angeles', + Username = 'queryselector@testorg.com' + ); + } + + static Account insertAccount() { + Account account = new Account(Name = 'Test 1', NumberOfEmployees = 1000); + insert account; + return account; + } + + public class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector { + public SOQL_ProfileCache query() { + return new SOQL_ProfileCache(); + } + + private SOQL_ProfileCache() { + super(Profile.SObjectType); + with(Profile.Id, Profile.Name); + } + + public override SOQL.Queryable initialQuery() { + return SOQL.of(Profile.SObjectType).mockId(INITIAL_QUERY_MOCK_ID).systemMode().withoutSharing(); + } + } + + public class SOQL_ProfileCacheDefault extends SOQLCache implements SOQLCache.Selector { + public SOQL_ProfileCacheDefault query() { + return new SOQL_ProfileCacheDefault(); + } + + private SOQL_ProfileCacheDefault() { + super(Profile.SObjectType); + with(Profile.Id, Profile.Name, Profile.UserType); + } + } + + public class SOQL_UserCache extends SOQLCache implements SOQLCache.Selector { + public SOQL_UserCache query() { + return new SOQL_UserCache(); + } + + private SOQL_UserCache() { + super(User.SObjectType); + with(User.Id, User.Name); + } + + public override List additionalAllowedConditionFields() { + return new List{ User.Username }; + } + } } diff --git a/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator.cls b/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator.cls index d555df82..17d52481 100644 --- a/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator.cls +++ b/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator.cls @@ -2,216 +2,222 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * * PMD False Positives: * - CognitiveComplexity: It is a library and we tried to put everything into ONE class + * - ApexDoc: Variable names are self-documented. **/ -@SuppressWarnings('PMD.CognitiveComplexity') +@SuppressWarnings('PMD.CognitiveComplexity,PMD.ApexDoc') public inherited sharing class SOQLEvaluator { - public static SObjectEvaluable of(List staticQueryRecords) { - return new SObjectEvaluator(staticQueryRecords); - } - - public interface SObjectEvaluable { - // FIELD-LEVEL SECURITY - SObjectEvaluable stripInaccessible(); - SObjectEvaluable stripInaccessible(AccessType accessType); - // MOCKING - SObjectEvaluable mockId(String mockId); - // RESULT - Id toId(); - Set toIds(); - Set toIdsOf(SObjectField field); - Set toIdsOf(String relationshipName, SObjectField field); - Boolean doExist(); - Object toValueOf(SObjectField fieldToExtract); - Set toValuesOf(SObjectField fieldToExtract); - SObject toObject(); - List toList(); - Map toMap(); - Map toMap(SObjectField keyField); - Map toMap(String relationshipName, SObjectField targetKeyField); - Map toMap(SObjectField keyField, SObjectField valueField); - Map> toAggregatedMap(SObjectField keyField); - Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField); - Map> toAggregatedMap(SObjectField keyField, SObjectField valueField); - } - - public interface Mockable { - // SObject - Mockable thenReturn(SObject record); - Mockable thenReturn(List records); - } - - @TestVisible - private static Mockable mock(String mockId) { - if (!SOQLEvaluator.queryIdToMock.containsKey(mockId)) { - SOQLEvaluator.queryIdToMock.put(mockId, new List()); - } - SOQLEvaluator.queryIdToMock.get(mockId).add(new SoqlMock()); - return SOQLEvaluator.queryIdToMock.get(mockId).get(SOQLEvaluator.queryIdToMock.get(mockId).size() - 1); - } - - // Implementation - - private static Map> queryIdToMock = new Map>(); - - private class SObjectEvaluator implements SObjectEvaluable { - private List records; - private AccessType access; - private SOQL.Converter converter; - private List mocks = new List(); - - public SObjectEvaluator(List records) { - this.records = records; - this.converter = new SOQL.Converter(records.getSObjectType().toString()); - } - - public SObjectEvaluable mockId(String mockId) { - this.mocks = SOQLEvaluator.queryIdToMock.get(mockId) ?? new List(); - return this; - } - - public SObjectEvaluable stripInaccessible() { - return this.stripInaccessible(AccessType.READABLE); - } - - public SObjectEvaluable stripInaccessible(AccessType accessType) { - this.access = accessType; - return this; - } - - public Id toId() { - return this.toObject()?.Id; - } - - public Set toIds() { - return new Map(this.toList()).keySet(); - } - - public Set toIdsOf(SObjectField field) { - return this.converter.transform(this.toList()).toIdsOf(field); - } - - public Set toIdsOf(String relationshipName, SObjectField field) { - return this.converter.transform(this.toList()).toIdsOf(relationshipName, field); - } - - public Boolean doExist() { - return !this.toList().isEmpty(); - } - - public Object toValueOf(SObjectField fieldToExtract) { - return this.toObject()?.get(fieldToExtract); - } - - public Set toValuesOf(SObjectField fieldToExtract) { - return this.converter.transform(this.toList()).toValuesOf(fieldToExtract); - } - - public SObject toObject() { - List records = this.toList(); - - if (records.isEmpty()) { - return null; // handle: List has no rows for assignment to SObject - } - - if (records.size() > 1) { - throw new QueryException('List has more than 1 row for assignment to SObject'); - } - - return records[0]; - } - - public List toList() { - if (!this.mocks.isEmpty()) { - return this.getMockedListProxy(); - } - - if (this.access == null) { - return this.records; - } - - return System.Security.stripInaccessible(this.access, this.records).getRecords(); - } - - private List getMockedListProxy() { - if (this.mocks.size() == 1) { - return this.mocks[0].sObjectMock.get(); - } - return this.mocks.remove(0).sObjectMock.get(); - } - - public Map toMap() { - return this.converter.transform(this.toList()).toMap(); - } - - public Map toMap(SObjectField keyField) { - return this.converter.transform(this.toList()).toMap(keyField); - } - - public Map toMap(String relationshipName, SObjectField targetKeyField) { - return this.converter.transform(this.toList()).toMap(relationshipName, targetKeyField); - } - - public Map toMap(SObjectField keyField, SObjectField valueField) { - return this.converter.transform(this.toList()).toMap(keyField, valueField); - } - - public Map> toAggregatedMap(SObjectField keyField) { - return this.converter.transform(this.toList()).toAggregatedMap(keyField); - } - - public Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField) { - return this.converter.transform(this.toList()).toAggregatedMap(relationshipName, targetKeyField); - } - - public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { - return this.converter.transform(this.toList()).toAggregatedMap(keyField, valueField); - } - } - - public class SoqlMock implements Mockable { - public SObjectMock sObjectMock = new SObjectMock(); - - public Mockable thenReturn(SObject record) { - this.sObjectMock.add(record); - return this; - } - - public Mockable thenReturn(List records) { - this.sObjectMock.add(records); - return this; - } - } - - public class SObjectMock { - private List mockedRecords = new List(); - - public void add(SObject record) { - this.mockedRecords.add(record); - } - - public void add(List records) { - this.mockedRecords.addAll(records); - } - - public List get() { - if (!this.mockedRecords.isEmpty()) { - this.addIdToMockedRecords(); - } - - return this.mockedRecords; - } - - private void addIdToMockedRecords() { // Id is always added to mirror standard SOQL behavior - SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); - String sObjectPrefix = sObjectType.getDescribe().getKeyPrefix(); - - for (SObject record : this.mockedRecords) { - record.put('Id', record?.Id ?? SOQL.IdGenerator.get(sObjectPrefix)); - } - } - } + public static SObjectEvaluable of(List staticQueryRecords) { + return new SObjectEvaluator(staticQueryRecords); + } + + public interface SObjectEvaluable { + // FIELD-LEVEL SECURITY + SObjectEvaluable stripInaccessible(); + SObjectEvaluable stripInaccessible(AccessType accessType); + // MOCKING + SObjectEvaluable mockId(String mockId); + // RESULT + Id toId(); + Set toIds(); + Set toIdsOf(SObjectField field); + Set toIdsOf(String relationshipName, SObjectField field); + Boolean doExist(); + Object toValueOf(SObjectField fieldToExtract); + Set toValuesOf(SObjectField fieldToExtract); + SObject toObject(); + List toList(); + Map toMap(); + Map toMap(SObjectField keyField); + Map toMap(String relationshipName, SObjectField targetKeyField); + Map toMap(SObjectField keyField, SObjectField valueField); + Map> toAggregatedMap(SObjectField keyField); + Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField); + Map> toAggregatedMap(SObjectField keyField, SObjectField valueField); + Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField); + } + + public interface Mockable { + // SObject + Mockable thenReturn(SObject record); + Mockable thenReturn(List records); + } + + @TestVisible + private static Mockable mock(String mockId) { + if (!SOQLEvaluator.queryIdToMock.containsKey(mockId)) { + SOQLEvaluator.queryIdToMock.put(mockId, new List()); + } + SOQLEvaluator.queryIdToMock.get(mockId).add(new SoqlMock()); + return SOQLEvaluator.queryIdToMock.get(mockId).get(SOQLEvaluator.queryIdToMock.get(mockId).size() - 1); + } + + // Implementation + + private static Map> queryIdToMock = new Map>(); + + private class SObjectEvaluator implements SObjectEvaluable { + private List records; + private AccessType access; + private SOQL.Converter converter; + private List mocks = new List(); + + public SObjectEvaluator(List records) { + this.records = records; + this.converter = new SOQL.Converter(records.getSObjectType().toString()); + } + + public SObjectEvaluable mockId(String mockId) { + this.mocks = SOQLEvaluator.queryIdToMock.get(mockId) ?? new List(); + return this; + } + + public SObjectEvaluable stripInaccessible() { + return this.stripInaccessible(AccessType.READABLE); + } + + public SObjectEvaluable stripInaccessible(AccessType accessType) { + this.access = accessType; + return this; + } + + public Id toId() { + return this.toObject()?.Id; + } + + public Set toIds() { + return new Map(this.toList()).keySet(); + } + + public Set toIdsOf(SObjectField field) { + return this.converter.transform(this.toList()).toIdsOf(field); + } + + public Set toIdsOf(String relationshipName, SObjectField field) { + return this.converter.transform(this.toList()).toIdsOf(relationshipName, field); + } + + public Boolean doExist() { + return !this.toList().isEmpty(); + } + + public Object toValueOf(SObjectField fieldToExtract) { + return this.toObject()?.get(fieldToExtract); + } + + public Set toValuesOf(SObjectField fieldToExtract) { + return this.converter.transform(this.toList()).toValuesOf(fieldToExtract); + } + + public SObject toObject() { + List records = this.toList(); + + if (records.isEmpty()) { + return null; // handle: List has no rows for assignment to SObject + } + + if (records.size() > 1) { + throw new QueryException('List has more than 1 row for assignment to SObject'); + } + + return records[0]; + } + + public List toList() { + if (!this.mocks.isEmpty()) { + return this.getMockedListProxy(); + } + + if (this.access == null) { + return this.records; + } + + return System.Security.stripInaccessible(this.access, this.records).getRecords(); + } + + private List getMockedListProxy() { + if (this.mocks.size() == 1) { + return this.mocks[0].sObjectMock.get(); + } + return this.mocks.remove(0).sObjectMock.get(); + } + + public Map toMap() { + return this.converter.transform(this.toList()).toMap(); + } + + public Map toMap(SObjectField keyField) { + return this.converter.transform(this.toList()).toMap(keyField); + } + + public Map toMap(String relationshipName, SObjectField targetKeyField) { + return this.converter.transform(this.toList()).toMap(relationshipName, targetKeyField); + } + + public Map toMap(SObjectField keyField, SObjectField valueField) { + return this.converter.transform(this.toList()).toMap(keyField, valueField); + } + + public Map> toAggregatedMap(SObjectField keyField) { + return this.converter.transform(this.toList()).toAggregatedMap(keyField); + } + + public Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField) { + return this.converter.transform(this.toList()).toAggregatedMap(relationshipName, targetKeyField); + } + + public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { + return this.converter.transform(this.toList()).toAggregatedMap(keyField, valueField); + } + + public Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField) { + return this.converter.transform(this.toList()).toAggregatedMap(keyField, relationshipName, targetKeyField); + } + } + + public class SoqlMock implements Mockable { + public SObjectMock sObjectMock = new SObjectMock(); + + public Mockable thenReturn(SObject record) { + this.sObjectMock.add(record); + return this; + } + + public Mockable thenReturn(List records) { + this.sObjectMock.add(records); + return this; + } + } + + public class SObjectMock { + private List mockedRecords = new List(); + + public void add(SObject record) { + this.mockedRecords.add(record); + } + + public void add(List records) { + this.mockedRecords.addAll(records); + } + + public List get() { + if (!this.mockedRecords.isEmpty()) { + this.addIdToMockedRecords(); + } + + return this.mockedRecords; + } + + private void addIdToMockedRecords() { // Id is always added to mirror standard SOQL behavior + SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); + String sObjectPrefix = sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix(); + + for (SObject record : this.mockedRecords) { + record.put('Id', record?.Id ?? SOQL.IdGenerator.get(sObjectPrefix)); + } + } + } } diff --git a/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator_Test.cls b/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator_Test.cls index 1d8bec61..5d229d75 100644 --- a/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator_Test.cls +++ b/force-app/main/default/classes/main/soql-evaluator/SOQLEvaluator_Test.cls @@ -1,627 +1,668 @@ /** + * @description Tests for the SOQLEvaluator class. * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * - **/ + * PMD False Positives: + * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class + * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class +**/ +@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity') @IsTest private class SOQLEvaluator_Test { - @IsTest - static void sObjectStripInaccessible() { - // Setup - insert new Task(Subject = 'Test', Type = 'Other'); - - System.runAs(minimumAccessUser()) { - // Test - Task task = (Task) SOQLEvaluator.of(new WithoutSharing().getTasks()).stripInaccessible().toObject(); - - Exception queryException = null; - - String inaccessibleFieldValue; - - try { - inaccessibleFieldValue = task.Type; - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'The query exception should not be null.'); - Assert.areEqual( - 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', - queryException.getMessage(), - 'The user should not have access to the \'Type\' field.' - ); - } - } - - @IsTest - static void sObjectToId() { - // Setup - insertAccount(); - - // Test - Id accountId = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toId(); - - // Verify - Assert.isNotNull(accountId, 'The account id should not be null.'); - } - - @IsTest - static void sObjectToIdWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); - - // Test - Id accountId = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toId(); - - // Verify - Assert.isNotNull(accountId, 'The account id should not be null.'); - } - - @IsTest - static void sObjectsToIds() { - // Setup - insertAccounts(); - - // Test - Set accountIds = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toIds(); - - // Verify - Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); - } - - @IsTest - static void sObjectsToIdsWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - Set accountIds = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toIds(); - - // Verify - Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); - } - - @IsTest - static void sObjectsToIdsOf() { - // Setup - insertAccountsWithParents(); - - // Test - Set parentAccountIds = SOQLEvaluator.of([SELECT Id, ParentId FROM Account WHERE ParentId != null]).toIdsOf(Account.ParentId); - - // Verify - Assert.areEqual(2, parentAccountIds.size(), 'The size of the returned set should be 2, because only two accounts have a parent.'); - } - - @IsTest - static void sObjectsToIdsOfWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Contact(FirstName = 'Test 1', LastName = 'Test 1', AccountId = SOQL.IdGenerator.get(Account.SObjectType)), - new Contact(FirstName = 'Test 2', LastName = 'Test 2', AccountId = SOQL.IdGenerator.get(Account.SObjectType)) - }); - - // Test - Set accountIds = SOQLEvaluator.of([SELECT Id, FirstName, LastName FROM Contact]).mockId('mockingQuery').toIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); - } - - @IsTest - static void sObjectsToIdsOfRelationshipField() { - // Setup - insertAccountsWithParents(); - - // Test - Set createdByIds = SOQLEvaluator.of([SELECT Id, Parent.CreatedById FROM Account WHERE ParentId != null]).toIdsOf('Parent', Account.CreatedById); - - // Verify - Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); - } - - @IsTest - static void sObjectsToIdsOfRelationshipFieldWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Parent = new Account(ParentId = SOQL.IdGenerator.get(Account.SObjectType))), - new Account(Name = 'Test 2', Parent = new Account(ParentId = SOQL.IdGenerator.get(Account.SObjectType))) - }); - - // Test - Set parentIds = SOQLEvaluator.of([SELECT Id, Parent.ParentId FROM Account WHERE ParentId != null]).mockId('mockingQuery').toIdsOf('Parent', Account.ParentId); - - // Verify - Assert.areEqual(2, parentIds.size(), 'The size of the returned set should be 2, because the accounts have a parent.'); - } - - @IsTest - static void sObjectDoExist() { - // Setup - insertAccount(); - - // Test - Boolean doesExist = SOQLEvaluator.of([SELECT Id, Name FROM Account]).doExist(); - - // Verify - Assert.isTrue(doesExist, 'The account should exist.'); - } - - @IsTest - static void sObjectDoExistWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); - - // Test - Boolean doesExist = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').doExist(); - - // Verify - Assert.isTrue(doesExist, 'The account should exist.'); - } - - @IsTest - static void sObjectToValueOf() { - // Setup - insertAccount(); - - // Test - String accountName = (String) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toValueOf(Account.Name); - - // Verify - Assert.areEqual('Test Account', accountName, 'The account name should be "Test Account".'); - } - - @IsTest - static void sObjectToValueOfWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); - - // Test - String accountName = (String)SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toValueOf(Account.Name); - - // Verify - Assert.areEqual('Test Account', accountName, 'The account name should be "Test Account".'); - } - - @IsTest - static void sObjectsToValuesOf() { - // Setup - insertAccounts(); - - // Test - Set accountNames = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toValuesOf(Account.Name); - - // Verify - Assert.areEqual(2, accountNames.size(), 'The size of the returned set should be 2.'); - } - - @IsTest - static void sObjectsToValuesOfWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - Set accountNames = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toValuesOf(Account.Name); - - // Verify - Assert.areEqual(2, accountNames.size(), 'The size of the returned set should be 2.'); - } - - @IsTest - static void sObjectToObject() { - // Setup - insertAccount(); - - // Test - Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); - - // Verify - Assert.areEqual('Test Account', account.get('Name'), 'The account name should be "Test Account".'); - } - - @IsTest - static void sObjectToObjectWithMultipleRows() { - // Setup - insertAccounts(); - Exception queryException = null; - - // Test - try { - Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); - } catch (Exception e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'The exception should be thrown, because there are more than 1 row for assignment to SObject.'); - } - - @IsTest - static void sObjectToObjectWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); - - // Test - Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); - - // Verify - Assert.areEqual('Test Account', account.get('Name'), 'The account name should be "Test Account".'); - } - - @IsTest - static void sObjectToObjectWhenNoRecord() { - // Test - Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); - - // Verify - Assert.isNull(account, 'The account should be null.'); - } - - @IsTest - static void sObjectsToList() { - // Setup - insertAccounts(); - - // Test - List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toList(); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned list should be 2.'); - } - - @IsTest - static void sObjectsToListWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned list should be 2.'); - } - - @IsTest - static void sObjectsToListWhenNoRecords() { - // Test - List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toList(); - - // Verify - Assert.areEqual(0, accounts.size(), 'The size of the returned list should be 0.'); - } - - @IsTest - static void sObjectsToMap() { - // Setup - insertAccounts(); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toMap(); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWhenNoRecords() { - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(); - - // Verify - Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToMapWithKeyField() { - // Setup - insertAccounts(); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(Account.Name); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithKeyFieldWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toMap(Account.Name); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithKeyFieldWhenNoRecords() { - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(Account.Name); - - // Verify - Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToMapWithRelatedKeyField() { - // Setup - insertAccountsWithParents(); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).toMap('Parent', Account.Name); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithRelatedKeyFieldWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Parent = new Account(Name = 'Parent 1')), - new Account(Name = 'Test 2', Parent = new Account(Name = 'Parent 2')) - }); - - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).mockId('mockingQuery').toMap('Parent', Account.Name); - - // Verify - Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithRelatedKeyFieldWhenNoRecords() { - // Test - Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).toMap('Parent', Account.Name); - - // Verify - Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToMapWithKeyAndValueFields() { - // Setup - insertAccounts(); - - // Test - Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(2, nameToDescription.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithKeyAndValueFieldsWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Description = 'Description 1'), - new Account(Name = 'Test 2', Description = 'Description 2') - }); - - // Test - Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).mockId('mockingQuery').toMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(2, nameToDescription.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToMapWithKeyAndValueFieldsWhenNoRecords() { - // Test - Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(0, nameToDescription.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToAggregatedMap() { - // Setup - insertAccounts(); - - // Test - Map> nameToAccounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toAggregatedMap(Account.Name); - - // Verify - Assert.areEqual(2, nameToAccounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Industry = 'IT'), - new Account(Name = 'Test 2', Industry = 'IT') - }); - - // Test - Map> industryToAccounts = SOQLEvaluator.of([SELECT Id, Name, Industry FROM Account]).mockId('mockingQuery').toAggregatedMap(Account.Industry); - - // Verify - Assert.areEqual(1, industryToAccounts.size(), 'The size of the returned map should be 1, because there is only one industry.'); - } - - @IsTest - static void sObjectsToAggregatedMapWhenNoRecords() { - // Test - Map> nameToAccounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toAggregatedMap(Account.Name); - - // Verify - Assert.areEqual(0, nameToAccounts.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithRelatedKeyField() { - // Setup - insertAccountsWithParents(); - - // Test - Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).toAggregatedMap('Parent', Account.Name); - - // Verify - Assert.areEqual(2, parentNameToAccounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithRelatedKeyFieldWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Parent = new Account(Name = 'Parent 1')), - new Account(Name = 'Test 2', Parent = new Account(Name = 'Parent 2')) - }); - - // Test - Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).mockId('mockingQuery').toAggregatedMap('Parent', Account.Name); - - // Verify - Assert.areEqual(2, parentNameToAccounts.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithRelatedKeyFieldWhenNoRecords() { - // Test - Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != null]).toAggregatedMap('Parent', Account.Name); - - // Verify - Assert.areEqual(0, parentNameToAccounts.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithKeyAndValueFields() { - // Setup - insertAccounts(); - - // Test - Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toAggregatedMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(2, nameToDescriptions.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithKeyAndValueFieldsWithMocking() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1', Description = 'Description 1'), - new Account(Name = 'Test 2', Description = 'Description 2') - }); - - // Test - Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).mockId('mockingQuery').toAggregatedMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(2, nameToDescriptions.size(), 'The size of the returned map should be 2.'); - } - - @IsTest - static void sObjectsToAggregatedMapWithKeyAndValueFieldsWhenNoRecords() { - // Test - Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toAggregatedMap(Account.Name, Account.Description); - - // Verify - Assert.areEqual(0, nameToDescriptions.size(), 'The size of the returned map should be 0.'); - } - - @IsTest - static void sObjectsMockStack() { - // Setup - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 2')); - SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 3')); - - // Test - Account account1 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); - Account account2 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); - Account account3 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); - Account account4 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); - - // Verify - Assert.areEqual('Test 1', account1.get('Name'), 'The account name should be "Test 1".'); - Assert.areEqual('Test 2', account2.get('Name'), 'The account name should be "Test 2".'); - Assert.areEqual('Test 3', account3.get('Name'), 'The account name should be "Test 3".'); - Assert.areEqual('Test 3', account4.get('Name'), 'The account name should be "Test 3".'); - } - - static Account insertAccount() { - Account account = new Account(Name = 'Test Account'); - insert account; - return account; - } - - static List insertAccounts() { - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Description 1'), - new Account(Name = 'Test 2', Description = 'Description 2') - }; - insert accounts; - return accounts; - } - - static List insertAccountsWithParents() { - List parentAccounts = new List{ - new Account(Name = 'Test 1 Parent'), - new Account(Name = 'Test 2 Parent') - }; - insert parentAccounts; - - List accounts = new List{ - new Account(Name = 'Test 1', ParentId = parentAccounts[0].Id), - new Account(Name = 'Test 2', ParentId = parentAccounts[1].Id) - }; - insert accounts; - - return accounts; - } - - static User minimumAccessUser() { - return new User( - Alias = 'newUser', - Email = 'newuser@testorg.com', - EmailEncodingKey = 'UTF-8', - LastName = 'Testing', - LanguageLocaleKey = 'en_US', - LocaleSidKey = 'en_US', - Profile = new Profile(Name = 'Minimum Access - Salesforce'), - TimeZoneSidKey = 'America/Los_Angeles', - UserName = 'queryselector@testorg.com' - ); - } - - public without sharing class WithoutSharing { - public List getTasks() { - return [SELECT Id, Subject, Type FROM Task WITH SYSTEM_MODE]; - } - } + @IsTest + static void sObjectStripInaccessible() { + // Setup + insert new Task(Subject = 'Test', Type = 'Other'); + + System.runAs(minimumAccessUser()) { + // Test + Task task = (Task) SOQLEvaluator.of(new WithoutSharing().getTasks()).stripInaccessible().toObject(); + + Exception queryException = null; + + String inaccessibleFieldValue; + + try { + inaccessibleFieldValue = task.Type; + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'The query exception should not be null.'); + Assert.areEqual( + 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', + queryException.getMessage(), + 'The user should not have access to the \'Type\' field.' + ); + } + } + + @IsTest + static void sObjectToId() { + // Setup + insertAccount(); + + // Test + Id accountId = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toId(); + + // Verify + Assert.isNotNull(accountId, 'The account id should not be null.'); + } + + @IsTest + static void sObjectToIdWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); + + // Test + Id accountId = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toId(); + + // Verify + Assert.isNotNull(accountId, 'The account id should not be null.'); + } + + @IsTest + static void sObjectsToIds() { + // Setup + insertAccounts(); + + // Test + Set accountIds = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toIds(); + + // Verify + Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); + } + + @IsTest + static void sObjectsToIdsWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + Set accountIds = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toIds(); + + // Verify + Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); + } + + @IsTest + static void sObjectsToIdsOf() { + // Setup + insertAccountsWithParents(); + + // Test + Set parentAccountIds = SOQLEvaluator.of([SELECT Id, ParentId FROM Account WHERE ParentId != NULL]).toIdsOf(Account.ParentId); + + // Verify + Assert.areEqual(2, parentAccountIds.size(), 'The size of the returned set should be 2, because only two accounts have a parent.'); + } + + @IsTest + static void sObjectsToIdsOfWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Contact(FirstName = 'Test 1', LastName = 'Test 1', AccountId = SOQL.IdGenerator.get(Account.SObjectType)), + new Contact(FirstName = 'Test 2', LastName = 'Test 2', AccountId = SOQL.IdGenerator.get(Account.SObjectType)) + }); + + // Test + Set accountIds = SOQLEvaluator.of([SELECT Id, FirstName, LastName FROM Contact]).mockId('mockingQuery').toIdsOf(Contact.AccountId); + + // Verify + Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be 2.'); + } + + @IsTest + static void sObjectsToIdsOfRelationshipField() { + // Setup + insertAccountsWithParents(); + + // Test + Set createdByIds = SOQLEvaluator.of([SELECT Id, Parent.CreatedById FROM Account WHERE ParentId != NULL]).toIdsOf('Parent', Account.CreatedById); + + // Verify + Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); + } + + @IsTest + static void sObjectsToIdsOfRelationshipFieldWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Parent = new Account(ParentId = SOQL.IdGenerator.get(Account.SObjectType))), + new Account(Name = 'Test 2', Parent = new Account(ParentId = SOQL.IdGenerator.get(Account.SObjectType))) + }); + + // Test + Set parentIds = SOQLEvaluator.of([SELECT Id, Parent.ParentId FROM Account WHERE ParentId != NULL]).mockId('mockingQuery').toIdsOf('Parent', Account.ParentId); + + // Verify + Assert.areEqual(2, parentIds.size(), 'The size of the returned set should be 2, because the accounts have a parent.'); + } + + @IsTest + static void sObjectDoExist() { + // Setup + insertAccount(); + + // Test + Boolean doesExist = SOQLEvaluator.of([SELECT Id, Name FROM Account]).doExist(); + + // Verify + Assert.isTrue(doesExist, 'The account should exist.'); + } + + @IsTest + static void sObjectDoExistWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); + + // Test + Boolean doesExist = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').doExist(); + + // Verify + Assert.isTrue(doesExist, 'The account should exist.'); + } + + @IsTest + static void sObjectToValueOf() { + // Setup + insertAccount(); + + // Test + String accountName = (String) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toValueOf(Account.Name); + + // Verify + Assert.areEqual('Test Account', accountName, 'The account name should be "Test Account".'); + } + + @IsTest + static void sObjectToValueOfWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); + + // Test + String accountName = (String)SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toValueOf(Account.Name); + + // Verify + Assert.areEqual('Test Account', accountName, 'The account name should be "Test Account".'); + } + + @IsTest + static void sObjectsToValuesOf() { + // Setup + insertAccounts(); + + // Test + Set accountNames = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toValuesOf(Account.Name); + + // Verify + Assert.areEqual(2, accountNames.size(), 'The size of the returned set should be 2.'); + } + + @IsTest + static void sObjectsToValuesOfWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + Set accountNames = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toValuesOf(Account.Name); + + // Verify + Assert.areEqual(2, accountNames.size(), 'The size of the returned set should be 2.'); + } + + @IsTest + static void sObjectToObject() { + // Setup + insertAccount(); + + // Test + Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); + + // Verify + Assert.areEqual('Test Account', account.get('Name'), 'The account name should be "Test Account".'); + } + + @IsTest + static void sObjectToObjectWithMultipleRows() { + // Setup + insertAccounts(); + Exception queryException = null; + + // Test + try { + SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); + } catch (Exception e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'The exception should be thrown, because there are more than 1 row for assignment to SObject.'); + } + + @IsTest + static void sObjectToObjectWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test Account')); + + // Test + Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual('Test Account', account.get('Name'), 'The account name should be "Test Account".'); + } + + @IsTest + static void sObjectToObjectWhenNoRecord() { + // Test + Account account = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toObject(); + + // Verify + Assert.isNull(account, 'The account should be null.'); + } + + @IsTest + static void sObjectsToList() { + // Setup + insertAccounts(); + + // Test + List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toList(); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned list should be 2.'); + } + + @IsTest + static void sObjectsToListWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned list should be 2.'); + } + + @IsTest + static void sObjectsToListWhenNoRecords() { + // Test + List accounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toList(); + + // Verify + Assert.areEqual(0, accounts.size(), 'The size of the returned list should be 0.'); + } + + @IsTest + static void sObjectsToMap() { + // Setup + insertAccounts(); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toMap(); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWhenNoRecords() { + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(); + + // Verify + Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToMapWithKeyField() { + // Setup + insertAccounts(); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(Account.Name); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithKeyFieldWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toMap(Account.Name); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithKeyFieldWhenNoRecords() { + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Name FROM Account]).toMap(Account.Name); + + // Verify + Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToMapWithRelatedKeyField() { + // Setup + insertAccountsWithParents(); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).toMap('Parent', Account.Name); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithRelatedKeyFieldWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Parent = new Account(Name = 'Parent 1')), + new Account(Name = 'Test 2', Parent = new Account(Name = 'Parent 2')) + }); + + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).mockId('mockingQuery').toMap('Parent', Account.Name); + + // Verify + Assert.areEqual(2, accounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithRelatedKeyFieldWhenNoRecords() { + // Test + Map accounts = (Map) SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).toMap('Parent', Account.Name); + + // Verify + Assert.areEqual(0, accounts.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToMapWithKeyAndValueFields() { + // Setup + insertAccounts(); + + // Test + Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(2, nameToDescription.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithKeyAndValueFieldsWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Description = 'Description 1'), + new Account(Name = 'Test 2', Description = 'Description 2') + }); + + // Test + Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).mockId('mockingQuery').toMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(2, nameToDescription.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToMapWithKeyAndValueFieldsWhenNoRecords() { + // Test + Map nameToDescription = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(0, nameToDescription.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToAggregatedMap() { + // Setup + insertAccounts(); + + // Test + Map> nameToAccounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toAggregatedMap(Account.Name); + + // Verify + Assert.areEqual(2, nameToAccounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Industry = 'IT'), + new Account(Name = 'Test 2', Industry = 'IT') + }); + + // Test + Map> industryToAccounts = SOQLEvaluator.of([SELECT Id, Name, Industry FROM Account]).mockId('mockingQuery').toAggregatedMap(Account.Industry); + + // Verify + Assert.areEqual(1, industryToAccounts.size(), 'The size of the returned map should be 1, because there is only one industry.'); + } + + @IsTest + static void sObjectsToAggregatedMapWhenNoRecords() { + // Test + Map> nameToAccounts = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toAggregatedMap(Account.Name); + + // Verify + Assert.areEqual(0, nameToAccounts.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithRelatedKeyField() { + // Setup + insertAccountsWithParents(); + + // Test + Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).toAggregatedMap('Parent', Account.Name); + + // Verify + Assert.areEqual(2, parentNameToAccounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithRelatedKeyFieldWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Parent = new Account(Name = 'Parent 1')), + new Account(Name = 'Test 2', Parent = new Account(Name = 'Parent 2')) + }); + + // Test + Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).mockId('mockingQuery').toAggregatedMap('Parent', Account.Name); + + // Verify + Assert.areEqual(2, parentNameToAccounts.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithRelatedKeyFieldWhenNoRecords() { + // Test + Map> parentNameToAccounts = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).toAggregatedMap('Parent', Account.Name); + + // Verify + Assert.areEqual(0, parentNameToAccounts.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndValueFields() { + // Setup + insertAccounts(); + + // Test + Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toAggregatedMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(2, nameToDescriptions.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndValueFieldsWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Description = 'Description 1'), + new Account(Name = 'Test 2', Description = 'Description 2') + }); + + // Test + Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).mockId('mockingQuery').toAggregatedMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(2, nameToDescriptions.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndValueFieldsWhenNoRecords() { + // Test + Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Description FROM Account]).toAggregatedMap(Account.Name, Account.Description); + + // Verify + Assert.areEqual(0, nameToDescriptions.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndRelationshipValueFields() { + // Setup + insertAccountsWithParents(); + + // Test + Map> nameToDescriptions = SOQLEvaluator.of([SELECT Id, Name, Parent.Name FROM Account WHERE ParentId != NULL]).toAggregatedMap(Account.Name, 'Parent', Account.Name); + + // Verify + Assert.areEqual(2, nameToDescriptions.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndRelationshipValueFieldsWithMocking() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1', Parent = new Account(Name = 'Parent 1')), + new Account(Name = 'Test 2', Parent = new Account(Name = 'Parent 2')) + }); + + // Test + Map> parentNameToAccountNames = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).mockId('mockingQuery').toAggregatedMap(Account.Name, 'Parent', Account.Name); + + // Verify + Assert.areEqual(2, parentNameToAccountNames.size(), 'The size of the returned map should be 2.'); + } + + @IsTest + static void sObjectsToAggregatedMapWithKeyAndRelationshipValueFieldsWhenNoRecords() { + // Test + Map> parentNameToAccountNames = SOQLEvaluator.of([SELECT Id, Parent.Name FROM Account WHERE ParentId != NULL]).toAggregatedMap(Account.Name, 'Parent', Account.Name); + + // Verify + Assert.areEqual(0, parentNameToAccountNames.size(), 'The size of the returned map should be 0.'); + } + + @IsTest + static void sObjectsMockStack() { + // Setup + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 2')); + SOQLEvaluator.mock('mockingQuery').thenReturn(new Account(Name = 'Test 3')); + + // Test + Account account1 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); + Account account2 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); + Account account3 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); + Account account4 = (Account) SOQLEvaluator.of([SELECT Id, Name FROM Account]).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual('Test 1', account1.get('Name'), 'The account name should be "Test 1".'); + Assert.areEqual('Test 2', account2.get('Name'), 'The account name should be "Test 2".'); + Assert.areEqual('Test 3', account3.get('Name'), 'The account name should be "Test 3".'); + Assert.areEqual('Test 3', account4.get('Name'), 'The account name should be "Test 3".'); + } + + static Account insertAccount() { + Account account = new Account(Name = 'Test Account'); + insert account; + return account; + } + + static List insertAccounts() { + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Description 1'), + new Account(Name = 'Test 2', Description = 'Description 2') + }; + insert accounts; + return accounts; + } + + static List insertAccountsWithParents() { + List parentAccounts = new List{ + new Account(Name = 'Test 1 Parent'), + new Account(Name = 'Test 2 Parent') + }; + insert parentAccounts; + + List accounts = new List{ + new Account(Name = 'Test 1', ParentId = parentAccounts[0].Id), + new Account(Name = 'Test 2', ParentId = parentAccounts[1].Id) + }; + insert accounts; + + return accounts; + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + Profile = new Profile(Name = 'Minimum Access - Salesforce'), + TimeZoneSidKey = 'America/Los_Angeles', + Username = 'queryselector@testorg.com' + ); + } + + private without sharing class WithoutSharing { + private List getTasks() { + return [SELECT Id, Subject, Type FROM Task WITH SYSTEM_MODE]; + } + } } diff --git a/force-app/main/default/classes/main/standard-soql/SOQL.cls b/force-app/main/default/classes/main/standard-soql/SOQL.cls index f82ebae5..61b53b35 100644 --- a/force-app/main/default/classes/main/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/main/standard-soql/SOQL.cls @@ -2,7 +2,7 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query @@ -18,2838 +18,2964 @@ **/ @SuppressWarnings('PMD.ExcessivePublicCount,PMD.ExcessiveClassLength,PMD.FieldNamingConventions,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.PropertyNamingConventions,PMD.FieldDeclarationsShouldBeAtStart,PMD.ApexDoc,PMD.ExcessiveParameterList,PMD.NcssTypeCount') public virtual inherited sharing class SOQL implements Queryable { - public interface Selector { - Queryable query(); - } - - public static Queryable of(SObjectType ofObject) { - return new SOQL(ofObject); - } - - public static Queryable of(String ofObject) { - return new SOQL(ofObject); - } - - public interface Queryable { - // SELECT - Queryable with(SObjectField field); - Queryable with(SObjectField field1, SObjectField field2); - Queryable with(SObjectField field1, SObjectField field2, SObjectField field3); - Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Queryable with(List fields); - Queryable with(Iterable fields); - Queryable with(String fields); - Queryable with(SObjectField field, String alias); - Queryable with(String relationshipName, SObjectField field); - Queryable with(String relationshipName, SObjectField field1, SObjectField field2); - Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3); - Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Queryable with(String relationshipName, Iterable fields); - Queryable with(SubQuery subQuery); - Queryable withFieldSet(String fieldSetName); - // SELECT - AGGREGATE FUNCTIONS - Queryable count(); - Queryable count(SObjectField field); - Queryable count(SObjectField field, String alias); - Queryable count(String relationshipName, SObjectField field); - Queryable count(String relationshipName, SObjectField field, String alias); - Queryable avg(SObjectField field); - Queryable avg(SObjectField field, String alias); - Queryable avg(String relationshipName, SObjectField field); - Queryable avg(String relationshipName, SObjectField field, String alias); - Queryable countDistinct(SObjectField field); - Queryable countDistinct(SObjectField field, String alias); - Queryable countDistinct(String relationshipName, SObjectField field); - Queryable countDistinct(String relationshipName, SObjectField field, String alias); - Queryable min(SObjectField field); - Queryable min(SObjectField field, String alias); - Queryable min(String relationshipName, SObjectField field); - Queryable min(String relationshipName, SObjectField field, String alias); - Queryable max(SObjectField field); - Queryable max(SObjectField field, String alias); - Queryable max(String relationshipName, SObjectField field); - Queryable max(String relationshipName, SObjectField field, String alias); - Queryable sum(SObjectField field); - Queryable sum(SObjectField field, String alias); - Queryable sum(String relationshipName, SObjectField field); - Queryable sum(String relationshipName, SObjectField field, String alias); - // SELECT - GROUPING - Queryable grouping(SObjectField field, String alias); - // SELECT - toLabel - Queryable toLabel(SObjectField field); - Queryable toLabel(SObjectField field, String alias); - Queryable toLabel(String field); - Queryable toLabel(String field, String alias); - // SELECT - FORMAT - Queryable format(SObjectField field); - Queryable format(SObjectField field, String alias); - // USING SCOPE - Queryable delegatedScope(); - Queryable mineScope(); - Queryable mineAndMyGroupsScope(); - Queryable myTerritoryScope(); - Queryable myTeamTerritoryScope(); - Queryable teamScope(); - // WHERE - Queryable whereAre(FilterGroup filterGroup); - Queryable whereAre(Filter filter); - Queryable whereAre(String conditions); - Queryable conditionLogic(String order); - Queryable anyConditionMatching(); - // GROUP BY - Queryable groupBy(SObjectField field); - Queryable groupBy(String field); - Queryable groupBy(String relationshipName, SObjectField field); - Queryable groupByRollup(SObjectField field); - Queryable groupByRollup(String relationshipName, SObjectField field); - Queryable groupByCube(SObjectField field); - Queryable groupByCube(String relationshipName, SObjectField field); - // HAVING - Queryable have(HavingFilterGroup havingFilterGroup); - Queryable have(HavingFilter havingFilter); - Queryable have(String havingConditions); - Queryable havingConditionLogic(String havingConditionsOrder); - Queryable anyHavingConditionMatching(); - // WITH DATA CATEGORY - Queryable withDataCategory(DataCategoryFilter dataCategoryFilter); - // ORDER BY - Queryable orderBy(SObjectField field); - Queryable orderBy(String field); - Queryable orderBy(String field, String direction); - Queryable orderBy(String relationshipName, SObjectField field); - Queryable orderByCount(SObjectField field); - Queryable sortDesc(); - Queryable sort(String direction); - Queryable nullsLast(); - Queryable nullsOrder(String nullsOrder); - // LIMIT - Queryable setLimit(Integer amount); - // OFFSET - Queryable offset(Integer startingRow); - // FOR - Queryable forReference(); - Queryable forView(); - Queryable forUpdate(); - Queryable allRows(); - // FIELD-LEVEL SECURITY - Queryable userMode(); - Queryable systemMode(); - Queryable stripInaccessible(); - Queryable stripInaccessible(AccessType accessType); - // SHARING MODE - Queryable withSharing(); - Queryable withoutSharing(); - // MOCKING - Queryable mockId(String queryIdentifier); - // DEBUGGING - Queryable preview(); - Map binding(); - // PREDEFINED - Queryable byId(SObject record); - Queryable byId(Id recordId); - Queryable byIds(Iterable recordIds); - Queryable byIds(List records); - Queryable byRecordType(String recordTypeDeveloperName); - // RESULT - Id toId(); - Set toIds(); - Set toIdsOf(SObjectField field); - Set toIdsOf(String relationshipName, SObjectField field); - Boolean doExist(); - String toString(); - Object toValueOf(SObjectField fieldToExtract); - Set toValuesOf(SObjectField fieldToExtract); - Set toValuesOf(String relationshipName, SObjectField targetKeyField); - Integer toInteger(); - SObject toObject(); - List toList(); - List toAggregated(); - List toAggregatedProxy(); - Map toMap(); - Map toMap(SObjectField keyField); - Map toMap(String relationshipName, SObjectField targetKeyField); - Map toMap(SObjectField keyField, SObjectField valueField); - Map> toAggregatedMap(SObjectField keyField); - Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField); - Map> toAggregatedMap(SObjectField keyField, SObjectField valueField); - Database.QueryLocator toQueryLocator(); - } - - public interface SubQuery { - SubQuery of(String ofObject); - // SELECT - SubQuery with(SObjectField field); - SubQuery with(SObjectField field1, SObjectField field2); - SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3); - SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - SubQuery with(Iterable fields); - SubQuery with(String fields); - SubQuery with(String relationshipName, Iterable fields); - SubQuery with(SubQuery subQuery); - // WHERE - SubQuery whereAre(FilterGroup filterGroup); - SubQuery whereAre(Filter filter); - // ORDER BY - SubQuery orderBy(SObjectField field); - SubQuery orderBy(String field); - SubQuery orderBy(String relationshipName, SObjectField field); - SubQuery sortDesc(); - SubQuery sort(String direction); - SubQuery nullsLast(); - // LIMIT - SubQuery setLimit(Integer amount); - // OFFSET - SubQuery offset(Integer startingRow); - // FOR - SubQuery forReference(); - SubQuery forView(); - // ADDITIONAL - String getChildRelationshipName(); - } - - public interface FilterGroup { - // ADD CONDITION - FilterGroup add(FilterGroup filterGroup); - FilterGroup add(Filter filter); - FilterGroup add(String dynamicCondition); - FilterGroup add(List filters); - FilterGroup add(List dynamicConditions); - // ORDER - FilterGroup anyConditionMatching(); - FilterGroup conditionLogic(String order); - // ADDITIONAL - FilterGroup ignoreWhen(Boolean logicExpression); - - Boolean hasValues(); - } - - public interface Filter { - // FIELDS - Filter id(); - Filter recordType(); - Filter name(); - Filter with(SObjectField field); - Filter with(String field); - Filter with(String relationshipName, SObjectField field); - // COMPARATORS - Filter isNull(); - Filter isNotNull(); - Filter isTrue(); - Filter isFalse(); - Filter equal(Object value); - Filter notEqual(Object value); - Filter lessThan(Object value); - Filter lessOrEqual(Object value); - Filter greaterThan(Object value); - Filter greaterOrEqual(Object value); - Filter containsSome(Iterable values); - Filter contains(String value); - Filter contains(String prefix, String value, String suffix); - Filter notContains(String value); - Filter notContains(String prefix, String value, String suffix); - Filter endsWith(String value); - Filter notEndsWith(String value); - Filter startsWith(String value); - Filter notStartsWith(String value); - Filter isIn(Iterable iterable); - Filter isIn(InnerJoin joinQuery); - Filter notIn(Iterable iterable); - Filter notIn(InnerJoin joinQuery); - Filter includesAll(Iterable values); - Filter includesSome(Iterable values); - Filter excludesAll(Iterable values); - Filter excludesSome(Iterable values); - // ADDITIONAL - Filter asDateLiteral(); - Filter ignoreWhen(Boolean logicExpression); - - Boolean hasValue(); - } - - public interface InnerJoin { - InnerJoin of(SObjectType ofObject); - // SELECT - InnerJoin with(SObjectField field); - // WHERE - InnerJoin whereAre(FilterGroup filterGroup); - InnerJoin whereAre(Filter filter); - } - - public interface HavingFilterGroup { - // ADD CONDITION - HavingFilterGroup add(HavingFilterGroup havingFilterGroup); - HavingFilterGroup add(HavingFilter havingFilter); - HavingFilterGroup add(String dynamicHaving); - // ORDER - HavingFilterGroup anyConditionMatching(); - HavingFilterGroup conditionLogic(String order); - - Boolean hasValues(); - } - - public interface HavingFilter { - // FIELDS - HavingFilter with(SObjectField field); - HavingFilter with(String field); - HavingFilter count(SObjectField field); - HavingFilter avg(SObjectField field); - HavingFilter countDistinct(SObjectField field); - HavingFilter min(SObjectField field); - HavingFilter max(SObjectField field); - HavingFilter sum(SObjectField field); - // COMPARATORS - HavingFilter isNull(); - HavingFilter isNotNull(); - HavingFilter isTrue(); - HavingFilter isFalse(); - HavingFilter equal(Object value); - HavingFilter notEqual(Object value); - HavingFilter lessThan(Object value); - HavingFilter lessOrEqual(Object value); - HavingFilter greaterThan(Object value); - HavingFilter greaterOrEqual(Object value); - HavingFilter contains(String value); - HavingFilter contains(String prefix, String value, String suffix); - HavingFilter notContains(String value); - HavingFilter notContains(String prefix, String value, String suffix); - HavingFilter startsWith(String value); - HavingFilter notStartsWith(String value); - HavingFilter endsWith(String value); - HavingFilter notEndsWith(String value); - HavingFilter isIn(Iterable iterable); - HavingFilter notIn(Iterable iterable); - - Boolean hasValue(); - } - - public interface DataCategoryFilterGroup { - DataCategoryFilterGroup add(DataCategoryFilter dataCategoryFilter); - } - - public interface DataCategoryFilter { - // FIELDS - DataCategoryFilter with(String field); - // COMPARATORS - DataCategoryFilter at(String category); - DataCategoryFilter at(Iterable categories); - DataCategoryFilter above(String category); - DataCategoryFilter above(Iterable categories); - DataCategoryFilter below(String category); - DataCategoryFilter below(Iterable categories); - DataCategoryFilter aboveOrBelow(String category); - DataCategoryFilter aboveOrBelow(Iterable categories); - - Boolean hasValue(); - } - - public interface AggregateResultProxy { - Object get(String field); - Map getPopulatedFieldsAsMap(); - } - - public static SubQuery SubQuery { - get { return new SoqlSubQuery(); } - } - - public static FilterGroup FilterGroup { - get { return new SoqlFilterGroup(); } - } - - public static Filter Filter { - get { return new SoqlFilter(); } - } - - public static InnerJoin InnerJoin { - get { return new SoqlJoinQuery(); } - } - - public static HavingFilterGroup HavingFilterGroup { - get { return new SoqlHavingFilterGroup(); } - } - - public static HavingFilter HavingFilter { - get { return new SoqlHavingFilter(); } - } - - public static DataCategoryFilter DataCategoryFilter { - get { return new SoqlDataCategoryFilter(); } - } - - // Mocking - - @TestVisible - private static Mockable mock(String mockId) { - if (!SOQL.queryIdToMock.containsKey(mockId)) { - SOQL.queryIdToMock.put(mockId, new List()); - } - SOQL.queryIdToMock.get(mockId).add(new SoqlMock()); - return SOQL.queryIdToMock.get(mockId).get(SOQL.queryIdToMock.get(mockId).size() - 1); - } - - @TestVisible - public static RandomIdGenerator IdGenerator = new RandomIdGenerator(); - - public interface Mockable { - // SObject - Mockable thenReturn(SObject record); - Mockable thenReturn(List records); - // AggregateResultProxy - Mockable thenReturn(List> aggregatedResults); - Mockable thenReturn(Map aggregatedResult); - // Count - Mockable thenReturn(Integer count); - } - - // Backward support - it's going to be removed in the future - - @TestVisible // deprecated - private static void setMock(String mockId, SObject record) { - SOQL.createLegacyMock(mockId).thenReturn(record); - } - - @TestVisible // deprecated - private static void setMock(String mockId, List records) { - SOQL.createLegacyMock(mockId).thenReturn(records); - } - - @TestVisible // deprecated - private static void setCountMock(String mockId, Integer amount) { - SOQL.createLegacyMock(mockId).thenReturn(amount); - } - - private static SoqlMock createLegacyMock(String mockId) { - SoqlMock mock = new SoqlMock().useLegacyMockingBehavior(); - SOQL.queryIdToMock.put(mockId, new List{ mock }); - return mock; - } - - // Implementation - - private static Map> queryIdToMock = new Map>(); - - private static Binder binder = new Binder(); - private static Integer syncQueriesIssued = 0; - - private SoqlBuilder builder; - private Executor executor; - private Converter converter; - - public SOQL(SObjectType ofObject) { - this(ofObject.toString()); - } - - public SOQL(String ofObject) { - this.builder = new SoqlBuilder(ofObject); - this.executor = new Executor(builder); - this.converter = new Converter(ofObject); - } - - public Queryable with(SObjectField field) { - this.builder.fields.plainFields.add(field.toString()); - return this; - } - - public Queryable with(SObjectField field1, SObjectField field2) { - return this.with(field1).with(field2); - } - - public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3) { - return this.with(field1, field2).with(field3); - } - - public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.with(field1, field2, field3).with(field4); - } - - public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.with(field1, field2, field3, field4).with(field5); - } - - public Queryable with(List fields) { - this.builder.fields.plainFields.add(fields); - return this; - } - - public Queryable with(Iterable fields) { - return this.with(String.join(fields, ',')); - } - - public Queryable withFieldSet(String fieldSetName) { - this.builder.fields.withFieldSet(fieldSetName); - return this; - } - - public Queryable with(String fields) { - this.builder.fields.with(fields); - return this; - } - - public Queryable with(SObjectField field, String alias) { - return this.with(field.toString(), alias); - } - - private Queryable with(String field, String alias) { - this.builder.fields.aggregateFields.add(field, alias); - return this; - } - - public Queryable with(String relationshipName, SObjectField field) { - return this.with(relationshipName, new List{ field }); - } - - public Queryable with(String relationshipName, SObjectField field1, SObjectField field2) { - return this.with(relationshipName, new List{ field1, field2 }); - } - - public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3) { - return this.with(relationshipName, new List{ field1, field2, field3 }); - } - - public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.with(relationshipName, new List{ field1, field2, field3, field4 }); - } - - public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.with(relationshipName, new List{ field1, field2, field3, field4, field5 }); - } - - public Queryable with(String relationshipName, Iterable fields) { - this.builder.fields.relationshipFields.add(relationshipName, fields); - return this; - } - - public Queryable with(SubQuery subQuery) { - this.builder.subQueries.add(subQuery); - return this; - } - - public Queryable count() { - this.builder.fields.count(); - return this; - } - - public Queryable count(SObjectField field) { - return this.count(field, ''); - } - - public Queryable count(SObjectField field, String alias) { - return this.withAggregateFunction('COUNT', field, alias); - } - - public Queryable count(String relationshipName, SObjectField field) { - return this.count(relationshipName, field, ''); - } - - public Queryable count(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('COUNT', relationshipName, field, alias); - } - - public Queryable avg(SObjectField field) { - return this.avg(field, ''); - } - - public Queryable avg(SObjectField field, String alias) { - return this.withAggregateFunction('AVG', field, alias); - } - - public Queryable avg(String relationshipName, SObjectField field) { - return this.avg(relationshipName, field, ''); - } - - public Queryable avg(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('AVG', relationshipName, field, alias); - } - - public Queryable countDistinct(SObjectField field) { - return this.countDistinct(field, ''); - } - - public Queryable countDistinct(SObjectField field, String alias) { - return this.withAggregateFunction('COUNT_DISTINCT', field, alias); - } - - public Queryable countDistinct(String relationshipName, SObjectField field) { - return this.countDistinct(relationshipName, field, ''); - } - - public Queryable countDistinct(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('COUNT_DISTINCT', relationshipName, field, alias); - } - - public Queryable min(SObjectField field) { - return this.min(field, ''); - } - - public Queryable min(SObjectField field, String alias) { - return this.withAggregateFunction('MIN', field, alias); - } - - public Queryable min(String relationshipName, SObjectField field) { - return this.min(relationshipName, field, ''); - } - - public Queryable min(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('MIN', relationshipName, field, alias); - } - - public Queryable max(SObjectField field) { - return this.max(field, ''); - } - - public Queryable max(SObjectField field, String alias) { - return this.withAggregateFunction('MAX', field, alias); - } - - public Queryable max(String relationshipName, SObjectField field) { - return this.max(relationshipName, field, ''); - } - - public Queryable max(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('MAX', relationshipName, field, alias); - } - - public Queryable sum(SObjectField field) { - return this.sum(field, ''); - } - - public Queryable sum(SObjectField field, String alias) { - return this.withAggregateFunction('SUM', field, alias); - } - - public Queryable sum(String relationshipName, SObjectField field) { - return this.sum(relationshipName, field, ''); - } - - public Queryable sum(String relationshipName, SObjectField field, String alias) { - return this.withAggregateFunction('SUM', relationshipName, field, alias); - } - - public Queryable grouping(SObjectField field, String alias) { - return this.withAggregateFunction('GROUPING', field, alias); - } - - private Queryable withAggregateFunction(String function, SObjectField field, String alias) { - this.builder.fields.aggregateFields.add(function, field, alias); - return this; - } - - private Queryable withAggregateFunction(String function, String relationship, SObjectField field, String alias) { - this.builder.fields.aggregateFields.add(function, relationship, field, alias); - return this; - } - - public Queryable toLabel(SObjectField field) { - return this.toLabel(field.toString()); - } - - public Queryable toLabel(SObjectField field, String alias) { - return this.toLabel(field.toString(), alias); - } - - public Queryable toLabel(String field) { - return this.toLabel(field, ''); - } - - public Queryable toLabel(String field, String alias) { - return this.withFunction('toLabel', field, alias); - } - - public Queryable format(SObjectField field) { - return this.format(field, ''); - } - - public Queryable format(SObjectField field, String alias) { - return this.withFunction('FORMAT', field.toString(), alias); - } - - private Queryable withFunction(String function, String field, String alias) { - this.builder.fields.functionsFields.add(function, field, alias); - return this; - } - - public Queryable delegatedScope() { - return this.scope('DELEGATED'); - } - - public Queryable mineScope() { - return this.scope('MINE'); - } - - public Queryable mineAndMyGroupsScope() { - return this.scope('MINE_AND_MY_GROUPS'); - } - - public Queryable myTerritoryScope() { - return this.scope('MY_TERRITORY'); - } - - public Queryable myTeamTerritoryScope() { - return this.scope('MY_TEAM_TERRITORY'); - } - - public Queryable teamScope() { - return this.scope('TEAM'); - } - - private Queryable scope(String scope) { - this.builder.scope.set(scope); - return this; - } - - public Queryable whereAre(FilterGroup filterGroup) { - this.builder.conditions.add(filterGroup); - return this; - } - - public Queryable whereAre(Filter filter) { - this.builder.conditions.add(filter); - return this; - } - - public Queryable whereAre(String conditions) { - this.builder.conditions.add(conditions); - return this; - } - - public Queryable conditionLogic(String conditionLogic) { - this.builder.conditions.conditionLogic(conditionLogic); - return this; - } - - public Queryable anyConditionMatching() { - this.builder.conditions.anyConditionMatching(); - return this; - } - - public Queryable groupBy(SObjectField field) { - return this.groupBy(field.toString()); - } - - public Queryable groupBy(String field) { - this.builder.fields.withGroupedField(field); - this.builder.groupBy.with(field); - return this; - } - - public Queryable groupBy(String relationshipName, SObjectField field) { - this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); - this.builder.groupBy.with(relationshipName, field); - return this; - } - - public Queryable groupByRollup(SObjectField field) { - this.builder.fields.withGroupedField(field.toString()); - return this.groupBy(field, 'ROLLUP'); - } - - public Queryable groupByRollup(String relationshipName, SObjectField field) { - this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); - return this.groupBy(relationshipName, field, 'ROLLUP'); - } - - public Queryable groupByCube(SObjectField field) { - this.builder.fields.withGroupedField(field.toString()); - return this.groupBy(field, 'CUBE'); - } - - public Queryable groupByCube(String relationshipName, SObjectField field) { - this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); - return this.groupBy(relationshipName, field, 'CUBE'); - } - - private Queryable groupBy(SObjectField field, String function) { - this.builder.groupBy.with(field.toString(), function); - this.builder.fields.withGroupedField(field.toString()); - return this; - } - - private Queryable groupBy(String relationshipName, SObjectField field, String function) { - this.builder.groupBy.with(relationshipName, field.toString(), function); - this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); - return this; - } - - public Queryable have(HavingFilterGroup havingFilterGroup) { - this.builder.havingClause.add(havingFilterGroup); - return this; - } - - public Queryable have(HavingFilter havingFilter) { - this.builder.havingClause.add(havingFilter); - return this; - } - - public Queryable have(String havingConditions) { - this.builder.havingClause.add(havingConditions); - return this; - } - - public Queryable havingConditionLogic(String havingConditionsOrder) { - this.builder.havingClause.conditionLogic(havingConditionsOrder); - return this; - } - - public Queryable anyHavingConditionMatching() { - this.builder.havingClause.anyConditionMatching(); - return this; - } - - public Queryable withDataCategory(DataCategoryFilter dataCategoryFilter) { - this.builder.dataCategory.add(dataCategoryFilter); - return this; - } - - public Queryable orderBy(SObjectField field) { - return this.orderBy(field.toString()); - } - - public Queryable orderBy(String field) { - this.builder.orderBys.newOrderBy().with(field); - return this; - } - - public Queryable orderBy(String field, String direction) { - this.builder.orderBys.newOrderBy().with(field); - this.builder.orderBys.latestOrderBy().sortingOrder(direction); - return this; - } - - public Queryable orderBy(String relationshipName, SObjectField field) { - return this.orderBy(relationshipName + '.' + field.toString()); - } - - public Queryable orderByCount(SObjectField field) { - return this.orderBy('COUNT(' + field.toString() + ')'); - } - - public Queryable sortDesc() { - return this.sort('DESC'); - } - - public Queryable sort(String direction) { - this.builder.latestOrderBy.sortingOrder(direction); - return this; - } - - public Queryable nullsLast() { - return this.nullsOrder('LAST'); - } - - public Queryable nullsOrder(String nullsOrder) { - this.builder.latestOrderBy.nullsOrder(nullsOrder); - return this; - } - - public Queryable setLimit(Integer amount) { - this.builder.soqlLimit.set(amount); - return this; - } - - public Queryable offset(Integer startingRow) { - this.builder.soqlOffset.set(startingRow); - return this; - } - - public Queryable forReference() { - return this.setFor('FOR REFERENCE'); - } - - public Queryable forView() { - return this.setFor('FOR VIEW'); - } - - public Queryable forUpdate() { - return this.setFor('FOR UPDATE'); - } - - public Queryable allRows() { - return this.setFor('ALL ROWS'); - } - - private Queryable setFor(String forStatement) { - this.builder.soqlFor.set(forStatement); - return this; - } - - public Queryable userMode() { - this.executor.accessMode(AccessLevel.USER_MODE); - return this; - } - - public Queryable systemMode() { - this.executor.accessMode(AccessLevel.SYSTEM_MODE); - return this; - } - - public Queryable stripInaccessible() { - return this.stripInaccessible(AccessType.READABLE); - } - - public Queryable stripInaccessible(AccessType accessType) { - this.executor.stripInaccessible(accessType); - return this; - } - - public Queryable withSharing() { - this.executor.withSharing(); - return this; - } - - public Queryable withoutSharing() { - this.executor.withoutSharing(); - return this; - } - - public Queryable mockId(String queryIdentifier) { - this.executor.mock(SOQL.queryIdToMock.get(queryIdentifier)); - return this; - } - - public Queryable preview() { - System.debug(LoggingLevel.ERROR, '\n\n============ SOQL Preview ============\n' + this.toString() + '\n=======================================\n'); - System.debug(LoggingLevel.ERROR, '\n\n============ SOQL Binding ============\n' + JSON.serializePretty(this.binding()) + '\n=======================================\n'); - return this; - } - - public Map binding() { - return binder.getBindingMap(); - } - - public Id toId() { - this.builder.fields.clearAllFields(); // other fields not needed - return this.toObject()?.Id; - } - - public Set toIds() { - this.builder.fields.clearAllFields(); // other fields not needed - return this.toMap().keySet(); - } - - public Set toIdsOf(SObjectField field) { - return this.toIdsOf(field.toString()); - } - - public Set toIdsOf(String relationshipName, SObjectField field) { - return this.toIdsOf(relationshipName + '.' + field.toString()); - } - - private Set toIdsOf(String fieldToExtract) { - // https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result - this.builder.fields.clearAllFields(); // other fields not needed - return new Map( - this.with(fieldToExtract, 'Id') - .whereAre(Filter.with(fieldToExtract).isNotNull()) - .groupBy(fieldToExtract) - .toAggregated() - ).keySet(); - } - - public Boolean doExist() { - this.builder.fields.clearAllFields(); // other fields not needed - return this.setLimit(1).toList().size() > 0; - } - - public override String toString() { - binder.reset(); // clear binding before query build - return this.builder.toString(); - } - - public Object toValueOf(SObjectField fieldToExtract) { - this.builder.fields.clearAllFields(); // other fields not needed - return this.with(fieldToExtract).toObject()?.get(fieldToExtract); - } - - public Set toValuesOf(SObjectField fieldToExtract) { - return this.toValuesOf(fieldToExtract.toString()); - } - - public Set toValuesOf(String relationshipName, SObjectField targetKeyField) { - return this.toValuesOf(relationshipName + '.' + targetKeyField.toString()); - } - - private Set toValuesOf(String fieldToExtract) { - // https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result - this.builder.fields.clearAllFields(); // other fields not needed - return new Map( - this.with(fieldToExtract, 'Id') - .whereAre(Filter.with(fieldToExtract).isNotNull()) - .groupBy(fieldToExtract) - .toAggregated() - ).keySet(); - } - - public Integer toInteger() { - this.builder.fields.addCountWhenNotPresented(); - return this.executor.toInteger(); - } - - public SObject toObject() { - return this.executor.toObject(); - } - - public List toList() { - return this.executor.toList(); - } - - public List toAggregated() { - return this.toList(); - } - - public List toAggregatedProxy() { - return this.executor.toAggregatedProxy(); - } - - public Map toMap() { - return this.converter.transform(this.executor.toList()).toMap(); - } - - public Map toMap(SObjectField keyField) { - this.with(keyField); - return this.converter.transform(this.executor.toList()).toMap(keyField); - } - - public Map toMap(String relationshipName, SObjectField targetKeyField) { - this.with(relationshipName + '.' + targetKeyField.toString()); - return this.converter.transform(this.executor.toList()).toMap(relationshipName, targetKeyField); - } - - public Map toMap(SObjectField keyField, SObjectField valueField) { - this.builder.fields.clearAllFields(); // other fields not needed - this.with(keyField, valueField); - return this.converter.transform(this.executor.toList()).toMap(keyField, valueField); - } - - public Map> toAggregatedMap(SObjectField keyField) { - this.with(keyField); - return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField); - } - - public Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField) { - this.with(relationshipName + '.' + targetKeyField.toString()); - return this.converter.transform(this.executor.toList()).toAggregatedMap(relationshipName, targetKeyField); - } - - public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { - this.builder.fields.clearAllFields(); // other fields not needed - this.with(keyField, valueField); - return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField, valueField); - } - - public Database.QueryLocator toQueryLocator() { - return this.executor.toQueryLocator(); - } - - public Queryable byId(SObject record) { - return this.byId(record.Id); - } - - public Queryable byId(Id recordId) { - return this.whereAre(Filter.id().equal(recordId)); - } - - public Queryable byIds(Iterable recordIds) { - return this.whereAre(Filter.id().isIn(recordIds)); - } - - public Queryable byIds(List records) { - return this.whereAre(Filter.id().isIn(records)); - } - - public Queryable byRecordType(String recordTypeDeveloperName) { - return this.whereAre(Filter.recordType().equal(recordTypeDeveloperName)); - } - - private interface QueryClause { - String toString(); - } - - private class SoqlBuilder implements QueryClause { - private List clauses = new QueryClause[12]; - - public SoqlBuilder(String ofObject) { - this.clauses.set(0, new SoqlFields(ofObject)); - this.clauses.set(2, new SoqlFrom(ofObject)); - } - - public SoqlFields fields { - get { return (SoqlFields) this.clauses[0]; } - } - - public SoqlSubQueries subQueries { - get { return (SoqlSubQueries) getQueryClause(1, SoqlSubQueries.class); } - } - - public SoqlScope scope { - get { return (SoqlScope) getQueryClause(3, SoqlScope.class); } - } - - public MainFilterGroup conditions { - get { return (MainFilterGroup) getQueryClause(4, MainFilterGroup.class); } - } - - public MainDataCategoryGroup dataCategory { - get { return (MainDataCategoryGroup) getQueryClause(5, MainDataCategoryGroup.class); } - } - - public SoqlGroupBy groupBy { - get { return (SoqlGroupBy) getQueryClause(6, SoqlGroupBy.class); } - } - - public MainHavingGroup havingClause { - get { return (MainHavingGroup) getQueryClause(7, MainHavingGroup.class); } - } - - public SoqlOrderBy latestOrderBy { - get { return this.orderBys.latestOrderBy(); } - } - - public SoqlOrderBys orderBys { - get { return (SoqlOrderBys) getQueryClause(8, SoqlOrderBys.class); } - } - - public SoqlLimit soqlLimit { - get { return (SoqlLimit) getQueryClause(9, SoqlLimit.class); } - } - - public SoqlOffset soqlOffset { - get { return (SoqlOffset) getQueryClause(10, SoqlOffset.class); } - } - - public SoqlFor soqlFor { - get { return (SoqlFor) getQueryClause(11, SoqlFor.class); } - } - - private QueryClause getQueryClause(Integer position, System.Type queryClause) { - if (this.clauses[position] == null) { - this.clauses.set(position, (QueryClause) queryClause.newInstance()); - } - return this.clauses[position]; - } - - public override String toString() { - List queryParts = new List(); - - for (QueryClause clause : this.clauses) { - if (clause != null) { - queryParts.add(clause.toString()); - } - } - - return String.join(queryParts, ' ').trim(); - } - } - - private class SoqlFields implements QueryClause { - public PlainFields plainFields = new PlainFields(); - public RelationshipFields relationshipFields = new RelationshipFields(); - public FunctionsFields functionsFields = new FunctionsFields(); // toLabel, FORMAT - public AggregateFunctionsFields aggregateFields = new AggregateFunctionsFields(); - - private String ofObject; - private Set groupedFields = new Set(); - - public SoqlFields(String ofObject) { - this.ofObject = ofObject; - } - - public void count() { - this.clearAllFields(); // COUNT() must be the only element in the SELECT list. - this.aggregateFields.add('COUNT()', ''); - } - - public void with(String commaSeparatedFields) { - // Add to Set to avoid "duplicate field selected" error - for (String splittedField : commaSeparatedFields.split(',')) { - this.classifyField(splittedField); - } - } - - private void classifyField(String field) { - String trimmedField = field.trim(); - - if (this.functionsFields.isQualified(trimmedField)) { - this.functionsFields.add(trimmedField); - } else if (this.aggregateFields.isQualified(trimmedField)) { - this.aggregateFields.add(trimmedField); - } else if (this.relationshipFields.isQualified(trimmedField)) { - this.relationshipFields.add(trimmedField); - } else { - this.plainFields.add(trimmedField); - } - } - - public void withGroupedField(String field) { - this.groupedFields.add(field); - } - - public void withFieldSet(String fieldSetName) { - FieldSet fieldSet = Schema.describeSObjects(new List{ this.ofObject })[0].FieldSets.getMap().get(fieldSetName); - - if (fieldSet == null) { - throw new QueryException('FieldSet with name ' + fieldSetName + ' does not exist!'); - } - - for (Schema.FieldSetMember field : fieldSet.getFields()) { - String currentField = field.getFieldPath(); - - if (this.relationshipFields.isQualified(currentField)) { - this.relationshipFields.add(currentField); - } else { - this.plainFields.add(currentField); - } - } - } - - public void clearAllFields() { - this.plainFields.clear(); - this.relationshipFields.clear(); - this.aggregateFields.clear(); - this.functionsFields.clear(); - } - - public void addCountWhenNotPresented() { - if (this.aggregateFields.isEmpty()) { - this.count(); - } - } - - public override String toString() { - if ( - this.plainFields.isEmpty() && - this.relationshipFields.isEmpty() && - this.aggregateFields.isEmpty() && - this.functionsFields.isEmpty() - ) { - this.plainFields.add('Id'); - } - - if (!this.groupedFields.isEmpty() || !this.aggregateFields.isEmpty()) { - // To avoid "Field must be grouped or aggregated" error retain only grouped or aggregated fields - this.plainFields.retainAll(groupedFields); - this.relationshipFields.retainAll(groupedFields); - this.functionsFields.retainAll(groupedFields); - } - - List selectFields = new List(); - - selectFields.addAll(this.plainFields.get()); - selectFields.addAll(this.relationshipFields.get()); - selectFields.addAll(this.functionsFields.get()); - selectFields.addAll(this.aggregateFields.get()); - - return 'SELECT ' + String.join(selectFields, ', '); - } - } - - private abstract class SelectFields { - protected Set fields = new Set(); - - public void add(String field) { - this.fields.add(field); - } - - public Boolean isEmpty() { - return this.fields.isEmpty(); - } - - public void clear() { - this.fields.clear(); - } - - public Set get() { - return this.fields; - } - - public void retainAll(Set fieldsToRetain) { - this.fields.retainAll(fieldsToRetain); - } - } - - private class PlainFields extends SelectFields { - public void add(Iterable fields) { - for (SObjectField field : fields) { - this.add(field.toString()); - } - } - } - - private class RelationshipFields extends SelectFields { - public void add(String relationshipPath, Iterable fields) { - for (SObjectField field : fields) { - this.add(relationshipPath, field); - } - } - - public void add(String relationshipPath, SObjectField field) { - this.add(relationshipPath + '.' + field.toString()); - } - - public Boolean isQualified(String field) { - return !field.contains('(') && !field.contains(')') && field.contains('.'); - } - } - - private class AggregateFunctionsFields extends SelectFields { - private final Set AGGREGATE_FUNCTIONS = new Set{ 'COUNT', 'AVG', 'COUNT_DISTINCT', 'MIN', 'MAX', 'SUM' }; - private final Set DATE_FUNCTIONS = new Set{ 'CALENDAR_MONTH', 'CALENDAR_QUARTER', 'CALENDAR_YEAR', 'DAY_IN_MONTH', 'DAY_IN_WEEK', 'DAY_IN_YEAR', 'DAY_ONLY', 'FISCAL_MONTH', 'FISCAL_QUARTER', 'FISCAL_YEAR', 'HOUR_IN_DAY', 'WEEK_IN_MONTH', 'WEEK_IN_YEAR' }; - - public void add(String function, SObjectField field, String alias) { - this.add(function + '(' + field.toString() + ')', alias); - } - - public void add(String function, String relationship, SObjectField field, String alias) { - this.add(function + '(' + relationship + '.' + field.toString() + ')', alias); - } - - private void add(String aggregateFunction, String alias) { - if (String.isNotBlank(alias)) { - aggregateFunction += ' ' + alias; - } - this.add(aggregateFunction); - } - - private Boolean isQualified(String field) { - Boolean isFieldAliasing = !field.contains('(') && !field.contains(')') && field.contains(' '); - String functionName = field.split('\\(')[0].toUpperCase(); - return AGGREGATE_FUNCTIONS.contains(functionName) || DATE_FUNCTIONS.contains(functionName) || isFieldAliasing; - } - } - - private class FunctionsFields extends SelectFields { - private final Set FUNCTIONS = new Set{ 'CONVERTCURRENCY', 'CONVERTTIMEZONE', 'FORMAT', 'GROUPING', 'TOLABEL' }; - - public void add(String function, String field, String alias) { - this.add(function + '(' + field + ')', alias); - } - - public void add(String function, String alias) { - if (String.isNotBlank(alias)) { - function += ' ' + alias; - } - this.add(function); - } - - public Boolean isQualified(String field) { - String functionName = field.split('\\(')[0].toUpperCase(); - return FUNCTIONS.contains(functionName); - } - } - - private class SoqlSubQuery implements SubQuery { - private SoqlBuilder builder; - private String childRelationshipName; - - public SubQuery of(String ofObject) { - this.builder = new SoqlBuilder(ofObject); - this.childRelationshipName = ofObject; - return this; - } - - public SubQuery with(SObjectField field) { - this.builder.fields.plainFields.add(field.toString()); - return this; - } - - public SubQuery with(SObjectField field1, SObjectField field2) { - return this.with(field1).with(field2); - } - - public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3) { - return this.with(field1).with(field2).with(field3); - } - - public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.with(field1).with(field2).with(field3).with(field4); - } - - public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.with(field1).with(field2).with(field3).with(field4).with(field5); - } - - public SubQuery with(Iterable fields) { - this.builder.fields.plainFields.add(fields); - return this; - } - - public SubQuery with(String fields) { - this.builder.fields.with(fields); - return this; - } - - public SubQuery with(String relationshipName, Iterable fields) { - this.builder.fields.relationshipFields.add(relationshipName, fields); - return this; - } - - public SubQuery with(SubQuery subQuery) { - this.builder.subQueries.add(subQuery); - return this; - } - - public SubQuery whereAre(FilterGroup filterGroup) { - this.builder.conditions.add(filterGroup); - return this; - } - - public SubQuery whereAre(Filter filter) { - this.builder.conditions.add(filter); - return this; - } - - public SubQuery orderBy(SObjectField field) { - this.builder.orderBys.newOrderBy().with(field.toString()); - return this; - } - - public SubQuery orderBy(String field) { - this.builder.orderBys.newOrderBy().with(field); - return this; - } - - public SubQuery orderBy(String relationshipName, SObjectField field) { - this.builder.orderBys.newOrderBy().with(relationshipName + '.' + field.toString()); - return this; - } - - public SubQuery sortDesc() { - this.builder.latestOrderBy.sortingOrder('DESC'); - return this; - } - - public SubQuery sort(String direction) { - this.builder.latestOrderBy.sortingOrder(direction); - return this; - } - - public SubQuery nullsLast() { - this.builder.latestOrderBy.nullsOrder('LAST'); - return this; - } - - public SubQuery setLimit(Integer amount) { - this.builder.soqlLimit.set(amount); - return this; - } - - public SubQuery offset(Integer startingRow) { - this.builder.soqlOffset.set(startingRow); - return this; - } - - public SubQuery forReference() { - this.builder.soqlFor.set('FOR REFERENCE'); - return this; - } - - public SubQuery forView() { - this.builder.soqlFor.set('FOR VIEW'); - return this; - } - - public override String toString() { - return this.builder.toString(); - } - - public String getChildRelationshipName() { - return this.childRelationshipName; - } - } - - private class SoqlSubQueries implements QueryClause { - private List subQueries = new List(); - - public void add(SubQuery subQuery) { - this.subQueries.add(subQuery); - } - - public override String toString() { - List subQueriesStrings = new List(); - - for (SubQuery sub : this.subQueries) { - subQueriesStrings.add('(' + sub + ')'); - } - - return ', ' + String.join(subQueriesStrings, ', '); - } - - public Set get() { - Set subQueriesRelationshipNames = new Set(); - - for (SubQuery subQuery : this.subQueries) { - subQueriesRelationshipNames.add(subQuery.getChildRelationshipName()); - } - - return subQueriesRelationshipNames; - } - } - - private class SoqlFrom implements QueryClause { - private String objectApiName; - - public SoqlFrom(String objectType) { - this.objectApiName = objectType; - } - - public override String toString() { - return 'FROM ' + this.objectApiName; - } - } - - private class SoqlScope implements QueryClause { - private String scope = 'EVERYTHING'; - - public void set(String scope) { - this.scope = scope; - } - - public override String toString() { - return 'USING SCOPE ' + this.scope; - } - } - - private interface FilterClause { - String toString(); - Boolean hasValue(); - } - - private virtual class FilterBuilder { - protected List conditions = new List(); - protected String customOrder; - protected String connector = 'AND'; - - protected void add(FilterClause condition) { - if (condition.hasValue()) { - this.conditions.add(condition); - } - } - - protected String buildNested() { - return String.format(this.getOrderWithSpecialCharacters(), this.conditions); - } - - private String getOrderWithSpecialCharacters() { - String orderWithSpecialCharacters = this.getConditionsLogic(); - - for (Integer i = 0; i < this.conditions.size(); i++) { - orderWithSpecialCharacters = orderWithSpecialCharacters.replaceAll('\\b' + (i + 1).toString() + '\\b', '{' + i + '}'); - } - - return orderWithSpecialCharacters; // e.g ({0} AND ({1} AND {2})) - } - - private String getConditionsLogic() { - if (String.isNotEmpty(this.customOrder)) { - return this.customOrder; - } - - List defaultOrder = new List(); - - for (Integer i = 0; i < this.conditions.size(); i++) { - defaultOrder.add((i + 1).toString()); - } - - return String.join(defaultOrder, ' ' + this.connector + ' '); // e.g (0 AND 1 AND 2) - } - } - - private virtual class SoqlFilterGroup extends FilterBuilder implements FilterGroup { - public FilterGroup add(FilterGroup filterGroup) { - this.add(new FilterGroupAdapter(filterGroup)); - return this; - } - - public FilterGroup add(Filter filter) { - this.add(new FilterAdapter(filter)); - return this; - } - - public FilterGroup add(String dynamicCondition) { - this.add(new StringConditionAdapter(dynamicCondition)); - return this; - } - - public FilterGroup add(List filters) { - for (Filter filter: filters) { - this.add(filter); - } - return this; - } - - public FilterGroup add(List dynamicConditions) { - for (String dynamicCondition: dynamicConditions) { - this.add(dynamicCondition); - } - return this; - } - - public FilterGroup anyConditionMatching() { - this.connector = 'OR'; - return this; - } - - public FilterGroup conditionLogic(String order) { - this.customOrder = order; - return this; - } - - public FilterGroup ignoreWhen(Boolean logicExpression) { - if (logicExpression) { - this.conditions = new List(); - } - return this; - } - - public Boolean hasValues() { - return !this.conditions.isEmpty(); - } - - public virtual override String toString() { - return '(' + this.buildNested() + ')'; - } - } - - private class MainFilterGroup extends SoqlFilterGroup implements QueryClause { - public override String toString() { - if (!this.hasValues()) { - return ''; - } - - return 'WHERE ' + this.buildNested(); - } - } - - private class FilterGroupAdapter implements FilterClause { - private FilterGroup filterGroup; - - public FilterGroupAdapter(FilterGroup filterGroup) { - this.filterGroup = filterGroup; - } - - public Boolean hasValue() { - return this.filterGroup.hasValues(); - } - - public override String toString() { - return this.filterGroup.toString(); - } - } - - private class FilterAdapter implements FilterClause { - private Filter filter; - - public FilterAdapter(Filter filter) { - this.filter = filter; - } - - public Boolean hasValue() { - return this.filter.hasValue(); - } - - public override String toString() { - return this.filter.toString(); - } - } - - private class StringConditionAdapter implements FilterClause { - private String conditionString; - - public StringConditionAdapter(String dynamicCondition) { - this.conditionString = dynamicCondition; - } - - public Boolean hasValue() { - return String.isNotEmpty(this.conditionString); - } - - public override String toString() { - return this.conditionString; - } - } - - private class SoqlFilter implements Filter { - private String field; - private String comperator; - private Object value; - private String wrapper = '{0}'; - private Boolean skipBinding = false; - - public Filter id() { - return this.with('Id'); - } - - public Filter recordType() { - return this.with('RecordType.DeveloperName'); - } - - public Filter name() { - return this.with('Name'); - } - - public Filter with(SObjectField field) { - return this.with(field.toString()); - } - - public Filter with(String relationshipName, SObjectField field) { - return this.with(relationshipName + '.' + field.toString()); - } - - public Filter with(String field) { - this.field = field; - return this; - } - - public Filter isNull() { - return this.equal(null); - } - - public Filter isNotNull() { - return this.notEqual(null); - } - - public Filter isTrue() { - return this.equal(true); - } - - public Filter isFalse() { - return this.equal(false); - } - - public Filter equal(Object value) { - return this.set('=', value); - } - - public Filter notEqual(Object value) { - return this.set('!=', value); - } - - public Filter lessThan(Object value) { - return this.set('<', value); - } - - public Filter greaterThan(Object value) { - return this.set('>', value); - } - - public Filter lessOrEqual(Object value) { - return this.set('<=', value); - } - - public Filter greaterOrEqual(Object value) { - return this.set('>=', value); - } - - public Filter containsSome(Iterable values) { - return this.set('LIKE', values); - } - - public Filter contains(String value) { - return this.contains('%', formattedString(value), '%'); - } - - public Filter notContains(String value) { - return this.notLike().contains(value); - } - - public Filter endsWith(String value) { - return this.contains('%', formattedString(value), ''); - } - - public Filter notEndsWith(String value) { - return this.notLike().endsWith(value); - } - - public Filter startsWith(String value) { - return this.contains('', formattedString(value), '%'); - } - - public Filter notStartsWith(String value) { - return this.notLike().startsWith(value); - } - - public Filter contains(String prefix, String value, String suffix) { - return this.set('LIKE', prefix + formattedString(value) + suffix); - } - - public Filter notContains(String prefix, String value, String suffix) { - return this.notLike().contains(prefix, value, suffix); - } - - private String formattedString(String value) { - return value ?? value?.trim(); - } - - public Filter isIn(Iterable iterable) { - return this.set('IN', iterable); - } - - public Filter isIn(InnerJoin joinQuery) { - this.skipBinding = true; - return this.set('IN', joinQuery); - } - - private Filter notLike() { - this.wrapper = '(NOT {0})'; - return this; - } - - public Filter notIn(Iterable iterable) { - return this.set('NOT IN', iterable); - } - - public Filter notIn(InnerJoin joinQuery) { - this.skipBinding = true; - return this.set('NOT IN', joinQuery); - } - - public Filter includesAll(Iterable iterable) { - return this.setMultipicklistFilter('INCLUDES', iterable, ';'); - } - - public Filter includesSome(Iterable iterable) { - return this.setMultipicklistFilter('INCLUDES', iterable, '\', \''); - } - - public Filter excludesAll(Iterable iterable) { - return this.setMultipicklistFilter('EXCLUDES', iterable, '\', \''); - } - - public Filter excludesSome(Iterable iterable) { - return this.setMultipicklistFilter('EXCLUDES', iterable, ';'); - } - - public Filter setMultipicklistFilter(String operator, Iterable iterable, String separator) { - // Bind expressions can't be used with other clauses, such as INCLUDES, EXCLUDES - this.skipBinding = true; - return this.set(operator, '(\'' + String.join(iterable, separator) + '\')'); - } - - private Filter set(String comperator, Object value) { - this.value = value; - this.comperator = comperator; - return this; - } - - public Boolean hasValue() { - return String.isNotEmpty(this.field); - } - - public Filter ignoreWhen(Boolean logicExpression) { - if (logicExpression) { - // Set field as empty to meet hasValue and ignore condition - this.with(''); - } - return this; - } - - public Filter asDateLiteral() { - // Date Literals can't be binded - this.skipBinding = true; - return this; - } - - public override String toString() { - if (this.skipBinding) { - return String.format(this.wrapper, new List{ this.field + ' ' + this.comperator + ' ' + this.value }); - } - - return String.format(this.wrapper, new List{ this.field + ' ' + this.comperator + ' :' + binder.bind(value) }); - } - } - - private virtual class MainDataCategoryGroup extends FilterBuilder implements DataCategoryFilterGroup, QueryClause { - public DataCategoryFilterGroup add(DataCategoryFilter dataCategoryFilter) { - this.add(new DataCategoryFilterAdapter(dataCategoryFilter)); - return this; - } - - public override String toString() { - return 'WITH DATA CATEGORY ' + this.buildNested(); - } - } - - private class DataCategoryFilterAdapter implements FilterClause { - private DataCategoryFilter dataCategoryFilter; - - public DataCategoryFilterAdapter(DataCategoryFilter dataCategoryFilter) { - this.dataCategoryFilter = dataCategoryFilter; - } - - public Boolean hasValue() { - return this.dataCategoryFilter.hasValue(); - } - - public override String toString() { - return this.dataCategoryFilter.toString(); - } - } - - private class SoqlDataCategoryFilter implements DataCategoryFilter { - private String field; - private String comperator; - private String value; - - public DataCategoryFilter with(String field) { - this.field = field; - return this; - } - - public DataCategoryFilter at(String category) { - return this.set('AT', category); - } - - public DataCategoryFilter at(Iterable categories) { - return this.set('AT', categories); - } - - public DataCategoryFilter above(String category) { - return this.set('ABOVE', category); - } - - public DataCategoryFilter above(Iterable categories) { - return this.set('ABOVE', categories); - } - - public DataCategoryFilter below(String category) { - return this.set('BELOW', category); - } - - public DataCategoryFilter below(Iterable categories) { - return this.set('BELOW', categories); - } - - public DataCategoryFilter aboveOrBelow(String category) { - return this.set('ABOVE_OR_BELOW', category); - } - - public DataCategoryFilter aboveOrBelow(Iterable categories) { - return this.set('ABOVE_OR_BELOW', categories); - } - - public DataCategoryFilter set(String operator, Iterable iterable) { - this.comperator = operator; - this.value = '(' + String.join(iterable, ', ') + ')'; - return this; - } - - private DataCategoryFilter set(String comperator, String value) { - this.comperator = comperator; - this.value = value; - return this; - } - - public Boolean hasValue() { - return String.isNotEmpty(this.field); - } - - public override String toString() { - return this.field + ' ' + this.comperator + ' ' + this.value; - } - } - - private class SoqlJoinQuery implements InnerJoin { - private SoqlBuilder builder; - - public InnerJoin of(SObjectType ofObject) { - this.builder = new SoqlBuilder(ofObject.toString()); - return this; - } - - public InnerJoin with(SObjectField field) { - this.builder.fields.plainFields.add(field.toString()); - return this; - } - - public InnerJoin whereAre(FilterGroup filterGroup) { - this.builder.conditions.add(filterGroup); - return this; - } - - public InnerJoin whereAre(Filter filter) { - this.builder.conditions.add(filter); - return this; - } - - public override String toString() { - return '(' + this.builder.toString() + ')'; - } - } - - private class SoqlGroupBy implements QueryClause { - private Set groupByFields = new Set(); - private String groupByFunction = ''; - - public void with(String relationshipName, SObjectField field) { - this.with(relationshipName + '.' + field.toString()); - } - - private void with(String field) { - this.setGroupByFunction('{0}'); - this.groupByFields.add(field.trim()); - } - - public void with(String field, String function) { - this.setGroupByFunction(function + '({0})'); - this.groupByFields.add(field.trim()); - } - - public void with(String relationshipName, String field, String function) { - this.setGroupByFunction(function + '({0})'); - this.groupByFields.add((relationshipName + '.' + field).trim()); - } - - public void setGroupByFunction(String newGroupByFunction) { - if (String.isNotEmpty(groupByFunction) && groupByFunction != newGroupByFunction) { - throw new QueryException('You can\'t use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.'); - } - this.groupByFunction = newGroupByFunction; - } - - public override String toString() { - return 'GROUP BY ' + String.format(this.groupByFunction, new List{ String.join( this.groupByFields, ', ') }); - } - } - - private class SoqlOrderBys implements QueryClause { - public List orderBys = new List(); - - public SoqlOrderBy newOrderBy() { - this.orderBys.add(new SoqlOrderBy()); - return this.latestOrderBy(); - } - - public SoqlOrderBy latestOrderBy() { - return this.orderBys.get(orderBys.size() - 1); - } - - public override String toString() { - List orderFields = new List(); - - for (SoqlOrderBy orderBy : this.orderBys) { - orderFields.add(orderBy.toString()); - } - - return 'ORDER BY ' + String.join(orderFields, ', '); - } - } - - private virtual class SoqlHavingFilterGroup extends FilterBuilder implements HavingFilterGroup { - public HavingFilterGroup add(HavingFilterGroup filterGroup) { - this.add(new HavingFilterGroupAdapter(filterGroup)); - return this; - } - - public HavingFilterGroup add(HavingFilter filter) { - this.add(new HavingFilterAdapter(filter)); - return this; - } - - public HavingFilterGroup add(String filter) { - this.add(new StringConditionAdapter(filter)); - return this; - } - - public HavingFilterGroup anyConditionMatching() { - this.connector = 'OR'; - return this; - } - - public HavingFilterGroup conditionLogic(String order) { - this.customOrder = order; - return this; - } - - public virtual override String toString() { - return '(' + this.buildNested() + ')'; - } - - public Boolean hasValues() { - return !this.conditions.isEmpty(); - } - } - - private class MainHavingGroup extends SoqlHavingFilterGroup implements QueryClause { - public override String toString() { - return 'HAVING ' + this.buildNested(); - } - } - - private class HavingFilterGroupAdapter implements FilterClause { - private HavingFilterGroup havingFilterGroup; - - public HavingFilterGroupAdapter(HavingFilterGroup havingFilterGroup) { - this.havingFilterGroup = havingFilterGroup; - } - - public Boolean hasValue() { - return this.havingFilterGroup.hasValues(); - } - - public override String toString() { - return this.havingFilterGroup.toString(); - } - } - - private class HavingFilterAdapter implements FilterClause { - private HavingFilter havingFilter; - - public HavingFilterAdapter(HavingFilter havingFilter) { - this.havingFilter = havingFilter; - } - - public Boolean hasValue() { - return this.havingFilter.hasValue(); - } - - public override String toString() { - return this.havingFilter.toString(); - } - } - - private class SoqlHavingFilter implements HavingFilter { - private String field; - private String comperator; - private Object value; - private String wrapper = '{0}'; - - public HavingFilter with(SObjectField field) { - return this.with(field.toString()); - } - - public HavingFilter with(String field) { - this.field = field; - return this; - } - - public HavingFilter count(SObjectField field) { - return this.withAggregateFunction('COUNT', field); - } - - public HavingFilter avg(SObjectField field) { - return this.withAggregateFunction('AVG', field); - } - - public HavingFilter countDistinct(SObjectField field) { - return this.withAggregateFunction('COUNT_DISTINCT', field); - } - - public HavingFilter min(SObjectField field) { - return this.withAggregateFunction('MIN', field); - } - - public HavingFilter max(SObjectField field) { - return this.withAggregateFunction('MAX', field); - } - - public HavingFilter sum(SObjectField field) { - return this.withAggregateFunction('SUM', field); - } - - private HavingFilter withAggregateFunction(String aggregateFunction, SObjectField field) { - return this.withAggregateFunction(aggregateFunction, field.toString()); - } - - private HavingFilter withAggregateFunction(String aggregateFunction, String field) { - this.field = aggregateFunction + '(' + field + ')'; - return this; - } - - public HavingFilter isNull() { - return this.equal(null); - } - - public HavingFilter isNotNull() { - return this.notEqual(null); - } - - public HavingFilter isTrue() { - return this.equal(true); - } - - public HavingFilter isFalse() { - return this.equal(false); - } - - public HavingFilter equal(Object value) { - return this.set('=', value); - } - - public HavingFilter notEqual(Object value) { - return this.set('!=', value); - } - - public HavingFilter lessThan(Object value) { - return this.set('<', value); - } - - public HavingFilter greaterThan(Object value) { - return this.set('>', value); - } - - public HavingFilter lessOrEqual(Object value) { - return this.set('<=', value); - } - - public HavingFilter greaterOrEqual(Object value) { - return this.set('>=', value); - } - - public HavingFilter contains(String value) { - return this.contains('%', formattedString(value), '%'); - } - - public HavingFilter notContains(String value) { - return this.notLike().contains(value); - } - - public HavingFilter endsWith(String value) { - return this.contains('%', formattedString(value), ''); - } - - public HavingFilter notEndsWith(String value) { - return this.notLike().endsWith(value); - } - - public HavingFilter startsWith(String value) { - return this.contains('', formattedString(value), '%'); - } - - public HavingFilter notStartsWith(String value) { - return this.notLike().startsWith(value); - } - - public HavingFilter contains(String prefix, String value, String suffix) { - return this.set('LIKE', prefix + formattedString(value) + suffix); - } - - public HavingFilter notContains(String prefix, String value, String suffix) { - return this.notLike().contains(prefix, value, suffix); - } - - public HavingFilter isIn(Iterable iterable) { - return this.set('IN', iterable, '\', \''); - } - - public HavingFilter notIn(Iterable iterable) { - return this.set('NOT IN', iterable, '\', \''); - } - - private String formattedString(String value) { - return value ?? value?.trim(); - } - - private HavingFilter notLike() { - this.wrapper = '(NOT {0})'; - return this; - } - - public HavingFilter set(String operator, Iterable iterable, String separator) { - this.comperator = operator; - this.value = '(\'' + String.join(iterable, separator) + '\')'; - return this; - } - - private HavingFilter set(String comperator, Object value) { - this.comperator = comperator; - this.value = value instanceof String ? '\'' + value + '\'' : value; - return this; - } - - public Boolean hasValue() { - return String.isNotEmpty(this.field); - } - - public override String toString() { - return String.format(this.wrapper, new List{ this.field + ' ' + this.comperator + ' ' + this.value }); - } - } - - private class SoqlOrderBy implements QueryClause { - private String orderField; - private String sortingOrder = 'ASC'; - private String nullsOrder = 'FIRST'; - - public void with(String field) { - this.orderField = field; - } - - public void sortingOrder(String direction) { - this.sortingOrder = direction; - } - - public void nullsOrder(String nullsOrder) { - this.nullsOrder = nullsOrder; - } - - public override String toString() { - return this.orderField + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; - } - } - - private class SoqlLimit implements QueryClause { - private Integer soqlLimit; - - public void set(Integer soqlLimit) { - this.soqlLimit = soqlLimit; - } - - public override String toString() { - return 'LIMIT ' + this.soqlLimit; - } - } - - private class SoqlOffset implements QueryClause { - private Integer soqlOffset; - - public void set(Integer fromRow) { - this.soqlOffset = fromRow; - } - - public override String toString() { - return 'OFFSET ' + this.soqlOffset; - } - } - - private class SoqlFor implements QueryClause { - private String forStatement; - - public void set(String forStatement) { - this.forStatement = forStatement; - } - - public override String toString() { - return this.forStatement; - } - } - - private class Binder { - private Integer bindIndex = 0; - private Map binding = new Map(); - - public String bind(Object value) { - bindIndex++; - binding.put('v' + bindIndex, value); - return 'v' + bindIndex; - } - - public void reset() { - this.bindIndex = 0; - this.binding.clear(); - } - - public Map getBindingMap() { - return binding; - } - } - - private class SoqlMock implements Mockable { - public SObjectMock sObjectMock = new SObjectMock(); - public CountMock countMock = new CountMock(); - public AggregateResultProxys aggregateResultMock = new AggregateResultProxys(); - - public Mockable thenReturn(List> aggregatedResults) { - this.aggregateResultMock.add(aggregatedResults); - return this; - } - - public Mockable thenReturn(Map aggregatedResult) { - this.aggregateResultMock.add(aggregatedResult); - return this; - } - - public Mockable thenReturn(SObject record) { - this.sObjectMock.add(record); - return this; - } - - public Mockable thenReturn(List records) { - this.sObjectMock.add(records); - return this; - } - - public Mockable thenReturn(Integer count) { - this.countMock.set(count); - return this; - } - - public SoqlMock useLegacyMockingBehavior() { - this.sObjectMock.useLegacyMockingBehavior = true; - return this; - } - } - - public class SObjectMock { - private List mockedRecords = new List(); - public Boolean useLegacyMockingBehavior = false; - - public void add(SObject record) { - this.mockedRecords.add(record); - } - - public void add(List records) { - this.mockedRecords.addAll(records); - } - - public List get(SoqlFields fields, SoqlSubQueries subQueries) { - // AggregateResult can be mocked only with Id field for toIdsOf, toValueOf - if (this.useLegacyMockingBehavior || this.mockedRecords.isEmpty() || this.mockedRecords[0]?.getSObjectType() == AggregateResult.SObjectType) { - return this.mockedRecords; - } - - if (!fields.aggregateFields.isEmpty()) { - throw new QueryException('Use toAggregatedProxy() to mock AggregateResult records.'); - } - - this.addIdToMockedRecords(); - - if (!fields.relationshipFields.isEmpty() || !fields.functionsFields.isEmpty()) { - return this.mockedRecords; - } - - return this.stripAdditionalFields(fields.plainFields.get(), subQueries.get()); - } - - private void addIdToMockedRecords() { // Id is always added to mirror standard SOQL behavior - SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); - String sObjectPrefix = sObjectType.getDescribe().getKeyPrefix(); - - for (SObject record : this.mockedRecords) { - record.put('Id', record?.Id ?? IdGenerator.get(sObjectPrefix)); - } - } - - private List stripAdditionalFields(Set requestedFields, Set subQueriesRelationshipNames) { - List cleanedRecords = new List(); - - Type objectTypeName = Type.forName(this.mockedRecords[0].getSObjectType().toString()); - - for (SObject record : this.mockedRecords) { - Map recordFilteredFields = new Map{ 'Id' => record.Id }; - - this.stripAdditionalPlainFields(recordFilteredFields, record, requestedFields); - this.stripAdditionalSubQueries(recordFilteredFields, record, subQueriesRelationshipNames); - - // JSON.serialize and JSON.deserialize are used to copy not writable fields - cleanedRecords.add((SObject) JSON.deserialize(JSON.serialize(recordFilteredFields), objectTypeName)); - } - - return cleanedRecords; - } - - private void stripAdditionalPlainFields(Map recordFilteredFields, SObject record, Set requestedFields) { - for (String field : requestedFields) { - recordFilteredFields.put(field, record.get(field)); - } - } - - private void stripAdditionalSubQueries(Map recordFilteredFields, SObject record, Set subQueriesRelationshipNames) { - for (String subQueryRelationshipName : subQueriesRelationshipNames) { - recordFilteredFields.put(subQueryRelationshipName, new Map{ - 'totalSize' => record.getSObjects(subQueryRelationshipName)?.size() ?? 0, - 'done' => true, - 'records' => record.getSObjects(subQueryRelationshipName) ?? new List() - }); - } - } - } - - public class CountMock { - private Integer countMock; - - public void set(Integer count) { - this.countMock = count; - } - - public Integer get() { - return this.countMock; - } - } - - private class AggregateResultProxys { - private List aggregateResults = new List(); - - public AggregateResultProxys add(List aggregateResults) { - for (AggregateResult result : aggregateResults) { - this.aggregateResults.add(new SoqlAggregateResultProxy(result)); - } - return this; - } - - public AggregateResultProxys add(List> aggregateResults) { - for (Map result : aggregateResults) { - this.add(result); - } - return this; - } - - public AggregateResultProxys add(Map aggregateResult) { - this.aggregateResults.add(new SoqlAggregateResultProxy(aggregateResult)); - return this; - } - - public List get() { - return this.aggregateResults; - } - } - - private class SoqlAggregateResultProxy implements AggregateResultProxy { - private Map aggregateResult; - - public SoqlAggregateResultProxy(AggregateResult aggregateResult) { - this.aggregateResult = aggregateResult.getPopulatedFieldsAsMap(); - } - - public SoqlAggregateResultProxy(Map aggregateResult) { - this.aggregateResult = aggregateResult; - } - - public Object get(String field) { - return this.aggregateResult.get(field); - } - - public Map getPopulatedFieldsAsMap() { - return this.aggregateResult; - } - } - - private inherited sharing class Executor { - private DatabaseQuery sharingExecutor; - private AccessLevel accessMode; - private AccessType accessType; - private SoqlBuilder builder; - private List mocks = new List(); - - public Executor(SoqlBuilder builder) { - this.builder = builder; - this.accessMode = AccessLevel.USER_MODE; - this.sharingExecutor = new InheritedSharing(); - } - - public void withSharing() { - this.sharingExecutor = new WithSharing(); - } - - public void withoutSharing() { - this.sharingExecutor = new WithoutSharing(); - } - - public void stripInaccessible(AccessType type) { - this.accessType = type; - } - - public void accessMode(AccessLevel accessMode) { - this.accessMode = accessMode; - } - - public void mock(List mocks) { - this.mocks = mocks ?? new List(); - } - - public SObject toObject() { - List records = toList(); - - if (records.isEmpty()) { - return null; // handle: List has no rows for assignment to SObject - } - - if (records.size() > 1) { - throw new QueryException('List has more than 1 row for assignment to SObject'); - } - - return records[0]; - } - - public List toList() { - this.incrementQueryIssued(); - - if (!this.mocks.isEmpty()) { - return this.getMockedListProxy(); - } - - if (this.accessType == null) { - return this.sharingExecutor.toSObjects(this.builder.toString(), binder.getBindingMap(), this.accessMode); - } - - return System.Security.stripInaccessible( - this.accessType, - this.sharingExecutor.toSObjects(this.builder.toString(), binder.getBindingMap(), this.accessMode) - ).getRecords(); - } - - private List getMockedListProxy() { - if (this.mocks.size() == 1) { - return this.mocks[0].sObjectMock.get(this.builder.fields, this.builder.subQueries); - } - return this.mocks.remove(0).sObjectMock.get(this.builder.fields, this.builder.subQueries); - } - - public List toAggregatedProxy() { - this.incrementQueryIssued(); - - if (!this.mocks.isEmpty()) { - return this.getMockedAggregateProxy(); - } - - return new AggregateResultProxys().add(this.toList()).get(); - } - - private List getMockedAggregateProxy() { - if (this.mocks.size() == 1) { - return this.mocks[0].aggregateResultMock.get(); - } - return this.mocks.remove(0).aggregateResultMock.get(); - } - - public Integer toInteger() { - this.incrementQueryIssued(); - - if (!this.mocks.isEmpty()) { - return this.getMockedCount(); - } - - return this.sharingExecutor.toInteger(this.builder.toString(), binder.getBindingMap(), this.accessMode); - } - - private Integer getMockedCount() { - if (this.mocks.size() == 1) { - return this.mocks[0].countMock.get(); - } - return this.mocks.remove(0).countMock.get(); - } - - public Database.QueryLocator toQueryLocator() { - this.incrementQueryIssued(); - return this.sharingExecutor.toQueryLocator(this.builder.toString(), binder.getBindingMap(), this.accessMode); - } - - private void incrementQueryIssued() { - // This counter is used only in unit tests to also track mocked queries. - if (!System.Test.isRunningTest() || System.isBatch() || System.isFuture() || System.isQueueable() || System.isScheduled()) { - return; - } - - SOQL.syncQueriesIssued++; - - final Integer maxQueriesIssuedInSynchronousTransaction = 100; - - if (SOQL.syncQueriesIssued > maxQueriesIssuedInSynchronousTransaction) { - throw new QueryException('Too many SOQL queries.'); - } - } - } - - private interface DatabaseQuery { - List toSObjects(String query, Map binding, AccessLevel accessLevel); - Integer toInteger(String query, Map binding, AccessLevel accessLevel); - Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel); - } - - private inherited sharing class InheritedSharing implements DatabaseQuery { - public List toSObjects(String query, Map binding, AccessLevel accessLevel) { - return Database.queryWithBinds(query, binding, accessLevel); - } - - public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { - return Database.countQueryWithBinds(query, binding, accessLevel); - } - - public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { - return Database.getQueryLocatorWithBinds(query, binding, accessLevel); - } - } - - private without sharing class WithoutSharing implements DatabaseQuery { - public List toSObjects(String query, Map binding, AccessLevel accessLevel) { - return Database.queryWithBinds(query, binding, accessLevel); - } - - public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { - return Database.countQueryWithBinds(query, binding, accessLevel); - } - - public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { - return Database.getQueryLocatorWithBinds(query, binding, accessLevel); - } - } - - private with sharing class WithSharing implements DatabaseQuery { - public List toSObjects(String query, Map binding, AccessLevel accessLevel) { - return Database.queryWithBinds(query, binding, accessLevel); - } - - public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { - return Database.countQueryWithBinds(query, binding, accessLevel); - } + public interface Selector { + Queryable query(); + } + + public static Queryable of(SObjectType ofObject) { + return new SOQL(ofObject); + } + + public static Queryable of(String ofObject) { + return new SOQL(ofObject); + } + + public interface Queryable { + // SELECT + Queryable with(SObjectField field); + Queryable with(SObjectField field1, SObjectField field2); + Queryable with(SObjectField field1, SObjectField field2, SObjectField field3); + Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); + Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); + Queryable with(List fields); + Queryable with(Iterable fields); + Queryable with(String fields); + Queryable with(SObjectField field, String alias); + Queryable with(String relationshipName, SObjectField field); + Queryable with(String relationshipName, SObjectField field1, SObjectField field2); + Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3); + Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); + Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); + Queryable with(String relationshipName, Iterable fields); + Queryable with(SubQuery subQuery); + Queryable withFieldSet(String fieldSetName); + // SELECT - AGGREGATE FUNCTIONS + Queryable count(); + Queryable count(SObjectField field); + Queryable count(SObjectField field, String alias); + Queryable count(String relationshipName, SObjectField field); + Queryable count(String relationshipName, SObjectField field, String alias); + Queryable avg(SObjectField field); + Queryable avg(SObjectField field, String alias); + Queryable avg(String relationshipName, SObjectField field); + Queryable avg(String relationshipName, SObjectField field, String alias); + Queryable countDistinct(SObjectField field); + Queryable countDistinct(SObjectField field, String alias); + Queryable countDistinct(String relationshipName, SObjectField field); + Queryable countDistinct(String relationshipName, SObjectField field, String alias); + Queryable min(SObjectField field); + Queryable min(SObjectField field, String alias); + Queryable min(String relationshipName, SObjectField field); + Queryable min(String relationshipName, SObjectField field, String alias); + Queryable max(SObjectField field); + Queryable max(SObjectField field, String alias); + Queryable max(String relationshipName, SObjectField field); + Queryable max(String relationshipName, SObjectField field, String alias); + Queryable sum(SObjectField field); + Queryable sum(SObjectField field, String alias); + Queryable sum(String relationshipName, SObjectField field); + Queryable sum(String relationshipName, SObjectField field, String alias); + // SELECT - GROUPING + Queryable grouping(SObjectField field, String alias); + // SELECT - toLabel + Queryable toLabel(SObjectField field); + Queryable toLabel(SObjectField field, String alias); + Queryable toLabel(String field); + Queryable toLabel(String field, String alias); + // SELECT - FORMAT + Queryable format(SObjectField field); + Queryable format(SObjectField field, String alias); + // USING SCOPE + Queryable delegatedScope(); + Queryable mineScope(); + Queryable mineAndMyGroupsScope(); + Queryable myTerritoryScope(); + Queryable myTeamTerritoryScope(); + Queryable teamScope(); + // WHERE + Queryable whereAre(FilterGroup filterGroup); + Queryable whereAre(Filter filter); + Queryable whereAre(String conditions); + Queryable conditionLogic(String order); + Queryable anyConditionMatching(); + // PREDEFINED WHERE + Queryable byId(SObject record); + Queryable byId(Id recordId); + Queryable byIds(Iterable recordIds); + Queryable byIds(List records); + Queryable byRecordType(String recordTypeDeveloperName); + // GROUP BY + Queryable groupBy(SObjectField field); + Queryable groupBy(String field); + Queryable groupBy(String relationshipName, SObjectField field); + Queryable groupByRollup(SObjectField field); + Queryable groupByRollup(String relationshipName, SObjectField field); + Queryable groupByCube(SObjectField field); + Queryable groupByCube(String relationshipName, SObjectField field); + // HAVING + Queryable have(HavingFilterGroup havingFilterGroup); + Queryable have(HavingFilter havingFilter); + Queryable have(String havingConditions); + Queryable havingConditionLogic(String havingConditionsOrder); + Queryable anyHavingConditionMatching(); + // WITH DATA CATEGORY + Queryable withDataCategory(DataCategoryFilter dataCategoryFilter); + // ORDER BY + Queryable orderBy(SObjectField field); + Queryable orderBy(String field); + Queryable orderBy(String field, String direction); + Queryable orderBy(String relationshipName, SObjectField field); + Queryable orderByCount(SObjectField field); + Queryable sortDesc(); + Queryable sort(String direction); + Queryable nullsLast(); + Queryable nullsOrder(String nullsOrder); + // LIMIT + Queryable setLimit(Integer amount); + // OFFSET + Queryable offset(Integer startingRow); + // FOR + Queryable forReference(); + Queryable forView(); + Queryable forUpdate(); + Queryable allRows(); + // FIELD-LEVEL SECURITY + Queryable userMode(); + Queryable systemMode(); + Queryable stripInaccessible(); + Queryable stripInaccessible(AccessType accessType); + // SHARING MODE + Queryable withSharing(); + Queryable withoutSharing(); + // MOCKING + Queryable mockId(String queryIdentifier); + // DEBUGGING + Queryable preview(); + // SOBJECT + SObject toObject(); + // LIST + List toList(); + // COUNT + Integer toInteger(); + // QUERY LOCATOR + Database.QueryLocator toQueryLocator(); + // DO EXIST + Boolean doExist(); + // AGGREGATE RESULTS + List toAggregated(); + List toAggregatedProxy(); // use when you need mocking + // STRING + String toString(); + // IDS + Id toId(); + Set toIds(); + Set toIdsOf(SObjectField field); + Set toIdsOf(String relationshipName, SObjectField targetKeyField); + // VALUE + Object toValueOf(SObjectField fieldToExtract); + Set toValuesOf(SObjectField fieldToExtract); + Set toValuesOf(String relationshipName, SObjectField targetKeyField); + // ID MAP + Map toMap(); + Map toIdMapBy(SObjectField field); + Map toIdMapBy(String relationshipName, SObjectField targetKeyField); + Map> toAggregatedIdMapBy(SObjectField keyField); + Map> toAggregatedIdMapBy(String relationshipName, SObjectField targetKeyField); + // VALUE MAP + Map toMap(SObjectField keyField); + Map toMap(String relationshipName, SObjectField targetKeyField); + Map toMap(SObjectField keyField, SObjectField valueField); + Map> toAggregatedMap(SObjectField keyField); + Map> toAggregatedMap(String relationshipName, SObjectField targetKeyField); + Map> toAggregatedMap(SObjectField keyField, SObjectField valueField); + Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField); + + // for internal use + Map binding(); + } + + public interface SubQuery { + SubQuery of(String ofObject); + // SELECT + SubQuery with(SObjectField field); + SubQuery with(SObjectField field1, SObjectField field2); + SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3); + SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); + SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); + SubQuery with(Iterable fields); + SubQuery with(String fields); + SubQuery with(String relationshipName, Iterable fields); + SubQuery with(SubQuery subQuery); + // WHERE + SubQuery whereAre(FilterGroup filterGroup); + SubQuery whereAre(Filter filter); + // ORDER BY + SubQuery orderBy(SObjectField field); + SubQuery orderBy(String field); + SubQuery orderBy(String relationshipName, SObjectField field); + SubQuery sortDesc(); + SubQuery sort(String direction); + SubQuery nullsLast(); + // LIMIT + SubQuery setLimit(Integer amount); + // OFFSET + SubQuery offset(Integer startingRow); + // FOR + SubQuery forReference(); + SubQuery forView(); + // ADDITIONAL + String getChildRelationshipName(); + } + + public interface FilterGroup { + // ADD CONDITION + FilterGroup add(FilterGroup filterGroup); + FilterGroup add(Filter filter); + FilterGroup add(String dynamicCondition); + FilterGroup add(List filters); + FilterGroup add(List dynamicConditions); + // ORDER + FilterGroup anyConditionMatching(); + FilterGroup conditionLogic(String order); + // ADDITIONAL + FilterGroup ignoreWhen(Boolean logicExpression); + + Boolean hasValues(); + } + + public interface Filter { + // FIELDS + Filter id(); + Filter recordType(); + Filter name(); + Filter with(SObjectField field); + Filter with(String field); + Filter with(String relationshipName, SObjectField field); + // COMPARATORS + Filter isNull(); + Filter isNotNull(); + Filter isTrue(); + Filter isFalse(); + Filter equal(Object value); + Filter notEqual(Object value); + Filter lessThan(Object value); + Filter lessOrEqual(Object value); + Filter greaterThan(Object value); + Filter greaterOrEqual(Object value); + Filter containsSome(Iterable values); + Filter contains(String value); + Filter contains(String prefix, String value, String suffix); + Filter notContains(String value); + Filter notContains(String prefix, String value, String suffix); + Filter endsWith(String value); + Filter notEndsWith(String value); + Filter startsWith(String value); + Filter notStartsWith(String value); + Filter isIn(Iterable iterable); + Filter isIn(InnerJoin joinQuery); + Filter notIn(Iterable iterable); + Filter notIn(InnerJoin joinQuery); + Filter includesAll(Iterable values); + Filter includesSome(Iterable values); + Filter excludesAll(Iterable values); + Filter excludesSome(Iterable values); + // ADDITIONAL + Filter asDateLiteral(); + Filter ignoreWhen(Boolean logicExpression); + + Boolean hasValue(); + } + + public interface InnerJoin { + InnerJoin of(SObjectType ofObject); + // SELECT + InnerJoin with(SObjectField field); + // WHERE + InnerJoin whereAre(FilterGroup filterGroup); + InnerJoin whereAre(Filter filter); + } + + public interface HavingFilterGroup { + // ADD CONDITION + HavingFilterGroup add(HavingFilterGroup havingFilterGroup); + HavingFilterGroup add(HavingFilter havingFilter); + HavingFilterGroup add(String dynamicHaving); + // ORDER + HavingFilterGroup anyConditionMatching(); + HavingFilterGroup conditionLogic(String order); + + Boolean hasValues(); + } + + public interface HavingFilter { + // FIELDS + HavingFilter with(SObjectField field); + HavingFilter with(String field); + HavingFilter count(SObjectField field); + HavingFilter avg(SObjectField field); + HavingFilter countDistinct(SObjectField field); + HavingFilter min(SObjectField field); + HavingFilter max(SObjectField field); + HavingFilter sum(SObjectField field); + // COMPARATORS + HavingFilter isNull(); + HavingFilter isNotNull(); + HavingFilter isTrue(); + HavingFilter isFalse(); + HavingFilter equal(Object value); + HavingFilter notEqual(Object value); + HavingFilter lessThan(Object value); + HavingFilter lessOrEqual(Object value); + HavingFilter greaterThan(Object value); + HavingFilter greaterOrEqual(Object value); + HavingFilter contains(String value); + HavingFilter contains(String prefix, String value, String suffix); + HavingFilter notContains(String value); + HavingFilter notContains(String prefix, String value, String suffix); + HavingFilter startsWith(String value); + HavingFilter notStartsWith(String value); + HavingFilter endsWith(String value); + HavingFilter notEndsWith(String value); + HavingFilter isIn(Iterable iterable); + HavingFilter notIn(Iterable iterable); + + Boolean hasValue(); + } + + public interface DataCategoryFilterGroup { + DataCategoryFilterGroup add(DataCategoryFilter dataCategoryFilter); + } + + public interface DataCategoryFilter { + // FIELDS + DataCategoryFilter with(String field); + // COMPARATORS + DataCategoryFilter at(String category); + DataCategoryFilter at(Iterable categories); + DataCategoryFilter above(String category); + DataCategoryFilter above(Iterable categories); + DataCategoryFilter below(String category); + DataCategoryFilter below(Iterable categories); + DataCategoryFilter aboveOrBelow(String category); + DataCategoryFilter aboveOrBelow(Iterable categories); + + Boolean hasValue(); + } + + public interface AggregateResultProxy { + Object get(String field); + Map getPopulatedFieldsAsMap(); + } + + public static SubQuery SubQuery { + get { return new SoqlSubQuery(); } + } + + public static FilterGroup FilterGroup { + get { return new SoqlFilterGroup(); } + } + + public static Filter Filter { + get { return new SoqlFilter(); } + } + + public static InnerJoin InnerJoin { + get { return new SoqlJoinQuery(); } + } + + public static HavingFilterGroup HavingFilterGroup { + get { return new SoqlHavingFilterGroup(); } + } + + public static HavingFilter HavingFilter { + get { return new SoqlHavingFilter(); } + } + + public static DataCategoryFilter DataCategoryFilter { + get { return new SoqlDataCategoryFilter(); } + } + + // Mocking + + @TestVisible + private static Mockable mock(String mockId) { + if (!SOQL.queryIdToMock.containsKey(mockId)) { + SOQL.queryIdToMock.put(mockId, new List()); + } + SOQL.queryIdToMock.get(mockId).add(new SoqlMock()); + return SOQL.queryIdToMock.get(mockId).get(SOQL.queryIdToMock.get(mockId).size() - 1); + } + + public static RandomIdGenerator IdGenerator = new RandomIdGenerator(); + + public interface Mockable { + // SObject + Mockable thenReturn(SObject record); + Mockable thenReturn(List records); + // AggregateResultProxy + Mockable thenReturn(List> aggregatedResults); + Mockable thenReturn(Map aggregatedResult); + // Count + Mockable thenReturn(Integer count); + } + + // Backward support - it's going to be removed in the future + + @TestVisible // deprecated + private static void setMock(String mockId, SObject record) { + SOQL.createLegacyMock(mockId).thenReturn(record); + } + + @TestVisible // deprecated + private static void setMock(String mockId, List records) { + SOQL.createLegacyMock(mockId).thenReturn(records); + } + + @TestVisible // deprecated + private static void setCountMock(String mockId, Integer amount) { + SOQL.createLegacyMock(mockId).thenReturn(amount); + } + + private static SoqlMock createLegacyMock(String mockId) { + SoqlMock mock = new SoqlMock().useLegacyMockingBehavior(); + SOQL.queryIdToMock.put(mockId, new List{ mock }); + return mock; + } + + // Implementation + + private static Map> queryIdToMock = new Map>(); + + private static Binder binder = new Binder(); + private static Integer syncQueriesIssued = 0; + + private SoqlBuilder builder; + private Executor executor; + private Converter converter; + + public SOQL(SObjectType ofObject) { + this(ofObject.toString()); + } + + public SOQL(String ofObject) { + this.builder = new SoqlBuilder(ofObject); + this.executor = new Executor(builder); + this.converter = new Converter(ofObject); + } + + public Queryable with(SObjectField field) { + this.builder.fields.plainFields.add(field.toString()); + return this; + } + + public Queryable with(SObjectField field1, SObjectField field2) { + return this.with(field1).with(field2); + } + + public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3) { + return this.with(field1, field2).with(field3); + } + + public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { + return this.with(field1, field2, field3).with(field4); + } + + public Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { + return this.with(field1, field2, field3, field4).with(field5); + } + + public Queryable with(List fields) { + this.builder.fields.plainFields.add(fields); + return this; + } + + public Queryable with(Iterable fields) { + return this.with(String.join(fields, ',')); + } + + public Queryable withFieldSet(String fieldSetName) { + this.builder.fields.withFieldSet(fieldSetName); + return this; + } + + public Queryable with(String fields) { + this.builder.fields.with(fields); + return this; + } + + public Queryable with(SObjectField field, String alias) { + return this.with(field.toString(), alias); + } + + private Queryable with(String field, String alias) { + this.builder.fields.aggregateFields.add(field, alias); + return this; + } + + public Queryable with(String relationshipName, SObjectField field) { + return this.with(relationshipName, new List{ field }); + } + + public Queryable with(String relationshipName, SObjectField field1, SObjectField field2) { + return this.with(relationshipName, new List{ field1, field2 }); + } + + public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3) { + return this.with(relationshipName, new List{ field1, field2, field3 }); + } + + public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { + return this.with(relationshipName, new List{ field1, field2, field3, field4 }); + } + + public Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { + return this.with(relationshipName, new List{ field1, field2, field3, field4, field5 }); + } + + public Queryable with(String relationshipName, Iterable fields) { + this.builder.fields.relationshipFields.add(relationshipName, fields); + return this; + } + + public Queryable with(SubQuery subQuery) { + this.builder.subQueries.add(subQuery); + return this; + } + + public Queryable count() { + this.builder.fields.count(); + return this; + } + + public Queryable count(SObjectField field) { + return this.count(field, ''); + } + + public Queryable count(SObjectField field, String alias) { + return this.withAggregateFunction('COUNT', field, alias); + } + + public Queryable count(String relationshipName, SObjectField field) { + return this.count(relationshipName, field, ''); + } + + public Queryable count(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('COUNT', relationshipName, field, alias); + } + + public Queryable avg(SObjectField field) { + return this.avg(field, ''); + } + + public Queryable avg(SObjectField field, String alias) { + return this.withAggregateFunction('AVG', field, alias); + } + + public Queryable avg(String relationshipName, SObjectField field) { + return this.avg(relationshipName, field, ''); + } + + public Queryable avg(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('AVG', relationshipName, field, alias); + } + + public Queryable countDistinct(SObjectField field) { + return this.countDistinct(field, ''); + } + + public Queryable countDistinct(SObjectField field, String alias) { + return this.withAggregateFunction('COUNT_DISTINCT', field, alias); + } + + public Queryable countDistinct(String relationshipName, SObjectField field) { + return this.countDistinct(relationshipName, field, ''); + } + + public Queryable countDistinct(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('COUNT_DISTINCT', relationshipName, field, alias); + } + + public Queryable min(SObjectField field) { + return this.min(field, ''); + } + + public Queryable min(SObjectField field, String alias) { + return this.withAggregateFunction('MIN', field, alias); + } + + public Queryable min(String relationshipName, SObjectField field) { + return this.min(relationshipName, field, ''); + } + + public Queryable min(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('MIN', relationshipName, field, alias); + } + + public Queryable max(SObjectField field) { + return this.max(field, ''); + } + + public Queryable max(SObjectField field, String alias) { + return this.withAggregateFunction('MAX', field, alias); + } + + public Queryable max(String relationshipName, SObjectField field) { + return this.max(relationshipName, field, ''); + } + + public Queryable max(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('MAX', relationshipName, field, alias); + } + + public Queryable sum(SObjectField field) { + return this.sum(field, ''); + } + + public Queryable sum(SObjectField field, String alias) { + return this.withAggregateFunction('SUM', field, alias); + } + + public Queryable sum(String relationshipName, SObjectField field) { + return this.sum(relationshipName, field, ''); + } + + public Queryable sum(String relationshipName, SObjectField field, String alias) { + return this.withAggregateFunction('SUM', relationshipName, field, alias); + } + + public Queryable grouping(SObjectField field, String alias) { + return this.withAggregateFunction('GROUPING', field, alias); + } + + private Queryable withAggregateFunction(String function, SObjectField field, String alias) { + this.builder.fields.aggregateFields.add(function, field, alias); + return this; + } + + private Queryable withAggregateFunction(String function, String relationship, SObjectField field, String alias) { + this.builder.fields.aggregateFields.add(function, relationship, field, alias); + return this; + } + + public Queryable toLabel(SObjectField field) { + return this.toLabel(field.toString()); + } + + public Queryable toLabel(SObjectField field, String alias) { + return this.toLabel(field.toString(), alias); + } + + public Queryable toLabel(String field) { + return this.toLabel(field, ''); + } + + public Queryable toLabel(String field, String alias) { + return this.withFunction('toLabel', field, alias); + } + + public Queryable format(SObjectField field) { + return this.format(field, ''); + } + + public Queryable format(SObjectField field, String alias) { + return this.withFunction('FORMAT', field.toString(), alias); + } + + private Queryable withFunction(String function, String field, String alias) { + this.builder.fields.functionsFields.add(function, field, alias); + return this; + } + + public Queryable delegatedScope() { + return this.scope('DELEGATED'); + } + + public Queryable mineScope() { + return this.scope('MINE'); + } + + public Queryable mineAndMyGroupsScope() { + return this.scope('MINE_AND_MY_GROUPS'); + } + + public Queryable myTerritoryScope() { + return this.scope('MY_TERRITORY'); + } + + public Queryable myTeamTerritoryScope() { + return this.scope('MY_TEAM_TERRITORY'); + } + + public Queryable teamScope() { + return this.scope('TEAM'); + } + + private Queryable scope(String scope) { + this.builder.scope.set(scope); + return this; + } + + public Queryable whereAre(FilterGroup filterGroup) { + this.builder.conditions.add(filterGroup); + return this; + } + + public Queryable whereAre(Filter filter) { + this.builder.conditions.add(filter); + return this; + } + + public Queryable whereAre(String conditions) { + this.builder.conditions.add(conditions); + return this; + } + + public Queryable conditionLogic(String conditionLogic) { + this.builder.conditions.conditionLogic(conditionLogic); + return this; + } + + public Queryable anyConditionMatching() { + this.builder.conditions.anyConditionMatching(); + return this; + } + + public Queryable groupBy(SObjectField field) { + return this.groupBy(field.toString()); + } + + public Queryable groupBy(String field) { + this.builder.fields.withGroupedField(field); + this.builder.groupBy.with(field); + return this; + } + + public Queryable groupBy(String relationshipName, SObjectField field) { + this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); + this.builder.groupBy.with(relationshipName, field); + return this; + } + + public Queryable groupByRollup(SObjectField field) { + this.builder.fields.withGroupedField(field.toString()); + return this.groupBy(field, 'ROLLUP'); + } + + public Queryable groupByRollup(String relationshipName, SObjectField field) { + this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); + return this.groupBy(relationshipName, field, 'ROLLUP'); + } + + public Queryable groupByCube(SObjectField field) { + this.builder.fields.withGroupedField(field.toString()); + return this.groupBy(field, 'CUBE'); + } + + public Queryable groupByCube(String relationshipName, SObjectField field) { + this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); + return this.groupBy(relationshipName, field, 'CUBE'); + } + + private Queryable groupBy(SObjectField field, String function) { + this.builder.groupBy.with(field.toString(), function); + this.builder.fields.withGroupedField(field.toString()); + return this; + } + + private Queryable groupBy(String relationshipName, SObjectField field, String function) { + this.builder.groupBy.with(relationshipName, field.toString(), function); + this.builder.fields.withGroupedField(relationshipName + '.' + field.toString()); + return this; + } + + public Queryable have(HavingFilterGroup havingFilterGroup) { + this.builder.havingClause.add(havingFilterGroup); + return this; + } + + public Queryable have(HavingFilter havingFilter) { + this.builder.havingClause.add(havingFilter); + return this; + } + + public Queryable have(String havingConditions) { + this.builder.havingClause.add(havingConditions); + return this; + } + + public Queryable havingConditionLogic(String havingConditionsOrder) { + this.builder.havingClause.conditionLogic(havingConditionsOrder); + return this; + } + + public Queryable anyHavingConditionMatching() { + this.builder.havingClause.anyConditionMatching(); + return this; + } + + public Queryable withDataCategory(DataCategoryFilter dataCategoryFilter) { + this.builder.dataCategory.add(dataCategoryFilter); + return this; + } + + public Queryable orderBy(SObjectField field) { + return this.orderBy(field.toString()); + } + + public Queryable orderBy(String field) { + this.builder.orderBys.newOrderBy().with(field); + return this; + } + + public Queryable orderBy(String field, String direction) { + this.builder.orderBys.newOrderBy().with(field); + this.builder.orderBys.latestOrderBy().sortingOrder(direction); + return this; + } + + public Queryable orderBy(String relationshipName, SObjectField field) { + return this.orderBy(relationshipName + '.' + field.toString()); + } + + public Queryable orderByCount(SObjectField field) { + return this.orderBy('COUNT(' + field.toString() + ')'); + } + + public Queryable sortDesc() { + return this.sort('DESC'); + } + + public Queryable sort(String direction) { + this.builder.latestOrderBy.sortingOrder(direction); + return this; + } + + public Queryable nullsLast() { + return this.nullsOrder('LAST'); + } + + public Queryable nullsOrder(String nullsOrder) { + this.builder.latestOrderBy.nullsOrder(nullsOrder); + return this; + } + + public Queryable setLimit(Integer amount) { + this.builder.soqlLimit.set(amount); + return this; + } + + public Queryable offset(Integer startingRow) { + this.builder.soqlOffset.set(startingRow); + return this; + } + + public Queryable forReference() { + return this.setFor('FOR REFERENCE'); + } + + public Queryable forView() { + return this.setFor('FOR VIEW'); + } + + public Queryable forUpdate() { + return this.setFor('FOR UPDATE'); + } + + public Queryable allRows() { + return this.setFor('ALL ROWS'); + } + + private Queryable setFor(String forStatement) { + this.builder.soqlFor.set(forStatement); + return this; + } + + public Queryable userMode() { + this.executor.accessMode(AccessLevel.USER_MODE); + return this; + } + + public Queryable systemMode() { + this.executor.accessMode(AccessLevel.SYSTEM_MODE); + return this; + } + + public Queryable stripInaccessible() { + return this.stripInaccessible(AccessType.READABLE); + } + + public Queryable stripInaccessible(AccessType accessType) { + this.executor.stripInaccessible(accessType); + return this; + } + + public Queryable withSharing() { + this.executor.withSharing(); + return this; + } + + public Queryable withoutSharing() { + this.executor.withoutSharing(); + return this; + } + + public Queryable mockId(String queryIdentifier) { + this.executor.mock(SOQL.queryIdToMock.get(queryIdentifier)); + return this; + } + + public Queryable preview() { + System.debug(LoggingLevel.ERROR, '\n\n============ SOQL Preview ============\n' + this.toString() + '\n=======================================\n'); + System.debug(LoggingLevel.ERROR, '\n\n============ SOQL Binding ============\n' + JSON.serializePretty(this.binding()) + '\n=======================================\n'); + return this; + } + + public Map binding() { + return binder.getBindingMap(); + } + + public Id toId() { + this.builder.fields.clearAllFields(); // other fields not needed + return this.toObject()?.Id; + } + + public Set toIds() { + this.builder.fields.clearAllFields(); // other fields not needed + return this.toMap().keySet(); + } + + public Set toIdsOf(SObjectField field) { + return this.toIdsOf(field.toString()); + } + + public Set toIdsOf(String relationshipName, SObjectField field) { + return this.toIdsOf(relationshipName + '.' + field.toString()); + } + + private Set toIdsOf(String fieldToExtract) { + // https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result + this.builder.fields.clearAllFields(); // other fields not needed + return new Map( + this.with(fieldToExtract, 'Id') + .whereAre(Filter.with(fieldToExtract).isNotNull()) + .groupBy(fieldToExtract) + .toAggregated() + ).keySet(); + } + + public Boolean doExist() { + this.builder.fields.clearAllFields(); // other fields not needed + return this.setLimit(1).toList().size() > 0; + } + + public override String toString() { + binder.reset(); // clear binding before query build + return this.builder.toString(); + } + + public Object toValueOf(SObjectField fieldToExtract) { + this.builder.fields.clearAllFields(); // other fields not needed + return this.with(fieldToExtract).toObject()?.get(fieldToExtract); + } + + public Set toValuesOf(SObjectField fieldToExtract) { + return this.toValuesOf(fieldToExtract.toString()); + } + + public Set toValuesOf(String relationshipName, SObjectField targetKeyField) { + return this.toValuesOf(relationshipName + '.' + targetKeyField.toString()); + } + + private Set toValuesOf(String fieldToExtract) { + // https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result + this.builder.fields.clearAllFields(); // other fields not needed + return new Map( + this.with(fieldToExtract, 'Id') + .whereAre(Filter.with(fieldToExtract).isNotNull()) + .groupBy(fieldToExtract) + .toAggregated() + ).keySet(); + } + + public Integer toInteger() { + this.builder.fields.addCountWhenNotPresented(); + return this.executor.toInteger(); + } + + public SObject toObject() { + return this.executor.toObject(); + } + + public List toList() { + return this.executor.toList(); + } + + public List toAggregated() { + return this.toList(); + } + + public List toAggregatedProxy() { + return this.executor.toAggregatedProxy(); + } + + public Map toMap() { + return this.converter.transform(this.executor.toList()).toMap(); + } + + public Map toIdMapBy(SObjectField field) { + this.with(field); + this.whereAre(Filter.with(field).isNotNull()); + return this.converter.transform(this.executor.toList()).toIdMapBy(field); + } + + public Map 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); + } + + public Map> toAggregatedIdMapBy(SObjectField keyField) { + this.with(keyField); + this.whereAre(Filter.with(keyField).isNotNull()); + return this.converter.transform(this.executor.toList()).toAggregatedIdMapBy(keyField); + } + + public Map> 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); + } + + public Map toMap(SObjectField keyField) { + this.with(keyField); + this.whereAre(Filter.with(keyField).isNotNull()); + return this.converter.transform(this.executor.toList()).toMap(keyField); + } + + public Map 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); + } + + public Map 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); + } + + public Map> toAggregatedMap(SObjectField keyField) { + this.with(keyField); + this.whereAre(Filter.with(keyField).isNotNull()); + return this.converter.transform(this.executor.toList()).toAggregatedMap(keyField); + } + + public Map> 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); + } + + public Map> 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); + } + + public Map> 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); + } + + public Database.QueryLocator toQueryLocator() { + return this.executor.toQueryLocator(); + } + + public Queryable byId(SObject record) { + return this.byId(record.Id); + } + + public Queryable byId(Id recordId) { + return this.whereAre(Filter.id().equal(recordId)); + } + + public Queryable byIds(Iterable recordIds) { + return this.whereAre(Filter.id().isIn(recordIds)); + } + + public Queryable byIds(List records) { + return this.whereAre(Filter.id().isIn(records)); + } + + public Queryable byRecordType(String recordTypeDeveloperName) { + return this.whereAre(Filter.recordType().equal(recordTypeDeveloperName)); + } + + private interface QueryClause { + String toString(); + } + + private class SoqlBuilder implements QueryClause { + private List clauses = new QueryClause[12]; + + public SoqlBuilder(String ofObject) { + this.clauses.set(0, new SoqlFields(ofObject)); + this.clauses.set(2, new SoqlFrom(ofObject)); + } + + public SoqlFields fields { + get { return (SoqlFields) this.clauses[0]; } + } + + public SoqlSubQueries subQueries { + get { return (SoqlSubQueries) getQueryClause(1, SoqlSubQueries.class); } + } + + public SoqlScope scope { + get { return (SoqlScope) getQueryClause(3, SoqlScope.class); } + } + + public MainFilterGroup conditions { + get { return (MainFilterGroup) getQueryClause(4, MainFilterGroup.class); } + } + + public MainDataCategoryGroup dataCategory { + get { return (MainDataCategoryGroup) getQueryClause(5, MainDataCategoryGroup.class); } + } + + public SoqlGroupBy groupBy { + get { return (SoqlGroupBy) getQueryClause(6, SoqlGroupBy.class); } + } + + public MainHavingGroup havingClause { + get { return (MainHavingGroup) getQueryClause(7, MainHavingGroup.class); } + } + + public SoqlOrderBy latestOrderBy { + get { return this.orderBys.latestOrderBy(); } + } + + public SoqlOrderBys orderBys { + get { return (SoqlOrderBys) getQueryClause(8, SoqlOrderBys.class); } + } + + public SoqlLimit soqlLimit { + get { return (SoqlLimit) getQueryClause(9, SoqlLimit.class); } + } + + public SoqlOffset soqlOffset { + get { return (SoqlOffset) getQueryClause(10, SoqlOffset.class); } + } + + public SoqlFor soqlFor { + get { return (SoqlFor) getQueryClause(11, SoqlFor.class); } + } + + private QueryClause getQueryClause(Integer position, System.Type queryClause) { + if (this.clauses[position] == null) { + this.clauses.set(position, (QueryClause) queryClause.newInstance()); + } + return this.clauses[position]; + } + + public override String toString() { + List queryParts = new List(); + + for (QueryClause clause : this.clauses) { + if (clause != null) { + queryParts.add(clause.toString()); + } + } + + return String.join(queryParts, ' ').trim(); + } + } + + private class SoqlFields implements QueryClause { + public PlainFields plainFields = new PlainFields(); + public RelationshipFields relationshipFields = new RelationshipFields(); + public FunctionsFields functionsFields = new FunctionsFields(); // toLabel, FORMAT + public AggregateFunctionsFields aggregateFields = new AggregateFunctionsFields(); + + private String ofObject; + private Set groupedFields = new Set(); + + public SoqlFields(String ofObject) { + this.ofObject = ofObject; + } + + public void count() { + this.clearAllFields(); // COUNT() must be the only element in the SELECT list. + this.aggregateFields.add('COUNT()', ''); + } + + public void with(String commaSeparatedFields) { + // Add to Set to avoid "duplicate field selected" error + for (String splitField : commaSeparatedFields.split(',')) { + this.classifyField(splitField); + } + } + + private void classifyField(String field) { + String trimmedField = field.trim(); + + if (this.functionsFields.isQualified(trimmedField)) { + this.functionsFields.add(trimmedField); + } else if (this.aggregateFields.isQualified(trimmedField)) { + this.aggregateFields.add(trimmedField); + } else if (this.relationshipFields.isQualified(trimmedField)) { + this.relationshipFields.add(trimmedField); + } else { + this.plainFields.add(trimmedField); + } + } + + public void withGroupedField(String field) { + this.groupedFields.add(field); + } + + public void withFieldSet(String fieldSetName) { + FieldSet fieldSet = Schema.describeSObjects(new List{ this.ofObject }, SObjectDescribeOptions.DEFERRED)[0].fieldSets.getMap().get(fieldSetName); + + if (fieldSet == null) { + throw new QueryException('FieldSet with name ' + fieldSetName + ' does not exist!'); + } + + for (Schema.FieldSetMember field : fieldSet.getFields()) { + String currentField = field.getFieldPath(); + + if (this.relationshipFields.isQualified(currentField)) { + this.relationshipFields.add(currentField); + } else { + this.plainFields.add(currentField); + } + } + } + + public void clearAllFields() { + this.plainFields.clear(); + this.relationshipFields.clear(); + this.aggregateFields.clear(); + this.functionsFields.clear(); + } + + public void addCountWhenNotPresented() { + if (this.aggregateFields.isEmpty()) { + this.count(); + } + } + + public override String toString() { + if ( + this.plainFields.isEmpty() && + this.relationshipFields.isEmpty() && + this.aggregateFields.isEmpty() && + this.functionsFields.isEmpty() + ) { + this.plainFields.add('Id'); + } + + if (!this.groupedFields.isEmpty() || !this.aggregateFields.isEmpty()) { + // To avoid "Field must be grouped or aggregated" error retain only grouped or aggregated fields + this.plainFields.retainAll(groupedFields); + this.relationshipFields.retainAll(groupedFields); + this.functionsFields.retainAll(groupedFields); + } + + List selectFields = new List(); + + selectFields.addAll(this.plainFields.get()); + selectFields.addAll(this.relationshipFields.get()); + selectFields.addAll(this.functionsFields.get()); + selectFields.addAll(this.aggregateFields.get()); + + return 'SELECT ' + String.join(selectFields, ', '); + } + } + + private abstract class SelectFields { + protected Set fields = new Set(); + + public void add(String field) { + this.fields.add(field); + } + + public Boolean isEmpty() { + return this.fields.isEmpty(); + } + + public void clear() { + this.fields.clear(); + } + + public Set get() { + return this.fields; + } + + public void retainAll(Set fieldsToRetain) { + this.fields.retainAll(fieldsToRetain); + } + } + + private class PlainFields extends SelectFields { + public void add(Iterable fields) { + for (SObjectField field : fields) { + this.add(field.toString()); + } + } + } + + private class RelationshipFields extends SelectFields { + public void add(String relationshipPath, Iterable fields) { + for (SObjectField field : fields) { + this.add(relationshipPath, field); + } + } + + public void add(String relationshipPath, SObjectField field) { + this.add(relationshipPath + '.' + field.toString()); + } + + public Boolean isQualified(String field) { + return !field.contains('(') && !field.contains(')') && field.contains('.'); + } + } + + private class AggregateFunctionsFields extends SelectFields { + private final Set AGGREGATE_FUNCTIONS = new Set{ 'COUNT', 'AVG', 'COUNT_DISTINCT', 'MIN', 'MAX', 'SUM' }; + private final Set DATE_FUNCTIONS = new Set{ 'CALENDAR_MONTH', 'CALENDAR_QUARTER', 'CALENDAR_YEAR', 'DAY_IN_MONTH', 'DAY_IN_WEEK', 'DAY_IN_YEAR', 'DAY_ONLY', 'FISCAL_MONTH', 'FISCAL_QUARTER', 'FISCAL_YEAR', 'HOUR_IN_DAY', 'WEEK_IN_MONTH', 'WEEK_IN_YEAR' }; + + public void add(String function, SObjectField field, String alias) { + this.add(function + '(' + field.toString() + ')', alias); + } + + public void add(String function, String relationship, SObjectField field, String alias) { + this.add(function + '(' + relationship + '.' + field.toString() + ')', alias); + } + + private void add(String aggregateFunction, String alias) { + if (String.isNotBlank(alias)) { + aggregateFunction += ' ' + alias; + } + this.add(aggregateFunction); + } + + private Boolean isQualified(String field) { + Boolean isFieldAliasing = !field.contains('(') && !field.contains(')') && field.contains(' '); + String functionName = field.split('\\(')[0].toUpperCase(); + return AGGREGATE_FUNCTIONS.contains(functionName) || DATE_FUNCTIONS.contains(functionName) || isFieldAliasing; + } + } + + private class FunctionsFields extends SelectFields { + private final Set FUNCTIONS = new Set{ 'CONVERTCURRENCY', 'CONVERTTIMEZONE', 'FORMAT', 'GROUPING', 'TOLABEL' }; + + public void add(String function, String field, String alias) { + this.add(function + '(' + field + ')', alias); + } + + public void add(String function, String alias) { + if (String.isNotBlank(alias)) { + function += ' ' + alias; + } + this.add(function); + } + + public Boolean isQualified(String field) { + String functionName = field.split('\\(')[0].toUpperCase(); + return FUNCTIONS.contains(functionName); + } + } + + private class SoqlSubQuery implements SubQuery { + private SoqlBuilder builder; + private String childRelationshipName; + + public SubQuery of(String ofObject) { + this.builder = new SoqlBuilder(ofObject); + this.childRelationshipName = ofObject; + return this; + } + + public SubQuery with(SObjectField field) { + this.builder.fields.plainFields.add(field.toString()); + return this; + } + + public SubQuery with(SObjectField field1, SObjectField field2) { + return this.with(field1).with(field2); + } + + public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3) { + return this.with(field1).with(field2).with(field3); + } + + public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { + return this.with(field1).with(field2).with(field3).with(field4); + } + + public SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { + return this.with(field1).with(field2).with(field3).with(field4).with(field5); + } + + public SubQuery with(Iterable fields) { + this.builder.fields.plainFields.add(fields); + return this; + } + + public SubQuery with(String fields) { + this.builder.fields.with(fields); + return this; + } + + public SubQuery with(String relationshipName, Iterable fields) { + this.builder.fields.relationshipFields.add(relationshipName, fields); + return this; + } + + public SubQuery with(SubQuery subQuery) { + this.builder.subQueries.add(subQuery); + return this; + } + + public SubQuery whereAre(FilterGroup filterGroup) { + this.builder.conditions.add(filterGroup); + return this; + } + + public SubQuery whereAre(Filter filter) { + this.builder.conditions.add(filter); + return this; + } + + public SubQuery orderBy(SObjectField field) { + this.builder.orderBys.newOrderBy().with(field.toString()); + return this; + } + + public SubQuery orderBy(String field) { + this.builder.orderBys.newOrderBy().with(field); + return this; + } + + public SubQuery orderBy(String relationshipName, SObjectField field) { + this.builder.orderBys.newOrderBy().with(relationshipName + '.' + field.toString()); + return this; + } + + public SubQuery sortDesc() { + this.builder.latestOrderBy.sortingOrder('DESC'); + return this; + } + + public SubQuery sort(String direction) { + this.builder.latestOrderBy.sortingOrder(direction); + return this; + } + + public SubQuery nullsLast() { + this.builder.latestOrderBy.nullsOrder('LAST'); + return this; + } + + public SubQuery setLimit(Integer amount) { + this.builder.soqlLimit.set(amount); + return this; + } + + public SubQuery offset(Integer startingRow) { + this.builder.soqlOffset.set(startingRow); + return this; + } + + public SubQuery forReference() { + this.builder.soqlFor.set('FOR REFERENCE'); + return this; + } + + public SubQuery forView() { + this.builder.soqlFor.set('FOR VIEW'); + return this; + } + + public override String toString() { + return this.builder.toString(); + } + + public String getChildRelationshipName() { + return this.childRelationshipName; + } + } + + private class SoqlSubQueries implements QueryClause { + private List subQueries = new List(); + + public void add(SubQuery subQuery) { + this.subQueries.add(subQuery); + } + + public override String toString() { + List subQueriesStrings = new List(); + + for (SubQuery sub : this.subQueries) { + subQueriesStrings.add('(' + sub + ')'); + } + + return ', ' + String.join(subQueriesStrings, ', '); + } + + public Set get() { + Set subQueriesRelationshipNames = new Set(); + + for (SubQuery subQuery : this.subQueries) { + subQueriesRelationshipNames.add(subQuery.getChildRelationshipName()); + } + + return subQueriesRelationshipNames; + } + } + + private class SoqlFrom implements QueryClause { + private String objectApiName; + + public SoqlFrom(String objectType) { + this.objectApiName = objectType; + } + + public override String toString() { + return 'FROM ' + this.objectApiName; + } + } + + private class SoqlScope implements QueryClause { + private String scope = 'EVERYTHING'; + + public void set(String scope) { + this.scope = scope; + } + + public override String toString() { + return 'USING SCOPE ' + this.scope; + } + } + + private interface FilterClause { + String toString(); + Boolean hasValue(); + } + + private virtual class FilterBuilder { + protected List conditions = new List(); + protected String customOrder; + protected String connector = 'AND'; + + protected void add(FilterClause condition) { + if (condition.hasValue()) { + this.conditions.add(condition); + } + } + + protected String buildNested() { + return String.format(this.getOrderWithSpecialCharacters(), this.conditions); + } + + private String getOrderWithSpecialCharacters() { + String orderWithSpecialCharacters = this.getConditionsLogic(); + + for (Integer i = 0; i < this.conditions.size(); i++) { + orderWithSpecialCharacters = orderWithSpecialCharacters.replaceAll('\\b' + (i + 1).toString() + '\\b', '{' + i + '}'); + } + + return orderWithSpecialCharacters; // e.g ({0} AND ({1} AND {2})) + } + + private String getConditionsLogic() { + if (String.isNotEmpty(this.customOrder)) { + return this.customOrder; + } + + List defaultOrder = new List(); + + for (Integer i = 0; i < this.conditions.size(); i++) { + defaultOrder.add((i + 1).toString()); + } + + return String.join(defaultOrder, ' ' + this.connector + ' '); // e.g (0 AND 1 AND 2) + } + } + + private virtual class SoqlFilterGroup extends FilterBuilder implements FilterGroup { + public FilterGroup add(FilterGroup filterGroup) { + this.add(new FilterGroupAdapter(filterGroup)); + return this; + } + + public FilterGroup add(Filter filter) { + this.add(new FilterAdapter(filter)); + return this; + } + + public FilterGroup add(String dynamicCondition) { + this.add(new StringConditionAdapter(dynamicCondition)); + return this; + } + + public FilterGroup add(List filters) { + for (Filter filter: filters) { + this.add(filter); + } + return this; + } + + public FilterGroup add(List dynamicConditions) { + for (String dynamicCondition: dynamicConditions) { + this.add(dynamicCondition); + } + return this; + } + + public FilterGroup anyConditionMatching() { + this.connector = 'OR'; + return this; + } + + public FilterGroup conditionLogic(String order) { + this.customOrder = order; + return this; + } + + public FilterGroup ignoreWhen(Boolean logicExpression) { + if (logicExpression) { + this.conditions = new List(); + } + return this; + } + + public Boolean hasValues() { + return !this.conditions.isEmpty(); + } + + public virtual override String toString() { + return '(' + this.buildNested() + ')'; + } + } + + private class MainFilterGroup extends SoqlFilterGroup implements QueryClause { + public override String toString() { + if (!this.hasValues()) { + return ''; + } + + return 'WHERE ' + this.buildNested(); + } + } + + private class FilterGroupAdapter implements FilterClause { + private FilterGroup filterGroup; + + public FilterGroupAdapter(FilterGroup filterGroup) { + this.filterGroup = filterGroup; + } + + public Boolean hasValue() { + return this.filterGroup.hasValues(); + } + + public override String toString() { + return this.filterGroup.toString(); + } + } + + private class FilterAdapter implements FilterClause { + private Filter filter; + + public FilterAdapter(Filter filter) { + this.filter = filter; + } + + public Boolean hasValue() { + return this.filter.hasValue(); + } + + public override String toString() { + return this.filter.toString(); + } + } + + private class StringConditionAdapter implements FilterClause { + private String conditionString; + + public StringConditionAdapter(String dynamicCondition) { + this.conditionString = dynamicCondition; + } + + public Boolean hasValue() { + return String.isNotEmpty(this.conditionString); + } + + public override String toString() { + return this.conditionString; + } + } + + private class SoqlFilter implements Filter { + private String field; + private String comparator; + private Object value; + private String wrapper = '{0}'; + private Boolean skipBinding = false; + + public Filter id() { + return this.with('Id'); + } + + public Filter recordType() { + return this.with('RecordType.DeveloperName'); + } + + public Filter name() { + return this.with('Name'); + } + + public Filter with(SObjectField field) { + return this.with(field.toString()); + } + + public Filter with(String relationshipName, SObjectField field) { + return this.with(relationshipName + '.' + field.toString()); + } + + public Filter with(String field) { + this.field = field; + return this; + } + + public Filter isNull() { + return this.equal(null); + } + + public Filter isNotNull() { + return this.notEqual(null); + } + + public Filter isTrue() { + return this.equal(true); + } + + public Filter isFalse() { + return this.equal(false); + } + + public Filter equal(Object value) { + return this.set('=', value); + } + + public Filter notEqual(Object value) { + return this.set('!=', value); + } + + public Filter lessThan(Object value) { + return this.set('<', value); + } + + public Filter greaterThan(Object value) { + return this.set('>', value); + } + + public Filter lessOrEqual(Object value) { + return this.set('<=', value); + } + + public Filter greaterOrEqual(Object value) { + return this.set('>=', value); + } + + public Filter containsSome(Iterable values) { + return this.set('LIKE', values); + } + + public Filter contains(String value) { + return this.contains('%', formattedString(value), '%'); + } + + public Filter notContains(String value) { + return this.notLike().contains(value); + } + + public Filter endsWith(String value) { + return this.contains('%', formattedString(value), ''); + } + + public Filter notEndsWith(String value) { + return this.notLike().endsWith(value); + } + + public Filter startsWith(String value) { + return this.contains('', formattedString(value), '%'); + } + + public Filter notStartsWith(String value) { + return this.notLike().startsWith(value); + } + + public Filter contains(String prefix, String value, String suffix) { + return this.set('LIKE', prefix + formattedString(value) + suffix); + } + + public Filter notContains(String prefix, String value, String suffix) { + return this.notLike().contains(prefix, value, suffix); + } + + private String formattedString(String value) { + return value ?? value?.trim(); + } + + public Filter isIn(Iterable iterable) { + return this.set('IN', iterable); + } + + public Filter isIn(InnerJoin joinQuery) { + this.skipBinding = true; + return this.set('IN', joinQuery); + } + + private Filter notLike() { + this.wrapper = '(NOT {0})'; + return this; + } + + public Filter notIn(Iterable iterable) { + return this.set('NOT IN', iterable); + } + + public Filter notIn(InnerJoin joinQuery) { + this.skipBinding = true; + return this.set('NOT IN', joinQuery); + } + + public Filter includesAll(Iterable iterable) { + return this.setMultipicklistFilter('INCLUDES', iterable, ';'); + } + + public Filter includesSome(Iterable iterable) { + return this.setMultipicklistFilter('INCLUDES', iterable, '\', \''); + } + + public Filter excludesAll(Iterable iterable) { + return this.setMultipicklistFilter('EXCLUDES', iterable, '\', \''); + } + + public Filter excludesSome(Iterable iterable) { + return this.setMultipicklistFilter('EXCLUDES', iterable, ';'); + } + + public Filter setMultipicklistFilter(String operator, Iterable iterable, String separator) { + // Bind expressions can't be used with other clauses, such as INCLUDES, EXCLUDES + this.skipBinding = true; + return this.set(operator, '(\'' + String.join(iterable, separator) + '\')'); + } + + private Filter set(String comparator, Object value) { + this.value = value; + this.comparator = comparator; + return this; + } + + public Boolean hasValue() { + return String.isNotEmpty(this.field); + } + + public Filter ignoreWhen(Boolean logicExpression) { + if (logicExpression) { + // Set field as empty to meet hasValue and ignore condition + this.with(''); + } + return this; + } + + public Filter asDateLiteral() { + // Date Literals can't be bound + this.skipBinding = true; + return this; + } + + public override String toString() { + if (this.skipBinding) { + return String.format(this.wrapper, new List{ this.field + ' ' + this.comparator + ' ' + this.value }); + } + + return String.format(this.wrapper, new List{ this.field + ' ' + this.comparator + ' :' + binder.bind(value) }); + } + } + + private virtual class MainDataCategoryGroup extends FilterBuilder implements DataCategoryFilterGroup, QueryClause { + public DataCategoryFilterGroup add(DataCategoryFilter dataCategoryFilter) { + this.add(new DataCategoryFilterAdapter(dataCategoryFilter)); + return this; + } + + public override String toString() { + return 'WITH DATA CATEGORY ' + this.buildNested(); + } + } + + private class DataCategoryFilterAdapter implements FilterClause { + private DataCategoryFilter dataCategoryFilter; + + public DataCategoryFilterAdapter(DataCategoryFilter dataCategoryFilter) { + this.dataCategoryFilter = dataCategoryFilter; + } + + public Boolean hasValue() { + return this.dataCategoryFilter.hasValue(); + } + + public override String toString() { + return this.dataCategoryFilter.toString(); + } + } + + private class SoqlDataCategoryFilter implements DataCategoryFilter { + private String field; + private String comparator; + private String value; + + public DataCategoryFilter with(String field) { + this.field = field; + return this; + } + + public DataCategoryFilter at(String category) { + return this.set('AT', category); + } + + public DataCategoryFilter at(Iterable categories) { + return this.set('AT', categories); + } + + public DataCategoryFilter above(String category) { + return this.set('ABOVE', category); + } + + public DataCategoryFilter above(Iterable categories) { + return this.set('ABOVE', categories); + } + + public DataCategoryFilter below(String category) { + return this.set('BELOW', category); + } + + public DataCategoryFilter below(Iterable categories) { + return this.set('BELOW', categories); + } + + public DataCategoryFilter aboveOrBelow(String category) { + return this.set('ABOVE_OR_BELOW', category); + } + + public DataCategoryFilter aboveOrBelow(Iterable categories) { + return this.set('ABOVE_OR_BELOW', categories); + } + + public DataCategoryFilter set(String operator, Iterable iterable) { + this.comparator = operator; + this.value = '(' + String.join(iterable, ', ') + ')'; + return this; + } + + private DataCategoryFilter set(String comparator, String value) { + this.comparator = comparator; + this.value = value; + return this; + } + + public Boolean hasValue() { + return String.isNotEmpty(this.field); + } + + public override String toString() { + return this.field + ' ' + this.comparator + ' ' + this.value; + } + } + + private class SoqlJoinQuery implements InnerJoin { + private SoqlBuilder builder; + + public InnerJoin of(SObjectType ofObject) { + this.builder = new SoqlBuilder(ofObject.toString()); + return this; + } + + public InnerJoin with(SObjectField field) { + this.builder.fields.plainFields.add(field.toString()); + return this; + } + + public InnerJoin whereAre(FilterGroup filterGroup) { + this.builder.conditions.add(filterGroup); + return this; + } + + public InnerJoin whereAre(Filter filter) { + this.builder.conditions.add(filter); + return this; + } + + public override String toString() { + return '(' + this.builder.toString() + ')'; + } + } + + private class SoqlGroupBy implements QueryClause { + private Set groupByFields = new Set(); + private String groupByFunction = ''; + + public void with(String relationshipName, SObjectField field) { + this.with(relationshipName + '.' + field.toString()); + } + + private void with(String field) { + this.setGroupByFunction('{0}'); + this.groupByFields.add(field.trim()); + } + + public void with(String field, String function) { + this.setGroupByFunction(function + '({0})'); + this.groupByFields.add(field.trim()); + } + + public void with(String relationshipName, String field, String function) { + this.setGroupByFunction(function + '({0})'); + this.groupByFields.add((relationshipName + '.' + field).trim()); + } + + public void setGroupByFunction(String newGroupByFunction) { + if (String.isNotEmpty(groupByFunction) && groupByFunction != newGroupByFunction) { + throw new QueryException('You can\'t use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.'); + } + this.groupByFunction = newGroupByFunction; + } + + public override String toString() { + return 'GROUP BY ' + String.format(this.groupByFunction, new List{ String.join( this.groupByFields, ', ') }); + } + } + + private class SoqlOrderBys implements QueryClause { + public List orderBys = new List(); + + public SoqlOrderBy newOrderBy() { + this.orderBys.add(new SoqlOrderBy()); + return this.latestOrderBy(); + } + + public SoqlOrderBy latestOrderBy() { + return this.orderBys.get(orderBys.size() - 1); + } + + public override String toString() { + List orderFields = new List(); + + for (SoqlOrderBy orderBy : this.orderBys) { + orderFields.add(orderBy.toString()); + } + + return 'ORDER BY ' + String.join(orderFields, ', '); + } + } + + private virtual class SoqlHavingFilterGroup extends FilterBuilder implements HavingFilterGroup { + public HavingFilterGroup add(HavingFilterGroup filterGroup) { + this.add(new HavingFilterGroupAdapter(filterGroup)); + return this; + } + + public HavingFilterGroup add(HavingFilter filter) { + this.add(new HavingFilterAdapter(filter)); + return this; + } + + public HavingFilterGroup add(String filter) { + this.add(new StringConditionAdapter(filter)); + return this; + } + + public HavingFilterGroup anyConditionMatching() { + this.connector = 'OR'; + return this; + } + + public HavingFilterGroup conditionLogic(String order) { + this.customOrder = order; + return this; + } + + public virtual override String toString() { + return '(' + this.buildNested() + ')'; + } + + public Boolean hasValues() { + return !this.conditions.isEmpty(); + } + } + + private class MainHavingGroup extends SoqlHavingFilterGroup implements QueryClause { + public override String toString() { + return 'HAVING ' + this.buildNested(); + } + } + + private class HavingFilterGroupAdapter implements FilterClause { + private HavingFilterGroup havingFilterGroup; + + public HavingFilterGroupAdapter(HavingFilterGroup havingFilterGroup) { + this.havingFilterGroup = havingFilterGroup; + } + + public Boolean hasValue() { + return this.havingFilterGroup.hasValues(); + } + + public override String toString() { + return this.havingFilterGroup.toString(); + } + } + + private class HavingFilterAdapter implements FilterClause { + private HavingFilter havingFilter; + + public HavingFilterAdapter(HavingFilter havingFilter) { + this.havingFilter = havingFilter; + } + + public Boolean hasValue() { + return this.havingFilter.hasValue(); + } + + public override String toString() { + return this.havingFilter.toString(); + } + } + + private class SoqlHavingFilter implements HavingFilter { + private String field; + private String comparator; + private Object value; + private String wrapper = '{0}'; + + public HavingFilter with(SObjectField field) { + return this.with(field.toString()); + } + + public HavingFilter with(String field) { + this.field = field; + return this; + } + + public HavingFilter count(SObjectField field) { + return this.withAggregateFunction('COUNT', field); + } + + public HavingFilter avg(SObjectField field) { + return this.withAggregateFunction('AVG', field); + } + + public HavingFilter countDistinct(SObjectField field) { + return this.withAggregateFunction('COUNT_DISTINCT', field); + } + + public HavingFilter min(SObjectField field) { + return this.withAggregateFunction('MIN', field); + } + + public HavingFilter max(SObjectField field) { + return this.withAggregateFunction('MAX', field); + } + + public HavingFilter sum(SObjectField field) { + return this.withAggregateFunction('SUM', field); + } + + private HavingFilter withAggregateFunction(String aggregateFunction, SObjectField field) { + return this.withAggregateFunction(aggregateFunction, field.toString()); + } + + private HavingFilter withAggregateFunction(String aggregateFunction, String field) { + this.field = aggregateFunction + '(' + field + ')'; + return this; + } + + public HavingFilter isNull() { + return this.equal(null); + } + + public HavingFilter isNotNull() { + return this.notEqual(null); + } + + public HavingFilter isTrue() { + return this.equal(true); + } + + public HavingFilter isFalse() { + return this.equal(false); + } + + public HavingFilter equal(Object value) { + return this.set('=', value); + } + + public HavingFilter notEqual(Object value) { + return this.set('!=', value); + } + + public HavingFilter lessThan(Object value) { + return this.set('<', value); + } + + public HavingFilter greaterThan(Object value) { + return this.set('>', value); + } + + public HavingFilter lessOrEqual(Object value) { + return this.set('<=', value); + } + + public HavingFilter greaterOrEqual(Object value) { + return this.set('>=', value); + } + + public HavingFilter contains(String value) { + return this.contains('%', formattedString(value), '%'); + } + + public HavingFilter notContains(String value) { + return this.notLike().contains(value); + } + + public HavingFilter endsWith(String value) { + return this.contains('%', formattedString(value), ''); + } + + public HavingFilter notEndsWith(String value) { + return this.notLike().endsWith(value); + } + + public HavingFilter startsWith(String value) { + return this.contains('', formattedString(value), '%'); + } + + public HavingFilter notStartsWith(String value) { + return this.notLike().startsWith(value); + } + + public HavingFilter contains(String prefix, String value, String suffix) { + return this.set('LIKE', prefix + formattedString(value) + suffix); + } + + public HavingFilter notContains(String prefix, String value, String suffix) { + return this.notLike().contains(prefix, value, suffix); + } + + public HavingFilter isIn(Iterable iterable) { + return this.set('IN', iterable, '\', \''); + } + + public HavingFilter notIn(Iterable iterable) { + return this.set('NOT IN', iterable, '\', \''); + } + + private String formattedString(String value) { + return value ?? value?.trim(); + } + + private HavingFilter notLike() { + this.wrapper = '(NOT {0})'; + return this; + } + + public HavingFilter set(String operator, Iterable iterable, String separator) { + this.comparator = operator; + this.value = '(\'' + String.join(iterable, separator) + '\')'; + return this; + } + + private HavingFilter set(String comparator, Object value) { + this.comparator = comparator; + this.value = value instanceof String ? '\'' + value + '\'' : value; + return this; + } + + public Boolean hasValue() { + return String.isNotEmpty(this.field); + } + + public override String toString() { + return String.format(this.wrapper, new List{ this.field + ' ' + this.comparator + ' ' + this.value }); + } + } + + private class SoqlOrderBy implements QueryClause { + private String orderField; + private String sortingOrder = 'ASC'; + private String nullsOrder = 'FIRST'; + + public void with(String field) { + this.orderField = field; + } + + public void sortingOrder(String direction) { + this.sortingOrder = direction; + } + + public void nullsOrder(String nullsOrder) { + this.nullsOrder = nullsOrder; + } + + public override String toString() { + return this.orderField + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; + } + } + + private class SoqlLimit implements QueryClause { + private Integer soqlLimit; + + public void set(Integer soqlLimit) { + this.soqlLimit = soqlLimit; + } + + public override String toString() { + return 'LIMIT ' + this.soqlLimit; + } + } + + private class SoqlOffset implements QueryClause { + private Integer soqlOffset; + + public void set(Integer fromRow) { + this.soqlOffset = fromRow; + } + + public override String toString() { + return 'OFFSET ' + this.soqlOffset; + } + } + + private class SoqlFor implements QueryClause { + private String forStatement; + + public void set(String forStatement) { + this.forStatement = forStatement; + } + + public override String toString() { + return this.forStatement; + } + } + + private class Binder { + private Integer bindIndex = 0; + private Map binding = new Map(); + + public String bind(Object value) { + bindIndex++; + binding.put('v' + bindIndex, value); + return 'v' + bindIndex; + } + + public void reset() { + this.bindIndex = 0; + this.binding.clear(); + } + + public Map getBindingMap() { + return binding; + } + } + + private class SoqlMock implements Mockable { + public SObjectMock sObjectMock = new SObjectMock(); + public CountMock countMock = new CountMock(); + public AggregateResultProxies aggregateResultMock = new AggregateResultProxies(); + + public Mockable thenReturn(List> aggregatedResults) { + this.aggregateResultMock.add(aggregatedResults); + return this; + } + + public Mockable thenReturn(Map aggregatedResult) { + this.aggregateResultMock.add(aggregatedResult); + return this; + } + + public Mockable thenReturn(SObject record) { + this.sObjectMock.add(record); + return this; + } + + public Mockable thenReturn(List records) { + this.sObjectMock.add(records); + return this; + } + + public Mockable thenReturn(Integer count) { + this.countMock.set(count); + return this; + } + + public SoqlMock useLegacyMockingBehavior() { + this.sObjectMock.useLegacyMockingBehavior = true; + return this; + } + } + + private class SObjectMock { + private List mockedRecords = new List(); + private Boolean useLegacyMockingBehavior = false; + + private void add(SObject record) { + this.mockedRecords.add(record); + } + + private void add(List records) { + this.mockedRecords.addAll(records); + } + + private List get(SoqlFields fields, SoqlSubQueries subQueries) { + // AggregateResult can be mocked only with Id field for toIdsOf, toValueOf + if (this.useLegacyMockingBehavior || this.mockedRecords.isEmpty() || this.mockedRecords[0]?.getSObjectType() == AggregateResult.SObjectType) { + return this.mockedRecords; + } + + if (!fields.aggregateFields.isEmpty()) { + throw new QueryException('Use toAggregatedProxy() to mock AggregateResult records.'); + } + + this.addIdToMockedRecords(); + + if (!fields.relationshipFields.isEmpty() || !fields.functionsFields.isEmpty()) { + return this.mockedRecords; + } + + return this.stripAdditionalFields(fields.plainFields.get(), subQueries.get()); + } + + private void addIdToMockedRecords() { // Id is always added to mirror standard SOQL behavior + SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); + String sObjectPrefix = sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix(); + + for (SObject record : this.mockedRecords) { + record.put('Id', record?.Id ?? IdGenerator.get(sObjectPrefix)); + } + } + + private List stripAdditionalFields(Set requestedFields, Set subQueriesRelationshipNames) { + List cleanedRecords = new List(); + + Type objectTypeName = Type.forName(this.mockedRecords[0].getSObjectType().toString()); + + for (SObject record : this.mockedRecords) { + Map recordFilteredFields = new Map{ 'Id' => record.Id }; + + this.stripAdditionalPlainFields(recordFilteredFields, record, requestedFields); + this.stripAdditionalSubQueries(recordFilteredFields, record, subQueriesRelationshipNames); + + // JSON.serialize and JSON.deserialize are used to copy not writable fields + cleanedRecords.add((SObject) JSON.deserialize(JSON.serialize(recordFilteredFields), objectTypeName)); + } + + return cleanedRecords; + } + + private void stripAdditionalPlainFields(Map recordFilteredFields, SObject record, Set requestedFields) { + for (String field : requestedFields) { + recordFilteredFields.put(field, record.get(field)); + } + } + + private void stripAdditionalSubQueries(Map recordFilteredFields, SObject record, Set subQueriesRelationshipNames) { + for (String subQueryRelationshipName : subQueriesRelationshipNames) { + recordFilteredFields.put(subQueryRelationshipName, new Map{ + 'totalSize' => record.getSObjects(subQueryRelationshipName)?.size() ?? 0, + 'done' => true, + 'records' => record.getSObjects(subQueryRelationshipName) ?? new List() + }); + } + } + } + + private class CountMock { + private Integer countMock; + + private void set(Integer count) { + this.countMock = count; + } + + private Integer get() { + return this.countMock; + } + } + + private class AggregateResultProxies { + private List aggregateResults = new List(); + + public AggregateResultProxies add(List aggregateResults) { + for (AggregateResult result : aggregateResults) { + this.aggregateResults.add(new SoqlAggregateResultProxy(result)); + } + return this; + } + + public AggregateResultProxies add(List> aggregateResults) { + for (Map result : aggregateResults) { + this.add(result); + } + return this; + } + + public AggregateResultProxies add(Map aggregateResult) { + this.aggregateResults.add(new SoqlAggregateResultProxy(aggregateResult)); + return this; + } + + public List get() { + return this.aggregateResults; + } + } + + private class SoqlAggregateResultProxy implements AggregateResultProxy { + private Map aggregateResult; + + public SoqlAggregateResultProxy(AggregateResult aggregateResult) { + this.aggregateResult = aggregateResult.getPopulatedFieldsAsMap(); + } + + public SoqlAggregateResultProxy(Map aggregateResult) { + this.aggregateResult = aggregateResult; + } + + public Object get(String field) { + return this.aggregateResult.get(field); + } + + public Map getPopulatedFieldsAsMap() { + return this.aggregateResult; + } + } + + private inherited sharing class Executor { + private DatabaseQuery sharingExecutor; + private AccessLevel accessMode; + private AccessType accessType; + private SoqlBuilder builder; + private List mocks = new List(); + + public Executor(SoqlBuilder builder) { + this.builder = builder; + this.accessMode = AccessLevel.USER_MODE; + this.sharingExecutor = new InheritedSharing(); + } + + public void withSharing() { + this.sharingExecutor = new WithSharing(); + } + + public void withoutSharing() { + this.sharingExecutor = new WithoutSharing(); + } + + public void stripInaccessible(AccessType type) { + this.accessType = type; + } + + public void accessMode(AccessLevel accessMode) { + this.accessMode = accessMode; + } + + public void mock(List mocks) { + this.mocks = mocks ?? new List(); + } + + public SObject toObject() { + List records = toList(); + + if (records.isEmpty()) { + return null; // handle: List has no rows for assignment to SObject + } + + if (records.size() > 1) { + throw new QueryException('List has more than 1 row for assignment to SObject'); + } + + return records[0]; + } + + public List toList() { + this.incrementQueryIssued(); + + if (!this.mocks.isEmpty()) { + return this.getMockedListProxy(); + } + + if (this.accessType == null) { + return this.sharingExecutor.toSObjects(this.builder.toString(), binder.getBindingMap(), this.accessMode); + } + + return System.Security.stripInaccessible( + this.accessType, + this.sharingExecutor.toSObjects(this.builder.toString(), binder.getBindingMap(), this.accessMode) + ).getRecords(); + } + + private List getMockedListProxy() { + if (this.mocks.size() == 1) { + return this.mocks[0].sObjectMock.get(this.builder.fields, this.builder.subQueries); + } + return this.mocks.remove(0).sObjectMock.get(this.builder.fields, this.builder.subQueries); + } + + public List toAggregatedProxy() { + this.incrementQueryIssued(); + + if (!this.mocks.isEmpty()) { + return this.getMockedAggregateProxy(); + } + + return new AggregateResultProxies().add(this.toList()).get(); + } + + private List getMockedAggregateProxy() { + if (this.mocks.size() == 1) { + return this.mocks[0].aggregateResultMock.get(); + } + return this.mocks.remove(0).aggregateResultMock.get(); + } + + public Integer toInteger() { + this.incrementQueryIssued(); + + if (!this.mocks.isEmpty()) { + return this.getMockedCount(); + } + + return this.sharingExecutor.toInteger(this.builder.toString(), binder.getBindingMap(), this.accessMode); + } + + private Integer getMockedCount() { + if (this.mocks.size() == 1) { + return this.mocks[0].countMock.get(); + } + return this.mocks.remove(0).countMock.get(); + } + + public Database.QueryLocator toQueryLocator() { + this.incrementQueryIssued(); + return this.sharingExecutor.toQueryLocator(this.builder.toString(), binder.getBindingMap(), this.accessMode); + } + + private void incrementQueryIssued() { + // This counter is used only in unit tests to also track mocked queries. + if (!System.Test.isRunningTest() || System.isBatch() || System.isFuture() || System.isQueueable() || System.isScheduled()) { + return; + } + + SOQL.syncQueriesIssued++; + + final Integer maxQueriesIssuedInSynchronousTransaction = 100; + + if (SOQL.syncQueriesIssued > maxQueriesIssuedInSynchronousTransaction) { + throw new QueryException('Too many SOQL queries.'); + } + } + } + + private interface DatabaseQuery { + List toSObjects(String query, Map binding, AccessLevel accessLevel); + Integer toInteger(String query, Map binding, AccessLevel accessLevel); + Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel); + } + + private inherited sharing class InheritedSharing implements DatabaseQuery { + public List toSObjects(String query, Map binding, AccessLevel accessLevel) { + return Database.queryWithBinds(query, binding, accessLevel); + } + + public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { + return Database.countQueryWithBinds(query, binding, accessLevel); + } + + public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { + return Database.getQueryLocatorWithBinds(query, binding, accessLevel); + } + } + + private without sharing class WithoutSharing implements DatabaseQuery { + public List toSObjects(String query, Map binding, AccessLevel accessLevel) { + return Database.queryWithBinds(query, binding, accessLevel); + } + + public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { + return Database.countQueryWithBinds(query, binding, accessLevel); + } + + public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { + return Database.getQueryLocatorWithBinds(query, binding, accessLevel); + } + } + + private with sharing class WithSharing implements DatabaseQuery { + public List toSObjects(String query, Map binding, AccessLevel accessLevel) { + return Database.queryWithBinds(query, binding, accessLevel); + } + + public Integer toInteger(String query, Map binding, AccessLevel accessLevel) { + return Database.countQueryWithBinds(query, binding, accessLevel); + } - public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { - return Database.getQueryLocatorWithBinds(query, binding, accessLevel); - } - } + public Database.QueryLocator toQueryLocator(String query, Map binding, AccessLevel accessLevel) { + return Database.getQueryLocatorWithBinds(query, binding, accessLevel); + } + } - public inherited sharing class Converter { - private String ofObject; - private List recordsToTransform; + public inherited sharing class Converter { + private String ofObject; + private List recordsToTransform; - public Converter(String ofObject) { - this.ofObject = ofObject; - } - - public Converter transform(List recordsToTransform) { - this.recordsToTransform = recordsToTransform; - return this; - } + public Converter(String ofObject) { + this.ofObject = ofObject; + } - public Map toMap() { - Map recordPerId = (Map) Type.forName('Map').newInstance(); - recordPerId.putAll(this.recordsToTransform); - return recordPerId; - } + public Converter transform(List recordsToTransform) { + this.recordsToTransform = recordsToTransform; + return this; + } - public Map toMap(SObjectField keyField) { - Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); + public Map toMap() { + Map recordPerId = (Map) Type.forName('Map').newInstance(); + recordPerId.putAll(this.recordsToTransform); + return recordPerId; + } - for (SObject record : this.recordsToTransform) { - recordPerCustomKey.put(record.get(keyField).toString(), record); - } + public Map toIdMapBy(SObjectField field) { + Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); - return recordPerCustomKey; - } + for (SObject record : this.recordsToTransform) { + recordPerCustomKey.put((Id) record.get(field), record); + } - public Map toMap(String relationshipName, SObjectField keyField) { - Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); - List relationshipPathFields = relationshipName.split('\\.'); + return recordPerCustomKey; + } - for (SObject record : this.recordsToTransform) { - String key = extractNestedFieldValue(record, relationshipPathFields, keyField); - recordPerCustomKey.put(key, record); - } + public Map toIdMapBy(String relationshipName, SObjectField targetKeyField) { + Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); + List relationshipPathFields = relationshipName.split('\\.'); - return recordPerCustomKey; - } + for (SObject record : this.recordsToTransform) { + recordPerCustomKey.put((Id) extractNestedFieldValue(record, relationshipPathFields, targetKeyField), record); + } - public Map toMap(SObjectField keyField, SObjectField valueField) { - Map customValuePerCustomKey = new Map(); + return recordPerCustomKey; + } - for (SObject record : this.recordsToTransform) { - customValuePerCustomKey.put(record.get(keyField).toString(), record.get(valueField)?.toString()); - } + public Map> toAggregatedIdMapBy(SObjectField keyField) { + Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); - return customValuePerCustomKey; - } + for (SObject record : this.recordsToTransform) { + Id key = (Id) record.get(keyField); - public Set toIdsOf(SObjectField field) { - Set ids = new Set(); + if (!recordsPerCustomKey.containsKey(key)) { + recordsPerCustomKey.put(key, new List()); + } - for (SObject record : this.recordsToTransform) { - ids.add(record.get(field).toString()); - } + recordsPerCustomKey.get(key).add(record); + } - return ids; - } + return recordsPerCustomKey; + } - public Set toIdsOf(String relationshipName, SObjectField field) { - Set ids = new Set(); - List relationshipPathFields = relationshipName.split('\\.'); + public Map> toAggregatedIdMapBy(String relationshipName, SObjectField targetKeyField) { + Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); + List relationshipPathFields = relationshipName.split('\\.'); - for (SObject record : this.recordsToTransform) { - ids.add(extractNestedFieldValue(record, relationshipPathFields, field)); - } + for (SObject record : this.recordsToTransform) { + Id key = extractNestedFieldValue(record, relationshipPathFields, targetKeyField); - return ids; - } + if (!recordsPerCustomKey.containsKey(key)) { + recordsPerCustomKey.put(key, new List()); + } - public Set toValuesOf(SObjectField fieldToExtract) { - Set values = new Set(); + recordsPerCustomKey.get(key).add(record); + } - for (SObject record : this.recordsToTransform) { - values.add(record.get(fieldToExtract).toString()); - } + return recordsPerCustomKey; + } - return values; - } + public Map toMap(SObjectField keyField) { + Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); - public Map> toAggregatedMap(SObjectField keyField) { - Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); + for (SObject record : this.recordsToTransform) { + recordPerCustomKey.put(record.get(keyField).toString(), record); + } - for (SObject record : this.recordsToTransform) { - String key = String.valueOf(record.get(keyField)); + return recordPerCustomKey; + } - if (!recordsPerCustomKey.containsKey(key)) { - recordsPerCustomKey.put(key, new List()); - } + public Map toMap(String relationshipName, SObjectField keyField) { + Map recordPerCustomKey = (Map) Type.forName('Map').newInstance(); + List relationshipPathFields = relationshipName.split('\\.'); - recordsPerCustomKey.get(key).add(record); - } + for (SObject record : this.recordsToTransform) { + String key = extractNestedFieldValue(record, relationshipPathFields, keyField); + recordPerCustomKey.put(key, record); + } - return recordsPerCustomKey; - } + return recordPerCustomKey; + } - public Map> toAggregatedMap(String relationshipName, SObjectField keyField) { - Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); - List relationshipPathFields = relationshipName.split('\\.'); + public Map toMap(SObjectField keyField, SObjectField valueField) { + Map customValuePerCustomKey = new Map(); - for (SObject record : this.recordsToTransform) { - String key = extractNestedFieldValue(record, relationshipPathFields, keyField); + for (SObject record : this.recordsToTransform) { + customValuePerCustomKey.put(record.get(keyField).toString(), record.get(valueField)?.toString()); + } - if (!recordsPerCustomKey.containsKey(key)) { - recordsPerCustomKey.put(key, new List()); - } + return customValuePerCustomKey; + } - recordsPerCustomKey.get(key).add(record); - } + public Set toIdsOf(SObjectField field) { + Set ids = new Set(); - return recordsPerCustomKey; - } + for (SObject record : this.recordsToTransform) { + ids.add(record.get(field).toString()); + } - public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { - Map> customValuesPerCustomKey = new Map>(); + return ids; + } - for (SObject record : this.recordsToTransform) { - String key = String.valueOf(record.get(keyField)); + public Set toIdsOf(String relationshipName, SObjectField field) { + Set ids = new Set(); + List relationshipPathFields = relationshipName.split('\\.'); - if (!customValuesPerCustomKey.containsKey(key)) { - customValuesPerCustomKey.put(key, new List()); - } + for (SObject record : this.recordsToTransform) { + ids.add(extractNestedFieldValue(record, relationshipPathFields, field)); + } - customValuesPerCustomKey.get(key).add(record.get(valueField)?.toString()); - } + return ids; + } - return customValuesPerCustomKey; - } + public Set toValuesOf(SObjectField fieldToExtract) { + Set values = new Set(); - private String extractNestedFieldValue(SObject parentRecord, List relationshipPath, SObjectField targetField) { - SObject currentRecord = parentRecord; + for (SObject record : this.recordsToTransform) { + values.add(record.get(fieldToExtract).toString()); + } - for (String relationshipField : relationshipPath) { - currentRecord = currentRecord.getSObject(relationshipField); + return values; + } - if (currentRecord == null) { - return null; - } - } + public Map> toAggregatedMap(SObjectField keyField) { + Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); - return currentRecord.get(targetField).toString(); - } - } + for (SObject record : this.recordsToTransform) { + String key = String.valueOf(record.get(keyField)); - public class RandomIdGenerator { - private final String RANDOM_STRING_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + if (!recordsPerCustomKey.containsKey(key)) { + recordsPerCustomKey.put(key, new List()); + } - public Id get(SObjectType objectType) { - return get(objectType.getDescribe().getKeyPrefix()); - } + recordsPerCustomKey.get(key).add(record); + } - public Id get(String prefix) { - String randomPart = ''; + return recordsPerCustomKey; + } - while (randomPart.length() < 8) { - Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), RANDOM_STRING_CHARACTERS.length()); - randomPart += RANDOM_STRING_CHARACTERS.substring(idx, idx + 1); - } + public Map> toAggregatedMap(String relationshipName, SObjectField keyField) { + Map> recordsPerCustomKey = (Map>) Type.forName('Map>').newInstance(); + List relationshipPathFields = relationshipName.split('\\.'); - return Id.valueOf(prefix + '0000' + randomPart); - } - } -} + for (SObject record : this.recordsToTransform) { + String key = extractNestedFieldValue(record, relationshipPathFields, keyField); + + if (!recordsPerCustomKey.containsKey(key)) { + recordsPerCustomKey.put(key, new List()); + } + + recordsPerCustomKey.get(key).add(record); + } + + return recordsPerCustomKey; + } + + public Map> toAggregatedMap(SObjectField keyField, SObjectField valueField) { + Map> customValuesPerCustomKey = new Map>(); + + for (SObject record : this.recordsToTransform) { + String key = String.valueOf(record.get(keyField)); + + if (!customValuesPerCustomKey.containsKey(key)) { + customValuesPerCustomKey.put(key, new List()); + } + + customValuesPerCustomKey.get(key).add(record.get(valueField)?.toString()); + } + + return customValuesPerCustomKey; + } + + public Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField) { + Map> customValuesPerCustomKey = new Map>(); + List relationshipPathFields = relationshipName.split('\\.'); + + for (SObject record : this.recordsToTransform) { + String key = String.valueOf(record.get(keyField)); + String value = extractNestedFieldValue(record, relationshipPathFields, targetKeyField); + + if (!customValuesPerCustomKey.containsKey(key)) { + customValuesPerCustomKey.put(key, new List()); + } + + customValuesPerCustomKey.get(key).add(value); + } + + return customValuesPerCustomKey; + } + + private String extractNestedFieldValue(SObject parentRecord, List relationshipPath, SObjectField targetField) { + SObject currentRecord = parentRecord; + + for (String relationshipField : relationshipPath) { + currentRecord = currentRecord.getSObject(relationshipField); + + if (currentRecord == null) { + return null; + } + } + + return currentRecord.get(targetField).toString(); + } + } + + public class RandomIdGenerator { + private final String RANDOM_STRING_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + + public Id get(SObjectType objectType) { + return get(objectType.getDescribe().getKeyPrefix()); + } + + public Id get(String prefix) { + String randomPart = ''; + + while (randomPart.length() < 8) { + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), RANDOM_STRING_CHARACTERS.length()); + randomPart += RANDOM_STRING_CHARACTERS.substring(idx, idx + 1); + } + + return Id.valueOf(prefix + '0000' + randomPart); + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/main/standard-soql/SOQL_Test.cls b/force-app/main/default/classes/main/standard-soql/SOQL_Test.cls index e69dae81..f3c9900c 100644 --- a/force-app/main/default/classes/main/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/main/standard-soql/SOQL_Test.cls @@ -1,4750 +1,4873 @@ /** + * @description Tests for the SOQL class. * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE) * - * v6.0.2 + * v6.1.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexDoc: Variable names are self-documented. **/ -@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexDoc') +@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount') @IsTest private class SOQL_Test { - @IsTest - static void ofSObjectType() { - // Test - String soql = SOQL.of(Account.SObjectType).toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void ofString() { - // Test - String soql = SOQL.of('Account').toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void count() { - // Test - String soql = SOQL.of(Account.SObjectType).count().toString(); - - // Verify - Assert.areEqual('SELECT COUNT() FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countWithDefaultFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id, Account.Name) - .count() - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT() FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countSObjectField() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .count(Opportunity.Id) - .count(Opportunity.CampaignId) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Id), COUNT(CampaignId) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countSObjectFieldWithDefaultFields() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .with(Opportunity.LeadSource) - .count(Opportunity.Id) - .count(Opportunity.CampaignId) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Id), COUNT(CampaignId) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countWithAlias() { - // Test - String soql = SOQL.of(Account.SObjectType).count(Account.Name, 'names').toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) names FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countRelated() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .count('Account', Account.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Account.Name) FROM Contact', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countRelatedWithAlias() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .count('Account', Account.Name, 'names') - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Account.Name) names FROM Contact', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void avg() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .with(Opportunity.CampaignId) - .avg(Opportunity.Amount) - .groupBy(Opportunity.CampaignId) - .toString(); - - // Verify - Assert.areEqual('SELECT CampaignId, AVG(Amount) FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void avgWithAlias() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .with(Opportunity.CampaignId) - .avg(Opportunity.Amount, 'amount') - .groupBy(Opportunity.CampaignId) - .toString(); - - // Verify - Assert.areEqual('SELECT CampaignId, AVG(Amount) amount FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void avgRelated() { - // Test - String soql = SOQL.of(OpportunityLineItem.SObjectType) - .avg('Opportunity', Opportunity.Amount) - .toString(); - - // Verify - Assert.areEqual('SELECT AVG(Opportunity.Amount) FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void avgRelatedWithAlias() { - // Test - String soql = SOQL.of(OpportunityLineItem.SObjectType) - .avg('Opportunity', Opportunity.Amount, 'amount') - .toString(); - - // Verify - Assert.areEqual('SELECT AVG(Opportunity.Amount) amount FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countDistinct() { - // Test - String soql = SOQL.of(Lead.SObjectType).countDistinct(Lead.Company).toString(); - - // Verify - Assert.areEqual('SELECT COUNT_DISTINCT(Company) FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countDistinctWithAlias() { - // Test - String soql = SOQL.of(CampaignMember.SObjectType) - .countDistinct('Lead', Lead.Company) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT_DISTINCT(Lead.Company) FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countDistinctRelated() { - // Test - String soql = SOQL.of(CampaignMember.SObjectType) - .countDistinct('Lead', Lead.Company, 'company') - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT_DISTINCT(Lead.Company) company FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void countDistinctRelatedWithAlias() { - // Test - String soql = SOQL.of(Lead.SObjectType).countDistinct(Lead.Company, 'company').toString(); - - // Verify - Assert.areEqual('SELECT COUNT_DISTINCT(Company) company FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void min() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .with(Contact.FirstName, Contact.LastName) - .min(Contact.CreatedDate) - .groupBy(Contact.FirstName) - .groupBy(Contact.LastName) - .toString(); - - // Verify - Assert.areEqual( - 'SELECT FirstName, LastName, MIN(CreatedDate) FROM Contact GROUP BY FirstName, LastName', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void minWithAlias() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .with(Contact.FirstName, Contact.LastName) - .min(Contact.CreatedDate, 'createdDate') - .groupBy(Contact.FirstName) - .groupBy(Contact.LastName) - .toString(); - - // Verify - Assert.areEqual( - 'SELECT FirstName, LastName, MIN(CreatedDate) createdDate FROM Contact GROUP BY FirstName, LastName', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void minRelated() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .min('Account', Account.CreatedDate) - .toString(); - - // Verify - Assert.areEqual('SELECT MIN(Account.CreatedDate) FROM Contact', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void minRelatedWithAlias() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .min('Account', Account.CreatedDate, 'createdDate') - .toString(); - - // Verify - Assert.areEqual('SELECT MIN(Account.CreatedDate) createdDate FROM Contact', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void max() { - // Test - String soql = SOQL.of(Campaign.SObjectType) - .with(Campaign.Name) - .max(Campaign.BudgetedCost) - .groupBy(Campaign.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, MAX(BudgetedCost) FROM Campaign GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void maxWithAlias() { - // Test - String soql = SOQL.of(Campaign.SObjectType) - .with(Campaign.Name) - .max(Campaign.BudgetedCost, 'budgetedCost') - .groupBy(Campaign.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, MAX(BudgetedCost) budgetedCost FROM Campaign GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void maxRelated() { - // Test - String soql = SOQL.of(CampaignMember.SObjectType) - .max('Campaign', Campaign.BudgetedCost) - .toString(); - - // Verify - Assert.areEqual('SELECT MAX(Campaign.BudgetedCost) FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void maxRelatedAlias() { - // Test - String soql = SOQL.of(CampaignMember.SObjectType) - .max('Campaign', Campaign.BudgetedCost, 'budgetedCost') - .toString(); - - // Verify - Assert.areEqual('SELECT MAX(Campaign.BudgetedCost) budgetedCost FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void sum() { - // Test - String soql = SOQL.of(Opportunity.SObjectType).sum(Opportunity.Amount).toString(); - - // Verify - Assert.areEqual('SELECT SUM(Amount) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void sumWithAlias() { - // Test - String soql = SOQL.of(Opportunity.SObjectType).sum(Opportunity.Amount, 'amount').toString(); - - // Verify - Assert.areEqual('SELECT SUM(Amount) amount FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void sumRelated() { - // Test - String soql = SOQL.of(OpportunityLineItem.SObjectType) - .sum('Opportunity', Opportunity.Amount) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(Opportunity.Amount) FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void sumRelatedWithAlias() { - // Test - String soql = SOQL.of(OpportunityLineItem.SObjectType) - .sum('Opportunity', Opportunity.Amount, 'amount') - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(Opportunity.Amount) amount FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); - } - - @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, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void toLabelWithSObjectField() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.Company) - .toLabel(Lead.Status) - .toString(); - - // Verify - Assert.areEqual('SELECT Company, toLabel(Status) FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void toLabelWithStringField() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.Company) - .toLabel('RecordType.Name') - .toString(); - - // Verify - Assert.areEqual('SELECT Company, toLabel(RecordType.Name) FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void toLabelWithSObjectFieldAndAlias() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.Company) - .toLabel(Lead.Status, 'leadStatus') - .toString(); - - // Verify - Assert.areEqual('SELECT Company, toLabel(Status) leadStatus FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void toLabelWithStringFieldAndAlias() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.Company) - .toLabel('RecordType.Name', 'recordTypeName') - .toString(); - - // Verify - Assert.areEqual('SELECT Company, toLabel(RecordType.Name) recordTypeName FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withStringDateFunction() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with('CALENDAR_YEAR(CloseDate)') - .groupBy('CALENDAR_YEAR(CloseDate)') - .toString(); - - // Verify - Assert.areEqual('SELECT CALENDAR_YEAR(CloseDate) FROM Lead GROUP BY CALENDAR_YEAR(CloseDate)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void toLabelWithAliasAndDuplicatedFields() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.Company, Lead.Status) - .toLabel(Lead.Status, 'leadStatus') - .toString(); - - // Verify - Assert.areEqual('SELECT Company, Status, toLabel(Status) leadStatus FROM Lead', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void formatWithSObjectField() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .format(Opportunity.Amount) - .toString(); - - // Verify - Assert.areEqual('SELECT FORMAT(Amount) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void formatWithAlias() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .format(Opportunity.Amount, 'amt') - .toString(); - - // Verify - Assert.areEqual('SELECT FORMAT(Amount) amt FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withSObjectField() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(Account.BillingCity) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withTwoSObjectFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withThreeSObjectFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id, Account.Name, Account.BillingCity) - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFourSObjectFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id, Account.Name, Account.BillingCity, Account.AccountNumber) - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, BillingCity, AccountNumber FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFiveSObjectFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id, Account.Name, Account.BillingCity, Account.AccountNumber, Account.AccountSource) - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, BillingCity, AccountNumber, AccountSource FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withListOfSObjectFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(new List{ - Account.Id, - Account.Name, - Account.Industry, - Account.AccountNumber, - Account.AnnualRevenue, - Account.BillingCity - }).toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, Industry, AccountNumber, AnnualRevenue, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withListOfStringFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(new List{ - 'Id', - 'Name', - 'Industry', - 'AccountNumber', - 'AnnualRevenue', - 'BillingCity' - }).toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, Industry, AccountNumber, AnnualRevenue, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withStringFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with('Id, Name, BillingCity') - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withStringAggregationAndGroupingFields() { - // Test - String soql = SOQL.of(Opportunity.SObjectType) - .with('CampaignId campaign, AVG(Amount) amount') - .groupBy(Opportunity.CampaignId) - .toString(); - - // Verify - Assert.areEqual('SELECT CampaignId campaign, AVG(Amount) amount FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withStringFieldsAndToLabelFunction() { - // Test - String soql = SOQL.of(Case.SObjectType) - .with('Id, toLabel(Status), Subject') - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Subject, toLabel(Status) FROM Case', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFieldAlias() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, 'myAlias') - .groupBy(Account.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Name myAlias FROM Account GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withDuplicatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with('Id, Name, BillingCity') - .with(Account.Id, Account.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withRelatedField() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', User.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withTwoRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', User.Id, User.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withThreeRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', User.Id, User.Name, User.Alias) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFourRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', User.Id, User.Name, User.Alias, User.City) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias, CreatedBy.City FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFiveRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', User.Id, User.Name, User.Alias, User.City, User.CompanyName) - .toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias, CreatedBy.City, CreatedBy.CompanyName FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .with('CreatedBy', new List{ - User.Id, User.Name - }).toString(); - - // Verify - Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryField() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void withFieldSetException() { - // Setup - Exception queryException = null; - - // Test - try { - String soql = SOQL.of(Account.SObjectType) - .withFieldSet('FieldSetName') - .toString(); - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.areEqual('FieldSet with name FieldSetName does not exist!', queryException.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void subQueryTwoFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryThreeFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.AccountId) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryFourFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.AccountId, Contact.Email) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId, Email FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryFiveFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.AccountId, Contact.Email, Contact.Phone) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId, Email, Phone FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(new List{ - Contact.Id, Contact.Name - }) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryStringFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with('Id, Name') - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryRelatedFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .with('CreatedBy', new List{ - User.Id, User.Name - }) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name, CreatedBy.Id, CreatedBy.Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryTwoLevels() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.LastName) - .with(SOQL.SubQuery.of('Assets').with(Asset.AssetLevel)) - ) - .toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT LastName , (SELECT AssetLevel FROM Assets) FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryWhereFilterGroup() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Contact.LastName).equal('Doe')) - ) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts WHERE (LastName = :v1)) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryWhereFilter() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .whereAre(SOQL.Filter.with(Contact.LastName).equal('Doe')) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts WHERE LastName = :v1) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryOrderBySObjectField() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .orderBy(Contact.Name) - .sortDesc() - .nullsLast() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryOrderByStringField() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .orderBy('Name') - .sortDesc() - .nullsLast() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryOrderByDynamic() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .orderBy('Name') - .sort('ASC') - .nullsLast() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name ASC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryOrderByRelated() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .orderBy('CreatedBy', User.Name) - .sortDesc() - .nullsLast() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY CreatedBy.Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQuerySetLimit() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .setLimit(10) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts LIMIT 10) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryOffset() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .offset(100) - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts OFFSET 100) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryForReference() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .forReference() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts FOR REFERENCE) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void subQueryForView() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Name) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name) - .forView() - ).toString(); - - // Verify - Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts FOR VIEW) FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void multipleSubQueriesWithConditions() { - // Setup - String leadSource = 'Web'; - Date fromDate = Date.newInstance(2024, 1, 1); - Date toDate = Date.newInstance(2024, 1, 30); - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .with( - SOQL.SubQuery.of('Contacts') - .with(Contact.Id) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Contact.CreatedDate).lessOrEqual(fromDate)) - .add(SOQL.Filter.with(Contact.CreatedDate).isNull()) - .add(SOQL.Filter.with(Contact.CreatedDate).greaterOrEqual(toDate)) - .conditionLogic('1 AND (2 OR 3)') - ) - ) - .with( - SOQL.SubQuery.of('Opportunities') - .with(Opportunity.Id) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Opportunity.LeadSource).equal(leadSource)) - .add(SOQL.Filter.with(Contact.CreatedDate).equal(fromDate)) - ) - ) - .setLimit(1); - - // Verify - Assert.areEqual( - 'SELECT Id , (SELECT Id FROM Contacts WHERE (CreatedDate <= :v1 AND (CreatedDate = :v2 OR CreatedDate >= :v3))), (SELECT Id FROM Opportunities WHERE (LeadSource = :v4 AND CreatedDate = :v5)) FROM Account LIMIT 1', - builder.toString(), - 'The generated SOQL should match the expected one.' - ); - - Map binding = builder.binding(); - Assert.areEqual(fromDate, binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual(null, binding.get('v2'), 'The binding variable should match the expected value.'); - Assert.areEqual(toDate, binding.get('v3'), 'The binding variable should match the expected value.'); - Assert.areEqual(leadSource, binding.get('v4'), 'The binding variable should match the expected value.'); - Assert.areEqual(fromDate, binding.get('v5'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void delegatedScope() { - // Test - String soql = SOQL.of(Task.SObjectType) - .delegatedScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Task USING SCOPE DELEGATED', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void mineScope() { - // Test - String soql = SOQL.of(Account.SObjectType) - .mineScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account USING SCOPE MINE', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void mineAndMyGroupsScope() { - // Test - String soql = SOQL.of(ProcessInstanceWorkItem.SObjectType) - .mineAndMyGroupsScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM ProcessInstanceWorkitem USING SCOPE MINE_AND_MY_GROUPS', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void myTerritoryScope() { - // Test - String soql = SOQL.of(Account.SObjectType) - .myTerritoryScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account USING SCOPE MY_TERRITORY', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void myTeamTerritoryScope() { - // Test - String soql = SOQL.of(Account.SObjectType) - .myTeamTerritoryScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account USING SCOPE MY_TEAM_TERRITORY', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void teamScope() { - // Test - String soql = SOQL.of(Account.SObjectType) - .teamScope() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account USING SCOPE TEAM', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterId() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.id().isNotNull()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id != :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterRecordType() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.recordType().isNotNull()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE RecordType.DeveloperName != :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterName() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.name().isNotNull()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterWithRelatedField() { - // Test - SOQL.Queryable builder = SOQL.of(Contact.SObjectType) - .whereAre(SOQL.Filter.with('Account', Account.Name).equal('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Contact WHERE Account.Name = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterEqualString() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).equal('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotEqualString() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notEqual('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterLessThan() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).lessThan(10)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees < :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterGreaterThan() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).greaterThan(10)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees > :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterLessOrEqual() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).lessOrEqual(10)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees <= :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterGreaterOrEqual() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).greaterOrEqual(10)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterContains() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).contains('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('%Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterContainsNull() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).contains(null)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('%null%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotContains() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notContains('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('%Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterContainsValues() { - // Setup - List names = new List{ 'Acc', 'My' }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).containsSome(names)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterEndsWith() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).endsWith('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('%Test', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotEndsWith() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notEndsWith('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('%Test', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterStartsWith() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).startsWith('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotStartsWith() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notStartsWith('Test')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterCustomContains() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).contains('_', 'Test', '%')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('_Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterCustomNotContains() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notContains('_', 'Test', '%')); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('_Test%', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIsInSet() { - // Setup - Set names = new Set{ 'Test 1', 'Test 2' }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).isIn(names)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIsInList() { - // Setup - List names = new List{ 'Test 1', 'Test 2' }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).isIn(names)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotInSet() { - // Setup - Set names = new Set{ 'Test 1', 'Test 2' }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).notIn(names)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name NOT IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterNotInList() { - // Setup - List names = new List{ 'Test 1', 'Test 2' }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.BillingCity) - .whereAre(SOQL.Filter.with(Account.Name).notIn(names)); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Name, BillingCity FROM Account WHERE Name NOT IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIncludesAll() { - // Setup - List ratings = new List{ 'Hot', 'Warm' }; - - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id) - .whereAre(SOQL.Filter.with(Account.Rating).includesAll(ratings)) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE Rating INCLUDES (\'Hot;Warm\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterIncludesSome() { - // Setup - List ratings = new List{ 'Hot', 'Warm' }; - - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id) - .whereAre(SOQL.Filter.with(Account.Rating).includesSome(ratings)) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE Rating INCLUDES (\'Hot\', \'Warm\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterExcludesAll() { - // Setup - List ratings = new List{ 'Hot', 'Warm' }; - - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id) - .whereAre(SOQL.Filter.with(Account.Rating).excludesAll(ratings)) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE Rating EXCLUDES (\'Hot\', \'Warm\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterExcludesSome() { - // Setup - List ratings = new List{ 'Hot', 'Warm' }; - - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Id) - .whereAre(SOQL.Filter.with(Account.Rating).excludesSome(ratings)) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE Rating EXCLUDES (\'Hot;Warm\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterIsNull() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).isNull()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIsNotNull() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).isNotNull()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIsTrue() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.IsDeleted).isTrue()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE IsDeleted = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.isTrue((Boolean) binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterIsFalse() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.IsDeleted).isFalse()); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE IsDeleted = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.isFalse((Boolean) binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void filterDateLiteral() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.CreatedDate).greaterThan('LAST_N_QUARTERS:2').asDateLiteral()) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE CreatedDate > LAST_N_QUARTERS:2', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filtersGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - ); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', builder.toString(), 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void emptyFiltersGroup() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicFiltersGroup() { - // Setup - SOQL.FilterGroup filterGroup = SOQL.FilterGroup; - - filterGroup.add(SOQL.Filter.with(Account.Name).equal('Test')); - filterGroup.add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')); - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(filterGroup); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void dynamicFiltersListGroup() { - // Setup - SOQL.FilterGroup filterGroup = SOQL.FilterGroup; - - filterGroup.add(new List { - SOQL.Filter.with(Account.Name).equal('Test'), - SOQL.Filter.with(Account.BillingCity).equal('Krakow') - }); - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(filterGroup); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void dynamicStringFiltersGroup() { - // Setup - SOQL.FilterGroup filterGroup = SOQL.FilterGroup; - - filterGroup.add('Name = \'Test\''); - filterGroup.add('BillingCity = \'Krakow\''); - - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(filterGroup) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' AND BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicStringFiltersGroupWithAnyConditionMatching() { - // Setup - SOQL.FilterGroup filterGroup = SOQL.FilterGroup; - - filterGroup.add('Name = \'Test\''); - filterGroup.add('BillingCity = \'Krakow\''); - filterGroup.anyConditionMatching(); - - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(filterGroup) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' OR BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicStringFiltersListGroup() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(new List { - 'Name = \'Test\'', - 'BillingCity = \'Krakow\'' - }) - ).toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' AND BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicStringFiltersListGroupWithAnyConditionMatching() { - // Setup - SOQL.FilterGroup filterGroup = SOQL.FilterGroup; - - filterGroup.add(new List { - 'Name = \'Test\'', - 'BillingCity = \'Krakow\'' - }); - - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(new List { - 'Name = \'Test\'', - 'BillingCity = \'Krakow\'' - }).anyConditionMatching() - ).toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' OR BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicFiltersGroupOnSoqlInstance() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Industry).equal('IT')); - - Assert.areEqual('SELECT Id FROM Account WHERE Industry = :v1', builder.toString(), 'The generated SOQL should match the expected one.'); - - builder.whereAre( - SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - ); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE Industry = :v1 AND (Name = :v2 AND BillingCity = :v3)', builder.toString(), 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('IT', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Test', binding.get('v2'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v3'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void multipleFilterGroups() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('1'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('2'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('3'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('4'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('5'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('6'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('7'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('8'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('9'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('10'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('11'))); - - // Verify - String soql = builder.toString(); - Assert.areEqual( - 'SELECT Id FROM Account WHERE (Name LIKE :v1) AND (Name LIKE :v2) AND (Name LIKE :v3) AND (Name LIKE :v4) AND (Name LIKE :v5) AND (Name LIKE :v6) AND (Name LIKE :v7) AND (Name LIKE :v8) AND (Name LIKE :v9) AND (Name LIKE :v10) AND (Name LIKE :v11)', - soql, - 'The generated SOQL should match the expected one.' - ); - - Map binding = builder.binding(); - Assert.areEqual('%1%', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('%2%', binding.get('v2'), 'The binding variable should match the expected value.'); - Assert.areEqual('%3%', binding.get('v3'), 'The binding variable should match the expected value.'); - Assert.areEqual('%4%', binding.get('v4'), 'The binding variable should match the expected value.'); - Assert.areEqual('%5%', binding.get('v5'), 'The binding variable should match the expected value.'); - Assert.areEqual('%6%', binding.get('v6'), 'The binding variable should match the expected value.'); - Assert.areEqual('%7%', binding.get('v7'), 'The binding variable should match the expected value.'); - Assert.areEqual('%8%', binding.get('v8'), 'The binding variable should match the expected value.'); - Assert.areEqual('%9%', binding.get('v9'), 'The binding variable should match the expected value.'); - Assert.areEqual('%10%', binding.get('v10'), 'The binding variable should match the expected value.'); - Assert.areEqual('%11%', binding.get('v11'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void duplicatedConditionsInConditionOrder() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('1'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('2'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('3'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('4'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('5'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('6'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('7'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('8'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('9'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('10'))) - .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('11'))) - .conditionLogic('(1 AND 2 AND 3 AND 4 AND 5 AND 5 AND 7 AND 8 AND 9 AND 10 AND 11) OR (11 AND 1 AND 10)'); - - // Verify - String soql = builder.toString(); - Assert.areEqual( - 'SELECT Id FROM Account WHERE ((Name LIKE :v1) AND (Name LIKE :v2) AND (Name LIKE :v3) AND (Name LIKE :v4) AND (Name LIKE :v5) AND (Name LIKE :v5) AND (Name LIKE :v7) AND (Name LIKE :v8) AND (Name LIKE :v9) AND (Name LIKE :v10) AND (Name LIKE :v11)) OR ((Name LIKE :v11) AND (Name LIKE :v1) AND (Name LIKE :v10))', - soql, - 'The generated SOQL should match the expected one.' - ); - - Map binding = builder.binding(); - Assert.areEqual('%1%', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('%2%', binding.get('v2'), 'The binding variable should match the expected value.'); - Assert.areEqual('%3%', binding.get('v3'), 'The binding variable should match the expected value.'); - Assert.areEqual('%4%', binding.get('v4'), 'The binding variable should match the expected value.'); - Assert.areEqual('%5%', binding.get('v5'), 'The binding variable should match the expected value.'); - Assert.areEqual('%6%', binding.get('v6'), 'The binding variable should match the expected value.'); - Assert.areEqual('%7%', binding.get('v7'), 'The binding variable should match the expected value.'); - Assert.areEqual('%8%', binding.get('v8'), 'The binding variable should match the expected value.'); - Assert.areEqual('%9%', binding.get('v9'), 'The binding variable should match the expected value.'); - Assert.areEqual('%10%', binding.get('v10'), 'The binding variable should match the expected value.'); - Assert.areEqual('%11%', binding.get('v11'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void anyConditionMatchingForInnerGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - .anyConditionMatching() - ); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 OR BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void anyConditionMatchingForMainGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).equal('Test')) - .whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - .anyConditionMatching(); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void conditionLogicForMainGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Name).equal('Test')) - .whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - .conditionLogic('1 OR 2'); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void nestedFiltersGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - .add(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test 2')) - .add(SOQL.Filter.with(Account.BillingCity).equal('Warsaw')) - .conditionLogic('1 OR 2') - ) - ); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2 AND (Name = :v3 OR BillingCity = :v4))', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); - Assert.areEqual('Test 2', binding.get('v3'), 'The binding variable should match the expected value.'); - Assert.areEqual('Warsaw', binding.get('v4'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void innerJoinWithFilterGroup() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Id).isIn( - SOQL.InnerJoin.of(Contact.SObjectType) - .with(Contact.AccountId) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Contact.Name).equal('My Contact')) - ) - )); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE (Name = :v1))', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void inInnerJoin() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Id).isIn( - SOQL.InnerJoin.of(Contact.SObjectType) - .with(Contact.AccountId) - .whereAre(SOQL.Filter.with(Contact.Name).equal('My Contact')) - )); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE Name = :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void isNotInInnerJoin() { - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.Id).notIn( - SOQL.InnerJoin.of(Contact.SObjectType) - .with(Contact.AccountId) - .whereAre(SOQL.Filter.name().equal('My Contact')) - )); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Contact WHERE Name = :v1)', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void evaluateStringConditions() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= 10 AND NumberOfEmployees <= 20', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void evaluateEmptyConditions() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre('') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void evaluateStringConditionsAndGroup() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') - .whereAre(SOQL.Filter.name().equal('My Contact')) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= 10 AND NumberOfEmployees <= 20 AND Name = :v1', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void evaluateStringAndFilterInFilterGroup() { - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre( - SOQL.FilterGroup - .add('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') - .add(SOQL.Filter.name().equal('My Contact')) - ) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (NumberOfEmployees >= 10 AND NumberOfEmployees <= 20 AND Name = :v1)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterIgnoreWhen() { - // Setup - String accountName = ''; - - // Test - String soql = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) - .add(SOQL.Filter.name().contains(accountName).ignoreWhen(String.isEmpty(accountName))) - ) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE (BillingCity = :v1)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void filterGroupIgnoreWhen() { - // Setup - Boolean isPartnerUser = false; - - // Test - String soql = 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('MyAccount')) - ) - ) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account WHERE ((Industry = :v1 AND Name LIKE :v2))', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupBy() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .groupBy(Lead.LeadSource) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByString() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .groupBy('LeadSource') - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByRelated() { - // Test - String soql = SOQL.of(OpportunityLineItem.SObjectType) - .count(OpportunityLineItem.Name, 'count') - .groupBy('OpportunityLineItem.Opportunity.Account', Account.Id) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) count FROM OpportunityLineItem GROUP BY OpportunityLineItem.Opportunity.Account.Id', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByRelatedToAggregated() { - // Setup - insertAccounts(); - - // Test - List results = SOQL.of(Account.SObjectType) - .count(Account.Name, 'names') - .groupBy('Account.CreatedBy', User.Id) - .toAggregated(); - - // Verify - Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.'); - } - - @IsTest - static void groupByRelatedToAggregatedProxy() { - // Setup - insertAccounts(); - - // Test - List results = SOQL.of(Account.SObjectType) - .count(Account.Name, 'names') - .groupBy('Account.CreatedBy', User.Id) - .toAggregatedProxy(); - - // Verify - Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.'); - } - - @IsTest - static void groupByRollup() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name, 'cnt') - .with(Lead.LeadSource) - .groupByRollup(Lead.LeadSource) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(LeadSource)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByRollupManyFields() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name, 'cnt') - .with(Lead.Status, Lead.LeadSource) - .groupByRollup(Lead.Status) - .groupByRollup(Lead.LeadSource) - .toString(); - - // Verify - Assert.areEqual('SELECT Status, LeadSource, COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(Status, LeadSource)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByRollupRelated() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name, 'cnt') - .groupByRollup('ConvertedOpportunity', Opportunity.StageName) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(ConvertedOpportunity.StageName)', soql, 'The generated SOQL should match the expected one.'); - } - - @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, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByCubeManyFields() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Type, Account.BillingCountry) - .groupByCube(Account.Type) - .groupByCube(Account.BillingCountry) - .toString(); - - // Verify - Assert.areEqual('SELECT Type, BillingCountry FROM Account GROUP BY CUBE(Type, BillingCountry)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByCubeRelated() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name, 'cnt') - .groupByCube('ConvertedOpportunity', Opportunity.StageName) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) cnt FROM Lead GROUP BY CUBE(ConvertedOpportunity.StageName)', soql, 'The generated SOQL should match the expected one.'); - } - - @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 can\'t use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.', - queryException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @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, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void groupByWithDefaultFieldsAndAggregateFunction() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.FirstName, Lead.LastName, Lead.Email) - .count(Lead.Name) - .with(Lead.LeadSource) - .groupBy(Lead.LeadSource) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterWithSObjectField() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterWithStringField() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with('City').startsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterCount() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT(Name) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterAvg() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .avg(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.avg(Lead.Name).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, AVG(Name) FROM Lead GROUP BY LeadSource HAVING AVG(Name) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterCountDistinct() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .countDistinct(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.countDistinct(Lead.Name).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT_DISTINCT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT_DISTINCT(Name) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterMin() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .min(Lead.NumberOfEmployees) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.min(Lead.NumberOfEmployees).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, MIN(NumberOfEmployees) FROM Lead GROUP BY LeadSource HAVING MIN(NumberOfEmployees) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterMax() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .max(Lead.NumberOfEmployees) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.max(Lead.NumberOfEmployees).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, MAX(NumberOfEmployees) FROM Lead GROUP BY LeadSource HAVING MAX(NumberOfEmployees) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterSum() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(100)) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, SUM(AnnualRevenue) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) > 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsNull() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.with(Lead.LeadSource).isNull()) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource = null', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsNotNull() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.with(Lead.LeadSource).isNotNull()) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource HAVING LeadSource != null', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsTrue() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.IsConverted) - .have(SOQL.HavingFilter.with(Lead.IsConverted).isTrue()) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY IsConverted HAVING IsConverted = true', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsFalse() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.IsConverted) - .have(SOQL.HavingFilter.with(Lead.IsConverted).isFalse()) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY IsConverted HAVING IsConverted = false', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterEqualString() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.with(Lead.LeadSource).equal('Web')) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource = \'Web\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterEqualInteger() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).equal(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) = 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterNotEqualString() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.with(Lead.LeadSource).notEqual('Web')) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource != \'Web\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterNotEqualInteger() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).notEqual(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) != 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterLessThan() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).lessThan(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) < 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterGreaterThan() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) > 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterLessOrEqual() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).lessOrEqual(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) <= 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterGreaterOrEqual() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterOrEqual(10000)) - .toString(); - - // Verify - Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) >= 10000', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterContains() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).contains('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'%San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterCustomContains() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).contains('_', 'San', '%')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'_San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterNotContains() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notContains('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'%San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterCustomNotContains() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notContains('_', 'San', '%')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'_San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterStartsWith() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterNotStartsWith() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notStartsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterEndsWith() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).endsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'%San\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterNotEndsWith() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notEndsWith('San')) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'%San\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsInSet() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).isIn(new Set{ 'San Francisco', 'Los Angeles' })) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsInList() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).isIn(new List{ 'San Francisco', 'Los Angeles' })) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsNotInSet() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notIn(new Set{ 'San Francisco', 'Los Angeles' })) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City NOT IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterIsNotInList() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .sum(Lead.AnnualRevenue) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.with(Lead.City).notIn(new List{ 'San Francisco', 'Los Angeles' })) - .toString(); - - // Verify - Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City NOT IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void havingFilterGroup() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - ).toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 AND City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicHavingFilterGroup() { - // Setup - SOQL.HavingFilterGroup havingFilterGroup = SOQL.HavingFilterGroup; - - havingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)); - havingFilterGroup.add(SOQL.HavingFilter.with(Lead.City).startsWith('San')); - - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(havingFilterGroup) - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 AND City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicHavingFilterGroupOnSoqlInstance() { - // Test - SOQL.Queryable builder = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.WITH(Lead.LeadSource).equal('Web')); - - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING LeadSource = \'Web\'', builder.toString(), 'The generated SOQL should match the expected one.'); - - builder.have( - SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - ); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING LeadSource = \'Web\' AND (COUNT(Name) > 100 AND City LIKE \'San%\')', builder.toString(), 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void multipleHavingFilterGroups() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(1))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(2))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(3))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(4))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(5))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(6))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(7))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(8))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(9))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(10))) - .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(11))) - .anyHavingConditionMatching() - .toString(); - - // Verify - Assert.areEqual( - 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 1) OR (COUNT(Name) > 2) OR (COUNT(Name) > 3) OR (COUNT(Name) > 4) OR (COUNT(Name) > 5) OR (COUNT(Name) > 6) OR (COUNT(Name) > 7) OR (COUNT(Name) > 8) OR (COUNT(Name) > 9) OR (COUNT(Name) > 10) OR (COUNT(Name) > 11)', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void anyConditionMatchingForInnerHavingFilterGroups() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - .anyConditionMatching() - ).toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 OR City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void anyConditionMatchingForMainHavingFilterGroup() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - .anyHavingConditionMatching() - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING COUNT(Name) > 100 OR City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void conditionLogicForMainHavingFilterGroup() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .groupBy(Lead.City) - .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) - .havingConditionLogic('1 OR 2') - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING COUNT(Name) > 100 OR City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void nestedHavingFilterGroups() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilterGroup - .add(SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) - .add(SOQL.HavingFilter.count(Lead.Name).lessThan(200)) - ) - .add(SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(400)) - .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) - ) - ).toString(); - - // Verify - Assert.areEqual( - 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING ((COUNT(Name) > 100 AND COUNT(Name) < 200) AND (COUNT(Name) > 400 AND COUNT(Name) < 500))', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void evaluateStringHavingCondition() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have('COUNT(Name) > 100 AND COUNT(Name) < 200') - .toString(); - - // Verify - Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT(Name) > 100 AND COUNT(Name) < 200', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void evaluateStringHavingConditionAndHavingFilterGroup() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have('(COUNT(Name) > 100 AND COUNT(Name) < 200)') - .have(SOQL.HavingFilterGroup - .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(400)) - .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) - ).toString(); - - // Verify - Assert.areEqual( - 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING (COUNT(Name) > 100 AND COUNT(Name) < 200) AND (COUNT(Name) > 400 AND COUNT(Name) < 500)', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - @IsTest - static void evaluateStringInHavingFilterGroupAndHavingFilter() { - // Test - String soql = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .count(Lead.Name) - .groupBy(Lead.LeadSource) - .have(SOQL.HavingFilterGroup - .add('(COUNT(Name) > 100 AND COUNT(Name) < 200)') - .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) - ).toString(); - - // Verify - Assert.areEqual( - 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING ((COUNT(Name) > 100 AND COUNT(Name) < 200) AND COUNT(Name) < 500)', - soql, - 'The generated SOQL should match the expected one.' - ); - } - - // WITH DATA CATEGORY - - @IsTest - static void dataCategoryFilterWithStringField() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow('Europe__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAt() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').at('Europe__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c AT Europe__c', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAtMultiple() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').at(new List{ 'Europe__c', 'North_America__c' })) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c AT (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAbove() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').above('Europe__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE Europe__c', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAboveMultiple() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').above(new List{ 'Europe__c', 'North_America__c' })) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterBelow() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').below('Europe__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterBelowMultiple() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').below(new List{ 'Europe__c', 'North_America__c' })) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c BELOW (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAboveOrBelow() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow('Europe__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryFilterAboveOrBelowMultiple() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryWithMultipleDataCategoryFilters() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) - .withDataCategory(SOQL.DataCategoryFilter.with('Product__c').at(new List{ 'Product1__c', 'Product2__c' })) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c) AND Product__c AT (Product1__c, Product2__c)', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dataCategoryWithMultipleFilters() { - // Test - String soql = SOQL.of('Knowledge__kav') - .with('Title') - .whereAre(SOQL.Filter.with('PublishStatus').equal('Draft')) - .whereAre(SOQL.Filter.with('Language').equal('en_US')) - .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) - .withDataCategory(SOQL.DataCategoryFilter.with('Product__c').at('Product1__c')) - .toString(); - - // Verify - Assert.areEqual('SELECT Title FROM Knowledge__kav WHERE PublishStatus = :v1 AND Language = :v2 WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c) AND Product__c AT Product1__c', soql, 'The generated SOQL should match the expected one.'); - } - - // ORDER BY - - @IsTest - static void orderByString() { - // Test - String soql = SOQL.of(Account.SObjectType) - .orderBy('Industry').sortDesc().nullsLast() - .orderBy('Id') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ORDER BY Industry DESC NULLS LAST, Id ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void orderByDynamic() { - // Test - String soql = SOQL.of(Account.SObjectType) - .orderBy('Industry', 'ASC') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void dynamicNullsOrder() { - // Test - String soql = SOQL.of(Account.SObjectType) - .orderBy('Industry') - .nullsOrder('LAST') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS LAST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void orderByDynamicWithSort() { - // Test - String soql = SOQL.of(Account.SObjectType) - .orderBy('Industry') - .sort('ASC') - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void orderBy() { - // Test - String soql = SOQL.of(Account.SObjectType) - .orderBy(Account.Industry).sortDesc().nullsLast() - .orderBy(Account.Id) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ORDER BY Industry DESC NULLS LAST, Id ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void orderByRelated() { - // Test - String soql = SOQL.of(Contact.SObjectType) - .orderBy('Account', Account.Name) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Contact ORDER BY Account.Name ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void orderByCount() { - // Test - String soql = SOQL.of(Account.SObjectType) - .with(Account.Industry) - .groupBy(Account.Industry) - .orderByCount(Account.Industry).sortDesc().nullsLast() - .toString(); - - // Verify - Assert.areEqual('SELECT Industry FROM Account GROUP BY Industry ORDER BY COUNT(Industry) DESC NULLS LAST', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void setLimit() { - // Test - String soql = SOQL.of(Account.SObjectType) - .setLimit(100) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account LIMIT 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void offset() { - // Test - String soql = SOQL.of(Account.SObjectType) - .offset(100) - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account OFFSET 100', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void forReference() { - // Test - String soql = SOQL.of(Account.SObjectType) - .forReference() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account FOR REFERENCE', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void forView() { - // Test - String soql = SOQL.of(Account.SObjectType) - .forView() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account FOR VIEW', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void forUpdate() { - // Test - String soql = SOQL.of(Account.SObjectType) - .forUpdate() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account FOR UPDATE', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void allRows() { - // Test - String soql = SOQL.of(Account.SObjectType) - .allRows() - .toString(); - - // Verify - Assert.areEqual('SELECT Id FROM Account ALL ROWS', soql, 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void byId() { - // Setup - Id fakeAccountId = SOQL.IdGenerator.get(Account.SObjectType); - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType).byId(fakeAccountId); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(fakeAccountId, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void byIdSObject() { - // Setup - List cases = insertCases(); - - // Test - SOQL.Queryable builder = SOQL.of(Case.SObjectType).byId(cases[0]); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Case WHERE Id = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(cases[0].Id, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void byIdsSet() { - // Verify - Set accountIds = new Set{ SOQL.IdGenerator.get(Account.SObjectType) }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType).byIds(accountIds); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(accountIds, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void byIdsList() { - // Setup - List accountIds = new List{ SOQL.IdGenerator.get(Account.SObjectType) }; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType).byIds(accountIds); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(accountIds, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void byIdsSObjects() { - // Setup - List cases = insertCases(); - - // Test - SOQL.Queryable builder = SOQL.of(Case.SObjectType).byIds(cases); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Case WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(cases, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void byRecordType() { - // Setup - String recordTypeDeveloperName = 'Partner'; - - // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType).byRecordType(recordTypeDeveloperName); - - // Verify - String soql = builder.toString(); - Assert.areEqual('SELECT Id FROM Account WHERE RecordType.DeveloperName = :v1', soql, 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual(recordTypeDeveloperName, binding.get('v1'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void toObjectWithoutSharing() { - // Setup - Case testCase = new Case(Status = 'New', Origin = 'Web'); - insert testCase; - - System.runAs(minimumAccessUser()) { - // Test - Case resultCase = (Case) SOQL.of(Case.SObjectType).systemMode().withoutSharing().toObject(); - - // Verify - Assert.areEqual(testCase.Id, resultCase.Id, 'The case should be returned even if the user does not have access to it.'); - } - } - - @IsTest - static void toListWithoutSharing() { - // Setup - insertCases(); - - System.runAs(minimumAccessUser()) { - // Test - List cases = SOQL.of(Case.SObjectType).systemMode().withoutSharing().toList(); - - // Verify - Assert.areEqual(2, cases.size(), 'Cases should be returned even if the user does not have access to them.'); - } - } - - @IsTest - static void toIntegerWithoutSharing() { - // Setup - insertCases(); - - System.runAs(minimumAccessUser()) { - // Test - Integer casesAmount = SOQL.of(Case.SObjectType).count().systemMode().withoutSharing().toInteger(); - - // Verify - Assert.areEqual(2, casesAmount, 'The amount of two cases should be returned even if the user does not have access to them.'); - } - } - - @IsTest - static void toObjectWithSharing() { - // Setup - insert new Task(Subject = 'Test', Type = 'Other'); - - System.runAs(minimumAccessUser()) { - // Test - Task resultTask = (Task) SOQL.of(Task.SObjectType).systemMode().withSharing().toObject(); - - // Verify - Assert.isNull(resultTask, 'The user should not have access to the task.'); - } - } - - @IsTest - static void toListWithSharing() { - // Setup - insertTasks(); - - System.runAs(minimumAccessUser()) { - // Test - List tasks = SOQL.of(Task.SObjectType).systemMode().withSharing().toList(); - - // Verify - Assert.areEqual(0, tasks.size(), 'The user should not have access to the tasks.'); - } - } - - @IsTest - static void toIntegerWithSharing() { - insertTasks(); - - System.runAs(minimumAccessUser()) { - // Test - Integer tasksAmount = SOQL.of(Task.SObjectType).count().systemMode().withSharing().toInteger(); - - // Verify - Assert.areEqual(0, tasksAmount, 'The user should not have access to the tasks.'); - } - } - - @IsTest - static void userMode() { - // Setup - insert new Task(Subject = 'Test', Type = 'Other'); - - System.runAs(minimumAccessUser()) { - // Test - Exception queryException = null; - - try { - Task task = (Task) SOQL.of(Task.SObjectType) - .with(Task.Type) - .userMode() - .toObject(); - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.isTrue(queryException.getMessage().contains('No such column \'Type\' on entity \'Task\'.'), 'The user should not have access to the \'Type\' field.'); - } - } - - @IsTest - static void stripInaccessibleToObject() { - // Setup - insert new Task(Subject = 'Test', Type = 'Other'); - - System.runAs(minimumAccessUser()) { - // Test - Task task = (Task) SOQL.of(Task.SObjectType) - .with(Task.Type) - .systemMode() - .stripInaccessible() - .withoutSharing() - .toObject(); - - Exception queryException = null; - - String inaccessibleFieldValue; - - try { - inaccessibleFieldValue = task.Type; - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.areEqual( - 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', - queryException.getMessage(), - 'The user should not have access to the \'Type\' field.' - ); - } - } - - @IsTest - static void stripInaccessibleToList() { - // Setup - insertTasks(); - - System.runAs(minimumAccessUser()) { - // Test - List tasks = SOQL.of(Task.SObjectType) - .with(Task.Type) - .systemMode() - .stripInaccessible() - .withoutSharing() - .toList(); - - Exception queryException = null; - - String inaccessibleFieldValue; - - try { - inaccessibleFieldValue = tasks[0].Type; - } catch(Exception e) { - queryException = e; - } - - // Verify - Assert.areEqual( - 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', - queryException.getMessage(), - 'The user should not have access to the \'Type\' field.' - ); - } - } - - @IsTest - static void mockId() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }; - - // Test - SOQL.setMock('mockingQuery', accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(accounts, result, 'The mocked accounts should be returned.'); - } - - @IsTest - static void mockIdSpecifiedButQueryNotMocked() { - // Test - List accounts = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(0, accounts.size(), 'The mocked accounts should be returned.'); - } - - @IsTest - static void mockingSingleRecord() { - // Setup - Account testAccount = new Account(Name = 'Test 1'); - - // Test - SOQL.setMock('mockingQuery', testAccount); - Account result = (Account) SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toObject(); - - // Verify - Assert.areEqual(testAccount, result, 'The mocked account should be returned.'); - } - - @IsTest - static void mockNoResult() { - // Test - SOQL.setMock('mockingQuery', new List()); - Account result = (Account) SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toObject(); - - // Verify - Assert.isNull(result, 'The result should be null when no records are mocked.'); - } - - @IsTest - static void mockNoResultWithNull() { - // Test - SOQL.setMock('mockingQuery', (Account) null); - Account result = (Account) SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toObject(); - - // Verify - Assert.isNull(result, 'The result should be null when no records are mocked.'); - } - - @IsTest - static void mockNoResults() { - // Test - SOQL.mock('mockingQuery').thenReturn(new List()); - List result = SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.isTrue(result.isEmpty(), 'The result should be empty when no records are mocked.'); - } - - @IsTest - static void mockingMultipleRecords() { - // Setup - List testAccounts = new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }; - - // Test - SOQL.setMock('mockingQuery', testAccounts); - List result = SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(testAccounts, result, 'The mocked accounts should be returned.'); - } - - @IsTest - static void mockingSingleRecordWithMockableInterface() { - // Setup - Account testAccount = new Account(Name = 'Test 1'); - - // Test - SOQL.mock('mockingQuery').thenReturn(testAccount); - Account result = (Account) SOQL.of(Account.sObjectType).with(Account.Name).mockId('mockingQuery').toObject(); - - // Verify - Assert.areEqual(testAccount.Name, result.Name, 'The result account Name should be the same as the mocked account Name.'); - Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.'); - } - - @IsTest - static void mockingCount() { - // Test - SOQL.setCountMock('mockingQuery', 2); - Integer result = SOQL.of(Account.sObjectType).count().mockId('mockingQuery').toInteger(); - - // Verify - Assert.areEqual(2, result, 'The mocked count should be returned.'); - } - - @IsTest - static void mockingCountWithMockableInterface() { - // Test - SOQL.mock('mockingQuery').thenReturn(2); - Integer result = SOQL.of(Account.sObjectType).count().mockId('mockingQuery').toInteger(); - - // Verify - Assert.areEqual(2, result, 'The mocked count should be returned.'); - } - - @IsTest - static void mockWithManyPlainFields() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); - - for (Account mockedResult : result) { - Assert.isTrue(mockedResult.isSet(Account.Name), 'Only Account Name should be set.'); - Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); - Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); - } - } - - @IsTest - static void mockWithManyPlainFieldsWithId() { - // Setup - List accounts = new List{ - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Id, Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); - - for (Account mockedResult : result) { - Assert.isTrue(mockedResult.isSet(Account.Id), 'Only Account Id should be set.'); - Assert.isTrue(mockedResult.isSet(Account.Name), 'Only Account Name should be set.'); - Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); - Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); - } - } - - @IsTest - static void mockWithManyPlainFieldsAndSelectNotMockedField() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Industry).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); - - for (Account mockedResult : result) { - Assert.isTrue(mockedResult.isSet(Account.Industry), 'Only Account Industry should be set.'); - Assert.isNull(mockedResult.Industry, 'The Account Industry should be null because it was not specify in mocked records.'); - Assert.isNull(mockedResult.Name, 'The Account Name should not be set because it was not included in the SELECT clause.'); - Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); - Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); - } - } - - @IsTest - static void mockWithManyFieldAndSelectToLabel() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Name).toLabel(Account.Industry).mockId('mockingQuery').toList(); - - // Verify - for (Account mockedResult : result) { - Assert.isTrue(mockedResult.isSet(Account.Name), 'When toLabel presented all mocked fields should be returned as they are.'); - Assert.isTrue(mockedResult.isSet(Account.Description), 'When toLabel presented all mocked fields should be returned as they are.'); - Assert.isTrue(mockedResult.isSet(Account.Website), 'When toLabel presented all mocked fields should be returned as they are.'); - Assert.isFalse(mockedResult.isSet(Account.Industry), 'When toLabel presented all mocked fields should be returned as they are.'); - } - } - - @IsTest - static void mockWithManyFieldAndAliasing() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - QueryException soqlException = null; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - - try { - SOQL.of(Account.SObjectType).with(Account.Name).with(Account.Industry, 'accIndustry').mockId('mockingQuery').toList(); - } catch (QueryException e) { - soqlException = e; - } - - // Verify - Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.'); - Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supported'); - } - - @IsTest - static void mockWithManyFieldAndAggregateFunction() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - QueryException soqlException = null; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - - try { - SOQL.of(Account.SObjectType).count(Account.Industry).mockId('mockingQuery').toAggregated(); - } catch (QueryException e) { - soqlException = e; - } - - // Verify - Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.'); - Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supported'); - } - - @IsTest - static void mockWithManyFieldAndParentRelationship() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev', Parent = new Account(Name = 'Parent 1')), - new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev', Parent = new Account(Name = 'Parent 2')) - }; - - QueryException soqlException = null; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - List result = SOQL.of(Account.SObjectType).with(Account.Name, Account.Industry).with('Parent', Account.Name).mockId('mockingQuery').toList(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); - - for (Account mockedResult : result) { - Assert.isNotNull(mockedResult.Name, 'Account Name should be set.'); - Assert.isNotNull(mockedResult.Description, 'The Account Description should be set.'); - Assert.isNotNull(mockedResult.Website, 'The Account Website should be set.'); - Assert.isNotNull(mockedResult.Parent.Name, 'The Parent Account should be set.'); - Assert.isFalse(mockedResult.isSet(Account.Industry), 'Account Industry should be not set, because it\'s not mocked.'); - } - } - - @IsTest - static void mockWithAggregateResultProxy() { - // Setup - Map aggregateResult = new Map{ 'LeadSource' => 'Web', 'total' => 10}; - - // Test - SOQL.mock('mockingQuery').thenReturn(aggregateResult); - - List results = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - // Verify - Assert.areEqual(1, results.size(), 'The size of the aggregate results should match the mocked size.'); - - SOQL.AggregateResultProxy result = results[0]; - - Assert.isNotNull(result.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.'); - Assert.areEqual(10, result.get('total'), 'AggregateResult should contain the total field.'); - Assert.areEqual('Web', result.get('LeadSource'), 'AggregateResult should contain the LeadSource field.'); - } - - @IsTest - static void mockWithAggregateResultsProxy() { - // Setup - List> aggregateResults = new List>{ - new Map{ 'LeadSource' => 'Web', 'total' => 10}, - new Map{ 'LeadSource' => 'Phone', 'total' => 5}, - new Map{ 'LeadSource' => 'Email', 'total' => 3} - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(aggregateResults); - - List result = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - // Verify - Assert.areEqual(3, result.size(), 'The size of the aggregate results should match the mocked size.'); - - for (SOQL.AggregateResultProxy aggregateResult : result) { - Assert.isNotNull(aggregateResult.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.'); - Assert.isNotNull(aggregateResult.get('total'), 'AggregateResult should contain the total field.'); - Assert.isNotNull(aggregateResult.get('LeadSource'), 'AggregateResult should contain the LeadSource field.'); - } - } - - @IsTest - static void mockSubQuery() { - // Setup - List mocks = (List) JSON.deserialize( - JSON.serialize( - new List>{ - new Map{ - 'Name' => 'Account Name', - 'Industry' => 'IT', - 'Contacts' => new Map{ - 'totalSize' => 2, - 'done' => true, - 'records' => new List>{ - new Map{ 'Name' => 'Contact Name', 'Email' => 'contact.email@address.com', 'Phone' => '0987654321' }, - new Map{ 'Name' => 'Contact Name 2', 'Email' => 'contact2.email@address.com', 'Phone' => '1234567890' } - } - } - } - } - ), - List.class - ); - - SOQL.mock('mockingQuery').thenReturn(mocks); - - // Test - List result = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.Industry) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.Phone) - ) - .mockId('mockingQuery') - .toList(); - - // Verify - Assert.areEqual(1, result.size(), 'The size of the mocked accounts should match the expected one.'); - - Account account = result[0]; - - Assert.areEqual('Account Name', account.Name, 'The mocked account name should match the expected one.'); - Assert.areEqual('IT', account.Industry, 'The mocked account industry should match the expected one.'); - Assert.areEqual(2, account.Contacts.size(), 'The size of the mocked contacts should match the expected one.'); - - Contact contact1 = (Contact) account.Contacts[0]; - - Assert.areEqual('Contact Name', contact1.Name, 'The mocked contact name should match the expected one.'); - Assert.areEqual('contact.email@address.com', contact1.Email, 'The mocked contact email should match the expected one.'); - Assert.areEqual('0987654321', contact1.Phone, 'The mocked contact phone should match the expected one.'); - - Contact contact2 = (Contact) account.Contacts[1]; - - Assert.areEqual('Contact Name 2', contact2.Name, 'The mocked contact name should match the expected one.'); - Assert.areEqual('contact2.email@address.com', contact2.Email, 'The mocked contact email should match the expected one.'); - Assert.areEqual('1234567890', contact2.Phone, 'The mocked contact phone should match the expected one.'); - } - - @IsTest - static void mockSubQueryWithStrippingAdditionalFields() { - // Setup - List mocks = (List) JSON.deserialize( - JSON.serialize( - new List>{ - new Map{ - 'Name' => 'Account Name', - 'Industry' => 'IT', - 'Contacts' => new Map{ - 'totalSize' => 2, - 'done' => true, - 'records' => new List>{ - new Map{ 'Name' => 'Contact Name', 'Email' => 'contact.email@address.com', 'Phone' => '0987654321' }, - new Map{ 'Name' => 'Contact Name 2', 'Email' => 'contact2.email@address.com', 'Phone' => '1234567890' } - } - }, - 'Opportunities' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List>{ - new Map{ 'Name' => 'Opportunity Name', 'Amount' => 100000 } - } - } - } - } - ), - List.class - ); - - SOQL.mock('mockingQuery').thenReturn(mocks); - - // Test - List result = SOQL.of(Account.SObjectType) - .with(Account.Name, Account.Industry) - .with(SOQL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.Phone) - ) - .mockId('mockingQuery') - .toList(); - - // Verify - Account account = result[0]; - - Assert.areEqual(1, result.size(), 'The size of the mocked accounts should match the expected one.'); - Assert.areEqual(2, account.Contacts.size(), 'The size of the mocked opportunities should match the expected one.'); - Assert.areEqual(0, account.Opportunities.size(), 'The size of the mocked opportunities should be 0, because it was not requested.'); - } - - @IsTest - static void sObjectsMockStack() { - // Setup - SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); - SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 2')); - SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 3')); - - // Test - SOQL.Queryable query = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery'); - - Account acc1 = (Account) query.toObject(); - Account acc2 = (Account) query.toObject(); - Account acc3 = (Account) query.toObject(); - Account acc4 = (Account) query.toObject(); - - // Verify - Assert.areEqual('Test 1', acc1.Name, 'The returned account name should match the expected one.'); - Assert.areEqual('Test 2', acc2.Name, 'The returned account name should match the expected one.'); - Assert.areEqual('Test 3', acc3.Name, 'The returned account name should match the expected one.'); - Assert.areEqual('Test 3', acc4.Name, 'The returned account name should match the expected one.'); - } - - @IsTest - static void countMockStack() { - // Setup - SOQL.mock('mockingQuery').thenReturn(1); - SOQL.mock('mockingQuery').thenReturn(2); - SOQL.mock('mockingQuery').thenReturn(3); - - // Test - Integer result1 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); - Integer result2 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); - Integer result3 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); - Integer result4 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); - - // Verify - Assert.areEqual(1, result1, 'The returned count should match the expected one.'); - Assert.areEqual(2, result2, 'The returned count should match the expected one.'); - Assert.areEqual(3, result3, 'The returned count should match the expected one.'); - Assert.areEqual(3, result4, 'The returned count should match the expected one.'); - } - - @IsTest - static void aggregatedMockStack() { - // Setup - SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Web', 'total' => 10}); - SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Phone', 'total' => 5}); - SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Email', 'total' => 3}); - - // Test - List result1 = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - List result2 = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - List result3 = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - List result4 = SOQL.of(Lead.SObjectType) - .with(Lead.LeadSource) - .COUNT(Lead.Id, 'total') - .groupBy(Lead.LeadSource) - .mockId('mockingQuery') - .toAggregatedProxy(); - - // Verify - Assert.areEqual(1, result1.size(), 'The size of the aggregate results should match the mocked size.'); - Assert.areEqual(10, result1[0].get('total'), 'The total should match the expected one.'); - Assert.areEqual('Web', result1[0].get('LeadSource'), 'The LeadSource should match the expected one.'); - - Assert.areEqual(1, result2.size(), 'The size of the aggregate results should match the mocked size.'); - Assert.areEqual(5, result2[0].get('total'), 'The total should match the expected one.'); - Assert.areEqual('Phone', result2[0].get('LeadSource'), 'The LeadSource should match the expected one.'); - - Assert.areEqual(1, result3.size(), 'The size of the aggregate results should match the mocked size.'); - Assert.areEqual(3, result3[0].get('total'), 'The total should match the expected one.'); - Assert.areEqual('Email', result3[0].get('LeadSource'), 'The LeadSource should match the expected one.'); - - Assert.areEqual(1, result4.size(), 'The size of the aggregate results should match the mocked size.'); - Assert.areEqual(3, result4[0].get('total'), 'The total should match the expected one.'); - Assert.areEqual('Email', result4[0].get('LeadSource'), 'The LeadSource should match the expected one.'); - } - - @IsTest - static void toId() { - // Setup - Account acc = new Account(Name = 'Test 1'); - insert acc; - - // Test - Id accountId = SOQL.of(Account.SObjectType).byId(acc).toId(); - - // Verify - Assert.areEqual(acc.Id, accountId, 'The returned Id should match the expected one.'); - } - - @IsTest - static void toIdWithMocking() { - // Setup - SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); - - // Test - Id accountId = SOQL.of(Account.SObjectType).mockId('mockingQuery').toId(); - - // Verify - Assert.isNotNull(accountId, 'The returned Id should match the expected one.'); - } - - @IsTest - static void toIds() { - // Setup - List accounts = insertAccounts(); - - // Test - Set accountIds = SOQL.of(Account.SObjectType).byIds(accounts).toIds(); - - // Verify - Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void toIdsWithMocking() { - // Setup - SOQL.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - Set accountIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIds(); - - // Verify - Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be equal to the size of the mocked accounts.'); - } - - @IsTest - static void toIdsOf() { - // Setup - List accounts = insertAccountsWithParents(); - - // Test - Set parentIds = SOQL.of(Account.SObjectType).byIds(accounts).toIdsOf(Account.ParentId); - - // Verify - Assert.areEqual(accounts.size(), parentIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void toIdsOfWithMocking() { - // Setup - SOQL.mock('mockingQuery').thenReturn( - (List) JSON.deserialize( - JSON.serialize(new List>{ - new Map{ - 'Id' => SOQL.IdGenerator.get(Account.SObjectType) - }, - new Map{ - 'Id' => SOQL.IdGenerator.get(Account.SObjectType) - } - }), - List.class - ) - ); - - // Test - Set parentIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIdsOf(Account.ParentId); - - // Verify - Assert.areEqual(2, parentIds.size(), 'The size of the returned set should be equal to the size of the mocked accounts.'); - } - - @IsTest - static void toIdsOfRelationshipField() { - // Setup - List accounts = insertAccountsWithParents(); - - // Test - Set createdByIds = SOQL.of(Account.SObjectType).byIds(accounts).toIdsOf('Parent', Account.CreatedById); - - // Verify - Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); - } - - @IsTest - static void toIdsOfRelationshipFieldWithMocking() { - // Setup - SOQL.mock('mockingQuery').thenReturn( - (AggregateResult) JSON.deserialize( - JSON.serialize(new Map{ - 'Id' => SOQL.IdGenerator.get(User.SObjectType) - }), - AggregateResult.class - ) - ); - - // Test - Set createdByIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIdsOf('Parent', Account.CreatedById); - - // Verify - Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); - } - - @IsTest - static void doExist() { - // Setup - Account acc = new Account(Name = 'Test 1'); - insert acc; - - // Test - Boolean isRecordExist = SOQL.of(Account.SObjectType).byId(acc).doExist(); - - // Verify - Assert.isTrue(isRecordExist, 'The record with given Id should exist.'); - } - - @IsTest - static void multipleToStringExecutions() { - // Setup - Exception soqlException = null; - - SOQL.Queryable builder = SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.Industry).equal('IT')) - ); - - // Test - try { - builder.preview(); - builder.toString(); - builder.toString(); - builder.toList(); - } catch (Exception e) { - soqlException = e; - } - - // Verify - Assert.isNull(soqlException, 'The SOQL should be generated without any exceptions.'); - - Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND Industry = :v2)', builder.toString(), 'The generated SOQL should match the expected one.'); - - Map binding = builder.binding(); - Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); - Assert.areEqual('IT', binding.get('v2'), 'The binding variable should match the expected value.'); - } - - @IsTest - static void toValueOf() { - // Setup - String accountName = 'Test 1'; - - Account acc = new Account(Name = accountName); - insert acc; - - // Test - String resultAccName = (String) SOQL.of(Account.SObjectType).byId(acc).toValueOf(Account.Name); - - // Verify - Assert.areEqual(accountName, resultAccName, 'The returned account name should match the expected one.'); - } - - @IsTest - static void mockedToValueOf() { - // Setup - String accountName = 'Test 1'; - - // Test - SOQL.mock('mockingQuery').thenReturn(new Account(Name = accountName)); - String resultAccName = (String) SOQL.of(Account.SObjectType).mockId('mockingQuery').toValueOf(Account.Name); - - // Verify - Assert.areEqual(accountName, resultAccName, 'The returned account name should match the expected one.'); - } - - @IsTest - static void toValuesOf() { - // Setup - List accounts = insertAccounts(); - - // Test - Set accountNames = SOQL.of(Account.SObjectType).byIds(accounts).toValuesOf(Account.Name); - - // Verify - Assert.areEqual(2, accountNames.size(), 'The size of the account names set should be equal to the size of the accounts.'); - } - - @IsTest - static void toValuesOfWhenNoValues() { - // Setup - insertAccounts(); // Industry is empty - - // Test - Set accountsIndustries = SOQL.of(Account.SObjectType).toValuesOf(Account.Industry); - - // Verify - Assert.areEqual(0, accountsIndustries.size(), 'The size of the account industries set should be 0, because field is empty.'); - } - - @IsTest - static void toValuesOfWithDefaultFields() { - // Setup - List accounts = insertAccounts(); - - // Test - Set accountNames = SOQL.of(Account.SObjectType) - .with(Account.Industry) - .byIds(accounts) - .toValuesOf(Account.Name); - - // Verify - Assert.areEqual(2, accountNames.size(), 'The size of the account names set should be equal to the size of the accounts.'); - } - - @IsTest - static void toValueOfWhenRecordNotExist() { - // Test - String accountIndustry = (String) SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.name().equal('Test')) - .toValueOf(Account.Industry); - - // Verify - Assert.isNull(accountIndustry, 'The account industry should be null, because the record does not exist.'); - } - - @IsTest - static void toValuesOfRelationshipField() { - // Setup - List accounts = insertAccountsWithParents(); - - // Test - Set accountNames = SOQL.of(Account.SObjectType) - .byIds(accounts) - .toValuesOf('Parent', Account.Name); - - // Verify - Assert.areEqual(2, accountNames.size(), 'Each account should have a parent, so the size of the account names set should be equal to the size of the accounts.'); - } - - @IsTest - static void toValuesOfRelationshipFieldWhenRelatedRecordNotExist() { - // Test - Set accountNames = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.name().equal('Test')) - .toValuesOf('Parent', Account.Name); - - // Verify - Assert.isTrue(accountNames.isEmpty(), 'The account names set should be empty, because the related record does not exist.'); - } - - @IsTest - static void toObject() { - // Setup - Account acc = new Account(Name = 'Test 1'); - insert acc; - - // Test - Account result = (Account) SOQL.of(Account.SObjectType).toObject(); - - // Verify - Assert.areEqual(acc.Id, result.Id, 'The returned account should match the expected one.'); - } - - @IsTest - static void toObjectWithMultipleRows() { - // Setup - insertAccounts(); - Exception queryException = null; - - // Test - try { - Account result = (Account) SOQL.of(Account.SObjectType).toObject(); - } catch (Exception e) { - queryException = e; // List has more than 1 row for assignment to SObject - } - - // Verify - Assert.isNotNull(queryException, 'The exception should be thrown, because there are more than 1 row for assignment to SObject.'); - } - - @IsTest - static void toObjectWithoutRows() { - // Test - // When List has no rows for assignment to SObject null will be returned - Account account = (Account) SOQL.of(Account.SObjectType).toObject(); - - // Verify - Assert.isNull(account, 'The account should be null, because there are no rows for assignment to SObject.'); - } - - @IsTest - static void toList() { - // Setup - List accounts = insertAccounts(); - - // Test - List result = SOQL.of(Account.SObjectType).toList(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'The size of the result list should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void toAggregated() { - // Setup - List accounts = insertAccounts(); - - // Test - List result = SOQL.of(Account.SObjectType).count(Account.Name, 'names').toAggregated(); - - // Verify - Assert.areEqual(accounts.size(), result[0].get('names'), 'The count of the names should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void toIntegerWithoutSpecifiedCount() { - // Setup - List accounts = insertAccounts(); - - // Test - Integer accountsCount = SOQL.of(Account.SObjectType).toInteger(); - - // Verify - Assert.areEqual(accounts.size(), accountsCount, 'The COUNT() clause should be automatically added when the count() method is not specified. The count of the accounts should match the number of inserted accounts.'); - } - - @IsTest - static void toInteger() { - // Setup - List accounts = insertAccounts(); - - // Test - Integer result = SOQL.of(Account.SObjectType).count().toInteger(); - - // Verify - Assert.areEqual(accounts.size(), result, 'The count of the accounts should match the number of inserted accounts.'); - } - - @IsTest - static void toMap() { - // Setup - List accounts = insertAccounts(); - - // Test - Map result = (Map) SOQL.of(Account.SObjectType).toMap(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isNotNull(result.get(acc.Id), 'The result map should have a value for each inserted account Id.'); - } - } - - @IsTest - static void mockedToMapWithoutMockedId() { - // Setup - List accounts = new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - Map result = (Map) SOQL.of(Account.SObjectType).mockId('mockingQuery').toMap(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); - Assert.areEqual(accounts.size(), result.keySet().size(), 'Size of the result map should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void toMapWithMockingWhenOtherFieldsAreSpecified() { - // Setup - List accounts = new List{ - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') - }; - - // Test - SOQL.mock('mockingQuery').thenReturn(accounts); - Map result = (Map) SOQL.of(Account.SObjectType) - .with(Account.Name, Account.Description) - .mockId('mockingQuery') - .toMap(); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isNotNull(result.get(acc.Id), 'The result map should have a value for each inserted account Id.'); - Assert.isNotNull(result.get(acc.Id).Name, 'The result map should have a value for each inserted account Name.'); - Assert.isNotNull(result.get(acc.Id).Description, 'The result map should have a value for each inserted account Description.'); - Assert.isNull(result.get(acc.Id).Website, 'The result map should not have a value for each inserted account Website.'); - } - } - - @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(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isNotNull(result.get(acc.Name), 'The result map should have a value for each inserted account Name.'); - } - } - - @IsTest - static void toMapWithCustomRelationshipKey() { - // Setup - List accounts = insertAccountsWithParents(); - - // Test - Map result = (Map) SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.ParentId).isNotNull()) - .toMap('Parent', Account.Name); - - // Verify - Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the accounts with parents.'); - Assert.isNotNull(result.get('Test 1 Parent'), 'The result map should have a value for the key: "Test 1 Parent".'); - Assert.isNotNull(result.get('Test 2 Parent'), 'The result map should have a value for the key: "Test 2 Parent".'); - } - - @IsTest - static void toMapWithEmptyCustomRelationshipKey() { - // Setup - List accounts = insertAccounts(); - - // Test - Map result = (Map) SOQL.of(Account.SObjectType).toMap('Parent', Account.Name); - - // Verify - Assert.areEqual(1, result.size(), 'Size of the result map should be equal to 1'); - Assert.isNotNull(result.get(null), 'The result map should have a value for the key: null'); - } - - @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(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isNotNull(result.get(acc.Name), 'The result map should have a value for each inserted account Name.'); - Assert.areEqual(acc.Id, result.get(acc.Name), 'The result map should have an account Id for each account 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(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isFalse(result.get(acc.Name).isEmpty(), 'The result map should contain a non-empty account list for each account name.'); - } - } - - @IsTest - static void toAggregatedMapWithCustomRelationshipKey() { - // Setup - List accounts = insertAccountsWithParents(); - - // Test - Map> result = SOQL.of(Account.SObjectType) - .whereAre(SOQL.Filter.with(Account.ParentId).isNotNull()) - .toAggregatedMap('Parent.CreatedBy', User.Id); - - // Verify - Assert.areEqual(1, result.size(), 'Size of the result map should be 1, because the same user created all accounts.'); - Assert.areEqual(accounts.size(), result.get(UserInfo.getUserId()).size(), 'The result map should contain all accounts created by the current User.'); - } - - @IsTest - static void toAggregatedMapWithEmptyCustomKey() { - // Setup - insertAccounts(); - - // Test - Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry); - - // Verify - Assert.areEqual(1, result.size(), 'The result map should have only one key.'); // grouped by empty Industry - Assert.isTrue(result.containsKey(null), 'The result map should have a null key for 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(), 'Size of the result map should be equal to the size of the inserted accounts.'); - - for (Account acc : accounts) { - Assert.isFalse(result.get(acc.Name).isEmpty(), 'The result map should contain a non-empty list of account Ids for each account name.'); - } - } - - @IsTest - static void toAggregatedMapWithEmptyCustomKeyAndCustomValue() { - // Setup - List accounts = insertAccounts(); - - // Test - Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Industry, Account.Id); - - // Verify - Assert.areEqual(1, result.size(), 'The result map should have only one key.'); // grouped by empty Industry - Assert.isTrue(result.containsKey(null), 'The result map should have a null key for empty Industry.'); - } - - @IsTest - static void toQueryLocator() { - // Test - Database.QueryLocator queryLocator = SOQL.of(Account.SObjectType) - .with(Account.Id) - .with(Account.Name) - .toQueryLocator(); - - // Verify - Assert.areEqual('SELECT Id, Name FROM Account', queryLocator.getQuery(), 'The generated SOQL should match the expected one.'); - } - - @IsTest - static void toQueryLocatorWithSharing() { - // Setup - insertTasks(); - - System.runAs(minimumAccessUser()) { - // Test - Database.QueryLocator queryLocator = SOQL.of(Task.SObjectType).systemMode().withSharing().toQueryLocator(); - - // Verify - Assert.isFalse(queryLocator.iterator().hasNext(), 'The user should not have access to the tasks, so hasNext should return false.'); - } - } - - @IsTest - static void toQueryLocatorWithoutSharing() { - // Setup - insertTasks(); - - System.runAs(minimumAccessUser()) { - // Test - Database.QueryLocator queryLocator = SOQL.of(Task.SObjectType).systemMode().withoutSharing().toQueryLocator(); - - // Verify - Assert.isTrue(queryLocator.iterator().hasNext(), 'The user should have access to the tasks, so hasNext should return true.'); - } - } - - @IsTest - static void querySynchronousCloseToLimitWithMocking() { - // Setup - Exception queryException = null; - - SOQL.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - try { - Test.startTest(); - for (Integer i = 0 ; i < 100 ; i++) { - SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); - } - Test.stopTest(); - } catch (QueryException e) { - queryException = e; - } - - // Verify - Assert.isNull(queryException, 'The synchronous query should not have exceeded the limit.'); - } - - @IsTest - static void querySynchronousLimitExceptionWithMocking() { - // Setup - Exception queryException = null; - - SOQL.mock('mockingQuery').thenReturn(new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }); - - // Test - try { - Test.startTest(); - for (Integer i = 0 ; i < 101 ; i++) { - SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); - } - Test.stopTest(); - } catch (QueryException e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'The synchronous query should have exceeded the limit.'); - Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should not have been exceeded.'); - } - - @IsTest - static void querySynchronousLimitExceptionWithoutMocking() { - // Setup - Exception queryException = null; - - // Test - try { - for (Integer i = 0 ; i < 102 ; i++) { - SOQL.of(Account.SObjectType).toList(); - } - } catch (QueryException e) { - queryException = e; - } - - // Verify - Assert.isNotNull(queryException, 'The synchronous query should have exceeded the limit.'); - Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should have been exceeded.'); - } - - @IsTest - static void queryConverterToIdsOf() { - // Setup - List accounts = new List{ - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), ParentId = SOQL.IdGenerator.get(Account.SObjectType)), - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), ParentId = SOQL.IdGenerator.get(Account.SObjectType)) - }; - - // Test - Set accountIds = new SOQL.Converter('Account').transform(accounts).toIdsOf(Account.ParentId); - - // Verify - Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void queryConverterToIdsOfRelationshipField() { - // Setup - List accounts = new List{ - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Parent = new Account(Id = SOQL.IdGenerator.get(Account.SObjectType))), - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Parent = new Account(Id = SOQL.IdGenerator.get(Account.SObjectType))) - }; - - // Test - Set accountIds = new SOQL.Converter('Account').transform(accounts).toIdsOf('Parent', Account.Id); - - // Verify - Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); - } - - @IsTest - static void queryConverterToValuesOf() { - // Setup - List accounts = new List{ - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1'), - new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2') - }; - - // Test - Set accountNames = new SOQL.Converter('Account').transform(accounts).toValuesOf(Account.Name); - - // Verify - Assert.areEqual(accounts.size(), accountNames.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); - Assert.isTrue(accountNames.contains('Test 1'), 'The returned set should contain the account name "Test 1".'); - Assert.isTrue(accountNames.contains('Test 2'), 'The returned set should contain the account name "Test 2".'); - } - - @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') - @IsTest - static void preview() { - // Test - SOQL.of(Account.SObjectType).preview().toList(); - } - - @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') - @IsTest - static void previewWithConditions() { - // Test - SOQL.of(Account.SObjectType) - .whereAre(SOQL.FilterGroup - .add(SOQL.Filter.with(Account.Name).equal('Test')) - .add(SOQL.Filter.with(Account.Industry).equal('IT')) - ) - .preview() - .toList(); - } - - @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') - @IsTest - static void previewCount() { - // Test - SOQL.of(Account.SObjectType).count().preview().toInteger(); - } - - static List insertAccounts() { - List accounts = new List{ - new Account(Name = 'Test 1'), - new Account(Name = 'Test 2') - }; - insert accounts; - return accounts; - } - - static List insertAccountsWithParents() { - List parentAccounts = new List{ - new Account(Name = 'Test 1 Parent'), - new Account(Name = 'Test 2 Parent') - }; - insert parentAccounts; - - List accounts = new List{ - new Account(Name = 'Test 1', ParentId = parentAccounts[0].Id), - new Account(Name = 'Test 2', ParentId = parentAccounts[1].Id) - }; - insert accounts; - - return accounts; - } - - static List insertCases() { - List cases = new List{ - new Case(Status = 'New', Origin = 'Web'), - new Case(Status = 'New', Origin = 'Web') - }; - insert cases; - return cases; - } - - static void insertTasks() { - insert new List{ - new Task(Subject = 'Test', Type = 'Other'), - new Task(Subject = 'Test', Type = 'Other') - }; - } - - static User minimumAccessUser() { - return new User( - Alias = 'newUser', - Email = 'newuser@testorg.com', - EmailEncodingKey = 'UTF-8', - LastName = 'Testing', - LanguageLocaleKey = 'en_US', - LocaleSidKey = 'en_US', - Profile = new Profile(Name = 'Minimum Access - Salesforce'), - TimeZoneSidKey = 'America/Los_Angeles', - UserName = 'queryselector@testorg.com' - ); - } + @IsTest + static void ofSObjectType() { + // Test + String soql = SOQL.of(Account.SObjectType).toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void ofString() { + // Test + String soql = SOQL.of('Account').toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void count() { + // Test + String soql = SOQL.of(Account.SObjectType).count().toString(); + + // Verify + Assert.areEqual('SELECT COUNT() FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countWithDefaultFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .count() + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT() FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countSObjectField() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .count(Opportunity.Id) + .count(Opportunity.CampaignId) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Id), COUNT(CampaignId) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countSObjectFieldWithDefaultFields() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .with(Opportunity.LeadSource) + .count(Opportunity.Id) + .count(Opportunity.CampaignId) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Id), COUNT(CampaignId) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countWithAlias() { + // Test + String soql = SOQL.of(Account.SObjectType).count(Account.Name, 'names').toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) names FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countRelated() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .count('Account', Account.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Account.Name) FROM Contact', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countRelatedWithAlias() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .count('Account', Account.Name, 'names') + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Account.Name) names FROM Contact', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void avg() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .with(Opportunity.CampaignId) + .avg(Opportunity.Amount) + .groupBy(Opportunity.CampaignId) + .toString(); + + // Verify + Assert.areEqual('SELECT CampaignId, AVG(Amount) FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void avgWithAlias() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .with(Opportunity.CampaignId) + .avg(Opportunity.Amount, 'amount') + .groupBy(Opportunity.CampaignId) + .toString(); + + // Verify + Assert.areEqual('SELECT CampaignId, AVG(Amount) amount FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void avgRelated() { + // Test + String soql = SOQL.of(OpportunityLineItem.SObjectType) + .avg('Opportunity', Opportunity.Amount) + .toString(); + + // Verify + Assert.areEqual('SELECT AVG(Opportunity.Amount) FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void avgRelatedWithAlias() { + // Test + String soql = SOQL.of(OpportunityLineItem.SObjectType) + .avg('Opportunity', Opportunity.Amount, 'amount') + .toString(); + + // Verify + Assert.areEqual('SELECT AVG(Opportunity.Amount) amount FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countDistinct() { + // Test + String soql = SOQL.of(Lead.SObjectType).countDistinct(Lead.Company).toString(); + + // Verify + Assert.areEqual('SELECT COUNT_DISTINCT(Company) FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countDistinctWithAlias() { + // Test + String soql = SOQL.of(CampaignMember.SObjectType) + .countDistinct('Lead', Lead.Company) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT_DISTINCT(Lead.Company) FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countDistinctRelated() { + // Test + String soql = SOQL.of(CampaignMember.SObjectType) + .countDistinct('Lead', Lead.Company, 'company') + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT_DISTINCT(Lead.Company) company FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void countDistinctRelatedWithAlias() { + // Test + String soql = SOQL.of(Lead.SObjectType).countDistinct(Lead.Company, 'company').toString(); + + // Verify + Assert.areEqual('SELECT COUNT_DISTINCT(Company) company FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void min() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .with(Contact.FirstName, Contact.LastName) + .min(Contact.CreatedDate) + .groupBy(Contact.FirstName) + .groupBy(Contact.LastName) + .toString(); + + // Verify + Assert.areEqual( + 'SELECT FirstName, LastName, MIN(CreatedDate) FROM Contact GROUP BY FirstName, LastName', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void minWithAlias() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .with(Contact.FirstName, Contact.LastName) + .min(Contact.CreatedDate, 'createdDate') + .groupBy(Contact.FirstName) + .groupBy(Contact.LastName) + .toString(); + + // Verify + Assert.areEqual( + 'SELECT FirstName, LastName, MIN(CreatedDate) createdDate FROM Contact GROUP BY FirstName, LastName', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void minRelated() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .min('Account', Account.CreatedDate) + .toString(); + + // Verify + Assert.areEqual('SELECT MIN(Account.CreatedDate) FROM Contact', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void minRelatedWithAlias() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .min('Account', Account.CreatedDate, 'createdDate') + .toString(); + + // Verify + Assert.areEqual('SELECT MIN(Account.CreatedDate) createdDate FROM Contact', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void max() { + // Test + String soql = SOQL.of(Campaign.SObjectType) + .with(Campaign.Name) + .max(Campaign.BudgetedCost) + .groupBy(Campaign.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, MAX(BudgetedCost) FROM Campaign GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void maxWithAlias() { + // Test + String soql = SOQL.of(Campaign.SObjectType) + .with(Campaign.Name) + .max(Campaign.BudgetedCost, 'budgetedCost') + .groupBy(Campaign.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, MAX(BudgetedCost) budgetedCost FROM Campaign GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void maxRelated() { + // Test + String soql = SOQL.of(CampaignMember.SObjectType) + .max('Campaign', Campaign.BudgetedCost) + .toString(); + + // Verify + Assert.areEqual('SELECT MAX(Campaign.BudgetedCost) FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void maxRelatedAlias() { + // Test + String soql = SOQL.of(CampaignMember.SObjectType) + .max('Campaign', Campaign.BudgetedCost, 'budgetedCost') + .toString(); + + // Verify + Assert.areEqual('SELECT MAX(Campaign.BudgetedCost) budgetedCost FROM CampaignMember', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void sum() { + // Test + String soql = SOQL.of(Opportunity.SObjectType).sum(Opportunity.Amount).toString(); + + // Verify + Assert.areEqual('SELECT SUM(Amount) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void sumWithAlias() { + // Test + String soql = SOQL.of(Opportunity.SObjectType).sum(Opportunity.Amount, 'amount').toString(); + + // Verify + Assert.areEqual('SELECT SUM(Amount) amount FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void sumRelated() { + // Test + String soql = SOQL.of(OpportunityLineItem.SObjectType) + .sum('Opportunity', Opportunity.Amount) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(Opportunity.Amount) FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void sumRelatedWithAlias() { + // Test + String soql = SOQL.of(OpportunityLineItem.SObjectType) + .sum('Opportunity', Opportunity.Amount, 'amount') + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(Opportunity.Amount) amount FROM OpportunityLineItem', soql, 'The generated SOQL should match the expected one.'); + } + + @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, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void toLabelWithSObjectField() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.Company) + .toLabel(Lead.Status) + .toString(); + + // Verify + Assert.areEqual('SELECT Company, toLabel(Status) FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void toLabelWithStringField() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.Company) + .toLabel('RecordType.Name') + .toString(); + + // Verify + Assert.areEqual('SELECT Company, toLabel(RecordType.Name) FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void toLabelWithSObjectFieldAndAlias() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.Company) + .toLabel(Lead.Status, 'leadStatus') + .toString(); + + // Verify + Assert.areEqual('SELECT Company, toLabel(Status) leadStatus FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void toLabelWithStringFieldAndAlias() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.Company) + .toLabel('RecordType.Name', 'recordTypeName') + .toString(); + + // Verify + Assert.areEqual('SELECT Company, toLabel(RecordType.Name) recordTypeName FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withStringDateFunction() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with('CALENDAR_YEAR(CloseDate)') + .groupBy('CALENDAR_YEAR(CloseDate)') + .toString(); + + // Verify + Assert.areEqual('SELECT CALENDAR_YEAR(CloseDate) FROM Lead GROUP BY CALENDAR_YEAR(CloseDate)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void toLabelWithAliasAndDuplicatedFields() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.Company, Lead.Status) + .toLabel(Lead.Status, 'leadStatus') + .toString(); + + // Verify + Assert.areEqual('SELECT Company, Status, toLabel(Status) leadStatus FROM Lead', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void formatWithSObjectField() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .format(Opportunity.Amount) + .toString(); + + // Verify + Assert.areEqual('SELECT FORMAT(Amount) FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void formatWithAlias() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .format(Opportunity.Amount, 'amt') + .toString(); + + // Verify + Assert.areEqual('SELECT FORMAT(Amount) amt FROM Opportunity', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withSObjectField() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(Account.BillingCity) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withTwoSObjectFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withThreeSObjectFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name, Account.BillingCity) + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFourSObjectFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name, Account.BillingCity, Account.AccountNumber) + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, BillingCity, AccountNumber FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFiveSObjectFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name, Account.BillingCity, Account.AccountNumber, Account.AccountSource) + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, BillingCity, AccountNumber, AccountSource FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withListOfSObjectFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(new List{ + Account.Id, + Account.Name, + Account.Industry, + Account.AccountNumber, + Account.AnnualRevenue, + Account.BillingCity + }).toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, Industry, AccountNumber, AnnualRevenue, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withListOfStringFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(new List{ + 'Id', + 'Name', + 'Industry', + 'AccountNumber', + 'AnnualRevenue', + 'BillingCity' + }).toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, Industry, AccountNumber, AnnualRevenue, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withStringFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with('Id, Name, BillingCity') + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withStringAggregationAndGroupingFields() { + // Test + String soql = SOQL.of(Opportunity.SObjectType) + .with('CampaignId campaign, AVG(Amount) amount') + .groupBy(Opportunity.CampaignId) + .toString(); + + // Verify + Assert.areEqual('SELECT CampaignId campaign, AVG(Amount) amount FROM Opportunity GROUP BY CampaignId', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withStringFieldsAndToLabelFunction() { + // Test + String soql = SOQL.of(Case.SObjectType) + .with('Id, toLabel(Status), Subject') + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Subject, toLabel(Status) FROM Case', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFieldAlias() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, 'myAlias') + .groupBy(Account.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Name myAlias FROM Account GROUP BY Name', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withDuplicatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with('Id, Name, BillingCity') + .with(Account.Id, Account.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Id, Name, BillingCity FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withRelatedField() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', User.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withTwoRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', User.Id, User.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withThreeRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', User.Id, User.Name, User.Alias) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFourRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', User.Id, User.Name, User.Alias, User.City) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias, CreatedBy.City FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFiveRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', User.Id, User.Name, User.Alias, User.City, User.CompanyName) + .toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name, CreatedBy.Alias, CreatedBy.City, CreatedBy.CompanyName FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .with('CreatedBy', new List{ + User.Id, User.Name + }).toString(); + + // Verify + Assert.areEqual('SELECT Name, BillingCity, CreatedBy.Id, CreatedBy.Name FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryField() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void withFieldSetException() { + // Setup + Exception queryException = null; + + // Test + try { + SOQL.of(Account.SObjectType) + .withFieldSet('FieldSetName') + .toString(); + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual('FieldSet with name FieldSetName does not exist!', queryException.getMessage(), 'The exception message should match the expected one.'); + } + + @IsTest + static void subQueryTwoFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryThreeFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name, Contact.AccountId) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryFourFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name, Contact.AccountId, Contact.Email) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId, Email FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryFiveFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name, Contact.AccountId, Contact.Email, Contact.Phone) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name, AccountId, Email, Phone FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(new List{ + Contact.Id, Contact.Name + }) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryStringFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with('Id, Name') + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryRelatedFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .with('CreatedBy', new List{ + User.Id, User.Name + }) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name, CreatedBy.Id, CreatedBy.Name FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryTwoLevels() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.LastName) + .with(SOQL.SubQuery.of('Assets').with(Asset.AssetLevel)) + ) + .toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT LastName , (SELECT AssetLevel FROM Assets) FROM Contacts) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryWhereFilterGroup() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Contact.LastName).equal('Doe')) + ) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts WHERE (LastName = :v1)) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryWhereFilter() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .whereAre(SOQL.Filter.with(Contact.LastName).equal('Doe')) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts WHERE LastName = :v1) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryOrderBySObjectField() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .orderBy(Contact.Name) + .sortDesc() + .nullsLast() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryOrderByStringField() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .orderBy('Name') + .sortDesc() + .nullsLast() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryOrderByDynamic() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .orderBy('Name') + .sort('ASC') + .nullsLast() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY Name ASC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryOrderByRelated() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .orderBy('CreatedBy', User.Name) + .sortDesc() + .nullsLast() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts ORDER BY CreatedBy.Name DESC NULLS LAST) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQuerySetLimit() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .setLimit(10) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts LIMIT 10) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryOffset() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .offset(100) + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts OFFSET 100) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryForReference() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .forReference() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts FOR REFERENCE) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryForView() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name) + .forView() + ).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contacts FOR VIEW) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void multipleSubQueriesWithConditions() { + // Setup + String leadSource = 'Web'; + Date fromDate = Date.newInstance(2024, 1, 1); + Date toDate = Date.newInstance(2024, 1, 30); + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .with( + SOQL.SubQuery.of('Contacts') + .with(Contact.Id) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Contact.CreatedDate).lessOrEqual(fromDate)) + .add(SOQL.Filter.with(Contact.CreatedDate).isNull()) + .add(SOQL.Filter.with(Contact.CreatedDate).greaterOrEqual(toDate)) + .conditionLogic('1 AND (2 OR 3)') + ) + ) + .with( + SOQL.SubQuery.of('Opportunities') + .with(Opportunity.Id) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Opportunity.LeadSource).equal(leadSource)) + .add(SOQL.Filter.with(Contact.CreatedDate).equal(fromDate)) + ) + ) + .setLimit(1); + + // Verify + Assert.areEqual( + 'SELECT Id , (SELECT Id FROM Contacts WHERE (CreatedDate <= :v1 AND (CreatedDate = :v2 OR CreatedDate >= :v3))), (SELECT Id FROM Opportunities WHERE (LeadSource = :v4 AND CreatedDate = :v5)) FROM Account LIMIT 1', + builder.toString(), + 'The generated SOQL should match the expected one.' + ); + + Map binding = builder.binding(); + Assert.areEqual(fromDate, binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual(null, binding.get('v2'), 'The binding variable should match the expected value.'); + Assert.areEqual(toDate, binding.get('v3'), 'The binding variable should match the expected value.'); + Assert.areEqual(leadSource, binding.get('v4'), 'The binding variable should match the expected value.'); + Assert.areEqual(fromDate, binding.get('v5'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void delegatedScope() { + // Test + String soql = SOQL.of(Task.SObjectType) + .delegatedScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Task USING SCOPE DELEGATED', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void mineScope() { + // Test + String soql = SOQL.of(Account.SObjectType) + .mineScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account USING SCOPE MINE', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void mineAndMyGroupsScope() { + // Test + String soql = SOQL.of(ProcessInstanceWorkitem.SObjectType) + .mineAndMyGroupsScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM ProcessInstanceWorkitem USING SCOPE MINE_AND_MY_GROUPS', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void myTerritoryScope() { + // Test + String soql = SOQL.of(Account.SObjectType) + .myTerritoryScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account USING SCOPE MY_TERRITORY', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void myTeamTerritoryScope() { + // Test + String soql = SOQL.of(Account.SObjectType) + .myTeamTerritoryScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account USING SCOPE MY_TEAM_TERRITORY', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void teamScope() { + // Test + String soql = SOQL.of(Account.SObjectType) + .teamScope() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account USING SCOPE TEAM', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterId() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.id().isNotNull()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id != :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterRecordType() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.recordType().isNotNull()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE RecordType.DeveloperName != :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterName() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.name().isNotNull()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterWithRelatedField() { + // Test + SOQL.Queryable builder = SOQL.of(Contact.SObjectType) + .whereAre(SOQL.Filter.with('Account', Account.Name).equal('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Contact WHERE Account.Name = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterEqualString() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).equal('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotEqualString() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notEqual('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterLessThan() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).lessThan(10)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees < :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterGreaterThan() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).greaterThan(10)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees > :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterLessOrEqual() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).lessOrEqual(10)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees <= :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterGreaterOrEqual() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.NumberOfEmployees).greaterOrEqual(10)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(10, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterContains() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).contains('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('%Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterContainsNull() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).contains(null)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('%null%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotContains() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notContains('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('%Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterContainsValues() { + // Setup + List names = new List{ 'Acc', 'My' }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).containsSome(names)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterEndsWith() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).endsWith('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('%Test', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotEndsWith() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notEndsWith('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('%Test', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterStartsWith() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).startsWith('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotStartsWith() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notStartsWith('Test')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterCustomContains() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).contains('_', 'Test', '%')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name LIKE :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('_Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterCustomNotContains() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notContains('_', 'Test', '%')); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (NOT Name LIKE :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('_Test%', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIsInSet() { + // Setup + Set names = new Set{ 'Test 1', 'Test 2' }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).isIn(names)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIsInList() { + // Setup + List names = new List{ 'Test 1', 'Test 2' }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).isIn(names)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotInSet() { + // Setup + Set names = new Set{ 'Test 1', 'Test 2' }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).notIn(names)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name NOT IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterNotInList() { + // Setup + List names = new List{ 'Test 1', 'Test 2' }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.BillingCity) + .whereAre(SOQL.Filter.with(Account.Name).notIn(names)); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Name, BillingCity FROM Account WHERE Name NOT IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(names, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIncludesAll() { + // Setup + List ratings = new List{ 'Hot', 'Warm' }; + + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id) + .whereAre(SOQL.Filter.with(Account.Rating).includesAll(ratings)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE Rating INCLUDES (\'Hot;Warm\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterIncludesSome() { + // Setup + List ratings = new List{ 'Hot', 'Warm' }; + + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id) + .whereAre(SOQL.Filter.with(Account.Rating).includesSome(ratings)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE Rating INCLUDES (\'Hot\', \'Warm\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterExcludesAll() { + // Setup + List ratings = new List{ 'Hot', 'Warm' }; + + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id) + .whereAre(SOQL.Filter.with(Account.Rating).excludesAll(ratings)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE Rating EXCLUDES (\'Hot\', \'Warm\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterExcludesSome() { + // Setup + List ratings = new List{ 'Hot', 'Warm' }; + + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Id) + .whereAre(SOQL.Filter.with(Account.Rating).excludesSome(ratings)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE Rating EXCLUDES (\'Hot;Warm\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterIsNull() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).isNull()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIsNotNull() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).isNotNull()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name != :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(null, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIsTrue() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.IsDeleted).isTrue()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE IsDeleted = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.isTrue((Boolean) binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterIsFalse() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.IsDeleted).isFalse()); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE IsDeleted = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.isFalse((Boolean) binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterDateLiteral() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.CreatedDate).greaterThan('LAST_N_QUARTERS:2').asDateLiteral()) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE CreatedDate > LAST_N_QUARTERS:2', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filtersGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + ); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', builder.toString(), 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void emptyFiltersGroup() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicFiltersGroup() { + // Setup + SOQL.FilterGroup filterGroup = SOQL.FilterGroup; + + filterGroup.add(SOQL.Filter.with(Account.Name).equal('Test')); + filterGroup.add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')); + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(filterGroup); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void dynamicFiltersListGroup() { + // Setup + SOQL.FilterGroup filterGroup = SOQL.FilterGroup; + + filterGroup.add(new List { + SOQL.Filter.with(Account.Name).equal('Test'), + SOQL.Filter.with(Account.BillingCity).equal('Krakow') + }); + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(filterGroup); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void dynamicStringFiltersGroup() { + // Setup + SOQL.FilterGroup filterGroup = SOQL.FilterGroup; + + filterGroup.add('Name = \'Test\''); + filterGroup.add('BillingCity = \'Krakow\''); + + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(filterGroup) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' AND BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicStringFiltersGroupWithAnyConditionMatching() { + // Setup + SOQL.FilterGroup filterGroup = SOQL.FilterGroup; + + filterGroup.add('Name = \'Test\''); + filterGroup.add('BillingCity = \'Krakow\''); + filterGroup.anyConditionMatching(); + + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(filterGroup) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' OR BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicStringFiltersListGroup() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(new List { + 'Name = \'Test\'', + 'BillingCity = \'Krakow\'' + }) + ).toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' AND BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicStringFiltersListGroupWithAnyConditionMatching() { + // Setup + SOQL.FilterGroup filterGroup = SOQL.FilterGroup; + + filterGroup.add(new List { + 'Name = \'Test\'', + 'BillingCity = \'Krakow\'' + }); + + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(new List { + 'Name = \'Test\'', + 'BillingCity = \'Krakow\'' + }).anyConditionMatching() + ).toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (Name = \'Test\' OR BillingCity = \'Krakow\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicFiltersGroupOnSoqlInstance() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Industry).equal('IT')); + + Assert.areEqual('SELECT Id FROM Account WHERE Industry = :v1', builder.toString(), 'The generated SOQL should match the expected one.'); + + builder.whereAre( + SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + ); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE Industry = :v1 AND (Name = :v2 AND BillingCity = :v3)', builder.toString(), 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('IT', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Test', binding.get('v2'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v3'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void multipleFilterGroups() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('1'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('2'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('3'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('4'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('5'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('6'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('7'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('8'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('9'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('10'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('11'))); + + // Verify + String soql = builder.toString(); + Assert.areEqual( + 'SELECT Id FROM Account WHERE (Name LIKE :v1) AND (Name LIKE :v2) AND (Name LIKE :v3) AND (Name LIKE :v4) AND (Name LIKE :v5) AND (Name LIKE :v6) AND (Name LIKE :v7) AND (Name LIKE :v8) AND (Name LIKE :v9) AND (Name LIKE :v10) AND (Name LIKE :v11)', + soql, + 'The generated SOQL should match the expected one.' + ); + + Map binding = builder.binding(); + Assert.areEqual('%1%', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('%2%', binding.get('v2'), 'The binding variable should match the expected value.'); + Assert.areEqual('%3%', binding.get('v3'), 'The binding variable should match the expected value.'); + Assert.areEqual('%4%', binding.get('v4'), 'The binding variable should match the expected value.'); + Assert.areEqual('%5%', binding.get('v5'), 'The binding variable should match the expected value.'); + Assert.areEqual('%6%', binding.get('v6'), 'The binding variable should match the expected value.'); + Assert.areEqual('%7%', binding.get('v7'), 'The binding variable should match the expected value.'); + Assert.areEqual('%8%', binding.get('v8'), 'The binding variable should match the expected value.'); + Assert.areEqual('%9%', binding.get('v9'), 'The binding variable should match the expected value.'); + Assert.areEqual('%10%', binding.get('v10'), 'The binding variable should match the expected value.'); + Assert.areEqual('%11%', binding.get('v11'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void duplicatedConditionsInConditionOrder() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('1'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('2'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('3'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('4'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('5'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('6'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('7'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('8'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('9'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('10'))) + .whereAre(SOQL.FilterGroup.add(SOQL.Filter.with(Account.Name).contains('11'))) + .conditionLogic('(1 AND 2 AND 3 AND 4 AND 5 AND 5 AND 7 AND 8 AND 9 AND 10 AND 11) OR (11 AND 1 AND 10)'); + + // Verify + String soql = builder.toString(); + Assert.areEqual( + 'SELECT Id FROM Account WHERE ((Name LIKE :v1) AND (Name LIKE :v2) AND (Name LIKE :v3) AND (Name LIKE :v4) AND (Name LIKE :v5) AND (Name LIKE :v5) AND (Name LIKE :v7) AND (Name LIKE :v8) AND (Name LIKE :v9) AND (Name LIKE :v10) AND (Name LIKE :v11)) OR ((Name LIKE :v11) AND (Name LIKE :v1) AND (Name LIKE :v10))', + soql, + 'The generated SOQL should match the expected one.' + ); + + Map binding = builder.binding(); + Assert.areEqual('%1%', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('%2%', binding.get('v2'), 'The binding variable should match the expected value.'); + Assert.areEqual('%3%', binding.get('v3'), 'The binding variable should match the expected value.'); + Assert.areEqual('%4%', binding.get('v4'), 'The binding variable should match the expected value.'); + Assert.areEqual('%5%', binding.get('v5'), 'The binding variable should match the expected value.'); + Assert.areEqual('%6%', binding.get('v6'), 'The binding variable should match the expected value.'); + Assert.areEqual('%7%', binding.get('v7'), 'The binding variable should match the expected value.'); + Assert.areEqual('%8%', binding.get('v8'), 'The binding variable should match the expected value.'); + Assert.areEqual('%9%', binding.get('v9'), 'The binding variable should match the expected value.'); + Assert.areEqual('%10%', binding.get('v10'), 'The binding variable should match the expected value.'); + Assert.areEqual('%11%', binding.get('v11'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void anyConditionMatchingForInnerGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + .anyConditionMatching() + ); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 OR BillingCity = :v2)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void anyConditionMatchingForMainGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).equal('Test')) + .whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + .anyConditionMatching(); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void conditionLogicForMainGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Name).equal('Test')) + .whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + .conditionLogic('1 OR 2'); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void nestedFiltersGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + .add(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test 2')) + .add(SOQL.Filter.with(Account.BillingCity).equal('Warsaw')) + .conditionLogic('1 OR 2') + ) + ); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND BillingCity = :v2 AND (Name = :v3 OR BillingCity = :v4))', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('Krakow', binding.get('v2'), 'The binding variable should match the expected value.'); + Assert.areEqual('Test 2', binding.get('v3'), 'The binding variable should match the expected value.'); + Assert.areEqual('Warsaw', binding.get('v4'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void innerJoinWithFilterGroup() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Id).isIn( + SOQL.InnerJoin.of(Contact.SObjectType) + .with(Contact.AccountId) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Contact.Name).equal('My Contact')) + ) + )); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE (Name = :v1))', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void inInnerJoin() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Id).isIn( + SOQL.InnerJoin.of(Contact.SObjectType) + .with(Contact.AccountId) + .whereAre(SOQL.Filter.with(Contact.Name).equal('My Contact')) + )); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE Name = :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void isNotInInnerJoin() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.Id).notIn( + SOQL.InnerJoin.of(Contact.SObjectType) + .with(Contact.AccountId) + .whereAre(SOQL.Filter.name().equal('My Contact')) + )); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Contact WHERE Name = :v1)', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('My Contact', binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void evaluateStringConditions() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= 10 AND NumberOfEmployees <= 20', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void evaluateEmptyConditions() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre('') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void evaluateStringConditionsAndGroup() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') + .whereAre(SOQL.Filter.name().equal('My Contact')) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE NumberOfEmployees >= 10 AND NumberOfEmployees <= 20 AND Name = :v1', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void evaluateStringAndFilterInFilterGroup() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre( + SOQL.FilterGroup + .add('NumberOfEmployees >= 10 AND NumberOfEmployees <= 20') + .add(SOQL.Filter.name().equal('My Contact')) + ) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (NumberOfEmployees >= 10 AND NumberOfEmployees <= 20 AND Name = :v1)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterIgnoreWhen() { + // Setup + String accountName = ''; + + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.BillingCity).equal('Krakow')) + .add(SOQL.Filter.name().contains(accountName).ignoreWhen(String.isEmpty(accountName))) + ) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE (BillingCity = :v1)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterGroupIgnoreWhen() { + // Setup + Boolean isPartnerUser = false; + + // Test + String soql = 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('MyAccount')) + ) + ) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE ((Industry = :v1 AND Name LIKE :v2))', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupBy() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .groupBy(Lead.LeadSource) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByString() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .groupBy('LeadSource') + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByRelated() { + // Test + String soql = SOQL.of(OpportunityLineItem.SObjectType) + .count(OpportunityLineItem.Name, 'count') + .groupBy('OpportunityLineItem.Opportunity.Account', Account.Id) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) count FROM OpportunityLineItem GROUP BY OpportunityLineItem.Opportunity.Account.Id', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByRelatedToAggregated() { + // Setup + insertAccounts(); + + // Test + List results = SOQL.of(Account.SObjectType) + .count(Account.Name, 'names') + .groupBy('Account.CreatedBy', User.Id) + .toAggregated(); + + // Verify + Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.'); + } + + @IsTest + static void groupByRelatedToAggregatedProxy() { + // Setup + insertAccounts(); + + // Test + List results = SOQL.of(Account.SObjectType) + .count(Account.Name, 'names') + .groupBy('Account.CreatedBy', User.Id) + .toAggregatedProxy(); + + // Verify + Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.'); + } + + @IsTest + static void groupByRollup() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name, 'cnt') + .with(Lead.LeadSource) + .groupByRollup(Lead.LeadSource) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(LeadSource)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByRollupManyFields() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name, 'cnt') + .with(Lead.Status, Lead.LeadSource) + .groupByRollup(Lead.Status) + .groupByRollup(Lead.LeadSource) + .toString(); + + // Verify + Assert.areEqual('SELECT Status, LeadSource, COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(Status, LeadSource)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByRollupRelated() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name, 'cnt') + .groupByRollup('ConvertedOpportunity', Opportunity.StageName) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) cnt FROM Lead GROUP BY ROLLUP(ConvertedOpportunity.StageName)', soql, 'The generated SOQL should match the expected one.'); + } + + @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, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByCubeManyFields() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Type, Account.BillingCountry) + .groupByCube(Account.Type) + .groupByCube(Account.BillingCountry) + .toString(); + + // Verify + Assert.areEqual('SELECT Type, BillingCountry FROM Account GROUP BY CUBE(Type, BillingCountry)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByCubeRelated() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name, 'cnt') + .groupByCube('ConvertedOpportunity', Opportunity.StageName) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) cnt FROM Lead GROUP BY CUBE(ConvertedOpportunity.StageName)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void differentGroupByFunctionsException() { + // Setup + Exception queryException = null; + + // Test + try { + SOQL.of(Account.SObjectType) + .with(Account.Type) + .groupBy(Account.Type) + .groupByCube(Account.Type) + .toString(); + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual( + 'You can\'t use GROUP BY, GROUP BY ROLLUP and GROUP BY CUBE in the same query.', + queryException.getMessage(), + 'The exception message should match the expected one.' + ); + } + + @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, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void groupByWithDefaultFieldsAndAggregateFunction() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.FirstName, Lead.LastName, Lead.Email) + .count(Lead.Name) + .with(Lead.LeadSource) + .groupBy(Lead.LeadSource) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterWithSObjectField() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterWithStringField() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with('City').startsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterCount() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT(Name) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterAvg() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .avg(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.avg(Lead.Name).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, AVG(Name) FROM Lead GROUP BY LeadSource HAVING AVG(Name) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterCountDistinct() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .countDistinct(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.countDistinct(Lead.Name).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT_DISTINCT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT_DISTINCT(Name) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterMin() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .min(Lead.NumberOfEmployees) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.min(Lead.NumberOfEmployees).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, MIN(NumberOfEmployees) FROM Lead GROUP BY LeadSource HAVING MIN(NumberOfEmployees) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterMax() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .max(Lead.NumberOfEmployees) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.max(Lead.NumberOfEmployees).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, MAX(NumberOfEmployees) FROM Lead GROUP BY LeadSource HAVING MAX(NumberOfEmployees) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterSum() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, SUM(AnnualRevenue) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) > 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsNull() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.with(Lead.LeadSource).isNull()) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource = null', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsNotNull() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.with(Lead.LeadSource).isNotNull()) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource FROM Lead GROUP BY LeadSource HAVING LeadSource != null', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsTrue() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.IsConverted) + .have(SOQL.HavingFilter.with(Lead.IsConverted).isTrue()) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY IsConverted HAVING IsConverted = true', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsFalse() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.IsConverted) + .have(SOQL.HavingFilter.with(Lead.IsConverted).isFalse()) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY IsConverted HAVING IsConverted = false', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterEqualString() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.with(Lead.LeadSource).equal('Web')) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource = \'Web\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterEqualInteger() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).equal(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) = 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterNotEqualString() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.with(Lead.LeadSource).notEqual('Web')) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING LeadSource != \'Web\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterNotEqualInteger() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).notEqual(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) != 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterLessThan() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).lessThan(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) < 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterGreaterThan() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) > 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterLessOrEqual() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).lessOrEqual(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) <= 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterGreaterOrEqual() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterOrEqual(10000)) + .toString(); + + // Verify + Assert.areEqual('SELECT COUNT(Name) FROM Lead GROUP BY LeadSource HAVING SUM(AnnualRevenue) >= 10000', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterContains() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).contains('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'%San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterCustomContains() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).contains('_', 'San', '%')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'_San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterNotContains() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notContains('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'%San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterCustomNotContains() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notContains('_', 'San', '%')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'_San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterStartsWith() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterNotStartsWith() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notStartsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterEndsWith() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).endsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City LIKE \'%San\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterNotEndsWith() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notEndsWith('San')) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING (NOT City LIKE \'%San\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsInSet() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).isIn(new Set{ 'San Francisco', 'Los Angeles' })) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsInList() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).isIn(new List{ 'San Francisco', 'Los Angeles' })) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsNotInSet() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notIn(new Set{ 'San Francisco', 'Los Angeles' })) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City NOT IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterIsNotInList() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .sum(Lead.AnnualRevenue) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.City).notIn(new List{ 'San Francisco', 'Los Angeles' })) + .toString(); + + // Verify + Assert.areEqual('SELECT SUM(AnnualRevenue) FROM Lead GROUP BY City HAVING City NOT IN (\'San Francisco\', \'Los Angeles\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void havingFilterGroup() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + ).toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 AND City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicHavingFilterGroup() { + // Setup + SOQL.HavingFilterGroup havingFilterGroup = SOQL.HavingFilterGroup; + + havingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)); + havingFilterGroup.add(SOQL.HavingFilter.with(Lead.City).startsWith('San')); + + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(havingFilterGroup) + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 AND City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicHavingFilterGroupOnSoqlInstance() { + // Test + SOQL.Queryable builder = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.with(Lead.LeadSource).equal('Web')); + + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING LeadSource = \'Web\'', builder.toString(), 'The generated SOQL should match the expected one.'); + + builder.have( + SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + ); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING LeadSource = \'Web\' AND (COUNT(Name) > 100 AND City LIKE \'San%\')', builder.toString(), 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void multipleHavingFilterGroups() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(1))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(2))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(3))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(4))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(5))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(6))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(7))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(8))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(9))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(10))) + .have(SOQL.HavingFilterGroup.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(11))) + .anyHavingConditionMatching() + .toString(); + + // Verify + Assert.areEqual( + 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 1) OR (COUNT(Name) > 2) OR (COUNT(Name) > 3) OR (COUNT(Name) > 4) OR (COUNT(Name) > 5) OR (COUNT(Name) > 6) OR (COUNT(Name) > 7) OR (COUNT(Name) > 8) OR (COUNT(Name) > 9) OR (COUNT(Name) > 10) OR (COUNT(Name) > 11)', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void anyConditionMatchingForInnerHavingFilterGroups() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .add(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + .anyConditionMatching() + ).toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING (COUNT(Name) > 100 OR City LIKE \'San%\')', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void anyConditionMatchingForMainHavingFilterGroup() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + .anyHavingConditionMatching() + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING COUNT(Name) > 100 OR City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void conditionLogicForMainHavingFilterGroup() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .groupBy(Lead.City) + .have(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .have(SOQL.HavingFilter.with(Lead.City).startsWith('San')) + .havingConditionLogic('1 OR 2') + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource, City HAVING COUNT(Name) > 100 OR City LIKE \'San%\'', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void nestedHavingFilterGroups() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilterGroup + .add(SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100)) + .add(SOQL.HavingFilter.count(Lead.Name).lessThan(200)) + ) + .add(SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(400)) + .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) + ) + ).toString(); + + // Verify + Assert.areEqual( + 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING ((COUNT(Name) > 100 AND COUNT(Name) < 200) AND (COUNT(Name) > 400 AND COUNT(Name) < 500))', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void evaluateStringHavingCondition() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have('COUNT(Name) > 100 AND COUNT(Name) < 200') + .toString(); + + // Verify + Assert.areEqual('SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT(Name) > 100 AND COUNT(Name) < 200', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void evaluateStringHavingConditionAndHavingFilterGroup() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have('(COUNT(Name) > 100 AND COUNT(Name) < 200)') + .have(SOQL.HavingFilterGroup + .add(SOQL.HavingFilter.count(Lead.Name).greaterThan(400)) + .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) + ).toString(); + + // Verify + Assert.areEqual( + 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING (COUNT(Name) > 100 AND COUNT(Name) < 200) AND (COUNT(Name) > 400 AND COUNT(Name) < 500)', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void evaluateStringInHavingFilterGroupAndHavingFilter() { + // Test + String soql = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Name) + .groupBy(Lead.LeadSource) + .have(SOQL.HavingFilterGroup + .add('(COUNT(Name) > 100 AND COUNT(Name) < 200)') + .add(SOQL.HavingFilter.count(Lead.Name).lessThan(500)) + ).toString(); + + // Verify + Assert.areEqual( + 'SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING ((COUNT(Name) > 100 AND COUNT(Name) < 200) AND COUNT(Name) < 500)', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + // WITH DATA CATEGORY + + @IsTest + static void dataCategoryFilterWithStringField() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow('Europe__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAt() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').at('Europe__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c AT Europe__c', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAtMultiple() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').at(new List{ 'Europe__c', 'North_America__c' })) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c AT (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAbove() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').above('Europe__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE Europe__c', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAboveMultiple() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').above(new List{ 'Europe__c', 'North_America__c' })) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterBelow() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').below('Europe__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterBelowMultiple() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').below(new List{ 'Europe__c', 'North_America__c' })) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c BELOW (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAboveOrBelow() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow('Europe__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW Europe__c', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryFilterAboveOrBelowMultiple() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryWithMultipleDataCategoryFilters() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) + .withDataCategory(SOQL.DataCategoryFilter.with('Product__c').at(new List{ 'Product1__c', 'Product2__c' })) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c) AND Product__c AT (Product1__c, Product2__c)', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dataCategoryWithMultipleFilters() { + // Test + String soql = SOQL.of('Knowledge__kav') + .with('Title') + .whereAre(SOQL.Filter.with('PublishStatus').equal('Draft')) + .whereAre(SOQL.Filter.with('Language').equal('en_US')) + .withDataCategory(SOQL.DataCategoryFilter.with('Geography__c').aboveOrBelow(new List{ 'Europe__c', 'North_America__c' })) + .withDataCategory(SOQL.DataCategoryFilter.with('Product__c').at('Product1__c')) + .toString(); + + // Verify + Assert.areEqual('SELECT Title FROM Knowledge__kav WHERE PublishStatus = :v1 AND Language = :v2 WITH DATA CATEGORY Geography__c ABOVE_OR_BELOW (Europe__c, North_America__c) AND Product__c AT Product1__c', soql, 'The generated SOQL should match the expected one.'); + } + + // ORDER BY + + @IsTest + static void orderByString() { + // Test + String soql = SOQL.of(Account.SObjectType) + .orderBy('Industry').sortDesc().nullsLast() + .orderBy('Id') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ORDER BY Industry DESC NULLS LAST, Id ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void orderByDynamic() { + // Test + String soql = SOQL.of(Account.SObjectType) + .orderBy('Industry', 'ASC') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void dynamicNullsOrder() { + // Test + String soql = SOQL.of(Account.SObjectType) + .orderBy('Industry') + .nullsOrder('LAST') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS LAST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void orderByDynamicWithSort() { + // Test + String soql = SOQL.of(Account.SObjectType) + .orderBy('Industry') + .sort('ASC') + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ORDER BY Industry ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void orderBy() { + // Test + String soql = SOQL.of(Account.SObjectType) + .orderBy(Account.Industry).sortDesc().nullsLast() + .orderBy(Account.Id) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ORDER BY Industry DESC NULLS LAST, Id ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void orderByRelated() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .orderBy('Account', Account.Name) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Contact ORDER BY Account.Name ASC NULLS FIRST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void orderByCount() { + // Test + String soql = SOQL.of(Account.SObjectType) + .with(Account.Industry) + .groupBy(Account.Industry) + .orderByCount(Account.Industry).sortDesc().nullsLast() + .toString(); + + // Verify + Assert.areEqual('SELECT Industry FROM Account GROUP BY Industry ORDER BY COUNT(Industry) DESC NULLS LAST', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void setLimit() { + // Test + String soql = SOQL.of(Account.SObjectType) + .setLimit(100) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account LIMIT 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void offset() { + // Test + String soql = SOQL.of(Account.SObjectType) + .offset(100) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account OFFSET 100', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void forReference() { + // Test + String soql = SOQL.of(Account.SObjectType) + .forReference() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account FOR REFERENCE', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void forView() { + // Test + String soql = SOQL.of(Account.SObjectType) + .forView() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account FOR VIEW', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void forUpdate() { + // Test + String soql = SOQL.of(Account.SObjectType) + .forUpdate() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account FOR UPDATE', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void allRows() { + // Test + String soql = SOQL.of(Account.SObjectType) + .allRows() + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account ALL ROWS', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void byId() { + // Setup + Id fakeAccountId = SOQL.IdGenerator.get(Account.SObjectType); + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType).byId(fakeAccountId); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(fakeAccountId, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void byIdSObject() { + // Setup + List cases = insertCases(); + + // Test + SOQL.Queryable builder = SOQL.of(Case.SObjectType).byId(cases[0]); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Case WHERE Id = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(cases[0].Id, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void byIdsSet() { + // Verify + Set accountIds = new Set{ SOQL.IdGenerator.get(Account.SObjectType) }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType).byIds(accountIds); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(accountIds, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void byIdsList() { + // Setup + List accountIds = new List{ SOQL.IdGenerator.get(Account.SObjectType) }; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType).byIds(accountIds); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(accountIds, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void byIdsSObjects() { + // Setup + List cases = insertCases(); + + // Test + SOQL.Queryable builder = SOQL.of(Case.SObjectType).byIds(cases); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Case WHERE Id IN :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(cases, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void byRecordType() { + // Setup + String recordTypeDeveloperName = 'Partner'; + + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType).byRecordType(recordTypeDeveloperName); + + // Verify + String soql = builder.toString(); + Assert.areEqual('SELECT Id FROM Account WHERE RecordType.DeveloperName = :v1', soql, 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual(recordTypeDeveloperName, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void toObjectWithoutSharing() { + // Setup + Case testCase = new Case(Status = 'New', Origin = 'Web'); + insert testCase; + + System.runAs(minimumAccessUser()) { + // Test + Case resultCase = (Case) SOQL.of(Case.SObjectType).systemMode().withoutSharing().toObject(); + + // Verify + Assert.areEqual(testCase.Id, resultCase.Id, 'The case should be returned even if the user does not have access to it.'); + } + } + + @IsTest + static void toListWithoutSharing() { + // Setup + insertCases(); + + System.runAs(minimumAccessUser()) { + // Test + List cases = SOQL.of(Case.SObjectType).systemMode().withoutSharing().toList(); + + // Verify + Assert.areEqual(2, cases.size(), 'Cases should be returned even if the user does not have access to them.'); + } + } + + @IsTest + static void toIntegerWithoutSharing() { + // Setup + insertCases(); + + System.runAs(minimumAccessUser()) { + // Test + Integer casesAmount = SOQL.of(Case.SObjectType).count().systemMode().withoutSharing().toInteger(); + + // Verify + Assert.areEqual(2, casesAmount, 'The amount of two cases should be returned even if the user does not have access to them.'); + } + } + + @IsTest + static void toObjectWithSharing() { + // Setup + insert new Task(Subject = 'Test', Type = 'Other'); + + System.runAs(minimumAccessUser()) { + // Test + Task resultTask = (Task) SOQL.of(Task.SObjectType).systemMode().withSharing().toObject(); + + // Verify + Assert.isNull(resultTask, 'The user should not have access to the task.'); + } + } + + @IsTest + static void toListWithSharing() { + // Setup + insertTasks(); + + System.runAs(minimumAccessUser()) { + // Test + List tasks = SOQL.of(Task.SObjectType).systemMode().withSharing().toList(); + + // Verify + Assert.areEqual(0, tasks.size(), 'The user should not have access to the tasks.'); + } + } + + @IsTest + static void toIntegerWithSharing() { + insertTasks(); + + System.runAs(minimumAccessUser()) { + // Test + Integer tasksAmount = SOQL.of(Task.SObjectType).count().systemMode().withSharing().toInteger(); + + // Verify + Assert.areEqual(0, tasksAmount, 'The user should not have access to the tasks.'); + } + } + + @IsTest + static void userMode() { + // Setup + insert new Task(Subject = 'Test', Type = 'Other'); + + System.runAs(minimumAccessUser()) { + // Test + Exception queryException = null; + + try { + SOQL.of(Task.SObjectType) + .with(Task.Type) + .userMode() + .toObject(); + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.isTrue(queryException.getMessage().contains('No such column \'Type\' on entity \'Task\'.'), 'The user should not have access to the \'Type\' field.'); + } + } + + @IsTest + static void stripInaccessibleToObject() { + // Setup + insert new Task(Subject = 'Test', Type = 'Other'); + + System.runAs(minimumAccessUser()) { + // Test + Task task = (Task) SOQL.of(Task.SObjectType) + .with(Task.Type) + .systemMode() + .stripInaccessible() + .withoutSharing() + .toObject(); + + Exception queryException = null; + + String inaccessibleFieldValue; + + try { + inaccessibleFieldValue = task.Type; + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual( + 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', + queryException.getMessage(), + 'The user should not have access to the \'Type\' field.' + ); + } + } + + @IsTest + static void stripInaccessibleToList() { + // Setup + insertTasks(); + + System.runAs(minimumAccessUser()) { + // Test + List tasks = SOQL.of(Task.SObjectType) + .with(Task.Type) + .systemMode() + .stripInaccessible() + .withoutSharing() + .toList(); + + Exception queryException = null; + + String inaccessibleFieldValue; + + try { + inaccessibleFieldValue = tasks[0].Type; + } catch(Exception e) { + queryException = e; + } + + // Verify + Assert.areEqual( + 'SObject row was retrieved via SOQL without querying the requested field: Task.Type', + queryException.getMessage(), + 'The user should not have access to the \'Type\' field.' + ); + } + } + + @IsTest + static void mockId() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }; + + // Test + SOQL.setMock('mockingQuery', accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(accounts, result, 'The mocked accounts should be returned.'); + } + + @IsTest + static void mockIdSpecifiedButQueryNotMocked() { + // Test + List accounts = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(0, accounts.size(), 'The mocked accounts should be returned.'); + } + + @IsTest + static void mockingSingleRecord() { + // Setup + Account testAccount = new Account(Name = 'Test 1'); + + // Test + SOQL.setMock('mockingQuery', testAccount); + Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual(testAccount, result, 'The mocked account should be returned.'); + } + + @IsTest + static void mockNoResult() { + // Test + SOQL.setMock('mockingQuery', new List()); + Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject(); + + // Verify + Assert.isNull(result, 'The result should be null when no records are mocked.'); + } + + @IsTest + static void mockNoResultWithNull() { + // Test + SOQL.setMock('mockingQuery', (Account) null); + Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject(); + + // Verify + Assert.isNull(result, 'The result should be null when no records are mocked.'); + } + + @IsTest + static void mockNoResults() { + // Test + SOQL.mock('mockingQuery').thenReturn(new List()); + List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.isTrue(result.isEmpty(), 'The result should be empty when no records are mocked.'); + } + + @IsTest + static void mockingMultipleRecords() { + // Setup + List testAccounts = new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }; + + // Test + SOQL.setMock('mockingQuery', testAccounts); + List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(testAccounts, result, 'The mocked accounts should be returned.'); + } + + @IsTest + static void mockingSingleRecordWithMockableInterface() { + // Setup + Account testAccount = new Account(Name = 'Test 1'); + + // Test + SOQL.mock('mockingQuery').thenReturn(testAccount); + Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual(testAccount.Name, result.Name, 'The result account Name should be the same as the mocked account Name.'); + Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.'); + } + + @IsTest + static void mockingCount() { + // Test + SOQL.setCountMock('mockingQuery', 2); + Integer result = SOQL.of(Account.SObjectType).count().mockId('mockingQuery').toInteger(); + + // Verify + Assert.areEqual(2, result, 'The mocked count should be returned.'); + } + + @IsTest + static void mockingCountWithMockableInterface() { + // Test + SOQL.mock('mockingQuery').thenReturn(2); + Integer result = SOQL.of(Account.SObjectType).count().mockId('mockingQuery').toInteger(); + + // Verify + Assert.areEqual(2, result, 'The mocked count should be returned.'); + } + + @IsTest + static void mockWithManyPlainFields() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); + + for (Account mockedResult : result) { + Assert.isTrue(mockedResult.isSet(Account.Name), 'Only Account Name should be set.'); + Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); + Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); + } + } + + @IsTest + static void mockWithManyPlainFieldsWithId() { + // Setup + List accounts = new List{ + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Id, Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); + + for (Account mockedResult : result) { + Assert.isTrue(mockedResult.isSet(Account.Id), 'Only Account Id should be set.'); + Assert.isTrue(mockedResult.isSet(Account.Name), 'Only Account Name should be set.'); + Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); + Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); + } + } + + @IsTest + static void mockWithManyPlainFieldsAndSelectNotMockedField() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Industry).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); + + for (Account mockedResult : result) { + Assert.isTrue(mockedResult.isSet(Account.Industry), 'Only Account Industry should be set.'); + Assert.isNull(mockedResult.Industry, 'The Account Industry should be null because it was not specify in mocked records.'); + Assert.isNull(mockedResult.Name, 'The Account Name should not be set because it was not included in the SELECT clause.'); + Assert.isNull(mockedResult.Description, 'The Account Description should not be set because it was not included in the SELECT clause.'); + Assert.isNull(mockedResult.Website, 'The Account Website should not be set because it was not included in the SELECT clause.'); + } + } + + @IsTest + static void mockWithManyFieldAndSelectToLabel() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Name).toLabel(Account.Industry).mockId('mockingQuery').toList(); + + // Verify + for (Account mockedResult : result) { + Assert.isTrue(mockedResult.isSet(Account.Name), 'When toLabel presented all mocked fields should be returned as they are.'); + Assert.isTrue(mockedResult.isSet(Account.Description), 'When toLabel presented all mocked fields should be returned as they are.'); + Assert.isTrue(mockedResult.isSet(Account.Website), 'When toLabel presented all mocked fields should be returned as they are.'); + Assert.isFalse(mockedResult.isSet(Account.Industry), 'When toLabel presented all mocked fields should be returned as they are.'); + } + } + + @IsTest + static void mockWithManyFieldAndAliasing() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + QueryException soqlException = null; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + + try { + SOQL.of(Account.SObjectType).with(Account.Name).with(Account.Industry, 'accIndustry').mockId('mockingQuery').toList(); + } catch (QueryException e) { + soqlException = e; + } + + // Verify + Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.'); + Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supported'); + } + + @IsTest + static void mockWithManyFieldAndAggregateFunction() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + QueryException soqlException = null; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + + try { + SOQL.of(Account.SObjectType).count(Account.Industry).mockId('mockingQuery').toAggregated(); + } catch (QueryException e) { + soqlException = e; + } + + // Verify + Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.'); + Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supported'); + } + + @IsTest + static void mockWithManyFieldAndParentRelationship() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev', Parent = new Account(Name = 'Parent 1')), + new Account(Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev', Parent = new Account(Name = 'Parent 2')) + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + List result = SOQL.of(Account.SObjectType).with(Account.Name, Account.Industry).with('Parent', Account.Name).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'The result accounts size should be the same size of mocked accounts.'); + + for (Account mockedResult : result) { + Assert.isNotNull(mockedResult.Name, 'Account Name should be set.'); + Assert.isNotNull(mockedResult.Description, 'The Account Description should be set.'); + Assert.isNotNull(mockedResult.Website, 'The Account Website should be set.'); + Assert.isNotNull(mockedResult.Parent.Name, 'The Parent Account should be set.'); + Assert.isFalse(mockedResult.isSet(Account.Industry), 'Account Industry should be not set, because it\'s not mocked.'); + } + } + + @IsTest + static void mockWithAggregateResultProxy() { + // Setup + Map aggregateResult = new Map{ 'LeadSource' => 'Web', 'total' => 10}; + + // Test + SOQL.mock('mockingQuery').thenReturn(aggregateResult); + + List results = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + // Verify + Assert.areEqual(1, results.size(), 'The size of the aggregate results should match the mocked size.'); + + SOQL.AggregateResultProxy result = results[0]; + + Assert.isNotNull(result.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.'); + Assert.areEqual(10, result.get('total'), 'AggregateResult should contain the total field.'); + Assert.areEqual('Web', result.get('LeadSource'), 'AggregateResult should contain the LeadSource field.'); + } + + @IsTest + static void mockWithAggregateResultsProxy() { + // Setup + List> aggregateResults = new List>{ + new Map{ 'LeadSource' => 'Web', 'total' => 10}, + new Map{ 'LeadSource' => 'Phone', 'total' => 5}, + new Map{ 'LeadSource' => 'Email', 'total' => 3} + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(aggregateResults); + + List result = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + // Verify + Assert.areEqual(3, result.size(), 'The size of the aggregate results should match the mocked size.'); + + for (SOQL.AggregateResultProxy aggregateResult : result) { + Assert.isNotNull(aggregateResult.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.'); + Assert.isNotNull(aggregateResult.get('total'), 'AggregateResult should contain the total field.'); + Assert.isNotNull(aggregateResult.get('LeadSource'), 'AggregateResult should contain the LeadSource field.'); + } + } + + @IsTest + static void mockSubQuery() { + // Setup + List mocks = (List) JSON.deserialize( + JSON.serialize( + new List>{ + new Map{ + 'Name' => 'Account Name', + 'Industry' => 'IT', + 'Contacts' => new Map{ + 'totalSize' => 2, + 'done' => true, + 'records' => new List>{ + new Map{ 'Name' => 'Contact Name', 'Email' => 'contact.email@address.com', 'Phone' => '0987654321' }, + new Map{ 'Name' => 'Contact Name 2', 'Email' => 'contact2.email@address.com', 'Phone' => '1234567890' } + } + } + } + } + ), + List.class + ); + + SOQL.mock('mockingQuery').thenReturn(mocks); + + // Test + List result = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.Industry) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name, Contact.Phone) + ) + .mockId('mockingQuery') + .toList(); + + // Verify + Assert.areEqual(1, result.size(), 'The size of the mocked accounts should match the expected one.'); + + Account account = result[0]; + + Assert.areEqual('Account Name', account.Name, 'The mocked account name should match the expected one.'); + Assert.areEqual('IT', account.Industry, 'The mocked account industry should match the expected one.'); + Assert.areEqual(2, account.Contacts.size(), 'The size of the mocked contacts should match the expected one.'); + + Contact contact1 = (Contact) account.Contacts[0]; + + Assert.areEqual('Contact Name', contact1.Name, 'The mocked contact name should match the expected one.'); + Assert.areEqual('contact.email@address.com', contact1.Email, 'The mocked contact email should match the expected one.'); + Assert.areEqual('0987654321', contact1.Phone, 'The mocked contact phone should match the expected one.'); + + Contact contact2 = (Contact) account.Contacts[1]; + + Assert.areEqual('Contact Name 2', contact2.Name, 'The mocked contact name should match the expected one.'); + Assert.areEqual('contact2.email@address.com', contact2.Email, 'The mocked contact email should match the expected one.'); + Assert.areEqual('1234567890', contact2.Phone, 'The mocked contact phone should match the expected one.'); + } + + @IsTest + static void mockSubQueryWithStrippingAdditionalFields() { + // Setup + List mocks = (List) JSON.deserialize( + JSON.serialize( + new List>{ + new Map{ + 'Name' => 'Account Name', + 'Industry' => 'IT', + 'Contacts' => new Map{ + 'totalSize' => 2, + 'done' => true, + 'records' => new List>{ + new Map{ 'Name' => 'Contact Name', 'Email' => 'contact.email@address.com', 'Phone' => '0987654321' }, + new Map{ 'Name' => 'Contact Name 2', 'Email' => 'contact2.email@address.com', 'Phone' => '1234567890' } + } + }, + 'Opportunities' => new Map{ + 'totalSize' => 1, + 'done' => true, + 'records' => new List>{ + new Map{ 'Name' => 'Opportunity Name', 'Amount' => 100000 } + } + } + } + } + ), + List.class + ); + + SOQL.mock('mockingQuery').thenReturn(mocks); + + // Test + List result = SOQL.of(Account.SObjectType) + .with(Account.Name, Account.Industry) + .with(SOQL.SubQuery.of('Contacts') + .with(Contact.Id, Contact.Name, Contact.Phone) + ) + .mockId('mockingQuery') + .toList(); + + // Verify + Account account = result[0]; + + Assert.areEqual(1, result.size(), 'The size of the mocked accounts should match the expected one.'); + Assert.areEqual(2, account.Contacts.size(), 'The size of the mocked opportunities should match the expected one.'); + Assert.areEqual(0, account.Opportunities.size(), 'The size of the mocked opportunities should be 0, because it was not requested.'); + } + + @IsTest + static void sObjectsMockStack() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); + SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 2')); + SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 3')); + + // Test + SOQL.Queryable query = SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery'); + + Account acc1 = (Account) query.toObject(); + Account acc2 = (Account) query.toObject(); + Account acc3 = (Account) query.toObject(); + Account acc4 = (Account) query.toObject(); + + // Verify + Assert.areEqual('Test 1', acc1.Name, 'The returned account name should match the expected one.'); + Assert.areEqual('Test 2', acc2.Name, 'The returned account name should match the expected one.'); + Assert.areEqual('Test 3', acc3.Name, 'The returned account name should match the expected one.'); + Assert.areEqual('Test 3', acc4.Name, 'The returned account name should match the expected one.'); + } + + @IsTest + static void countMockStack() { + // Setup + SOQL.mock('mockingQuery').thenReturn(1); + SOQL.mock('mockingQuery').thenReturn(2); + SOQL.mock('mockingQuery').thenReturn(3); + + // Test + Integer result1 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); + Integer result2 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); + Integer result3 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); + Integer result4 = SOQL.of(Account.SObjectType).mockId('mockingQuery').count().toInteger(); + + // Verify + Assert.areEqual(1, result1, 'The returned count should match the expected one.'); + Assert.areEqual(2, result2, 'The returned count should match the expected one.'); + Assert.areEqual(3, result3, 'The returned count should match the expected one.'); + Assert.areEqual(3, result4, 'The returned count should match the expected one.'); + } + + @IsTest + static void aggregatedMockStack() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Web', 'total' => 10}); + SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Phone', 'total' => 5}); + SOQL.mock('mockingQuery').thenReturn(new Map{ 'LeadSource' => 'Email', 'total' => 3}); + + // Test + List result1 = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + List result2 = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + List result3 = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + List result4 = SOQL.of(Lead.SObjectType) + .with(Lead.LeadSource) + .count(Lead.Id, 'total') + .groupBy(Lead.LeadSource) + .mockId('mockingQuery') + .toAggregatedProxy(); + + // Verify + Assert.areEqual(1, result1.size(), 'The size of the aggregate results should match the mocked size.'); + Assert.areEqual(10, result1[0].get('total'), 'The total should match the expected one.'); + Assert.areEqual('Web', result1[0].get('LeadSource'), 'The LeadSource should match the expected one.'); + + Assert.areEqual(1, result2.size(), 'The size of the aggregate results should match the mocked size.'); + Assert.areEqual(5, result2[0].get('total'), 'The total should match the expected one.'); + Assert.areEqual('Phone', result2[0].get('LeadSource'), 'The LeadSource should match the expected one.'); + + Assert.areEqual(1, result3.size(), 'The size of the aggregate results should match the mocked size.'); + Assert.areEqual(3, result3[0].get('total'), 'The total should match the expected one.'); + Assert.areEqual('Email', result3[0].get('LeadSource'), 'The LeadSource should match the expected one.'); + + Assert.areEqual(1, result4.size(), 'The size of the aggregate results should match the mocked size.'); + Assert.areEqual(3, result4[0].get('total'), 'The total should match the expected one.'); + Assert.areEqual('Email', result4[0].get('LeadSource'), 'The LeadSource should match the expected one.'); + } + + @IsTest + static void toId() { + // Setup + Account acc = new Account(Name = 'Test 1'); + insert acc; + + // Test + Id accountId = SOQL.of(Account.SObjectType).byId(acc).toId(); + + // Verify + Assert.areEqual(acc.Id, accountId, 'The returned Id should match the expected one.'); + } + + @IsTest + static void toIdWithMocking() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new Account(Name = 'Test 1')); + + // Test + Id accountId = SOQL.of(Account.SObjectType).mockId('mockingQuery').toId(); + + // Verify + Assert.isNotNull(accountId, 'The returned Id should match the expected one.'); + } + + @IsTest + static void toIds() { + // Setup + List accounts = insertAccounts(); + + // Test + Set accountIds = SOQL.of(Account.SObjectType).byIds(accounts).toIds(); + + // Verify + Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void toIdsWithMocking() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + Set accountIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIds(); + + // Verify + Assert.areEqual(2, accountIds.size(), 'The size of the returned set should be equal to the size of the mocked accounts.'); + } + + @IsTest + static void toIdsOf() { + // Setup + List accounts = insertAccountsWithParents(); + + // Test + Set parentIds = SOQL.of(Account.SObjectType).byIds(accounts).toIdsOf(Account.ParentId); + + // Verify + Assert.areEqual(accounts.size(), parentIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void toIdsOfWithMocking() { + // Setup + SOQL.mock('mockingQuery').thenReturn( + (List) JSON.deserialize( + JSON.serialize(new List>{ + new Map{ + 'Id' => SOQL.IdGenerator.get(Account.SObjectType) + }, + new Map{ + 'Id' => SOQL.IdGenerator.get(Account.SObjectType) + } + }), + List.class + ) + ); + + // Test + Set parentIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIdsOf(Account.ParentId); + + // Verify + Assert.areEqual(2, parentIds.size(), 'The size of the returned set should be equal to the size of the mocked accounts.'); + } + + @IsTest + static void toIdsOfRelationshipField() { + // Setup + List accounts = insertAccountsWithParents(); + + // Test + Set createdByIds = SOQL.of(Account.SObjectType).byIds(accounts).toIdsOf('Parent', Account.CreatedById); + + // Verify + Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); + } + + @IsTest + static void toIdsOfRelationshipFieldWithMocking() { + // Setup + SOQL.mock('mockingQuery').thenReturn( + (AggregateResult) JSON.deserialize( + JSON.serialize(new Map{ + 'Id' => SOQL.IdGenerator.get(User.SObjectType) + }), + AggregateResult.class + ) + ); + + // Test + Set createdByIds = SOQL.of(Account.SObjectType).mockId('mockingQuery').toIdsOf('Parent', Account.CreatedById); + + // Verify + Assert.areEqual(1, createdByIds.size(), 'The size of the returned set should be 1, because the same user inserted the accounts.'); + } + + @IsTest + static void doExist() { + // Setup + Account acc = new Account(Name = 'Test 1'); + insert acc; + + // Test + Boolean isRecordExist = SOQL.of(Account.SObjectType).byId(acc).doExist(); + + // Verify + Assert.isTrue(isRecordExist, 'The record with given Id should exist.'); + } + + @IsTest + static void multipleToStringExecutions() { + // Setup + Exception soqlException = null; + + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.Industry).equal('IT')) + ); + + // Test + try { + builder.preview(); + builder.toString(); + builder.toString(); + builder.toList(); + } catch (Exception e) { + soqlException = e; + } + + // Verify + Assert.isNull(soqlException, 'The SOQL should be generated without any exceptions.'); + + Assert.areEqual('SELECT Id FROM Account WHERE (Name = :v1 AND Industry = :v2)', builder.toString(), 'The generated SOQL should match the expected one.'); + + Map binding = builder.binding(); + Assert.areEqual('Test', binding.get('v1'), 'The binding variable should match the expected value.'); + Assert.areEqual('IT', binding.get('v2'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void toValueOf() { + // Setup + String accountName = 'Test 1'; + + Account acc = new Account(Name = accountName); + insert acc; + + // Test + String resultAccName = (String) SOQL.of(Account.SObjectType).byId(acc).toValueOf(Account.Name); + + // Verify + Assert.areEqual(accountName, resultAccName, 'The returned account name should match the expected one.'); + } + + @IsTest + static void mockedToValueOf() { + // Setup + String accountName = 'Test 1'; + + // Test + SOQL.mock('mockingQuery').thenReturn(new Account(Name = accountName)); + String resultAccName = (String) SOQL.of(Account.SObjectType).mockId('mockingQuery').toValueOf(Account.Name); + + // Verify + Assert.areEqual(accountName, resultAccName, 'The returned account name should match the expected one.'); + } + + @IsTest + static void toValuesOf() { + // Setup + List accounts = insertAccounts(); + + // Test + Set accountNames = SOQL.of(Account.SObjectType).byIds(accounts).toValuesOf(Account.Name); + + // Verify + Assert.areEqual(2, accountNames.size(), 'The size of the account names set should be equal to the size of the accounts.'); + } + + @IsTest + static void toValuesOfWhenNoValues() { + // Setup + insertAccounts(); // Industry is empty + + // Test + Set accountsIndustries = SOQL.of(Account.SObjectType).toValuesOf(Account.Industry); + + // Verify + Assert.areEqual(0, accountsIndustries.size(), 'The size of the account industries set should be 0, because field is empty.'); + } + + @IsTest + static void toValuesOfWithDefaultFields() { + // Setup + List accounts = insertAccounts(); + + // Test + Set accountNames = SOQL.of(Account.SObjectType) + .with(Account.Industry) + .byIds(accounts) + .toValuesOf(Account.Name); + + // Verify + Assert.areEqual(2, accountNames.size(), 'The size of the account names set should be equal to the size of the accounts.'); + } + + @IsTest + static void toValueOfWhenRecordNotExist() { + // Test + String accountIndustry = (String) SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.name().equal('Test')) + .toValueOf(Account.Industry); + + // Verify + Assert.isNull(accountIndustry, 'The account industry should be null, because the record does not exist.'); + } + + @IsTest + static void toValuesOfRelationshipField() { + // Setup + List accounts = insertAccountsWithParents(); + + // Test + Set accountNames = SOQL.of(Account.SObjectType) + .byIds(accounts) + .toValuesOf('Parent', Account.Name); + + // Verify + Assert.areEqual(2, accountNames.size(), 'Each account should have a parent, so the size of the account names set should be equal to the size of the accounts.'); + } + + @IsTest + static void toValuesOfRelationshipFieldWhenRelatedRecordNotExist() { + // Test + Set accountNames = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.name().equal('Test')) + .toValuesOf('Parent', Account.Name); + + // Verify + Assert.isTrue(accountNames.isEmpty(), 'The account names set should be empty, because the related record does not exist.'); + } + + @IsTest + static void toObject() { + // Setup + Account acc = new Account(Name = 'Test 1'); + insert acc; + + // Test + Account result = (Account) SOQL.of(Account.SObjectType).toObject(); + + // Verify + Assert.areEqual(acc.Id, result.Id, 'The returned account should match the expected one.'); + } + + @IsTest + static void toObjectWithMultipleRows() { + // Setup + insertAccounts(); + Exception queryException = null; + + // Test + try { + SOQL.of(Account.SObjectType).toObject(); + } catch (Exception e) { + queryException = e; // List has more than 1 row for assignment to SObject + } + + // Verify + Assert.isNotNull(queryException, 'The exception should be thrown, because there are more than 1 row for assignment to SObject.'); + } + + @IsTest + static void toObjectWithoutRows() { + // Test + // When List has no rows for assignment to SObject null will be returned + Account account = (Account) SOQL.of(Account.SObjectType).toObject(); + + // Verify + Assert.isNull(account, 'The account should be null, because there are no rows for assignment to SObject.'); + } + + @IsTest + static void toList() { + // Setup + List accounts = insertAccounts(); + + // Test + List result = SOQL.of(Account.SObjectType).toList(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'The size of the result list should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void toAggregated() { + // Setup + List accounts = insertAccounts(); + + // Test + List result = SOQL.of(Account.SObjectType).count(Account.Name, 'names').toAggregated(); + + // Verify + Assert.areEqual(accounts.size(), result[0].get('names'), 'The count of the names should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void toIntegerWithoutSpecifiedCount() { + // Setup + List accounts = insertAccounts(); + + // Test + Integer accountsCount = SOQL.of(Account.SObjectType).toInteger(); + + // Verify + Assert.areEqual(accounts.size(), accountsCount, 'The COUNT() clause should be automatically added when the count() method is not specified. The count of the accounts should match the number of inserted accounts.'); + } + + @IsTest + static void toInteger() { + // Setup + List accounts = insertAccounts(); + + // Test + Integer result = SOQL.of(Account.SObjectType).count().toInteger(); + + // Verify + Assert.areEqual(accounts.size(), result, 'The count of the accounts should match the number of inserted accounts.'); + } + + @IsTest + static void toMap() { + // Setup + List accounts = insertAccounts(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).toMap(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Id), 'The result map should have a value for each inserted account Id.'); + } + } + + @IsTest + static void mockedToMapWithoutMockedId() { + // Setup + List accounts = new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + Map result = (Map) SOQL.of(Account.SObjectType).mockId('mockingQuery').toMap(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); + Assert.areEqual(accounts.size(), result.keySet().size(), 'Size of the result map should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void toMapWithMockingWhenOtherFieldsAreSpecified() { + // Setup + List accounts = new List{ + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1', Description = 'Test 1 Description', Website = 'www.beyondthecloud.dev'), + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2', Description = 'Test 2 Description', Website = 'www.beyondthecloud.dev') + }; + + // Test + SOQL.mock('mockingQuery').thenReturn(accounts); + + Map result = (Map) SOQL.of(Account.SObjectType) + .with(Account.Name, Account.Description) + .mockId('mockingQuery') + .toMap(); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Id), 'The result map should have a value for each inserted account Id.'); + Assert.isNotNull(result.get(acc.Id).Name, 'The result map should have a value for each inserted account Name.'); + Assert.isNotNull(result.get(acc.Id).Description, 'The result map should have a value for each inserted account Description.'); + Assert.isNull(result.get(acc.Id).Website, 'The result map should not have a value for each inserted account Website.'); + } + } + + @IsTest + static void toIdMapBy() { + // Setup + insertAccountsWithParents(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).with(Account.Id, Account.Name).toIdMapBy(Account.ParentId); + + // Verify + Assert.areEqual(2, result.size(), 'Size of the result map should be equal to the size of the inserted accounts with parents.'); + } + + @IsTest + static void mockedToIdMapBy() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Account 1', ParentId = SOQL.IdGenerator.get(Account.SObjectType)), + new Account(Name = 'Account 2', ParentId = SOQL.IdGenerator.get(Account.SObjectType)) + }); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name).mockId('mockingQuery') + .toIdMapBy(Account.ParentId); + + // Verify + Assert.areEqual(2, result.size(), 'Size of the result map should be equal to the size of the inserted accounts with parents.'); + } + + @IsTest + static void toIdMapByRelationshipField() { + // Setup + insertAccountsWithParents(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).toIdMapBy('Parent', Account.CreatedById); + + // Verify + Assert.areEqual(1, result.size(), 'Size of the result map should be equal to 1, because the accounts have the same created by user.'); + Assert.isTrue(result.containsKey(UserInfo.getUserId()), 'The result map should have a key: UserInfo.getUserId().'); + } + + @IsTest + static void mockedToIdMapByRelationshipField() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Account 1', Parent = new Account(Name = 'Parent 1', Id = SOQL.IdGenerator.get(Account.SObjectType))), + new Account(Name = 'Account 2', Parent = new Account(Name = 'Parent 2', Id = SOQL.IdGenerator.get(Account.SObjectType))) + }); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).mockId('mockingQuery').toIdMapBy('Parent', Account.Id); + + // Verify + Assert.areEqual(2, result.size(), 'Size of the result map should be equal to 2, because the accounts have 2 different parents.'); + } + + @IsTest + static void toAggregatedIdMapBy() { + // Setup + insertAccountsWithParents(); + + // Test + Map> result = (Map>) SOQL.of(Account.SObjectType).toAggregatedIdMapBy(Account.ParentId); + + // Verify + Assert.areEqual(2, result.size(), 'Size of the result map should be equal to 2, because the accounts have 2 parents.'); + } + + @IsTest + static void mockedtoAggregatedIdMapBy() { + // Setup + Id parentId = SOQL.IdGenerator.get(Account.SObjectType); + + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Account 1', ParentId = parentId), + new Account(Name = 'Account 2', ParentId = parentId) + }); + + // Test + Map> result = (Map>) SOQL.of(Account.SObjectType).mockId('mockingQuery').toAggregatedIdMapBy(Account.ParentId); + + // Verify + Assert.areEqual(1, result.size(), 'Size of the result map should be equal to 1, because the accounts have common parent.'); + } + + @IsTest + static void toAggregatedIdMapByRelationshipField() { + // Setup + insertAccountsWithParents(); + + // Test + Map> result = (Map>) SOQL.of(Account.SObjectType).mockId('mockingQuery').toAggregatedIdMapBy('Parent', Account.CreatedById); + + // Verify + Assert.areEqual(1, result.size(), 'Size of the result map should be equal to 1, because the same user created all accounts.'); + } + + @IsTest + static void mockedtoAggregatedIdMapByRelationshipField() { + // Setup + Id parentId = SOQL.IdGenerator.get(Account.SObjectType); + + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Account 1', Parent = new Account(Name = 'Parent 1', Id = parentId)), + new Account(Name = 'Account 2', Parent = new Account(Name = 'Parent 2', Id = parentId)) + }); + + // Test + Map> result = (Map>) SOQL.of(Account.SObjectType).mockId('mockingQuery').toAggregatedIdMapBy('Parent', Account.Id); + + // Verify + Assert.areEqual(1, result.size(), 'Size of the result map should be equal to 1, because all parent accounts have the same parent.'); + } + + @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(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Name), 'The result map should have a value for each inserted account Name.'); + } + } + + @IsTest + static void toMapWithCustomRelationshipKey() { + // Setup + List accounts = insertAccountsWithParents(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.ParentId).isNotNull()) + .toMap('Parent', Account.Name); + + // Verify + Assert.areEqual(accounts.size(), result.size(), 'Size of the result map should be equal to the size of the accounts with parents.'); + Assert.isNotNull(result.get('Test 1 Parent'), 'The result map should have a value for the key: "Test 1 Parent".'); + Assert.isNotNull(result.get('Test 2 Parent'), 'The result map should have a value for the key: "Test 2 Parent".'); + } + + @IsTest + static void toMapWithEmptyCustomRelationshipKey() { + // Setup + insertAccountsWithParents(); + + // Test + Map result = (Map) SOQL.of(Account.SObjectType).toMap('Parent', Account.Name); + + // Verify + Assert.areEqual(2, result.size(), 'Size of the result map should be equal to 2, because there are two accounts with different parents.'); + } + + @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(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isNotNull(result.get(acc.Name), 'The result map should have a value for each inserted account Name.'); + Assert.areEqual(acc.Id, result.get(acc.Name), 'The result map should have an account Id for each account 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(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isFalse(result.get(acc.Name).isEmpty(), 'The result map should contain a non-empty account list for each account name.'); + } + } + + @IsTest + static void toAggregatedMapWithCustomRelationshipKey() { + // Setup + List accounts = insertAccountsWithParents(); + + // Test + Map> result = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(Account.ParentId).isNotNull()) + .toAggregatedMap('Parent.CreatedBy', User.Id); + + // Verify + Assert.areEqual(1, result.size(), 'Size of the result map should be 1, because the same user created all accounts.'); + Assert.areEqual(accounts.size(), result.get(UserInfo.getUserId()).size(), 'The result map should contain all accounts created by the current User.'); + } + + @IsTest + static void toAggregatedMapWithEmptyCustomKey() { + // Setup + insertAccounts(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Name); + + // Verify + Assert.areEqual(2, result.size(), 'The result map should have only two keys, because there are two accounts with different names.'); + } + + @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(), 'Size of the result map should be equal to the size of the inserted accounts.'); + + for (Account acc : accounts) { + Assert.isFalse(result.get(acc.Name).isEmpty(), 'The result map should contain a non-empty list of account Ids for each account name.'); + } + } + + @IsTest + static void toAggregatedMapWithEmptyCustomKeyAndCustomValue() { + // Setup + insertAccounts(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Name, Account.Id); + + // Verify + Assert.areEqual(2, result.size(), 'The result map should have only two keys, because there are two accounts with different names.'); + } + + @IsTest + static void toAggregatedMapWithCustomKeyAndCustomRelationshipValue() { + // Setup + insertAccountsWithParents(); + + // Test + Map> result = SOQL.of(Account.SObjectType).toAggregatedMap(Account.Name, 'Parent', Account.Id); + + // Verify + Assert.areEqual(4, result.size(), 'The result map should have 4 keys for each account name.'); + } + + @IsTest + static void toQueryLocator() { + // Test + Database.QueryLocator queryLocator = SOQL.of(Account.SObjectType) + .with(Account.Id) + .with(Account.Name) + .toQueryLocator(); + + // Verify + Assert.areEqual('SELECT Id, Name FROM Account', queryLocator.getQuery(), 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void toQueryLocatorWithSharing() { + // Setup + insertTasks(); + + System.runAs(minimumAccessUser()) { + // Test + Database.QueryLocator queryLocator = SOQL.of(Task.SObjectType).systemMode().withSharing().toQueryLocator(); + + // Verify + Assert.isFalse(queryLocator.iterator().hasNext(), 'The user should not have access to the tasks, so hasNext should return false.'); + } + } + + @IsTest + static void toQueryLocatorWithoutSharing() { + // Setup + insertTasks(); + + System.runAs(minimumAccessUser()) { + // Test + Database.QueryLocator queryLocator = SOQL.of(Task.SObjectType).systemMode().withoutSharing().toQueryLocator(); + + // Verify + Assert.isTrue(queryLocator.iterator().hasNext(), 'The user should have access to the tasks, so hasNext should return true.'); + } + } + + @IsTest + static void querySynchronousCloseToLimitWithMocking() { + // Setup + Exception queryException = null; + + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + try { + Test.startTest(); + for (Integer i = 0 ; i < 100 ; i++) { + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + } + Test.stopTest(); + } catch (QueryException e) { + queryException = e; + } + + // Verify + Assert.isNull(queryException, 'The synchronous query should not have exceeded the limit.'); + } + + @IsTest + static void querySynchronousLimitExceptionWithMocking() { + // Setup + Exception queryException = null; + + SOQL.mock('mockingQuery').thenReturn(new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }); + + // Test + try { + Test.startTest(); + for (Integer i = 0 ; i < 101 ; i++) { + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + } + Test.stopTest(); + } catch (QueryException e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'The synchronous query should have exceeded the limit.'); + Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should not have been exceeded.'); + } + + @IsTest + static void querySynchronousLimitExceptionWithoutMocking() { + // Setup + Exception queryException = null; + + // Test + try { + for (Integer i = 0 ; i < 102 ; i++) { + SOQL.of(Account.SObjectType).toList(); + } + } catch (QueryException e) { + queryException = e; + } + + // Verify + Assert.isNotNull(queryException, 'The synchronous query should have exceeded the limit.'); + Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should have been exceeded.'); + } + + @IsTest + static void queryConverterToIdsOf() { + // Setup + List accounts = new List{ + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), ParentId = SOQL.IdGenerator.get(Account.SObjectType)), + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), ParentId = SOQL.IdGenerator.get(Account.SObjectType)) + }; + + // Test + Set accountIds = new SOQL.Converter('Account').transform(accounts).toIdsOf(Account.ParentId); + + // Verify + Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void queryConverterToIdsOfRelationshipField() { + // Setup + List accounts = new List{ + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Parent = new Account(Id = SOQL.IdGenerator.get(Account.SObjectType))), + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Parent = new Account(Id = SOQL.IdGenerator.get(Account.SObjectType))) + }; + + // Test + Set accountIds = new SOQL.Converter('Account').transform(accounts).toIdsOf('Parent', Account.Id); + + // Verify + Assert.areEqual(accounts.size(), accountIds.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); + } + + @IsTest + static void queryConverterToValuesOf() { + // Setup + List accounts = new List{ + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 1'), + new Account(Id = SOQL.IdGenerator.get(Account.SObjectType), Name = 'Test 2') + }; + + // Test + Set accountNames = new SOQL.Converter('Account').transform(accounts).toValuesOf(Account.Name); + + // Verify + Assert.areEqual(accounts.size(), accountNames.size(), 'The size of the returned set should be equal to the size of the inserted accounts.'); + Assert.isTrue(accountNames.contains('Test 1'), 'The returned set should contain the account name "Test 1".'); + Assert.isTrue(accountNames.contains('Test 2'), 'The returned set should contain the account name "Test 2".'); + } + + @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') + @IsTest + static void preview() { + // Test + SOQL.of(Account.SObjectType).preview().toList(); + } + + @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') + @IsTest + static void previewWithConditions() { + // Test + SOQL.of(Account.SObjectType) + .whereAre(SOQL.FilterGroup + .add(SOQL.Filter.with(Account.Name).equal('Test')) + .add(SOQL.Filter.with(Account.Industry).equal('IT')) + ) + .preview() + .toList(); + } + + @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveAsserts') + @IsTest + static void previewCount() { + // Test + SOQL.of(Account.SObjectType).count().preview().toInteger(); + } + + static List insertAccounts() { + List accounts = new List{ + new Account(Name = 'Test 1'), + new Account(Name = 'Test 2') + }; + insert accounts; + return accounts; + } + + static List insertAccountsWithParents() { + List parentAccounts = new List{ + new Account(Name = 'Test 1 Parent'), + new Account(Name = 'Test 2 Parent') + }; + insert parentAccounts; + + List accounts = new List{ + new Account(Name = 'Test 1', ParentId = parentAccounts[0].Id), + new Account(Name = 'Test 2', ParentId = parentAccounts[1].Id) + }; + insert accounts; + + return accounts; + } + + static List insertCases() { + List cases = new List{ + new Case(Status = 'New', Origin = 'Web'), + new Case(Status = 'New', Origin = 'Web') + }; + insert cases; + return cases; + } + + static void insertTasks() { + insert new List{ + new Task(Subject = 'Test', Type = 'Other'), + new Task(Subject = 'Test', Type = 'Other') + }; + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + Profile = new Profile(Name = 'Minimum Access - Salesforce'), + TimeZoneSidKey = 'America/Los_Angeles', + Username = 'queryselector@testorg.com' + ); + } } diff --git a/sfdx-project.json b/sfdx-project.json index 66b3f06b..91cf3ba4 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -8,5 +8,5 @@ "name": "soql-lib", "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "62.0" + "sourceApiVersion": "64.0" } diff --git a/website/docs/soql/api/soql.md b/website/docs/soql/api/soql.md index 9792ead0..1948866c 100644 --- a/website/docs/soql/api/soql.md +++ b/website/docs/soql/api/soql.md @@ -206,6 +206,10 @@ The following are methods for using `SOQL`: - [`toAggregatedMap(SObjectField keyField)`](#toaggregatedmap) - [`toAggregatedMap(String relationshipName, SObjectField targetKeyField)`](#toaggregatedmap-with-custom-relationship-key) - [`toAggregatedMap(SObjectField keyField, SObjectField valueField)`](#toaggregatedmap-with-custom-value) +- [`toIdMapBy(SObjectField field)`](#toidmapby) +- [`toIdMapBy(String relationshipName, SObjectField targetKeyField)`](#toidmapby-with-relationship) +- [`toAggregatedIdMapBy(SObjectField keyField)`](#toaggregateidmapby) +- [`toAggregatedIdMapBy(String relationshipName, SObjectField targetKeyField)`](#toaggregateidmapby-with-relationship) - [`toQueryLocator()`](#toquerylocator) ## INIT @@ -2558,6 +2562,8 @@ Map idToAccount = (Map) SOQL.of(Account.SObjectType).t ### toMap with custom key +**Note!** To improve query performance, a condition checking if the keyField is not null (`WHERE keyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2572,6 +2578,8 @@ Map nameToAccount = (Map) SOQL.of(Account.SObj ### toMap with custom relationship key +**Note!** To improve query performance, a condition checking if the targetKeyField is not null (`WHERE relationshipName.targetKeyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2586,6 +2594,8 @@ Map parentCreatedByEmailToAccount = (Map) SOQL ### toMap with custom key and value +**Note!** To improve query performance, a condition checking if the keyField is not null (`WHERE keyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2600,6 +2610,8 @@ Map nameToAccount = SOQL.of(Account.SObjectType).toMap(Account.N ### toAggregatedMap +**Note!** To improve query performance, a condition checking if the keyField is not null (`WHERE keyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2614,6 +2626,8 @@ Map> industryToAccounts = (Map>) SOQ ### toAggregatedMap with custom value +**Note!** To improve query performance, a condition checking if the keyField is not null (`WHERE keyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2628,6 +2642,8 @@ Map> industryToAccounts = SOQL.of(Account.SObjectType).toAg ### toAggregatedMap with custom relationship key +**Note!** To improve query performance, a condition checking if the targetKeyField is not null (`WHERE relationshipName.targetKeyField != null`) is automatically added to the query. + **Signature** ```apex @@ -2640,6 +2656,78 @@ Map> toAggregatedMap(String relationshipName, SObjectField Map> parentCreatedByEmailToAccounts = (Map>) SOQL.of(Account.SObjectType).toAggregatedMap('Parent.CreatedBy', User.Email); ``` +### toIdMapBy + +Creates a map where the key is the Id extracted from the specified field and the value is the SObject record. + +**Note!** To improve query performance, a condition checking if the field is not null (`WHERE field != null`) is automatically added to the query. + +**Signature** + +```apex +Map toIdMapBy(SObjectField field) +``` + +**Example** + +```apex +Map ownerIdToAccount = (Map) SOQL.of(Account.SObjectType).toIdMapBy(Account.OwnerId); +``` + +### toIdMapBy with relationship + +Creates a map where the key is the Id extracted from the specified relationship field and the value is the SObject record. + +**Note!** To improve query performance, a condition checking if the targetKeyField is not null (`WHERE relationshipName.targetKeyField != null`) is automatically added to the query. + +**Signature** + +```apex +Map toIdMapBy(String relationshipName, SObjectField targetKeyField) +``` + +**Example** + +```apex +Map parentIdToAccount = (Map) SOQL.of(Account.SObjectType).toIdMapBy('Parent', Account.Id); +``` + +### toAggregatedIdMapBy + +Creates a map where the key is the Id extracted from the specified field and the value is a list of SObject records grouped by that Id. + +**Note!** To improve query performance, a condition checking if the keyField is not null (`WHERE keyField != null`) is automatically added to the query. + +**Signature** + +```apex +Map> toAggregatedIdMapBy(SObjectField keyField) +``` + +**Example** + +```apex +Map> ownerIdToAccounts = (Map>) SOQL.of(Account.SObjectType).toAggregatedIdMapBy(Account.OwnerId); +``` + +### toAggregatedIdMapBy with relationship + +Creates a map where the key is the Id extracted from the specified relationship field and the value is a list of SObject records grouped by that Id. + +**Note!** To improve query performance, a condition checking if the targetKeyField is not null (`WHERE relationshipName.targetKeyField != null`) is automatically added to the query. + +**Signature** + +```apex +Map> toAggregatedIdMapBy(String relationshipName, SObjectField targetKeyField) +``` + +**Example** + +```apex +Map> parentIdToAccounts = (Map>) SOQL.of(Account.SObjectType).toAggregatedIdMapBy('Parent', Account.Id); +``` + ### toQueryLocator **Signature** diff --git a/website/docs/soql/examples/result.md b/website/docs/soql/examples/result.md index 98769b40..d5e3ffe6 100644 --- a/website/docs/soql/examples/result.md +++ b/website/docs/soql/examples/result.md @@ -368,6 +368,94 @@ Map> accountNamesByIndustry = SOQL.of(Account.SObjectType) .toAggregatedMap(Account.Industry, Account.Name); ``` +## toIdMapBy + +**Apex** + +```apex title="Traditional Approach" +Map ownerIdToAccount = new Map(); + +for (Account acc : [SELECT Id, Name, OwnerId FROM Account WHERE OwnerId != null]) { + ownerIdToAccount.put(acc.OwnerId, acc); +} +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +Map ownerIdToAccount = (Map) SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .toIdMapBy(Account.OwnerId); +``` + +## toIdMapBy with relationship + +**Apex** + +```apex title="Traditional Approach" +Map parentIdToAccount = new Map(); + +for (Account acc : [SELECT Id, Name, Parent.Id FROM Account WHERE Parent.Id != null]) { + parentIdToAccount.put(acc.Parent.Id, acc); +} +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +Map parentIdToAccount = (Map) SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .toIdMapBy('Parent', Account.Id); +``` + +## toAggregatedIdMapBy + +**Apex** + +```apex title="Traditional Approach" +Map> ownerIdToAccounts = new Map>(); + +for (Account acc : [SELECT Id, Name, OwnerId FROM Account WHERE OwnerId != null]) { + if (!ownerIdToAccounts.containsKey(acc.OwnerId)) { + ownerIdToAccounts.put(acc.OwnerId, new List()); + } + + ownerIdToAccounts.get(acc.OwnerId).add(acc); +} +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +Map> ownerIdToAccounts = (Map>) SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .toAggregatedIdMapBy(Account.OwnerId); +``` + +## toAggregatedIdMapBy with relationship + +**Apex** + +```apex title="Traditional Approach" +Map> parentIdToAccounts = new Map>(); + +for (Account acc : [SELECT Id, Name, Parent.Id FROM Account WHERE Parent.Id != null]) { + if (!parentIdToAccounts.containsKey(acc.Parent.Id)) { + parentIdToAccounts.put(acc.Parent.Id, new List()); + } + + parentIdToAccounts.get(acc.Parent.Id).add(acc); +} +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +Map> parentIdToAccounts = (Map>) SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .toAggregatedIdMapBy('Parent', Account.Id); +``` + ## toQueryLocator **Apex** @@ -379,5 +467,7 @@ Database.QueryLocator queryLocator = Database.getQueryLocator('SELECT Id FROM AC **SOQL Lib** ```apex title="SOQL Lib Approach" -Database.QueryLocator queryLocator = SOQL.of(Account.SObjectType).toQueryLocator(); +Database.QueryLocator queryLocator = SOQL.of(Account.SObjectType) + .with(Account.Id, Account.Name) + .toQueryLocator(); ``` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index b8373a7b..9967ac05 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -10,7 +10,7 @@ const config = { favicon: 'img/favicon.ico', url: 'https://soql.beyondthecloud.dev/', baseUrl: '/', - organizationName: 'Beyond The Cloud', + organizationName: 'Beyond The Cloud Sp. z o.o.', projectName: 'soql-lib', onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'throw', @@ -69,7 +69,7 @@ const config = { ], colorMode: { defaultMode: 'dark', - disableSwitch: true, + disableSwitch: false, }, docs: { sidebar: { diff --git a/website/src/pages/index.js b/website/src/pages/index.js index e7b53a08..1ad64060 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -8,15 +8,15 @@ export default function Home() { description="The SOQL Lib provides functional constructs for SOQL queries in Apex.">
- SOQL Lib -

+ SOQL Lib +

Everything you need to build SOQL queries in Salesforce APEX.

@@ -57,19 +57,19 @@ export function Features() {
-

+

What's in SOQL Lib?

-

+

The SOQL Lib provides functional constructs for SOQL queries in Apex.

{features.map((feature, index) => ( -
-

{feature.title}

-

{feature.description}

+
+

{feature.title}

+

{feature.description}

))}