Skip to content

Releases: beyond-the-cloud-dev/dml-lib

v3.1.0

12 May 20:10

Choose a tag to compare

v3.1.0 - 12-May-2026

Scope

  • SObject Type Detection Hardening for List<SObject> Collections
  • More Forgiving DML.retrieveResultFor Behavior
  • 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, including DML.cls and DML_Full_Test.cls)
  • force-app/ — default working directory for development (non-package)
  • internal/ — removed (its contents merged into package/)

v3.0.0

31 Jan 20:02

Choose a tag to compare

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 DmlCommand classes with DmlStrategy pattern for cleaner separation of concerns
  • Split orchestration into DependencyOrchestrator (INSERT/UPSERT) and LinearOrchestrator (UPDATE/DELETE/UNDELETE/MERGE/PUBLISH)
  • Introduced StrategiesStorage for strategy instance management
  • Changed from Kahn's algorithm on SObject types to Kahn's algorithm on individual records
  • Improved RandomIdGenerator with deterministic counter-based ID generation
  • Added ProcessingGroup class for efficient record batching
  • Introduced ExecutionResult and SObjectAggregatedResult for better result handling
  • Copyright year updated to 2026

v2.0.0

21 Dec 20:14

Choose a tag to compare

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 DmlOperation classes to a Command pattern (DmlCommand)
  • Introduced Orchestrator class 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

12 Dec 22:04

Choose a tag to compare

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 public to global to support package versioning
  • Enables cross-namespace usage when installed as a package

Public Mocking API

  • DML.mock() and DML.retrieveResultFor() methods are now globally accessible
  • Previously required @TestVisible annotation for test access

Installation Options

Unlocked Package (with namespace btcdev):

v1.8.0

04 Dec 20:26

Choose a tag to compare

Release Notes - v1.8.0

New Features

discardWork() Method

  • Added new discardWork() method to the Commitable interface
  • 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 the Result parameter
  • Enables hooks to access and process DML operation results
  • Signature changed from void after() to void after(Result result)

v1.7.0

29 Nov 16:01

Choose a tag to compare

v1.7.0-beta

v1.7.0

v1.6.0-beta

24 Nov 20:11

Choose a tag to compare

v1.6.0

v1.5.0 (Beta)

20 Nov 20:32

Choose a tag to compare

v1.5.0-beta

DML Results

v1.4.0 (Beta)

02 Nov 11:17

Choose a tag to compare

v1.4.0-beta

todos

v1.3.0 (Beta)

01 Nov 22:12
613883a

Choose a tag to compare

v1.3.0 (Beta)