From 6a364262d075572917ad7193c7c3da274c9f384b Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Sat, 16 May 2026 16:59:12 +0200 Subject: [PATCH 01/11] getQueries --- .../default/classes/standard-soql/SOQL.cls | 9 ++++++ .../classes/standard-soql/SOQL_Test.cls | 32 +++++++++++++++++++ .../default/classes/standard-soql/SOQL.cls | 8 +++++ .../classes/standard-soql/SOQL_Full_Test.cls | 32 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index bdd415b3..0dc0548e 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -444,6 +444,15 @@ public virtual inherited sharing class SOQL implements Queryable { } } + @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 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..f8c9c7b1 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -3825,6 +3825,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 diff --git a/package/main/default/classes/standard-soql/SOQL.cls b/package/main/default/classes/standard-soql/SOQL.cls index 3bfa8ccc..9831f173 100644 --- a/package/main/default/classes/standard-soql/SOQL.cls +++ b/package/main/default/classes/standard-soql/SOQL.cls @@ -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) { 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..230b7d05 100644 --- a/package/main/default/classes/standard-soql/SOQL_Full_Test.cls +++ b/package/main/default/classes/standard-soql/SOQL_Full_Test.cls @@ -4666,6 +4666,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 From aa7acdf087ca71ec714ea68daa51e39b81237aa3 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Sat, 16 May 2026 17:00:28 +0200 Subject: [PATCH 02/11] v6.11.0 --- force-app/main/default/classes/cached-soql/SOQLCache.cls | 2 +- force-app/main/default/classes/cached-soql/SOQLCache_Test.cls | 2 +- force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls | 2 +- .../main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls | 2 +- force-app/main/default/classes/standard-soql/SOQL.cls | 2 +- force-app/main/default/classes/standard-soql/SOQL_Test.cls | 2 +- package/main/default/classes/cached-soql/SOQLCache.cls | 2 +- .../main/default/classes/cached-soql/SOQLCache_Full_Test.cls | 2 +- package/main/default/classes/soql-evaluator/SOQLEvaluator.cls | 2 +- .../default/classes/soql-evaluator/SOQLEvaluator_Full_Test.cls | 2 +- package/main/default/classes/standard-soql/SOQL.cls | 2 +- package/main/default/classes/standard-soql/SOQL_Full_Test.cls | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) 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 0dc0548e..32014747 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 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 f8c9c7b1..fa847262 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 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 9831f173..50c3dd5b 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 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 230b7d05..07b0d111 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 From dac68a7a2de6f69ceb44bb1957e4396de133337b Mon Sep 17 00:00:00 2001 From: Ondrej Kratochvil <26163421+kratoon@users.noreply.github.com> Date: Sat, 16 May 2026 21:21:58 +0200 Subject: [PATCH 03/11] Skip ID generation for virtual sObjects without key prefix (#302) Add null check for sObject key prefix before generating record IDs. Prevents IDs from being created for virtual records such as UserRecordAccess, which do not have persistent IDs or a valid prefix. Without this check, mocking of such records fails with an exception: ``` System.StringException: Invalid id: null0000jfqTrzkY Class.SOQL.RandomIdGenerator.get: line 3401, column 1 ``` --- .../main/default/classes/standard-soql/SOQL.cls | 6 +++++- .../default/classes/standard-soql/SOQL_Test.cls | 15 +++++++++++++++ .../main/default/classes/standard-soql/SOQL.cls | 6 +++++- .../classes/standard-soql/SOQL_Full_Test.cls | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index 32014747..aa5cb7cc 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -2824,10 +2824,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)); } 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 fa847262..26bf2954 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -3016,6 +3016,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 diff --git a/package/main/default/classes/standard-soql/SOQL.cls b/package/main/default/classes/standard-soql/SOQL.cls index 50c3dd5b..3cf3e40c 100644 --- a/package/main/default/classes/standard-soql/SOQL.cls +++ b/package/main/default/classes/standard-soql/SOQL.cls @@ -2665,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 07b0d111..71e26020 100644 --- a/package/main/default/classes/standard-soql/SOQL_Full_Test.cls +++ b/package/main/default/classes/standard-soql/SOQL_Full_Test.cls @@ -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 From 30fe937c25b3944c28ce1a66713a27aa58c344e7 Mon Sep 17 00:00:00 2001 From: Ethan Phillippi Date: Wed, 20 May 2026 13:55:20 -0400 Subject: [PATCH 04/11] Distance Function (#273) --- .../default/classes/standard-soql/SOQL.cls | 102 ++++++++++- .../classes/standard-soql/SOQL_Test.cls | 42 +++++ sfdx-project.json | 2 +- website/docs/soql/api/soql-distance.md | 160 ++++++++++++++++++ website/docs/soql/examples/distance.md | 57 +++++++ 5 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 website/docs/soql/api/soql-distance.md create mode 100644 website/docs/soql/examples/distance.md diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index aa5cb7cc..bb806e08 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -135,6 +135,8 @@ 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 orderBy(Distance distance, String direction); Queryable sortDesc(); Queryable sort(String direction); Queryable nullsLast(); @@ -231,6 +233,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 +274,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 +399,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 +461,13 @@ 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()) { @@ -1002,6 +1026,19 @@ 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 orderBy(Distance distance, String direction) { + this.builder.orderBys.newOrderBy().with(distance); + this.builder.orderBys.latestOrderBy().sortingOrder(direction); + return this; + } + @NamespaceAccessible public Queryable sortDesc() { return this.sort('DESC'); @@ -1755,6 +1792,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; @@ -2022,6 +2064,7 @@ public virtual inherited sharing class SOQL implements Queryable { private class SoqlFilter implements Filter { private String field; + private Distance distance; private String comparator; private Object value; private String wrapper = '{0}'; @@ -2052,6 +2095,11 @@ public virtual inherited sharing class SOQL implements Queryable { return this; } + public Filter with(Distance distance) { + this.distance = distance; + return this; + } + public Filter isNull() { return this.equal(null); } @@ -2206,7 +2254,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public Boolean hasValue() { - return String.isNotEmpty(this.field); + return String.isNotEmpty(this.field) || this.distance != null; } public Filter ignoreWhen(Boolean logicExpression) { @@ -2224,7 +2272,8 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - return String.format(this.wrapper, new List{ this.field + ' ' + this.comparator + (this.skipBinding ? ' ' + this.value : ' :' + binder.bind(this.value)) }); + String fieldStr = this.distance != null ? this.distance.toString() : this.field; + return String.format(this.wrapper, new List{ fieldStr + ' ' + this.comparator + (this.skipBinding ? ' ' + this.value : ' :' + binder.bind(this.value)) }); } } @@ -2637,6 +2686,7 @@ public virtual inherited sharing class SOQL implements Queryable { private class SoqlOrderBy implements QueryClause { private String orderField; + private Distance orderDistance; private String sortingOrder = 'ASC'; private String nullsOrder = 'FIRST'; @@ -2644,6 +2694,10 @@ public virtual inherited sharing class SOQL implements Queryable { this.orderField = field; } + public void with(Distance distance) { + this.orderDistance = distance; + } + public void sortingOrder(String direction) { this.sortingOrder = direction; } @@ -2653,7 +2707,8 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - return this.orderField + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; + String fieldStr = this.orderDistance != null ? this.orderDistance.toString() : this.orderField; + return fieldStr + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; } } @@ -2940,6 +2995,47 @@ public virtual inherited sharing class SOQL implements Queryable { } } + private class SoqlDistance implements Distance { + private String field; + private Location comparisonLocation; + private String unit = 'km'; + + 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 = location; + + return this; + } + public Distance between(Decimal latitude, Decimal longitude) { + return this.between(Location.newInstance(latitude, longitude)); + } + + public Distance mi() { + this.unit = 'mi'; + + return this; + } + public Distance km() { + this.unit = 'km'; + + return this; + } + + public override String toString() { + return String.format('DISTANCE({0},:{1},\'\'{2}\'\')', new List{ this.field, binder.bind(this.comparisonLocation), this.unit }); + } + } + private class ExceptionMock { private QueryException queryException; 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 26bf2954..669bc13c 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -3942,4 +3942,46 @@ private class SOQL_Test { // Test SOQL.of(Account.SObjectType).count().preview().toInteger(); } + + @IsTest + static void distanceFilter() { + String query = SOQL.of(Account.sObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) + .toString(); + + Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress,:v1,\'mi\') < :v2', query, 'The generate soql does not match the expected value'); + } + @IsTest + static void distanceFilterRelationship() { + String query = SOQL.of(Contact.sObjectType) + .whereAre(SOQL.Filter.with(SOQL.Distance.of('Account', Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) + .toString(); + + Assert.areEqual('SELECT Id FROM Contact WHERE DISTANCE(Account.BillingAddress,:v1,\'mi\') < :v2', query, 'The generate soql does not match the expected value'); + } + + @IsTest + static void distanceOrder() { + String query = SOQL.of(Account.sObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km()).toString(); + + Assert.areEqual('SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,:v1,\'km\') ASC NULLS FIRST', query, 'The generate soql does not match the expected value'); + } + + @IsTest + static void distanceOrderDesc() { + String query = SOQL.of(Account.sObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km(), 'DESC').toString(); + + Assert.areEqual('SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,:v1,\'km\') DESC NULLS FIRST', query, 'The generate soql does not match the expected value'); + } + + @IsTest + static void distanceSubqueryOrder() { + String query = SOQL.of(Account.sObjectType).with(SOQL.subquery.of('Contacts').orderBy(SOQL.Distance.of(Contact.MailingAddress).between(0, 0).km())).toString(); + + Assert.areEqual( + 'SELECT Id , (SELECT Id FROM Contacts ORDER BY DISTANCE(MailingAddress,:v1,\'km\') ASC NULLS FIRST) FROM Account', + query, + 'The generate soql does not match the expected value' + ); + } } 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..d577aa44 --- /dev/null +++ b/website/docs/soql/api/soql-distance.md @@ -0,0 +1,160 @@ +--- +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) +- [`of(String relationshipName, SObjectField ofField)`](#of) + +[**COMPERATORS**](#comperators) + +- [`between(Location loc)`](#between) +- [`between(Decimal latitude, Decimal longitude)`](#between) + +[**UNITS**](#units) + +- [`mi()`](#mi) +- [`km()`](#km) + +## FIELDS +### of + +- `DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi')` + +**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(); +``` + +## COMPERATORS + +### between + +- `DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi')` + +**Signature** + +```apex +Distance between(Location loc) +Distance between(Decimal latitude, Decimal longitude) +``` + +**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(); +``` + +## 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..c50eb394 --- /dev/null +++ b/website/docs/soql/examples/distance.md @@ -0,0 +1,57 @@ +--- +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(); +``` From a7e36741901874fc4e89778b4bc665f02f847566 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Wed, 20 May 2026 20:38:38 +0200 Subject: [PATCH 05/11] Distance function refactoring --- .../default/classes/standard-soql/SOQL.cls | 28 +-- .../classes/standard-soql/SOQL_Test.cls | 173 +++++++++++++----- website/docs/soql/api/soql-distance.md | 150 +++++++++------ website/docs/soql/examples/distance.md | 30 ++- 4 files changed, 251 insertions(+), 130 deletions(-) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index bb806e08..7ba702e5 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -136,7 +136,6 @@ public virtual inherited sharing class SOQL implements Queryable { Queryable orderBy(String relationshipName, SObjectField field); Queryable orderByCount(SObjectField field); Queryable orderBy(Distance distance); - Queryable orderBy(Distance distance, String direction); Queryable sortDesc(); Queryable sort(String direction); Queryable nullsLast(); @@ -1032,13 +1031,6 @@ public virtual inherited sharing class SOQL implements Queryable { return this; } - @NamespaceAccessible - public Queryable orderBy(Distance distance, String direction) { - this.builder.orderBys.newOrderBy().with(distance); - this.builder.orderBys.latestOrderBy().sortingOrder(direction); - return this; - } - @NamespaceAccessible public Queryable sortDesc() { return this.sort('DESC'); @@ -2686,7 +2678,6 @@ public virtual inherited sharing class SOQL implements Queryable { private class SoqlOrderBy implements QueryClause { private String orderField; - private Distance orderDistance; private String sortingOrder = 'ASC'; private String nullsOrder = 'FIRST'; @@ -2695,7 +2686,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public void with(Distance distance) { - this.orderDistance = distance; + this.orderField = distance.toString(); } public void sortingOrder(String direction) { @@ -2707,8 +2698,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - String fieldStr = this.orderDistance != null ? this.orderDistance.toString() : this.orderField; - return fieldStr + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; + return this.orderField + ' ' + this.sortingOrder + ' NULLS ' + this.nullsOrder; } } @@ -2997,42 +2987,40 @@ public virtual inherited sharing class SOQL implements Queryable { private class SoqlDistance implements Distance { private String field; - private Location comparisonLocation; 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 = 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() { this.unit = 'mi'; - return this; } + public Distance km() { this.unit = 'km'; - return this; } public override String toString() { - return String.format('DISTANCE({0},:{1},\'\'{2}\'\')', new List{ this.field, binder.bind(this.comparisonLocation), this.unit }); + return String.format('DISTANCE({0},{1},\'\'{2}\'\')', new List{ this.field, this.comparisonLocation, this.unit }); } } 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 669bc13c..6555829f 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -819,6 +819,21 @@ 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 @@ -1387,6 +1402,86 @@ 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 +2830,42 @@ 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 @@ -3942,46 +4073,4 @@ private class SOQL_Test { // Test SOQL.of(Account.SObjectType).count().preview().toInteger(); } - - @IsTest - static void distanceFilter() { - String query = SOQL.of(Account.sObjectType) - .whereAre(SOQL.Filter.with(SOQL.Distance.of(Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) - .toString(); - - Assert.areEqual('SELECT Id FROM Account WHERE DISTANCE(BillingAddress,:v1,\'mi\') < :v2', query, 'The generate soql does not match the expected value'); - } - @IsTest - static void distanceFilterRelationship() { - String query = SOQL.of(Contact.sObjectType) - .whereAre(SOQL.Filter.with(SOQL.Distance.of('Account', Account.BillingAddress).between(Location.newInstance(0, 0)).mi()).lessThan(5)) - .toString(); - - Assert.areEqual('SELECT Id FROM Contact WHERE DISTANCE(Account.BillingAddress,:v1,\'mi\') < :v2', query, 'The generate soql does not match the expected value'); - } - - @IsTest - static void distanceOrder() { - String query = SOQL.of(Account.sObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km()).toString(); - - Assert.areEqual('SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,:v1,\'km\') ASC NULLS FIRST', query, 'The generate soql does not match the expected value'); - } - - @IsTest - static void distanceOrderDesc() { - String query = SOQL.of(Account.sObjectType).orderBy(SOQL.Distance.of(Account.BillingAddress).between(0, 0).km(), 'DESC').toString(); - - Assert.areEqual('SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,:v1,\'km\') DESC NULLS FIRST', query, 'The generate soql does not match the expected value'); - } - - @IsTest - static void distanceSubqueryOrder() { - String query = SOQL.of(Account.sObjectType).with(SOQL.subquery.of('Contacts').orderBy(SOQL.Distance.of(Contact.MailingAddress).between(0, 0).km())).toString(); - - Assert.areEqual( - 'SELECT Id , (SELECT Id FROM Contacts ORDER BY DISTANCE(MailingAddress,:v1,\'km\') ASC NULLS FIRST) FROM Account', - query, - 'The generate soql does not match the expected value' - ); - } } diff --git a/website/docs/soql/api/soql-distance.md b/website/docs/soql/api/soql-distance.md index d577aa44..3ec4764a 100644 --- a/website/docs/soql/api/soql-distance.md +++ b/website/docs/soql/api/soql-distance.md @@ -8,13 +8,12 @@ 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(); + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -135.0) + .mi() + ).lessThan(5)) + .toList(); ``` ## Methods @@ -23,13 +22,13 @@ The following are methods for `Distance`. [**FIELDS**](#fields) -- [`of(SObjectField ofField)`](#of) -- [`of(String relationshipName, SObjectField ofField)`](#of) +- [`of(SObjectField ofField)`](#of-sobject-field) +- [`of(String relationshipName, SObjectField ofField)`](#of-related-field) [**COMPERATORS**](#comperators) -- [`between(Location loc)`](#between) -- [`between(Decimal latitude, Decimal longitude)`](#between) +- [`between(Location loc)`](#between-location) +- [`between(Decimal latitude, Decimal longitude)`](#between-latitude-longitude) [**UNITS**](#units) @@ -37,9 +36,10 @@ The following are methods for `Distance`. - [`km()`](#km) ## FIELDS -### of -- `DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi')` +### of sobject field + +Specify the geolocation field used in the distance calculation. **Signature** @@ -52,30 +52,83 @@ Distance of(SObjectField ofField) ```sql SELECT Id FROM Account -WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi') < 5 +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(); + .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 +### between location -- `DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi')` +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) ``` @@ -84,19 +137,16 @@ Distance between(Decimal latitude, Decimal longitude) ```sql SELECT Id FROM Account -WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi') < 5 +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(); + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); ``` ## UNITS @@ -116,18 +166,16 @@ Distance mi() ```sql SELECT Id FROM Account -WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'mi') < 5 +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(); + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Account.BillingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); ``` ### km @@ -145,16 +193,14 @@ Distance km() ```sql SELECT Id FROM Account -WHERE DISTANCE(BillingAddress, GEOLOCATION(72.0,-136.0), 'km') < 5 +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(); + .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 index c50eb394..46d5f51e 100644 --- a/website/docs/soql/examples/distance.md +++ b/website/docs/soql/examples/distance.md @@ -17,21 +17,19 @@ For more details check [SOQL API - DISTANCE](../api/soql-distance.md). ```sql title="Traditional SOQL" SELECT Id FROM Contact -WHERE DISTANCE(MailingAddress, GEOLOCATION(72.0,-136.0), 'mi') < 5 +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(); + .whereAre(SOQL.Filter.with( + SOQL.Distance.of(Contact.MailingAddress) + .between(72.0, -136.0) + .mi() + ).lessThan(5)) + .toList(); ``` ## ORDERING @@ -41,17 +39,17 @@ SOQL.of(Contact.SObjectType) ```sql title="Traditional SOQL" SELECT Id FROM Contact -ORDER BY DISTANCE(MailingAddress, GEOLOCATION(72.0,-136.0), 'mi') +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(); + .orderBy( + SOQL.Distance.of(Contact.MailingAddress) + .between(72.0, -136.0) + .mi() + ) + .toList(); ``` From f0a1cc62e1241fbdff338cb631e4944ce3c01af2 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Thu, 21 May 2026 11:58:15 +0200 Subject: [PATCH 06/11] Refactoring --- .../main/default/classes/standard-soql/SOQL.cls | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index 7ba702e5..262a7230 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -2056,7 +2056,6 @@ public virtual inherited sharing class SOQL implements Queryable { private class SoqlFilter implements Filter { private String field; - private Distance distance; private String comparator; private Object value; private String wrapper = '{0}'; @@ -2088,7 +2087,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public Filter with(Distance distance) { - this.distance = distance; + this.field = distance.toString(); return this; } @@ -2246,7 +2245,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public Boolean hasValue() { - return String.isNotEmpty(this.field) || this.distance != null; + return String.isNotEmpty(this.field); } public Filter ignoreWhen(Boolean logicExpression) { @@ -2264,8 +2263,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - String fieldStr = this.distance != null ? this.distance.toString() : this.field; - return String.format(this.wrapper, new List{ fieldStr + ' ' + this.comparator + (this.skipBinding ? ' ' + this.value : ' :' + binder.bind(this.value)) }); + return String.format(this.wrapper, new List{ this.field + ' ' + this.comparator + (this.skipBinding ? ' ' + this.value : ' :' + binder.bind(this.value)) }); } } @@ -3010,12 +3008,15 @@ public virtual inherited sharing class SOQL implements Queryable { } public Distance mi() { - this.unit = 'mi'; - return this; + return this.unit('mi'); } public Distance km() { - this.unit = 'km'; + return this.unit('km'); + } + + private Distance unit(String unit) { + this.unit = unit; return this; } From 0dbc5bb89ab16e63cb2a3299128365db31ba8b66 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Thu, 21 May 2026 11:58:59 +0200 Subject: [PATCH 07/11] Formatting --- .../classes/standard-soql/SOQL_Test.cls | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) 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 6555829f..8ac497f2 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -822,9 +822,7 @@ 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(); + 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( @@ -1432,18 +1430,24 @@ private class SOQL_Test { .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.'); + 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(); + 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.'); + 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 @@ -1464,7 +1468,11 @@ private class SOQL_Test { .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.'); + 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.'); @@ -2836,7 +2844,11 @@ private class SOQL_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.'); + 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 @@ -2845,7 +2857,11 @@ private class SOQL_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.'); + 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 @@ -2854,7 +2870,11 @@ private class SOQL_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.'); + 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 @@ -2863,7 +2883,11 @@ private class SOQL_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.'); + 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 From 9c7e92943d66e2dc5660dee0b4e453258f318f76 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Thu, 21 May 2026 13:03:24 +0200 Subject: [PATCH 08/11] Simplifying --- .../default/classes/standard-soql/SOQL.cls | 2 +- .../classes/standard-soql/SOQL_Test.cls | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index 262a7230..9b51a78c 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -3021,7 +3021,7 @@ public virtual inherited sharing class SOQL implements Queryable { } public override String toString() { - return String.format('DISTANCE({0},{1},\'\'{2}\'\')', new List{ this.field, this.comparisonLocation, this.unit }); + return 'DISTANCE(' + this.field + ', ' + this.comparisonLocation + ',\' ' + this.unit + '\')'; } } 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 8ac497f2..06255d49 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -826,7 +826,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id , (SELECT Id FROM Contacts ORDER BY DISTANCE(MailingAddress,GEOLOCATION(0.0,0.0),\'km\') ASC NULLS FIRST) FROM Account', + '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.' ); @@ -1408,7 +1408,7 @@ private class SOQL_Test { .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.'); + 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 @@ -1419,7 +1419,7 @@ private class SOQL_Test { .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.'); + 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 @@ -1431,7 +1431,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Contact WHERE DISTANCE(Account.BillingAddress,GEOLOCATION(0.0,0.0),\'mi\') < :v1', + 'SELECT Id FROM Contact WHERE DISTANCE(Account.BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') < :v1', soql, 'The generated SOQL should match the expected one.' ); @@ -1444,7 +1444,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress,GEOLOCATION(37.775,-122.418),\'mi\') < :v1', + 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(37.775,-122.418),\' mi\') < :v1', soql, 'The generated SOQL should match the expected one.' ); @@ -1458,7 +1458,7 @@ private class SOQL_Test { .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.'); + 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 @@ -1469,7 +1469,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress,GEOLOCATION(0.0,0.0),\'mi\') < :v1', + 'SELECT Id FROM Account WHERE DISTANCE(BillingAddress, GEOLOCATION(0.0,0.0),\' mi\') < :v1', builder.toString(), 'The generated SOQL should match the expected one.' ); @@ -1487,7 +1487,7 @@ private class SOQL_Test { .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.'); + 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 @@ -2845,7 +2845,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,GEOLOCATION(0.0,0.0),\'km\') ASC NULLS FIRST', + '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.' ); @@ -2858,7 +2858,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,GEOLOCATION(37.775,-122.418),\'mi\') ASC NULLS FIRST', + '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.' ); @@ -2871,7 +2871,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Contact ORDER BY DISTANCE(Account.BillingAddress,GEOLOCATION(0.0,0.0),\'km\') ASC NULLS FIRST', + '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.' ); @@ -2884,7 +2884,7 @@ private class SOQL_Test { // Verify Assert.areEqual( - 'SELECT Id FROM Account ORDER BY DISTANCE(BillingAddress,GEOLOCATION(0.0,0.0),\'km\') DESC NULLS LAST', + '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.' ); From 60b4eebac5d4dc2e452f1e18243c86f9a0b42630 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 22 May 2026 21:59:15 +0200 Subject: [PATCH 09/11] Cache fallback --- .../classes/cached-soql/CacheManager.cls | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) 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; } From 1fc3a392a6691585911b35b4006ea7ac7d245a82 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 22 May 2026 22:04:47 +0200 Subject: [PATCH 10/11] Date update --- force-app/main/default/classes/cached-soql/CacheManagerTest.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 251609d742cde5608a5003da31d5f640f748b95f Mon Sep 17 00:00:00 2001 From: Piotr Gajek Date: Sat, 23 May 2026 08:21:21 +0200 Subject: [PATCH 11/11] Queryable to SubQuery (#308) * Queryable to SubQuery * SubQuery with Queryable --- .../default/classes/standard-soql/SOQL.cls | 32 ++++-- .../classes/standard-soql/SOQL_Test.cls | 102 +++++++++++++++++- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index 9b51a78c..506803b1 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -204,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); @@ -539,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; @@ -550,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); @@ -1143,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 @@ -1375,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)); } @@ -1681,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; } @@ -2358,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; } @@ -3041,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 06255d49..7a058483 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -868,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 @@ -876,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) @@ -898,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.'); @@ -4097,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; + } + } }