diff --git a/force-app/main/default/classes/cached-soql/CacheManager.cls b/force-app/main/default/classes/cached-soql/CacheManager.cls index 6bb7583f..f9c6d0fc 100644 --- a/force-app/main/default/classes/cached-soql/CacheManager.cls +++ b/force-app/main/default/classes/cached-soql/CacheManager.cls @@ -101,15 +101,24 @@ public with sharing class CacheManager { protected abstract Cache.Partition getPartition(String partitionName); public Boolean contains(String key) { - return this.isAvailable() ? this.platformCachePartition.contains(key) : this.fallback.contains(key); + if (this.isAvailable()) { + return this.platformCachePartition.contains(key); + } + return this.fallback.contains(key); } public Set getKeys() { - return this.isAvailable() ? this.platformCachePartition.getKeys() : this.fallback.getKeys(); + if (this.isAvailable()) { + return this.platformCachePartition.getKeys(); + } + return this.fallback.getKeys(); } public Object get(String key) { - return this.isAvailable() ? this.platformCachePartition.get(key) : this.fallback.get(key); + if (this.isAvailable()) { + return this.platformCachePartition.get(key); + } + return this.fallback.get(key); } public void remove(String key) { @@ -144,7 +153,13 @@ public with sharing class CacheManager { public override Cache.Partition getPartition(String partitionName) { try { - return Cache.Org.getPartition(partitionName); + Cache.OrgPartition partition = Cache.Org.getPartition(partitionName); + + if (partition.getCapacity() == 0) { + return null; + } + + return partition; } catch (Cache.Org.OrgCacheException e) { return null; } @@ -161,7 +176,14 @@ public with sharing class CacheManager { if (!Cache.Session.isAvailable()) { return null; } - return Cache.Session.getPartition(partitionName); + + Cache.SessionPartition partition = Cache.Session.getPartition(partitionName); + + if (partition.getCapacity() == 0) { + return null; + } + + return partition; } catch (Cache.Session.SessionCacheException e) { return null; } diff --git a/force-app/main/default/classes/cached-soql/CacheManagerTest.cls b/force-app/main/default/classes/cached-soql/CacheManagerTest.cls index 9a2aae05..4b5be509 100644 --- a/force-app/main/default/classes/cached-soql/CacheManagerTest.cls +++ b/force-app/main/default/classes/cached-soql/CacheManagerTest.cls @@ -1,5 +1,5 @@ /** - * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) + * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/cache-manager/blob/main/LICENSE) **/ @IsTest diff --git a/force-app/main/default/classes/cached-soql/SOQLCache.cls b/force-app/main/default/classes/cached-soql/SOQLCache.cls index 9f07edc4..bc105201 100644 --- a/force-app/main/default/classes/cached-soql/SOQLCache.cls +++ b/force-app/main/default/classes/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.10.2 + * v6.11.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query diff --git a/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls b/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls index 37519a3a..1cc94066 100644 --- a/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls +++ b/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls @@ -3,7 +3,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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class diff --git a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls index fc81f230..0e55d408 100644 --- a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls +++ b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.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.10.2 + * v6.11.0 * * PMD False Positives: * - CognitiveComplexity: It is a library and we tried to put everything into ONE class diff --git a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls index ee5a6a85..687a78fa 100644 --- a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls +++ b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls @@ -3,7 +3,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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index bdd415b3..506803b1 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -2,7 +2,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query @@ -135,6 +135,7 @@ public virtual inherited sharing class SOQL implements Queryable { Queryable orderBy(String field, String direction); Queryable orderBy(String relationshipName, SObjectField field); Queryable orderByCount(SObjectField field); + Queryable orderBy(Distance distance); Queryable sortDesc(); Queryable sort(String direction); Queryable nullsLast(); @@ -203,12 +204,14 @@ public virtual inherited sharing class SOQL implements Queryable { Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField); // for internal use + Builder builder(); Map binding(); } @NamespaceAccessible public interface SubQuery { SubQuery of(String ofObject); + SubQuery of(String ofObject, Queryable query); // SELECT SubQuery with(SObjectField field); SubQuery with(SObjectField field1, SObjectField field2); @@ -231,6 +234,7 @@ public virtual inherited sharing class SOQL implements Queryable { SubQuery orderBy(SObjectField field); SubQuery orderBy(String field); SubQuery orderBy(String relationshipName, SObjectField field); + SubQuery orderBy(Distance distance); SubQuery sortDesc(); SubQuery sort(String direction); SubQuery nullsLast(); @@ -271,6 +275,7 @@ public virtual inherited sharing class SOQL implements Queryable { Filter with(SObjectField field); Filter with(String field); Filter with(String relationshipName, SObjectField field); + Filter with(Distance distance); // COMPARATORS Filter isNull(); Filter isNotNull(); @@ -395,6 +400,19 @@ public virtual inherited sharing class SOQL implements Queryable { Map getPopulatedFieldsAsMap(); } + @NamespaceAccessible + public interface Distance { + // field + Distance of(SObjectField ofField); + Distance of(String relationship, SObjectField ofField); + // comparison + Distance between(System.Location location); + Distance between(Decimal latitude, Decimal longitude); + // unit + Distance mi(); + Distance km(); + } + @NamespaceAccessible public static SubQuery SubQuery { get { @@ -444,6 +462,22 @@ public virtual inherited sharing class SOQL implements Queryable { } } + @NamespaceAccessible + public static Distance Distance { + get { + return new SoqlDistance(); + } + } + + @NamespaceAccessible + public static Integer getQueries() { + if (System.Test.isRunningTest() && !System.isBatch() && !System.isFuture() && !System.isQueueable() && !System.isScheduled()) { + return SOQL.syncQueriesIssued; + } + + return Limits.getQueries(); + } + // Mocking @NamespaceAccessible @@ -507,7 +541,7 @@ public virtual inherited sharing class SOQL implements Queryable { private static Binder binder = new Binder(); private static Integer syncQueriesIssued = 0; - private SoqlBuilder builder; + private Builder builder; private Executor executor; private Converter converter; @@ -518,7 +552,7 @@ public virtual inherited sharing class SOQL implements Queryable { @NamespaceAccessible public SOQL(String ofObject) { - this.builder = new SoqlBuilder(ofObject); + this.builder = new Builder(ofObject); this.executor = new Executor(builder); this.converter = new Converter(ofObject); @@ -993,6 +1027,12 @@ public virtual inherited sharing class SOQL implements Queryable { return this.orderBy('COUNT(' + field.toString() + ')'); } + @NamespaceAccessible + public Queryable orderBy(Distance distance) { + this.builder.orderBys.newOrderBy().with(distance); + return this; + } + @NamespaceAccessible public Queryable sortDesc() { return this.sort('DESC'); @@ -1105,6 +1145,10 @@ public virtual inherited sharing class SOQL implements Queryable { return binder.getBindingMap(); } + public Builder builder() { + return this.builder; + } + @NamespaceAccessible public Id toId() { this.builder.fields.clearAllFields(); // other fields not needed @@ -1337,10 +1381,10 @@ public virtual inherited sharing class SOQL implements Queryable { String toString(); } - private class SoqlBuilder implements QueryClause { + private class Builder implements QueryClause { private List clauses = new QueryClause[12]; - public SoqlBuilder(String ofObject) { + public Builder(String ofObject) { this.clauses.set(0, new SoqlFields(ofObject)); this.clauses.set(2, new SoqlFrom(ofObject)); } @@ -1643,11 +1687,17 @@ public virtual inherited sharing class SOQL implements Queryable { } private class SoqlSubQuery implements SubQuery { - private SoqlBuilder builder; + private Builder builder; private String childRelationshipName; public SubQuery of(String ofObject) { - this.builder = new SoqlBuilder(ofObject); + this.builder = new Builder(ofObject); + this.childRelationshipName = ofObject; + return this; + } + + public SubQuery of(String ofObject, Queryable query) { + this.builder = query.builder(); this.childRelationshipName = ofObject; return this; } @@ -1746,6 +1796,11 @@ public virtual inherited sharing class SOQL implements Queryable { return this; } + public SubQuery orderBy(Distance distance) { + this.builder.orderBys.newOrderBy().with(distance); + return this; + } + public SubQuery sortDesc() { this.builder.latestOrderBy.sortingOrder('DESC'); return this; @@ -2043,6 +2098,11 @@ public virtual inherited sharing class SOQL implements Queryable { return this; } + public Filter with(Distance distance) { + this.field = distance.toString(); + return this; + } + public Filter isNull() { return this.equal(null); } @@ -2310,10 +2370,10 @@ public virtual inherited sharing class SOQL implements Queryable { } private class SoqlJoinQuery implements InnerJoin { - private SoqlBuilder builder; + private Builder builder; public InnerJoin of(SObjectType ofObject) { - this.builder = new SoqlBuilder(ofObject.toString()); + this.builder = new Builder(ofObject.toString()); return this; } @@ -2635,6 +2695,10 @@ public virtual inherited sharing class SOQL implements Queryable { this.orderField = field; } + public void with(Distance distance) { + this.orderField = distance.toString(); + } + public void sortingOrder(String direction) { this.sortingOrder = direction; } @@ -2815,10 +2879,14 @@ public virtual inherited sharing class SOQL implements Queryable { } private void addIdToMockedRecords() { - // Id is always added to mirror standard SOQL behavior + // Id is added to mirror standard SOQL behavior SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); String sObjectPrefix = sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix(); + if (String.isBlank(sObjectPrefix)) { + return; + } + for (SObject record : this.mockedRecords) { record.put('Id', record?.Id ?? IdGenerator.get(sObjectPrefix)); } @@ -2927,6 +2995,48 @@ public virtual inherited sharing class SOQL implements Queryable { } } + private class SoqlDistance implements Distance { + private String field; + private String unit = 'km'; + private String comparisonLocation; + + public Distance of(SObjectField ofField) { + this.field = ofField.toString(); + return this; + } + + public Distance of(String relationship, SObjectField ofField) { + this.field = relationship + '.' + ofField.toString(); + return this; + } + + public Distance between(System.Location location) { + this.comparisonLocation = 'GEOLOCATION(' + location.getLatitude() + ',' + location.getLongitude() + ')'; + return this; + } + + public Distance between(Decimal latitude, Decimal longitude) { + return this.between(Location.newInstance(latitude, longitude)); + } + + public Distance mi() { + return this.unit('mi'); + } + + public Distance km() { + return this.unit('km'); + } + + private Distance unit(String unit) { + this.unit = unit; + return this; + } + + public override String toString() { + return 'DISTANCE(' + this.field + ', ' + this.comparisonLocation + ',\' ' + this.unit + '\')'; + } + } + private class ExceptionMock { private QueryException queryException; @@ -2943,10 +3053,10 @@ public virtual inherited sharing class SOQL implements Queryable { private DatabaseQuery sharingExecutor; private AccessLevel accessMode; private AccessType accessType; - private SoqlBuilder builder; + private Builder builder; private List mocks = new List(); - public Executor(SoqlBuilder builder) { + public Executor(Builder builder) { this.builder = builder; this.accessMode = AccessLevel.USER_MODE; this.sharingExecutor = new InheritedSharing(); diff --git a/force-app/main/default/classes/standard-soql/SOQL_Test.cls b/force-app/main/default/classes/standard-soql/SOQL_Test.cls index 657ad5ba..7a058483 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -3,7 +3,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class @@ -819,6 +819,19 @@ private class SOQL_Test { ); } + @IsTest + static void subQueryOrderByDistance() { + // Test + String soql = SOQL.of(Account.SObjectType).with(SOQL.SubQuery.of('Contacts').orderBy(SOQL.Distance.of(Contact.MailingAddress).between(0, 0).km())).toString(); + + // Verify + Assert.areEqual( + 'SELECT Id , (SELECT Id FROM Contacts ORDER BY DISTANCE(MailingAddress, GEOLOCATION(0.0,0.0),\' km\') ASC NULLS FIRST) FROM Account', + soql, + 'The generated SOQL should match the expected one.' + ); + } + @IsTest static void subQuerySetLimit() { // Test @@ -855,6 +868,72 @@ private class SOQL_Test { 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 subQueryFromSelector() { + // Test + String soql = SOQL.of(Account.SObjectType).with(Account.Name).with(SOQL.SubQuery.of('Contacts', new SOQL_Contact())).toString(); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contact) FROM Account', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void subQueryFromSelectorWithFilter() { + // Test + SOQL.Queryable soql = SOQL.of(Account.SObjectType).with(Account.Name).with(SOQL.SubQuery.of('Contacts', new SOQL_Contact().byLastName('Doe'))); + + // Verify + Assert.areEqual('SELECT Name , (SELECT Id, Name FROM Contact WHERE LastName = :v1) FROM Account', soql.toString(), 'The generated SOQL should match the expected one.'); + Assert.areEqual('Doe', soql.binding().get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void subQueryFromSelectorWithOrderBy() { + // Test + SOQL.Queryable soql = SOQL.of(Account.SObjectType) + .with(Account.Name) + .with(SOQL.SubQuery.of('Contacts', new SOQL_Contact().byFirstName('John').orderBy(Contact.Name).sortDesc().nullsLast())); + + // Verify + Assert.areEqual( + 'SELECT Name , (SELECT Id, Name FROM Contact WHERE FirstName = :v1 ORDER BY Name DESC NULLS LAST) FROM Account', + soql.toString(), + 'The generated SOQL should match the expected one.' + ); + Assert.areEqual('John', soql.binding().get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void subQueryFromSelectorWithLimit() { + // Test + SOQL.Queryable soql = SOQL.of(Account.SObjectType).with(Account.Name).with(SOQL.SubQuery.of('Contacts', new SOQL_Contact().byEmailDomain('example.com').setLimit(10))); + + // Verify + Assert.areEqual( + 'SELECT Name , (SELECT Id, Name FROM Contact WHERE Email LIKE :v1 LIMIT 10) FROM Account', + soql.toString(), + 'The generated SOQL should match the expected one.' + ); + Assert.areEqual('%@example.com', soql.binding().get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void subQueryFromSelectorWithOffset() { + // Setup + Id accountId = SOQL.IdGenerator.get(Account.SObjectType); + + // Test + SOQL.Queryable soql = SOQL.of(Account.SObjectType).with(Account.Name).with(SOQL.SubQuery.of('Contacts', new SOQL_Contact().byAccountId(accountId).offset(100))); + + // Verify + Assert.areEqual( + 'SELECT Name , (SELECT Id, Name FROM Contact WHERE AccountId = :v1 OFFSET 100) FROM Account', + soql.toString(), + 'The generated SOQL should match the expected one.' + ); + Assert.areEqual(accountId, soql.binding().get('v1'), 'The binding variable should match the expected value.'); + } + @IsTest static void multipleSubQueriesWithConditions() { // Setup @@ -863,7 +942,7 @@ private class SOQL_Test { Date toDate = Date.newInstance(2024, 1, 30); // Test - SOQL.Queryable builder = SOQL.of(Account.SObjectType) + SOQL.Queryable soql = SOQL.of(Account.SObjectType) .with( SOQL.SubQuery.of('Contacts') .with(Contact.Id) @@ -885,11 +964,11 @@ private class SOQL_Test { // 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(), + soql.toString(), 'The generated SOQL should match the expected one.' ); - Map binding = builder.binding(); + Map binding = soql.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.'); @@ -1387,6 +1466,96 @@ private class SOQL_Test { Assert.areEqual('SELECT Id FROM Account WHERE CreatedDate > LAST_N_QUARTERS:2', soql, 'The generated SOQL should match the expected one.'); } + @IsTest + static void filterDistanceInMiles() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') < :v1', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterDistanceInKilometers() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).km()).lessThan(5)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' km\') < :v1', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterDistanceWithRelationship() { + // Test + String soql = SOQL.of(Contact.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of('Account', Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) + .toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Contact WHERE DISTANCE(Account.BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') < :v1', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void filterDistanceWithLatLngOverload() { + // Test + String soql = SOQL.of(Account.SObjectType).whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(37.775, -122.418).mi()).lessThan(5)).toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(37.775,-122.418),\' mi\') < :v1', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void filterDistancePreservesLatLngOrder() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(10.5, 20.5)).mi()).lessThan(5)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(10.5,20.5),\' mi\') < :v1', soql, 'The generated SOQL should match the expected one.'); + } + + @IsTest + static void filterDistanceBindsOnlyThreshold() { + // Test + SOQL.Queryable builder = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') < :v1', + builder.toString(), + 'The generated SOQL should match the expected one.' + ); + + Map binding = builder.binding(); + Assert.areEqual(1, binding.size(), 'Only the distance threshold should be bound — GEOLOCATION is inlined as a literal.'); + Assert.areEqual(5, binding.get('v1'), 'The binding variable should match the expected value.'); + } + + @IsTest + static void filterDistanceGreaterThan() { + // Test + String soql = SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).greaterThan(100)) + .toString(); + + // Verify + Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') > :v1', soql, 'The generated SOQL should match the expected one.'); + } + @IsTest static void filtersGroup() { // Test @@ -2735,6 +2904,58 @@ private class SOQL_Test { 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 orderByDistanceInKilometers() { + // Test + String soql = SOQL.of(Account.SObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km()).toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' km\') ASC NULLS FIRST', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void orderByDistanceInMiles() { + // Test + String soql = SOQL.of(Account.SObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(37.775, -122.418)).mi()).toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress, GEOLOCATION(37.775,-122.418),\' mi\') ASC NULLS FIRST', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void orderByDistanceWithRelationship() { + // Test + String soql = SOQL.of(Contact.SObjectType).orderBy(SOQL.Distance.of('Account', Account.BillingAddress).between(0, 0).km()).toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Contact ORDER BY DISTANCE(Account.BillingAddress, GEOLOCATION(0.0,0.0),\' km\') ASC NULLS FIRST', + soql, + 'The generated SOQL should match the expected one.' + ); + } + + @IsTest + static void orderByDistanceDescNullsLast() { + // Test + String soql = SOQL.of(Account.SObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km()).sortDesc().nullsLast().toString(); + + // Verify + Assert.areEqual( + 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' km\') DESC NULLS LAST', + soql, + 'The generated SOQL should match the expected one.' + ); + } + @IsTest static void setLimit() { // Test @@ -3016,6 +3237,21 @@ private class SOQL_Test { Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.'); } + @IsTest + static void mockingSingleRecordWhereSobjectPrefixIsNull() { + // Setup + Id recordId = SOQL.IdGenerator.get(Account.SObjectType); + UserRecordAccess testRecord = (UserRecordAccess) JSON.deserialize(JSON.serialize(new Map{ 'RecordId' => recordId }), UserRecordAccess.class); + + // Test + SOQL.mock('mockingQuery').thenReturn(testRecord); + UserRecordAccess result = (UserRecordAccess) SOQL.of(UserRecordAccess.SObjectType).with(UserRecordAccess.RecordId).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual(recordId, result.RecordId, 'The result record ID should be the same as the mocked record ID.'); + Assert.isNull(result.Id, 'The result record Id should be null because it\'s not a real record and the SObject prefix is null.'); + } + @IsTest static void mockingQueryException() { // Test @@ -3825,6 +4061,38 @@ private class SOQL_Test { Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should have been exceeded.'); } + @IsTest + static void getQueriesInitiallyZero() { + // Verify + Assert.areEqual(0, SOQL.getQueries(), 'The number of issued queries should be 0 before any query is executed.'); + } + + @IsTest + static void getQueriesIncrementsAfterMockedQuery() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List()); + + // Test + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(1, SOQL.getQueries(), 'The number of issued queries should be 1 after one mocked query is executed.'); + } + + @IsTest + static void getQueriesTracksMultipleMockedQueries() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List()); + + // Test + for (Integer i = 0; i < 5; i++) { + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + } + + // Verify + Assert.areEqual(5, SOQL.getQueries(), 'The number of issued queries should match the number of mocked queries executed.'); + } + @IsTest static void queryConverterToIdsOf() { // Setup @@ -3895,4 +4163,34 @@ private class SOQL_Test { // Test SOQL.of(Account.SObjectType).count().preview().toInteger(); } + + public inherited sharing class SOQL_Contact extends SOQL { + public SOQL_Contact() { + super(Contact.SObjectType); + with(Contact.Id, Contact.Name); + systemMode(); + withoutSharing(); + mockId('SOQL_Contact'); + } + + public SOQL_Contact byLastName(String lastName) { + whereAre(Filter.with(Contact.LastName).equal(lastName)); + return this; + } + + public SOQL_Contact byFirstName(String firstName) { + whereAre(Filter.with(Contact.FirstName).equal(firstName).ignoreWhen(firstName == null)); + return this; + } + + public SOQL_Contact byAccountId(Id accountId) { + whereAre(Filter.with(Contact.AccountId).equal(accountId)); + return this; + } + + public SOQL_Contact byEmailDomain(String domain) { + whereAre(Filter.with(Contact.Email).endsWith('@' + domain)); + return this; + } + } } diff --git a/package/main/default/classes/cached-soql/SOQLCache.cls b/package/main/default/classes/cached-soql/SOQLCache.cls index 6ea87f5b..27d2b2ae 100644 --- a/package/main/default/classes/cached-soql/SOQLCache.cls +++ b/package/main/default/classes/cached-soql/SOQLCache.cls @@ -2,7 +2,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query diff --git a/package/main/default/classes/cached-soql/SOQLCache_Full_Test.cls b/package/main/default/classes/cached-soql/SOQLCache_Full_Test.cls index f13a9ca8..3259357f 100644 --- a/package/main/default/classes/cached-soql/SOQLCache_Full_Test.cls +++ b/package/main/default/classes/cached-soql/SOQLCache_Full_Test.cls @@ -3,7 +3,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class diff --git a/package/main/default/classes/soql-evaluator/SOQLEvaluator.cls b/package/main/default/classes/soql-evaluator/SOQLEvaluator.cls index 1b0e2e46..549a438b 100644 --- a/package/main/default/classes/soql-evaluator/SOQLEvaluator.cls +++ b/package/main/default/classes/soql-evaluator/SOQLEvaluator.cls @@ -2,7 +2,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - CognitiveComplexity: It is a library and we tried to put everything into ONE class diff --git a/package/main/default/classes/soql-evaluator/SOQLEvaluator_Full_Test.cls b/package/main/default/classes/soql-evaluator/SOQLEvaluator_Full_Test.cls index 4d0f908a..3796cf7c 100644 --- a/package/main/default/classes/soql-evaluator/SOQLEvaluator_Full_Test.cls +++ b/package/main/default/classes/soql-evaluator/SOQLEvaluator_Full_Test.cls @@ -3,7 +3,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class diff --git a/package/main/default/classes/standard-soql/SOQL.cls b/package/main/default/classes/standard-soql/SOQL.cls index 3bfa8ccc..3cf3e40c 100644 --- a/package/main/default/classes/standard-soql/SOQL.cls +++ b/package/main/default/classes/standard-soql/SOQL.cls @@ -2,7 +2,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query @@ -423,6 +423,14 @@ global virtual inherited sharing class SOQL implements Queryable { } } + global static Integer getQueries() { + if (System.Test.isRunningTest() && !System.isBatch() && !System.isFuture() && !System.isQueueable() && !System.isScheduled()) { + return SOQL.syncQueriesIssued; + } + + return Limits.getQueries(); + } + // Mocking global static Mockable mock(SObjectType ofObject) { @@ -2657,10 +2665,14 @@ global virtual inherited sharing class SOQL implements Queryable { } private void addIdToMockedRecords() { - // Id is always added to mirror standard SOQL behavior + // Id is added to mirror standard SOQL behavior SObjectType sObjectType = this.mockedRecords[0].getSObjectType(); String sObjectPrefix = sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix(); + if (String.isBlank(sObjectPrefix)) { + return; + } + for (SObject record : this.mockedRecords) { record.put('Id', record?.Id ?? IdGenerator.get(sObjectPrefix)); } diff --git a/package/main/default/classes/standard-soql/SOQL_Full_Test.cls b/package/main/default/classes/standard-soql/SOQL_Full_Test.cls index 031444ef..71e26020 100644 --- a/package/main/default/classes/standard-soql/SOQL_Full_Test.cls +++ b/package/main/default/classes/standard-soql/SOQL_Full_Test.cls @@ -3,7 +3,7 @@ * Copyright (c) 2026 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.10.2 + * v6.11.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class @@ -3293,6 +3293,21 @@ private class SOQL_Full_Test { Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.'); } + @IsTest + static void mockingSingleRecordWhereSobjectPrefixIsNull() { + // Setup + Id recordId = SOQL.IdGenerator.get(Account.SObjectType); + UserRecordAccess testRecord = (UserRecordAccess) JSON.deserialize(JSON.serialize(new Map{ 'RecordId' => recordId }), UserRecordAccess.class); + + // Test + SOQL.mock('mockingQuery').thenReturn(testRecord); + UserRecordAccess result = (UserRecordAccess) SOQL.of(UserRecordAccess.SObjectType).with(UserRecordAccess.RecordId).mockId('mockingQuery').toObject(); + + // Verify + Assert.areEqual(recordId, result.RecordId, 'The result record ID should be the same as the mocked record ID.'); + Assert.isNull(result.Id, 'The result record Id should be null because it\'s not a real record and the SObject prefix is null.'); + } + @IsTest static void mockingQueryException() { // Test @@ -4666,6 +4681,38 @@ private class SOQL_Full_Test { Assert.areEqual('Too many SOQL queries.', queryException.getMessage(), 'The synchronous query should have been exceeded.'); } + @IsTest + static void getQueriesInitiallyZero() { + // Verify + Assert.areEqual(0, SOQL.getQueries(), 'The number of issued queries should be 0 before any query is executed.'); + } + + @IsTest + static void getQueriesIncrementsAfterMockedQuery() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List()); + + // Test + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + + // Verify + Assert.areEqual(1, SOQL.getQueries(), 'The number of issued queries should be 1 after one mocked query is executed.'); + } + + @IsTest + static void getQueriesTracksMultipleMockedQueries() { + // Setup + SOQL.mock('mockingQuery').thenReturn(new List()); + + // Test + for (Integer i = 0; i < 5; i++) { + SOQL.of(Account.SObjectType).mockId('mockingQuery').toList(); + } + + // Verify + Assert.areEqual(5, SOQL.getQueries(), 'The number of issued queries should match the number of mocked queries executed.'); + } + @IsTest static void queryConverterToIdsOf() { // Setup diff --git a/sfdx-project.json b/sfdx-project.json index c6fab581..2a845cef 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -36,4 +36,4 @@ "SOQL Lib@6.10.1-1": "04tP60000036lfFIAQ", "SOQL Lib@6.10.2-1": "04tP60000038Fo9IAE" } -} \ No newline at end of file +} diff --git a/website/docs/soql/api/soql-distance.md b/website/docs/soql/api/soql-distance.md new file mode 100644 index 00000000..3ec4764a --- /dev/null +++ b/website/docs/soql/api/soql-distance.md @@ -0,0 +1,206 @@ +--- +sidebar_position: 30 +--- + +# Distance + +Perform location based calculations. + +```apex +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -135.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +## Methods + +The following are methods for `Distance`. + +[**FIELDS**](#fields) + +- [`of(SObjectField ofField)`](#of-sobject-field) +- [`of(String relationshipName, SObjectField ofField)`](#of-related-field) + +[**COMPERATORS**](#comperators) + +- [`between(Location loc)`](#between-location) +- [`between(Decimal latitude, Decimal longitude)`](#between-latitude-longitude) + +[**UNITS**](#units) + +- [`mi()`](#mi) +- [`km()`](#km) + +## FIELDS + +### of sobject field + +Specify the geolocation field used in the distance calculation. + +**Signature** + +```apex +Distance of(SObjectField ofField) +``` + +**Example** + +```sql +SELECT Id +FROM Account +WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` +```apex +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +### of related field + +Specify a parent relationship geolocation field used in the distance calculation. + +**Signature** + +```apex +Distance of(String relationshipName, SObjectField ofField) +``` + +**Example** + +```sql +SELECT Id +FROM Contact +WHERE DISTANCE(Account.BillingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` +```apex +SOQL.of(Contact.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of('Account', Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +## COMPERATORS + +### between location + +Specify the target geolocation using a `Location` instance. + +**Signature** + +```apex +Distance between(Location loc) +``` + +**Example** + +```sql +SELECT Id +FROM Account +WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` +```apex +Location loc = Location.newInstance(72.0, -136.0); + +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(loc) + .mi() + ).lessThan(5)) + .toList(); +``` + +### between latitude longitude + +Specify the target geolocation using latitude and longitude values. + +**Signature** + +```apex +Distance between(Decimal latitude, Decimal longitude) +``` + +**Example** + +```sql +SELECT Id +FROM Account +WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` +```apex +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +## UNITS + +### mi + +Return the distance in miles. + +**Signature** + +```apex +Distance mi() +``` + +**Example** + +```sql +SELECT Id +FROM Account +WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` +```apex +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +### km + +Return the distance in kilometers. + +**Signature** + +```apex +Distance km() +``` + +**Example** + +```sql +SELECT Id +FROM Account +WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0, -136.0), 'km') < 5 +``` +```apex +SOQL.of(Account.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .km() + ).lessThan(5)) + .toList(); +``` diff --git a/website/docs/soql/examples/distance.md b/website/docs/soql/examples/distance.md new file mode 100644 index 00000000..46d5f51e --- /dev/null +++ b/website/docs/soql/examples/distance.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 30 +--- + +# DISTANCE + +For more details check [SOQL API - DISTANCE](../api/soql-distance.md). + +> **NOTE! 🚨** +> All examples use inline queries built with the SOQL Lib Query Builder. +> If you are using a selector, replace `SOQL.of(...)` with `YourSelectorName.query()`. + +## FILTER + +**SOQL** + +```sql title="Traditional SOQL" +SELECT Id +FROM Contact +WHERE DISTANCE(MailingAddress, GEOLOCATION(72.0, -136.0), 'mi') < 5 +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +SOQL.of(Contact.SObjectType) + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Contact.MailingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); +``` + +## ORDERING + +**SOQL** + +```sql title="Traditional SOQL" +SELECT Id +FROM Contact +ORDER BY DISTANCE(MailingAddress, GEOLOCATION(72.0, -136.0), 'mi') +``` + +**SOQL Lib** + +```apex title="SOQL Lib Approach" +SOQL.of(Contact.SObjectType) + .orderBy( + SOQL.Distance.of(Contact.MailingAddress) + .between(72.0, -136.0) + .mi() + ) + .toList(); +```