Releases: beyond-the-cloud-dev/dml-lib
v3.1.0
v3.1.0 - 12-May-2026
Scope
- SObject Type Detection Hardening for
List<SObject>Collections - More Forgiving
DML.retrieveResultForBehavior - Repository Restructure (Package Directory)
Fixes
Reliable SObject Type Detection for List<SObject> Collections (#33)
List<SObject>.getSObjectType() returns null when the list is declared as a generic List<SObject> (rather than a concrete List<Account>, etc.), which previously caused the wrong strategy to be selected — or a NullPointerException — when callers passed a generic collection into DML operations.
The library now resolves the SObject type by iterating the records themselves (skipping null entries) and throws a clear DmlException if mixed SObject types are detected in a single list.
// Previously: silently misbehaved or threw NPE
// Now: works correctly
List<SObject> records = new List<SObject>{ new Account(Name = 'A'), new Account(Name = 'B') };
new DML().toInsert(records).commitWork();
// Now: throws a clear exception
// "Mixed SObject types in a single List<SObject> operation are not supported."
List<SObject> mixed = new List<SObject>{ new Account(Name = 'A'), new Contact(LastName = 'B') };
new DML().toInsert(mixed).commitWork();The same resolution logic is applied to toUpsert(List<SObject>, SObjectField).
Thanks to @tom-gangemi for the contribution.
Improvements
DML.retrieveResultFor No Longer Throws for Unknown Identifiers
DML.retrieveResultFor(String dmlIdentifier) previously threw a DmlException if no result was registered for the given identifier. It now returns an empty DML.Result, which is easier to work with in mocked test scenarios and conditional flows that may or may not have executed a commit.
// Before (v3.0.x): threw DmlException('No result found for dml identifier: ...')
// Now (v3.1.0): returns an empty DML.Result
DML.Result result = DML.retrieveResultFor('unknownIdentifier');
Assert.areEqual(0, result.all().size());Internal Changes
Repository Restructure: package/ Directory
The packaged source has been moved into a dedicated package/ directory:
package/— packaged source (DML Lib package contents, includingDML.clsandDML_Full_Test.cls)force-app/— default working directory for development (non-package)internal/— removed (its contents merged intopackage/)
v3.0.0
v3.0.0 - 31-January-2026
Scope
- Automatic Record-Level Dependency Resolution
- Performance Improvements
- Internal Architecture Refactoring
DML
- Automatic dependency resolution at the record level (not just SObject type level)
- Wave-based execution ensures parents are committed before dependents
- Optimal DML grouping minimizes the number of database operations
New Features
Automatic Record-Level Dependency Resolution
DML Lib now automatically tracks dependencies between individual records and executes them in optimized "waves". When you define relationships using withRelationship(), the library builds a dependency graph and ensures parent records are committed before their dependents.
Example: Complex Multi-Level Dependencies
Account parentAccount = new Account(Name = 'Parent');
Account childAccount = new Account(Name = 'Child');
Contact contact = new Contact(LastName = 'Smith');
Opportunity opportunity = new Opportunity(Name = 'Deal', StageName = 'New', CloseDate = Date.today());
new DML()
.toInsert(parentAccount)
.toInsert(DML.Record(childAccount).withRelationship(Account.ParentId, parentAccount))
.toInsert(DML.Record(contact).withRelationship(Contact.AccountId, childAccount))
.toInsert(DML.Record(opportunity).withRelationship(Opportunity.AccountId, childAccount))
.commitWork();
// Execution waves:
// Wave 1: parentAccount (no dependencies)
// Wave 2: childAccount (depends on parentAccount)
// Wave 3: contact, opportunity (both depend on childAccount)Wave-Based Execution
Records are now organized into execution waves based on their dependencies. Records in the same wave that share the same operation type and SObject type are batched into a single DML statement.
Example: Optimal DML Grouping
new DML()
.toInsert(account1)
.toInsert(account2)
.toUpsert(account3)
.toUpsert(account4)
.toInsert(DML.Record(account5).withRelationship(Account.ParentId, account2))
.toInsert(DML.Record(contact1).withRelationship(Contact.AccountId, account2))
.toInsert(DML.Record(contact2).withRelationship(Contact.AccountId, account3))
.toInsert(opportunity1)
.toInsert(DML.Record(opportunity2).withRelationship(Opportunity.AccountId, account4))
.toInsert(lead1)
.commitWork();
// Result: 7 DML statements executed (optimally grouped)| DML # | Operation | SObject | Records | Reason |
|---|---|---|---|---|
| 1 | INSERT | Account | account1, account2 | No dependencies, same bucket |
| 2 | UPSERT | Account | account3, account4 | No dependencies, different operation |
| 3 | INSERT | Opportunity | opportunity1 | No dependencies |
| 4 | INSERT | Lead | lead1 | No dependencies |
| 5 | INSERT | Account | account5 | Depends on account2 |
| 6 | INSERT | Contact | contact1, contact2 | Depend on account2 and account3 |
| 7 | INSERT | Opportunity | opportunity2 | Depends on account4 |
Improvements
Validation Improvements
Added null check validation when creating records by ID:
// Now throws: "Invalid argument: recordId. Record ID cannot be null."
DML.Record(null);Documentation
- Updated Registration documentation with detailed explanation of the dependency graph algorithm
- Added visual diagrams showing wave-based execution
- Added examples demonstrating optimal DML grouping
🚨 Breaking Changes 🚨
Removed: Custom Execution Order Constructor
The constructor that accepted a custom execution order has been removed:
// ❌ No longer available
new DML(new List<SObjectType>{ Account.SObjectType, Contact.SObjectType });
// ✅ Use automatic dependency resolution instead
new DML()
.toInsert(account)
.toInsert(DML.Record(contact).withRelationship(Contact.AccountId, account))
.commitWork();Migration: Remove the custom execution order parameter. The library now automatically determines the correct execution order based on defined relationships.
Changed: Dependency Detection
Previously, the library tracked dependencies at the SObject type level. Now it tracks at the record level, which provides more accurate execution ordering but may result in different execution patterns for complex scenarios.
Before (v2.x): All Accounts inserted first, then all Contacts
After (v3.x): Records inserted in waves based on actual dependencies between specific records
Internal Refactoring
- Replaced
DmlCommandclasses withDmlStrategypattern for cleaner separation of concerns - Split orchestration into
DependencyOrchestrator(INSERT/UPSERT) andLinearOrchestrator(UPDATE/DELETE/UNDELETE/MERGE/PUBLISH) - Introduced
StrategiesStoragefor strategy instance management - Changed from Kahn's algorithm on SObject types to Kahn's algorithm on individual records
- Improved
RandomIdGeneratorwith deterministic counter-based ID generation - Added
ProcessingGroupclass for efficient record batching - Introduced
ExecutionResultandSObjectAggregatedResultfor better result handling - Copyright year updated to 2026
v2.0.0
v2.0.0 - 21-December-2025
Scope
- New Features: Merge DML, Hard Delete, External ID Upsert, Exception Mocking
- Improvements: Duplicate Registration Handling, Refactored Architecture
DML
- Added
toMerge()method for merging duplicate records - Added
toHardDelete()method for permanently deleting records (bypasses recycle bin) - Added
toUpsert()with External ID field parameter - Added
combineOnDuplicate()for handling duplicate record registrations - Added exception mocking capabilities for testing error scenarios
New Features
Merge DML Support
New toMerge() method allows merging duplicate records into a master record. Supports all merge scenarios including single/multiple duplicates and ID-based merges.
Example: Merge Duplicate Records
Account masterAccount = [SELECT Id FROM Account WHERE ...];
Account duplicate1 = [SELECT Id FROM Account WHERE ...];
Account duplicate2 = [SELECT Id FROM Account WHERE ...];
new DML()
.toMerge(masterAccount, new List<Account>{ duplicate1, duplicate2 })
.systemMode()
.withoutSharing()
.commitWork();Supported Signatures
Commitable toMerge(SObject mergeToRecord, SObject duplicatedRecord);
Commitable toMerge(SObject mergeToRecord, List<SObject> duplicateRecords);
Commitable toMerge(SObject mergeToRecord, Id duplicatedRecordId);
Commitable toMerge(SObject mergeToRecord, Iterable<Id> duplicatedRecordIds);Hard Delete Support
New toHardDelete() method permanently deletes records, bypassing the Salesforce recycle bin.
Example: Hard Delete Records
new DML()
.toHardDelete(account)
.commitWork();
// Delete multiple records by ID
new DML()
.toHardDelete(accountIds)
.commitWork();Upsert with External ID Field
The toUpsert() method now supports specifying an external ID field for upsert operations.
Example: Upsert with External ID
new DML()
.toUpsert(contact, Contact.MyExternalId__c)
.commitWork();
// Multiple records
new DML()
.toUpsert(contacts, Contact.MyExternalId__c)
.commitWork();Duplicate Registration Handling
New combineOnDuplicate() method automatically merges duplicate registrations into a single record. When the same record ID is registered multiple times, field values from later registrations override earlier ones.
Example: Combine Duplicates
Account account = [SELECT Id, Name FROM Account LIMIT 1];
new DML()
.combineOnDuplicate()
.toUpdate(new Account(Id = account.Id, Name = 'New Account 1', Website = 'mywebsite.com'))
.toUpdate(new Account(Id = account.Id, Name = 'New Account 2'))
.commitWork();
// Result: Account updated with Name = 'New Account 2' and Website = 'mywebsite.com'Without combineOnDuplicate(), registering duplicate records throws: Duplicate records found during registration. Fix the code or use the combineOnDuplicate() method.
Exception Mocking
New mocking capabilities to simulate DML errors in unit tests. Supports both global and per-SObject type error scenarios.
Example: Mock DML Exceptions
@IsTest
static void shouldHandleInsertErrors() {
DML.mock('dmlMockId').exceptionOnInserts();
Exception expectedException = null;
try {
new DML()
.toInsert(new Account(Name = 'Test'))
.identifier('dmlMockId')
.commitWork();
} catch (DmlException e) {
expectedException = e;
}
Assert.isNotNull(expectedException);
}Supported Exception Mocking Methods
// All operations of a type
Mockable exceptionOnInserts();
Mockable exceptionOnUpdates();
Mockable exceptionOnUpserts();
Mockable exceptionOnDeletes();
Mockable exceptionOnUndeletes();
Mockable exceptionOnMerges();
Mockable exceptionOnPublishes();
// Per SObject type
Mockable exceptionOnInsertsFor(SObjectType objectType);
Mockable exceptionOnUpdatesFor(SObjectType objectType);
Mockable exceptionOnUpsertsFor(SObjectType objectType);
Mockable exceptionOnDeletesFor(SObjectType objectType);
Mockable exceptionOnUndeletesFor(SObjectType objectType);
Mockable exceptionOnMergesFor(SObjectType objectType);
Mockable exceptionOnPublishesFor(SObjectType objectType);Improvements
New Result Methods
Added new methods to access merge operation results:
List<OperationResult> merges();
OperationResult mergesOf(Schema.SObjectType objectType);New Mocking Methods
Added new mocking methods for merge operations:
Mockable allMerges();
Mockable mergesFor(SObjectType objectType);Documentation
- Added comprehensive documentation for Registration (deduplication strategy, registration order, minimal DMLs)
- Added documentation for Merge operations
- Added mocking documentation for Merge operations
Internal Refactoring
- Refactored from
DmlOperationclasses to a Command pattern (DmlCommand) - Introduced
Orchestratorclass for coordinating DML operations - Improved deduplication handling with strategy pattern (
DuplicateCombineStrategy) - Fixed typo: "rgistration" → "registration" in cyclic dependency error message
- Fixed typo: "is not a marked as" → "is not marked as" in external ID validation
🚨 Breaking Changes 🚨
There are no breaking changes in this release. All public APIs remain backward compatible. The internal refactoring from DmlOperation to DmlCommand pattern is internal to the library.
v1.9.0
Release Notes - v1.9.0
New Features
Automatic DML Execution Order
- Implemented Kahn's topological sort algorithm for automatic dependency resolution
- DML operations are now automatically ordered based on parent-child relationships
Unlocked Package Distribution
- DML Lib is now available as an unlocked package with namespace
btcdev - Package ID:
04tP6000002AvdNIAS - Install via Salesforce UI or CLI
Deploy via Button
- Added one-click deployment option using GitHub Deploy button
- Simplifies installation for quick setup
Improvements
Global Access Modifier
- Class and interfaces changed from
publictoglobalto support package versioning - Enables cross-namespace usage when installed as a package
Public Mocking API
DML.mock()andDML.retrieveResultFor()methods are now globally accessible- Previously required
@TestVisibleannotation for test access
Installation Options
Unlocked Package (with namespace btcdev):
v1.8.0
Release Notes - v1.8.0
New Features
discardWork() Method
- Added new
discardWork()method to theCommitableinterface - Allows users to clear all queued DML operations without executing them
- Useful for scenarios where pending work needs to be cancelled
Improvements
Enhanced Hook Interface
- The
after()hook method now receives theResultparameter - Enables hooks to access and process DML operation results
- Signature changed from
void after()tovoid after(Result result)
v1.7.0
v1.7.0-beta v1.7.0
v1.6.0-beta
v1.6.0
v1.5.0 (Beta)
v1.5.0-beta DML Results
v1.4.0 (Beta)
v1.4.0-beta todos
v1.3.0 (Beta)
v1.3.0 (Beta)